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 error carries the span of the node it broke on, not a cursor the evaluator wrote down. Error (types/error.rs) holds span: Option<Span>None at mint, stamped by the break path with the innermost enclosing node’s span as it unwinds (Error::at_span). Since a Span is already tagged with its FileId, the source identity is the value’s own and no ambient position has to be maintained.

SourceDb (source.rs) resolves that id at render time. It is the registry of every source the session has loaded, keyed by FileId, living on SessionState::sources: append-only for the session’s whole life, so a nested run can never re-mint a FileId an outer run’s live spans still name. Shell::install_script_context registers a source and install_root_context additionally seeds SessionState::root_file with the current run’s root (FileId::DUMMY between runs); SourceDb::next_id peeks the id a registration will mint, so a compiler can stamp a program’s spans before the source they name is itself in the db. Hosts read the db after a run returns to render.

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.)

CallSite (diagnostic.rs) is the audit-and-wire shape a Span resolves to — script name plus 1-indexed (line, col) — which hosts read off every observation, command or capability check alike. It rides the audit collector rather than the run frame, so an observation carries the position of the dispatch that produced it.

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 the error’s Span against a SourceDb) / _compact, with cmd_error and shell_warning for unstructured command-layer output. Color is gated through ansi::use_color.

format_runtime_error_auto picks between the two by asking where the error came from, not what the input looked like: it takes compact_root: Option<FileId>Some(root) when the input compiled to a single command, carrying that input’s own id — and renders compact only when the error’s span is absent or names root. A single command that dispatches into an rc alias, a sourced function or a lambda from an earlier run faults in text the user cannot see, so it gets the caret.

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. text.rs is also the single home of the nucleo fuzzy matcher (rank, and rank_by for an item that is not its own haystack), so every filtered list a user is offered — completion menus, pickers, the exarch command popup — ranks the same way. 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.