Step 03 · Rustc-style diagnostics

The Diagnostic struct is small and intentional:

struct Diagnostic {
    Severity    severity;     // Error / Warning / Note
    std::string code;         // "E0202"
    std::string message;
    Span        span;
    std::string hint;         // "help: try ..."
};

Renderer output:

error[E0202]: expected expression, got `;`
  --> ex.ml:1:10
    |
  1 | print 1 +;
    |          ^
    | help: try a number, a variable, or `(`

The four lines after the header are:

  1. Gutter blank line matching the line-number width.
  2. Source line with the offending text.
  3. Caret line — spaces to the column, then ^ for the span.
  4. Hint (optional) — what to try next.

Why no ANSI colours

Easier to test (string comparison), easier to pipe to less, easier to integrate with editors that re-style errors. A real CLI adds a --color=auto flag that wraps error and the caret in red — trivial to layer on top.

Codes

Rust assigns each error an E#### code, Swift uses descriptive IDs, TypeScript uses TS####. Benefits:

  • Documentation hooks (rustc --explain E0382).
  • Stable references in tutorials/bug reports.
  • Tooling can ignore-list specific codes.

We pre-assign blocks: E01xx lex errors, E02xx parse, E03xx semantic. Cheap up-front; pays for itself the first time someone googles minilang E0301.

Spans across multiple lines

The renderer currently assumes the span fits on one line. Multi-line spans (if {\n bad\n}) need the line bar repeated with ~~~ for trailing lines and ^^^ on the start. We left this as a 30-minute extension exercise — pattern-match what rustc does.

Suggestions / fix-its

hint is plain text. A richer system attaches a replacement span + replacement text that an IDE can apply automatically:

struct FixIt { Span span; std::string replacement; };

Useful but easy to get wrong — apply two fix-its that overlap and you corrupt the file. Production compilers (rustc, clang-tidy) keep them gated behind explicit user invocation.