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

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::tokenize and parser::parse keep 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's compile), 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/parser on 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 parse doesn't expose that tree. Rejected: this would mean korrin-lsp reaching into parser internals that aren't public API, which is a worse coupling than either of the two options above.