ArkScript
A small, lisp-inspired, functional scripting language
Exceptions.cpp
Go to the documentation of this file.
1#include <Ark/Exceptions.hpp>
2
3#include <cassert>
4#include <sstream>
5#include <algorithm>
6#include <fmt/core.h>
7#include <fmt/color.h>
8#include <fmt/ostream.h>
9
10#include <Ark/Constants.hpp>
11#include <Ark/Utils.hpp>
12#include <Ark/Files.hpp>
13#include <Ark/Literals.hpp>
15
16namespace Ark::Diagnostics
17{
24
25 inline bool isPairableChar(const char c)
26 {
27 return c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}';
28 }
29
30 void colorizeLine(const std::string& line, LineColorContextCounts& line_color_context_counts, std::ostream& ss)
31 {
32 // clang-format off
33 constexpr std::array pairing_color {
34 fmt::color::light_blue,
35 fmt::color::light_green,
36 fmt::color::light_salmon,
37 fmt::color::light_yellow,
38 fmt::color::light_cyan,
39 fmt::color::light_coral
40 };
41 // clang-format on
42 constexpr std::size_t pairing_color_size = pairing_color.size();
43
44 for (const char& c : line)
45 {
46 if (isPairableChar(c))
47 {
48 std::size_t pairing_color_index = 0;
49
50 switch (c)
51 {
52 case '(':
53 pairing_color_index = static_cast<std::size_t>(std::abs(line_color_context_counts.open_parentheses)) % pairing_color_size;
54 line_color_context_counts.open_parentheses++;
55 break;
56 case ')':
57 line_color_context_counts.open_parentheses--;
58 pairing_color_index = static_cast<std::size_t>(std::abs(line_color_context_counts.open_parentheses)) % pairing_color_size;
59 break;
60 case '[':
61 pairing_color_index = static_cast<std::size_t>(std::abs(line_color_context_counts.open_square_braces)) % pairing_color_size;
62 line_color_context_counts.open_square_braces++;
63 break;
64 case ']':
65 line_color_context_counts.open_square_braces--;
66 pairing_color_index = static_cast<std::size_t>(std::abs(line_color_context_counts.open_square_braces)) % pairing_color_size;
67 break;
68 case '{':
69 pairing_color_index = static_cast<std::size_t>(std::abs(line_color_context_counts.open_curly_braces)) % pairing_color_size;
70 line_color_context_counts.open_curly_braces++;
71 break;
72 case '}':
73 line_color_context_counts.open_curly_braces--;
74 pairing_color_index = static_cast<std::size_t>(std::abs(line_color_context_counts.open_curly_braces)) % pairing_color_size;
75 break;
76 default:
77 break;
78 }
79
80 fmt::print(ss, "{}", fmt::styled(c, fmt::fg(pairing_color[pairing_color_index])));
81 }
82 else
83 fmt::print(ss, "{}", c);
84 }
85 }
86
88 std::ostream& os,
89 const std::string& filename,
90 const std::optional<std::string>& expr,
91 const std::size_t sym_size,
92 const std::size_t target_line,
93 const std::size_t col_start,
94 const std::optional<CodeErrorContext>& maybe_context, // can not be populated at runtime, only compile time
95 const bool whole_line,
96 const bool colorize)
97 {
98 assert(!(maybe_context && whole_line) && "Can not create error context when a context is given AND the whole line has to be underlined");
99
100 using namespace Ark::literals;
101
102 auto show_file_location = [&] {
103 if (filename != ARK_NO_NAME_FILE)
104 fmt::print(os, "In file {}:{}\n", filename, target_line + 1);
105 if (expr)
106 fmt::print(os, "At {} @ {}:{}\n", expr.value(), target_line + 1, col_start);
107 };
108
109 auto compute_start_end_window = [](const std::size_t center_of_window, const std::size_t line_count) {
110 std::size_t start = center_of_window >= 3 ? center_of_window - 3 : 0;
111 std::size_t end = center_of_window + 3 <= line_count ? center_of_window + 3 : line_count;
112 return std::make_pair(start, end);
113 };
114
115 auto print_line = [&os, colorize](const std::size_t i, const std::vector<std::string>& lines, LineColorContextCounts& color_context) {
116 // show current line with its number
117 fmt::print(os, "{: >5} |{}", i + 1, !lines[i].empty() ? " " : "");
118 if (colorize)
119 colorizeLine(lines[i], color_context, os);
120 else
121 fmt::print(os, "{}", lines[i]);
122 fmt::print(os, "\n");
123 };
124
125 const std::string line_no_num = " |";
126
127 auto print_context_hint = [&os, &maybe_context, &line_no_num, colorize]() mutable {
128 if (!maybe_context)
129 return;
130
131 fmt::print(os, "{}", line_no_num);
132 fmt::print(
133 os,
134 "{: <{}}{}\n",
135 // padding os spaces
136 " ",
137 std::max(1_z, maybe_context->col), // fixing padding when the error is on the first character
138 // underline the parent of the error in red
139 fmt::styled(
140 maybe_context->is_macro_expansion ? "^ macro expansion started here" : "^ expression started here",
141 colorize ? fmt::fg(fmt::color::red) : fmt::text_style()));
142 };
143
144 const std::string code = filename == ARK_NO_NAME_FILE ? "" : Utils::readFile(filename);
145 const std::vector<std::string> lines = Utils::splitString(code, '\n');
146 if (target_line >= lines.size() || code.empty())
147 {
148 // show the "in file..." before early return
149 show_file_location();
150 return;
151 }
152
153 auto [first_line, last_line] = compute_start_end_window(target_line, lines.size());
154 // overflow is non-zero when the expression doesn't fit on the target line
155 std::size_t overflow = (col_start + sym_size <= lines[target_line].size()) ? 0 : sym_size;
156
157 const bool ctx_same_file = maybe_context && maybe_context->filename == filename;
158 const bool ctx_in_window = ctx_same_file && maybe_context &&
159 maybe_context->line >= first_line &&
160 maybe_context->line < last_line;
161
162 std::size_t start_line_skipping_at = 0;
163 std::size_t stop_line_skipping_at = first_line;
164 if (ctx_same_file && !ctx_in_window)
165 {
166 // showing the context will require an ellipsis, to avoid showing too many lines in the error message
167 if (maybe_context->line + 3 < first_line)
168 start_line_skipping_at = maybe_context->line + 3;
169 else
170 stop_line_skipping_at = start_line_skipping_at;
171
172 // due to how context works, if it points to the same file,
173 // we are guaranteed it will be before our error
174 first_line = maybe_context->line >= 3 ? maybe_context->line - 3 : 0;
175 }
176 else if (maybe_context && !ctx_same_file && !maybe_context->filename.empty())
177 {
178 // show the location of the parent of our error first
179 fmt::print(os, "Error originated from file {}:{}\n", maybe_context->filename, maybe_context->line + 1);
180
181 const std::vector<std::string> ctx_source_lines = Utils::splitString(Utils::readFile(maybe_context->filename), '\n');
182 auto [ctx_first_line, ctx_last_line] = compute_start_end_window(maybe_context->line, ctx_source_lines.size());
183 LineColorContextCounts line_color_context_counts;
184
185 for (auto i = ctx_first_line; i < ctx_last_line; ++i)
186 {
187 print_line(i, ctx_source_lines, line_color_context_counts);
188 if (i == maybe_context->line)
189 print_context_hint();
190 }
191
192 fmt::print(os, "\n");
193 }
194
195 show_file_location();
196 LineColorContextCounts line_color_context_counts;
197
198 for (auto i = first_line; i < last_line && i < lines.size(); ++i)
199 {
200 if (i >= start_line_skipping_at && i < stop_line_skipping_at)
201 continue;
202 print_line(i, lines, line_color_context_counts);
203
204 // if the error context is in the current file, point to it as the parent of our error
205 if (maybe_context && i == maybe_context->line && i != target_line)
206 print_context_hint();
207
208 // if the next line number wants us to skip line, and start != stop (meaning they got adjusted),
209 // display an ellipsis
210 if (i + 1 == start_line_skipping_at && i + 1 != stop_line_skipping_at)
211 fmt::print(os, " ... |\n");
212
213 // show where the error occurred (do not mark empty lines as being part of the error when we have overflow)
214 if (i == target_line || (i > target_line && overflow > 0 && !lines[i].empty()))
215 {
216 fmt::print(os, "{}", line_no_num);
217
218 if (!whole_line)
219 {
220 std::size_t line_first_char = lines[i].find_first_not_of(" \t\v");
221 line_first_char = line_first_char == std::string::npos ? 0 : line_first_char;
222
223 // if we have an overflow then we start at the beginning of the line (first non-space character)
224 const std::size_t curr_col_start = (i == target_line) ? col_start : (overflow == 0 ? col_start : line_first_char + 1);
225 // if we have an overflow, it is used as the end of the line
226 const std::size_t col_end = (i == target_line) ? std::min<std::size_t>(col_start + sym_size, lines[target_line].size())
227 : std::min<std::size_t>(line_first_char + overflow, lines[i].size());
228 // update the overflow to avoid going here again if not needed
229 // using min between overflow and what we need to delete to avoid underflow
230 overflow -= std::min(overflow, lines[i].size() - line_first_char);
231 // if there is overflow left, and it's the last line of the context, extend it
232 if (overflow > 0 && i + 1 == last_line)
233 ++last_line;
234
235 // show the error where it's at, using the normal process, if there is no context OR if the context line is different from the error line
236 if (!maybe_context || maybe_context->line != target_line)
237 fmt::print(
238 os,
239 "{: <{}}{:~<{}}\n",
240 // padding of spaces
241 " ",
242 std::max(1_z, std::min(curr_col_start, col_end)), // fixing padding when the error is on the first character
243 // underline the error in red
244 fmt::styled("^", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()),
245 curr_col_start < col_end ? col_end - curr_col_start : 1);
246 else if (i == target_line) // maybe_context has a value, i == target_line to avoid having to deal with overflow
247 {
248 const auto padding_size = std::max(1_z, maybe_context->col);
249
250 fmt::print(
251 os,
252 "{: <{}}{}{}{}\n",
253 // padding of spaces
254 " ",
255 padding_size,
256 // indicate where the parent is, with color
257 fmt::styled("│", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()),
258 // yet another padding of spaces between the parent and error column (if need be)
259 // -2 to account for the │ and then └
260 (col_start - maybe_context->col <= 2) ? "" : fmt::format("{: <{}}", " ", col_start - maybe_context->col - 2),
261 // underline the error in red
262 fmt::styled("└─ error", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()));
263 // new line, some spacing between the error and the parent
264 fmt::print(os, "{}{: <{}}{}\n", line_no_num, " ", padding_size, fmt::styled("│", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()));
265 // new line, now show the "expression started here for the source"
266 fmt::print(
267 os,
268 "{}{: <{}}{}\n",
269 line_no_num,
270 // padding of spaces
271 " ",
272 padding_size,
273 fmt::styled(
274 maybe_context->is_macro_expansion ? "└─ macro expansion started here" : "└─ expression started here",
275 colorize ? fmt::fg(fmt::color::red) : fmt::text_style()));
276 }
277 }
278 else
279 {
280 // first non-whitespace character of the line
281 // +1 for the leading whitespace after ` |` before the code
282 const std::size_t curr_col_start = lines[i].find_first_not_of(" \t\v") + 1;
283
284 // highlight the current line but skip any leading whitespace
285 fmt::print(
286 os,
287 "{: <{}}{:~<{}}\n",
288 // padding of spaces
289 " ",
290 curr_col_start,
291 // underline the whole line in red
292 fmt::styled("^", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()),
293 lines[target_line].size() - curr_col_start);
294 }
295 }
296 }
297 }
298
299 void helper(std::ostream& os, const std::string& message, const bool colorize,
300 const std::string& filename,
301 const std::optional<std::string>& expr, const std::size_t sym_size,
302 const std::size_t line, const std::size_t column,
303 const std::optional<CodeErrorContext>& maybe_context = std::nullopt)
304 {
305 makeContext(os, filename, expr, sym_size, line, column, maybe_context, /* whole_line= */ false, colorize);
306
307 const auto message_lines = Utils::splitString(message, '\n');
308 for (const auto& text : message_lines)
309 fmt::print(os, " {}\n", text);
310 }
311
312 std::string makeContextWithNode(const std::string& message, const internal::Node& node)
313 {
314 std::stringstream ss;
315
316 std::size_t size = 3;
317 if (node.isStringLike())
318 size = node.string().size();
319
320 helper(
321 ss,
322 message,
323 true,
324 node.filename(),
325 node.repr(),
326 size,
327 node.line(),
328 node.col());
329
330 return ss.str();
331 }
332
333 void generate(const CodeError& e, std::ostream& os, bool colorize)
334 {
335#ifdef ARK_BUILD_EXE
336 if (const char* nocolor = std::getenv("NOCOLOR"); nocolor != nullptr)
337 colorize = false;
338#endif
339
340 std::string escaped_symbol;
341 if (e.context.symbol.has_value())
342 {
343 switch (e.context.symbol.value().codepoint())
344 {
345 case '\n': escaped_symbol = "'\\n'"; break;
346 case '\r': escaped_symbol = "'\\r'"; break;
347 case '\t': escaped_symbol = "'\\t'"; break;
348 case '\v': escaped_symbol = "'\\v'"; break;
349 case '\0': escaped_symbol = "EOF"; break;
350 case ' ': escaped_symbol = "' '"; break;
351 default:
352 escaped_symbol = e.context.symbol.value().c_str();
353 }
354 }
355 else
356 escaped_symbol = e.context.expr;
357
358 helper(
359 os,
360 e.what(),
361 colorize,
363 escaped_symbol,
364 e.context.expr.size(),
365 e.context.line,
366 e.context.col,
368 }
369}
Lots of utilities about string, filesystem and more.
Constants used by ArkScript.
#define ARK_NO_NAME_FILE
Definition Constants.hpp:26
ArkScript homemade exceptions.
Lots of utilities about the filesystem.
User defined literals for Ark internals.
AST node used by the parser, optimizer and compiler.
A node of an Abstract Syntax Tree for ArkScript.
Definition Node.hpp:30
const std::string & filename() const noexcept
Return the filename in which this node was created.
Definition Node.cpp:174
const std::string & string() const noexcept
Return the string held by the value (if the node type allows it)
Definition Node.cpp:38
std::string repr() const noexcept
Compute a representation of the node without any comments or additional sugar, colors,...
Definition Node.cpp:189
std::size_t col() const noexcept
Get the column at which this node was created.
Definition Node.cpp:169
bool isStringLike() const noexcept
Check if the node is a string like node.
Definition Node.cpp:88
std::size_t line() const noexcept
Get the line at which this node was created.
Definition Node.cpp:164
bool isPairableChar(const char c)
void helper(std::ostream &os, const std::string &message, const bool colorize, const std::string &filename, const std::optional< std::string > &expr, const std::size_t sym_size, const std::size_t line, const std::size_t column, const std::optional< CodeErrorContext > &maybe_context=std::nullopt)
void colorizeLine(const std::string &line, LineColorContextCounts &line_color_context_counts, std::ostream &ss)
ARK_API void generate(const CodeError &e, std::ostream &os=std::cout, bool colorize=true)
Generate a diagnostic from an error and print it to the standard output.
ARK_API std::string makeContextWithNode(const std::string &message, const internal::Node &node)
Helper used by the compiler to generate a colorized context from a node.
ARK_API void makeContext(std::ostream &os, const std::string &filename, const std::optional< std::string > &expr, std::size_t sym_size, std::size_t target_line, std::size_t col_start, const std::optional< CodeErrorContext > &maybe_context, bool whole_line, bool colorize)
Helper to create a colorized context to report errors to the user.
std::string readFile(const std::string &name)
Helper to read a file.
Definition Files.hpp:47
std::vector< std::string > splitString(const std::string &source, const char sep)
Cut a string into pieces, given a character separator.
Definition Utils.hpp:31
const std::size_t line
const std::string expr
const std::size_t col
const std::string filename
const std::optional< internal::utf8_char_t > symbol
CodeError thrown by the compiler (parser, macro processor, optimizer, and compiler itself)
const std::optional< CodeErrorContext > additional_context
const CodeErrorContext context