Static diagnostics render at the seam, and never enter the registry
A diagnostic that crosses the protocol as a sentence has already lost. The caret, the code, the label and the hint are the diagnostic; the string is only how it travels.
Decision
RunReport::into_reportrenders every diagnostic, static and runtime alike.Report::Staticcarries{ rendered, status }— the whole report, prefix through hint — and the host prints it and exits on the status. TheDiagnostics { Parse, Types, Host }wire enum is gone: no host classified on it, so it carried nothing but a chance to render three ways.StaticDiagnostics::Parse/Typescarry theSourcetheir carets point into.Hostdoes not: it is spanless, and the invariant is type-level.- A failed compile registers nothing.
compile_runpeeks aFileIdso the program’s spans carry this run’s identity, and returns beforeinstall_root_contextever mints it. Since no program survives a static failure, no live span can reference its text — andSourceDbis append-only for the session’s whole life precisely because live spans index it. Putting the text there would leave a slot that nothing can reach and nothing can reclaim, once per typo, forever. StaticDiagnostics::renderis the single renderer, and settles the exit status alongside the text: 2 for a parse failure, 1 for a type failure, the host error’s own otherwise.Report::host_faultshapes the engine’s own refusals through it too.- A hook run registers nothing either. Its program is an already-compiled
value, so
run_framedhas no text; the entry it used to append named the empty string. That cost one permanent slot per prompt draw, and one per keystroke under abuffer-changehook.
Why
The seam already said so. into_report’s own doc names it “the last point at
which the engine’s SourceDb is in hand”, promising the host “the full string
— prefix, status, hint, caret”. Runtime errors kept that promise. Static ones
could not: they are produced before the source is registered, so at projection
time the registry was in hand but empty.
The regression came in with ad916c82 (host-seam Phase 1). Routing the REPL
through the transport handed it Report instead of RunReport, so it no
longer held a ParseError or a TypeError, and the ariadne calls were dropped
rather than moved into core. What remained was ParseError.message — no span,
no lex_kind — and TypeError’s Display, which prints raw byte offsets and
drops code(), render_label() and hint(). Every host on the wire got a
sentence: the REPL, exarch, and so the model reading exarch’s tool output.
Only one path still looked right, by bypassing the wire: batch.rs
runs its own parse/elaborate/typecheck ahead of the run door (the structural
REPL frontend did too, until it was retired). That the fix
restores the same phrasing that path already prints is the point — one
renderer, so they cannot drift.
Alternatives rejected
- Register the source before compiling. Closes the peek-then-register gap,
and lets the static formatters read the db like the runtime one does. It also
spends a permanent, unreachable registry slot on every failed line, and
breaks the invariant
compile_runreasons from — that nothing registers between the peek andinstall_root_context. The registry is not a cache; it is what live spans resolve through. - Ship the structure over the wire. Serialising
ParseError/TypeErrorplus the source text would let each host render for itself. It contradicts the standing rule that a live error cannot cross the protocol (Ending::Raisedalready carriesrendered, not anError), and it buys nothing: no host wants structure now that the structural frontend is retired. - Restore the compact REPL parse-error heuristic.
should_use_compact_parse_errorfired on a message that no longer exists anywhere in the tree; restoring it restores dead code. Every parse error renders with ariadne until a concrete error proves too noisy.