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:
- How is a source position represented on tokens and AST nodes?
- How are errors from different stages (lexer, parser, resolver, runtime) represented?
- How are they rendered to a terminal?
Decision
- 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.u32keeps aSpanat 8 bytes and caps source size at 4 GiB. - One [
Diagnostic] type for every stage. It carries a stable [ErrorCode] (E00xxlexer …E03xxruntime), a message, ordered labelled spans (first = primary), notes, and an optional help line. Stages differ only in which codes they raise. - Rendering uses the
ariadnecrate, configured for byte indexing. Korrin owns theDiagnostictype;ariadneis an implementation detail of therenderfunction 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
codeandspan, not on rendered text, so message wording can be improved without breaking tests. ariadnecounts in characters by default; Korrin passes byte offsets and setsIndexType::Byte, so the two never disagree even with non-ASCII source.- If
ariadneis ever swapped out, onlydiagnostics::renderchanges. - 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 toariadne;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.