ArkScript
A small, lisp-inspired, functional scripting language
Diagnostics.cpp
Go to the documentation of this file.
2
3#include <cassert>
4#include <sstream>
5#include <algorithm>
6#include <fmt/color.h>
7#include <fmt/ostream.h>
8
9#include <Ark/Constants.hpp>
10#include <Ark/Utils/Utils.hpp>
14
15namespace Ark::Diagnostics
16{
17 using namespace Ark::literals;
18
19 void showFileLocation(std::ostream& os, const ErrorLocation& loc)
20 {
21 if (loc.filename != ARK_NO_NAME_FILE)
22 fmt::print(os, "In file {}:{}\n", loc.filename, loc.start.line + 1);
23 }
24
25 void hintWithContext(std::ostream& os, const std::optional<CodeErrorContext>& maybe_context, const bool colorize, const bool start)
26 {
27 if (!maybe_context)
28 return;
29
30 fmt::print(os, "{}", Printer::GhostLinePrefix);
31 fmt::print(
32 os,
33 "{: <{}}{}\n",
34 // padding os spaces
35 " ",
36 std::max(1_z, maybe_context->at.start.column), // fixing padding when the error is on the first character
37 // underline the parent of the error in red
38 fmt::styled(
39 start
40 ? (maybe_context->is_macro_expansion ? "┌─ macro expansion started here" : "┌─ expression started here")
41 : "└─ ... and ended here",
42 colorize ? fmt::fg(fmt::color::red) : fmt::text_style()));
43 }
44
46 const ErrorLocation& loc,
47 std::ostream& os,
48 const std::optional<CodeErrorContext>& maybe_context, // can not be populated at runtime, only compile time
49 const bool colorize)
50 {
51 assert(!(maybe_context && loc.wholeLineIsError()) && "Can not create error context when a context is given AND the whole line has to be underlined");
52
53 Printer source_printer(loc.filename, loc.start.line, loc.maybeEndLine(), colorize);
54 if (!source_printer.hasContent())
55 {
56 showFileLocation(os, loc);
57 return;
58 }
59
60 const bool ctx_same_file = maybe_context && maybe_context->filename == loc.filename;
61 const bool ctx_in_window = ctx_same_file && maybe_context && source_printer.coversLine(maybe_context->at.start.line);
62
63 if (ctx_same_file && !ctx_in_window)
64 source_printer.extendWindow(maybe_context->at.start.line);
65 else if (maybe_context && !ctx_same_file && !maybe_context->filename.empty())
66 {
67 // show the location of the parent of our error first
68 std::string uniformised_filename;
69 std::ranges::replace_copy(maybe_context->filename, std::back_inserter(uniformised_filename), '\\', '/');
70 fmt::print(os, "Error originated from file {}:{}\n", uniformised_filename, maybe_context->at.start.line + 1);
71
72 std::optional<decltype(internal::FilePos::line)> maybe_end_line = std::nullopt;
73 if (maybe_context->at.end)
74 maybe_end_line = maybe_context->at.end->line;
75 Printer printer(maybe_context->filename, maybe_context->at.start.line, maybe_end_line, colorize);
76
77 while (printer.hasContent())
78 {
80 hintWithContext(os, maybe_context, colorize, /* start= */ true);
81 printer.printLine(os);
82 if (printer.isLastLineOfTarget())
83 hintWithContext(os, maybe_context, colorize, /* start= */ false);
84 }
85
86 fmt::print(os, "\n");
87 }
88
89 showFileLocation(os, loc);
90
91 while (source_printer.hasContent())
92 {
93 const std::size_t i = source_printer.current();
94 const std::string& line = source_printer.currentLine();
95
96 // if the error context is in the current file, point to it as the parent of our error
97 if (ctx_same_file && i == maybe_context->at.start.line && i < loc.start.line)
98 hintWithContext(os, maybe_context, colorize, /* start= */ true);
99 source_printer.printLine(os);
100
101 // show where the error occurred
102 if (source_printer.isTargetLine() && !line.empty())
103 {
104 if (!loc.wholeLineIsError())
105 {
106 const std::size_t line_first_char = (line.find_first_not_of(" \t\v") == std::string::npos) ? 0 : line.find_first_not_of(" \t\v");
107
108 const std::size_t col_start = (i == loc.start.line) ? loc.start.column : line_first_char + 1;
109 // due to the `!loc.wholeLineIsError()` check, we are guaranteed to have a value in loc.end
110 const std::size_t col_end = (i == loc.end->line) ? loc.end->column : line.size();
111
112 // ignore the last line that is sometimes erroneous in multiline contexts
113 if (i == loc.end->line && loc.end->line != loc.start.line && col_start >= col_end)
114 continue;
115 fmt::print(os, "{}", Printer::GhostLinePrefix);
116
117 // show the error where it's at, using the normal process, if there is no context OR
118 // if the context line is different from the error line
119 if (!maybe_context || maybe_context->at.start.line != loc.start.line)
120 fmt::print(
121 os,
122 "{: <{}}{:~<{}}\n",
123 // padding of spaces
124 " ",
125 std::max(1_z, std::min(col_start, col_end)), // fixing padding when the error is on the first character
126 // underline the error in red
127 fmt::styled("^", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()),
128 col_start < col_end ? col_end - col_start : 1);
129 // cppcheck-suppress knownConditionTrueFalse ; suppressing error so that the condition is explicit to the reader
130 else if (maybe_context && maybe_context->at.start.line == loc.start.line && i == loc.start.line)
131 {
132 const auto padding_size = std::max(1_z, maybe_context->at.start.column);
133 const std::string inner_padding =
134 // -2 to account for the │ and then └
135 (loc.start.column < padding_size || loc.start.column - padding_size < 2)
136 ? ""
137 : std::string(std::max(1_z, loc.start.column - padding_size - 1), ' ');
138
139 fmt::print(
140 os,
141 "{: <{}}{}{}{}\n",
142 // padding of spaces
143 " ",
144 padding_size,
145 // indicate where the parent is, with color
146 fmt::styled("│", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()),
147 // yet another padding of spaces between the parent and error column (if need be)
148 inner_padding,
149 // underline the error in red
150 fmt::styled("└─ error", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()));
151 // new line, some spacing between the error and the parent
152 fmt::print(
153 os,
154 "{}{: <{}}{}\n", Printer::GhostLinePrefix,
155 " ",
156 padding_size,
157 fmt::styled("│", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()));
158 // new line, now show the "expression started here for the source"
159 fmt::print(
160 os,
161 "{}{: <{}}{}\n",
163 // padding of spaces
164 " ",
165 padding_size,
166 fmt::styled(
167 maybe_context->is_macro_expansion ? "└─ macro expansion started here" : "└─ expression started here",
168 colorize ? fmt::fg(fmt::color::red) : fmt::text_style()));
169 }
170 }
171 else
172 {
173 fmt::print(os, "{}", Printer::GhostLinePrefix);
174
175 // first non-whitespace character of the line
176 // +1 for the leading whitespace after ` |` before the code
177 const std::size_t col_start = line.find_first_not_of(" \t\v") + 1;
178
179 // highlight the current line but skip any leading whitespace
180 fmt::print(
181 os,
182 "{: <{}}{:~<{}}\n",
183 // padding of spaces
184 " ",
185 col_start,
186 // underline the whole line in red
187 fmt::styled("^", colorize ? fmt::fg(fmt::color::red) : fmt::text_style()),
188 line.size() - col_start);
189 }
190 }
191 }
192 }
193
194 void helper(std::ostream& os, const std::string& message, const bool colorize,
195 const std::string& filename, const internal::FileSpan& at,
196 const std::optional<CodeErrorContext>& maybe_context = std::nullopt)
197 {
198 std::string uniformised_filename;
199 std::ranges::replace_copy(filename, std::back_inserter(uniformised_filename), '\\', '/');
202 .filename = uniformised_filename,
203 .start = at.start,
204 .end = at.end },
205 os, maybe_context, colorize);
206
207 for (const auto& text : Utils::splitString(message, '\n'))
208 fmt::print(os, " {}\n", text);
209 }
210
211 std::string makeContextWithNode(const std::string& message, const internal::Node& node)
212 {
213 std::stringstream ss;
214
215 helper(
216 ss,
217 message,
218 true,
219 node.filename(),
220 node.position());
221
222 return ss.str();
223 }
224
225 void generate(const CodeError& e, std::ostream& os, bool colorize)
226 {
227#ifdef ARK_BUILD_EXE
228 if (const char* nocolor = std::getenv("NOCOLOR"); nocolor != nullptr)
229 colorize = false;
230#endif
231
232 helper(
233 os,
234 e.what(),
235 colorize,
237 e.context.at,
239 }
240}
Lots of utilities about string, filesystem and more.
Constants used by ArkScript.
#define ARK_NO_NAME_FILE
Definition Constants.hpp:28
Tools to report code errors nicely to the user.
User defined literals for Ark internals.
AST node used by the parser, optimizer and compiler.
Pretty printing utilities for diagnostics.
Source printer for diagnostics.
void extendWindow(std::size_t line_to_include)
Extend the window of lines to show, to include a given line. Useful to display the origin of an error...
const std::string & currentLine() const
static const std::string GhostLinePrefix
bool isTargetLine() const
Check if we printed the target line.
bool coversLine(std::size_t line_number) const
bool hasContent() const
Check if there are lines to print.
bool isNextLineTheFirstLineOfTarget() const
std::size_t current() const
void printLine(std::ostream &os)
Print the current line and advance by one.
A node of an Abstract Syntax Tree for ArkScript.
Definition Node.hpp:32
const std::string & filename() const noexcept
Return the filename in which this node was created.
Definition Node.cpp:164
FileSpan position() const noexcept
Get the span of the node (start and end)
Definition Node.cpp:159
void helper(std::ostream &os, const std::string &message, const bool colorize, const std::string &filename, const internal::FileSpan &at, const std::optional< CodeErrorContext > &maybe_context=std::nullopt)
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.
std::string makeContextWithNode(const std::string &message, const internal::Node &node)
Helper used by the compiler to generate a colorized context from a node.
void showFileLocation(std::ostream &os, const ErrorLocation &loc)
void hintWithContext(std::ostream &os, const std::optional< CodeErrorContext > &maybe_context, const bool colorize, const bool start)
ARK_API void makeContext(const ErrorLocation &loc, std::ostream &os, const std::optional< CodeErrorContext > &maybe_context, bool colorize)
Helper to create a colorized context to report errors to the user.
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 internal::FileSpan at
const std::string filename
CodeError thrown by the compiler (parser, macro processor, optimizer, and compiler itself)
const std::optional< CodeErrorContext > additional_context
const CodeErrorContext context
std::optional< internal::FilePos > end
std::string filename
Complete path to the file where the error is.
std::optional< decltype(internal::FilePos::line)> maybeEndLine() const noexcept
std::size_t column
0-indexed column number
Definition Position.hpp:23
std::size_t line
0-indexed line number
Definition Position.hpp:22
Describes a span for a node/atom in a file, its start position and end position.
Definition Position.hpp:35