ADR-044: One Location Table, and Every Abort Points at the Nikaia Line

Status: Superseded by ADR-300 — its D1-D3 are D9-D11 there Related: ADR-012 (errors are reported in the .nika file the user wrote — this is that rule reaching the one path it never reached), ADR-006 D6 (the panic hook, which must be sync and is where the lookup goes), ADR-036 (a stack trace is asked for; the site is free), ADR-043 (the occasion for this record, and not its reason).

1. Context

ADR-012 decides that a diagnostic names the .nika file the user wrote, and the compiler keeps that promise everywhere it reports something. An abort at run time is the one path where it does not, and cannot by the same means: there is no compiler left to translate anything, so Rust’s own message names the generated file and line.

That is the message a user sees when an index goes out of bounds, when something divides by zero, when a panic() is written, and — once ADR-043 D1 is built — when an arithmetic overflow happens. The overflow decision is what made this worth writing down now. It is not what makes it right: every one of those paths has the same defect today.

Measured, because the plan rests on it: a panic hook runs before the process goes down, under both panic strategies, and info.location() gives the generated file and line. Under -C panic=abort the hook printed its line and the process then aborted (exit 134); under panic=unwind, the same line and exit 101. So there is a place to put the translation and something to translate.

2. Decisions

D1 — One table beside the program: generated file and line to Nikaia file and line.

The emitter already knows both ends of every line it writes. The table is that knowledge kept rather than thrown away, and it belongs to the program rather than to the compiler, because it is needed at run time and the compiler is gone by then.

D2 — The panic hook looks the site up before it prints anything.

The hook exists, it is global, and it runs on every abort path (ADR-006 D6, Part I 7.2). So the translation goes in one place and reaches every case at once: an overflow, an index, a division by zero, an explicit panic().

One place, deliberately. A per-case fix would be four fixes and a fifth the day a fifth abort path arrives.

D3 — This stands on its own, and does not wait on the overflow.

Nothing in D1 or D2 is about arithmetic. ADR-043 adds one more caller to a path that is already wrong for three reasons, so this record is worth building whether or not that one is.

3. Consequences

ADR-012’s rule becomes true without an exception. Today it holds for everything the compiler says and for nothing the program says; after this it holds for both.

The hook must stay sync (ADR-006 D6): a lookup in a table is pure computation, so D2 asks nothing of it that the rule forbids. Worth stating because a lookup that read a file would.

What it costs the program. A table of the lines that can abort, carried in the binary. It is paid by every program and needed by the ones that fail, which is the same trade a panic message itself already makes.

What it does not do. It translates a site, not a stack. What a trace looks like is ADR-036’s question and is not reopened here.

4. What is built

Both decisions, and every abort path the project has.

The table is appended to the program as a const of (generated line, .nika file, .nika line), sorted so the lookup is a binary search. It is every mapped line, not the ones that “can abort”: which constructs abort is a list that would have to be kept correct as the emitter grows, and getting it wrong means an abort with no line — the defect this closes. The source map already knows which lines came from somewhere, so the table is that knowledge kept rather than a second judgement about it.

Appended, never prepended, because a table written above the program would move every line it names and correcting for its own height is a circle.

Where a line has two spans covering it, the outermost wins. SourceMap answers a byte with the innermost span, which is right for a diagnostic about a byte; a whole generated line is better named by the statement it came from than by the sub-expression that happens to start it.

The hook is installed by the generated fn main, before the runtime starts — the first lines that can abort are inside that start. It prints src/main.nika:2: the program stopped: attempt to multiply with overflow and keeps ADR-006 D6’s sync rule by doing nothing but a binary search.

A location the table does not know is handed to the hook installed before ours, which is Rust’s own, so a program is never worse off than it was. That is the half with its own test.

Four paths, one fix, as D2 argued: an overflow, a conversion that does not fit, a negative index and an index past the end. The two index cases needed one thing more — nikaia_std::index::at and the conversion helpers carry #[track_caller], so the location the hook is handed is the caller’s line of the generated file rather than a line of std’s own Rust, which would have been the same defect one crate over.

What is not built: a program emitted by emit_program — the path the tests and lower-std use — gets an empty table, because that path is handed one Parsed and no file name. The name has to be defined or the program does not compile, and empty is the fallback rather than a lie: such a program falls through to Rust’s own hook. Every path a user takes goes through modules::Program, which knows every file.

And a stack is still ADR-036’s question. This translates a site.

This site is open source. Improve this page.