Status: Accepted
Date: September 20, 2026
Related: ADR-300 (spans, the source map, and diagnostics on
the .nika line), ADR-288 (the flow carries the statement’s
byte, for the checker’s answers), Part III C.2 (what
every diagnostic owes a reader), C.1,
docs/project_status_and_roadmap.md Phase 2, which carried this as the
compiler’s own anyhow errors, which are still text without a position
Part III C.2 asks every diagnostic for four things: a
headline, the line with a caret under it, the reason, and a concrete way out.
Every NK… code gives all four. A refusal the lowering makes gave the
first and the last and nothing in between:
this lambda calls `fs::read_to_string`, which can pause - and a lambda that
pauses is not something this compiler can build yet. Call
`fs::read_to_string` outside the lambda and hand it the value, or give it a
`sync` body
True, and a reader with a file of any size is left searching for it.
The byte is already in hand. Flow::statement carries the byte the
statement being emitted starts at, threaded for the type checker’s answers
(ADR-288) — and it is the same number a caret wants.
Refused carries it, and refused_at! is refused! with the byte in front.
None stays the answer for a refusal about no statement in particular: a
manifest key, a switch, a whole file.
A refusal inside the expression and statement walks takes the statement’s byte from the flow in hand. A refusal about an item takes the item’s span, which the emitter’s item arm is already handed for the source map’s sake.
And a helper with no place of its own borrows the statement’s.
emit::interpolation works on the text of a string literal: it knows an
{ was never closed and cannot know where the string is, because it never saw
a file. The caller is emitting a statement and knows exactly where, so the
handover happens there — written once rather than at each call, which is what
keeps where from being something each refusal has to remember. The same
handover (at_the_statement) covers the grammar entry, the dsl family and
template.rs, which work on text.
A refusal about no statement keeps none. A build switch — a --target or a
user-parallelism nobody can spell — is about a manifest key and a whole
build; a line number for it would be an invention, and inventing one would be
worse than having none.
The lowering knows the statement and not the path; whoever catches the error at
the top knows neither, because a refusal travels as an anyhow::Error through
a build that has moved on. The one place with both is the per-unit loop, and
that is where a positioned refusal becomes the shape
ADR-300 gave every other diagnostic:
error: an `overlap` block needs at least two branches; one statement has nothing to overlap with (Part I, 8.1.2)
--> one.nika:4:5
4 | let x = overlap {
^
The same shape as the checker’s, deliberately: a rule enforced in the lowering and one enforced in the checker should not look different to a reader, who has no reason to know which half of this compiler refused them.
No code in front of it, because these refusals are not in the catalogue. Whether they should be is §4’s question.
NK….NK… numbers. They are rules this
compiler enforces, and the catalogue is the list of what it can say — but a
number is a promise that the rule stays, and several of these are this
compiler cannot build that yet rather than the language forbids it. Telling
those two apart is the work, and it is what a catalogue entry would have to
rest on.