Map: core / diagnostics

How every user-visible message — parse errors, type errors, runtime errors, exit-code hints — is located in source and rendered to the terminal.

Source locations — core/src/source.rs

A source position is a Span — a half-open byte range [start, end) tagged with a FileId, an opaque per-file handle. Line and column are not carried on IR or AST nodes; they are recovered at render time by handing the source text directly to ariadne. Where no narrower position is available, the position is Option<Span> = None uniformly across the AST, IR, and typechecker. Span::join merges two spans; Span::range yields the std::ops::Range<usize> ariadne expects. Carrying only byte offsets keeps the IR free of presentation state.

Runtime source identity — SourceDb

A runtime location carries a non-optional source identity, resolved once at render. A SourceLoc (diagnostic.rs — the script position of a runtime error) carries the FileId of the source its line/col index — the script or module active when the error was raised — alongside len. SourceDb (source.rs) is the registry of every source the current turn has loaded, keyed by FileId: it lives on SessionState::sources (a turn resets and seeds it at turn start; hosts read it after the turn returns to render), while the turn-local cursor — LocationCursor in diagnostic.rs, with the active-source cache, the current id, and the CallSite snapshot — rides on TurnState::loc. Shell::install_script_context registers each source and makes it current; ScriptContextGuard (module loads, the REPL plugin loader) saves and restores current around a load, and core’s per-turn TurnGuard swaps the whole cursor with the rest of the turn frame, while the registered source stays in the db.

This is the structural fix for the cross-source caret: a runtime error raised inside a sourced module carries the module’s FileId, so the renderer resolves the module’s text and draws the caret into the module’s bytes — not the top-level script’s. An id the renderer cannot resolve (the placeholder FileId::DUMMY, or an unregistered source) renders messageless rather than indexing an unrelated text. (structural-bug-prevention class 9.)

Parse and type errors render against the source they were just handed, so their entry points still take (file, source) strings: a module’s compile error is surfaced by the loader as a plain message, never reaching the runtime renderer.

Rendering — core/src/diagnostic.rs

All structured errors funnel through one module and render via the ariadne crate with source-span underlining; when no span is available a compact one-liner is used instead. The per-stage entry points are format_parse_error_ariadne, format_type_error_ariadne (each taking (file, source)), and format_runtime_error_ariadne / format_runtime_error_auto (resolving a SourceLoc against a SourceDb) / _compact, with cmd_error and shell_warning for unstructured command-layer output. Color is gated through ansi::use_color.

The raw ingredients of a span underline are exposed so an external renderer can draw one in its own coordinate system. text::byte_to_char (core/src/text.rs, the shared UTF-8 boundary snappers — byte offset → character offset, the unit ariadne and a TextArea cursor both count in) and TypeErrorKind::render_label (typecheck/explain.rs, a kind → its under-caret label phrase) are pub. The structural frontend reuses them to paint an in-place type-error underline whose label and caret agree word-for-word and column-for-column with the post-Enter ariadne report — the inline rendering belongs to that page, not here. Type-error prose generally lives beside the checker now: provenance is data on the error (Reason, typecheck/error.rs) and every user-facing sentence is a pure function of it in typecheck/explain.rs (typecheck).

Styling — core/src/ansi.rs

Escape constants and the color-gating predicates use_color / use_ui_color, which consult a TerminalState cached once at REPL startup via set_terminal. When the cache is empty use_color falls back to inline probing (batch runs and early-startup errors); use_ui_color is cache-only and yields false until set_terminal has run. Also the OSC helpers: osc_set_title, osc8_link, osc52_copy. Value-output styling (the REPL’s => prefix) lives instead in the ral crate’s repl::theme.

Exit-code hints — core/src/exit_hints.rs

ExitHints is a pure (command, exit-status) → explanation lookup table, populated via from_text and installed into the Shell; lookup is consulted when an external command fails. Loading the table is the caller’s concern.

Debug tracing — dbg_trace!

dbg_trace!(tag, …) is the single developer-facing trace primitive: a tagged stderr line in debug builds (red only where the ansi colour gate allows), nothing in release, no environment switch for the trace itself (one-debug-path). Its call sites are permanent instrumentation.