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