Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8. Byte-offset spans, one diagnostic type, ariadne for rendering

  • Status: Accepted
  • Date: 2026-09-02

Context

Good error messages are a feature, not a nicety, and they are hard to retrofit. Three sub-decisions, taken together:

  1. How is a source position represented on tokens and AST nodes?
  2. How are errors from different stages (lexer, parser, resolver, runtime) represented?
  3. How are they rendered to a terminal?

Decision

  1. Positions are byte-offset [Span]s — a { start: u32, end: u32 } pair of byte offsets into the source. Line and column are derived on demand from a [SourceMap], never stored on nodes. u32 keeps a Span at 8 bytes and caps source size at 4 GiB.
  2. One [Diagnostic] type for every stage. It carries a stable [ErrorCode] (E00xx lexer … E03xx runtime), a message, ordered labelled spans (first = primary), notes, and an optional help line. Stages differ only in which codes they raise.
  3. Rendering uses the ariadne crate, configured for byte indexing. Korrin owns the Diagnostic type; ariadne is an implementation detail of the render function and is not exposed in the public API.

Error codes are permanent: once E0203 means "continue outside loop", it always does. Wording may change; meaning may not.

Consequences

  • The CLI and the spec-test harness share one rendering path.
  • Tests assert on code and span, not on rendered text, so message wording can be improved without breaking tests.
  • ariadne counts in characters by default; Korrin passes byte offsets and sets IndexType::Byte, so the two never disagree even with non-ASCII source.
  • If ariadne is ever swapped out, only diagnostics::render changes.
  • Every error site must produce a span. Synthesized nodes carry Span::DUMMY; code that renders must not rely on those, and in practice a real span is always available at the point an error is raised.

Alternatives considered

  • (line, column) on every node. Redundant, and painful to keep correct across multi-line tokens and spans.
  • codespan-reporting. Comparable to ariadne; ariadne's default output is a little clearer and its byte-index mode fits our span representation.
  • Hand-rolled renderer. More control, but caret alignment, multi-line spans, and tab handling are genuinely fiddly and not where the project's effort should go in M1.
  • A distinct error enum per stage. More "typed", but forces the CLI and harness to handle four shapes and makes cross-stage batching awkward.