27. Diagnostics: first-failing-stage only for v1
- Status: Accepted
- Date: 2026-09-04
Context
korrin-lsp publishes diagnostics by lexing, parsing, and resolving a
document's text, exactly the chain the CLI's own front end runs
(crates/korrin-cli/src/main.rs's compile). korrin::lexer::tokenize and
korrin::parser::parse both discard their output entirely on any error — a
lexer error means no token stream is returned at all, not even a partial one,
and a parse error means no tree, by design (their own doc comments say so).
Each stage's failure short-circuits the ones after it: today, a file with a
syntax error shows only that syntax error, never a resolver-level problem
(return outside a function, super with no parent) that might sit further
down in the same file.
That was an acceptable, unremarked-upon property of a batch tool (korrin run, which stops at the first problem anyway) and becomes a visible product
decision the moment the same chain feeds live, on-every-keystroke diagnostics
in an editor: a user fixing one syntax error only then discovers a resolver
error on the next keystroke, one at a time, rather than seeing everything
wrong with the file at once.
The alternative is real: change lexer::tokenize and parser::parse to
return best-effort partial results even after recording an error, so later
stages can still run over whatever they managed to produce. The parser
already recovers from a syntax error internally (its synchronize()) — the
scaffolding partially exists. But exposing that as a new return shape is a
change to korrin's public front-end contract, not an editor feature: every
caller of these two functions needs auditing, including the differential
testing harness that holds the tree-walker and the VM to identical behavior
(ADR 0024) and the spec-example runner that executes every runnable block in
the documentation (ADR 0002). Neither of those exists to validate a
"diagnostics quality" feature; changing what they depend on for one is a
disproportionate risk this milestone doesn't need to take.
Decision
v1 ships with diagnostics only from the first stage that fails, matching
korrin run's behavior exactly. korrin-lsp's diagnostics::check calls
korrin::lexer::tokenize, then korrin::parser::parse, then
korrin::resolver::resolve, propagating whichever stage's Vec<Diagnostic>
comes back first and never reaching the later stages if an earlier one
failed.
The user-visible cost: a file with one syntax error surfaces only that error until it's fixed, then whatever the resolver finds appears on the next diagnostics pass — which happens on every keystroke, so in practice this resolves itself within one edit cycle rather than staying hidden. This is recorded here as a conscious, accepted v1 limitation, not discovered later as an unexplained gap.
Consequences
- No changes to
korrin's public front-end API.lexer::tokenizeandparser::parsekeep their documented all-or-nothing contract, and every existing caller (the CLI, the VM's own compiler front end, the differential and spec-example test harnesses) is unaffected. korrin-lsp's own diagnostics pipeline is three lines matching a pattern that already exists and is already trusted (crates/korrin-cli/src/main.rs'scompile), rather than a new code path exercising an unproven partial-result contract for the first time.- A file with several unrelated problems is fixed one visible layer at a time rather than all at once. Worth revisiting once the LSP has enough other features that this becomes the sharpest remaining edge — likely after hover and go-to-definition, not before.
Alternatives considered
- Return best-effort partial results from
lexer/parseron error. The real long-term fix, and plausible future work, but a front-end redesign belongs to its own milestone with its own ADR, tested against the existing differential and spec-example harnesses on its own terms — not folded silently into "add a language server." - Re-run each stage independently, ignoring earlier failures. For
example, always run the resolver against whatever partial tree the parser
produced internally before its own error return, even though
parsedoesn't expose that tree. Rejected: this would meankorrin-lspreaching into parser internals that aren't public API, which is a worse coupling than either of the two options above.