ADR-015: The Backend’s Lazy Diagnostics

Status: Accepted Supersedes: the measurement in ADR-014 D3 Related: ADR-296 D19 (user_parallelism chooses the executor and nothing else — the line D1 declines to cross), ADR-296 D10, ADR-016 (the next cost in line)

1. Context

A generated parser was running at about 4500 instructions per row against about 840 for the same aggregation written by hand in Rust. A file whose temperatures are all negative — so that the neg:"-"? in TENTHS succeeds instead of failing — parsed in half the time: one failing optional per row cost half the runtime, and the hand-written parser, which also looks for a - and usually does not find it, paid nothing.

The cost was not the failure. It was the bookkeeping the parser did about the failure, on a path where the parse then succeeded and nobody ever read it.

2. Decisions

D1 — Nikaia moves to the lazy-diagnostics backend and adds nothing of its own.

An entry point first parses with a zero-sized error type and, only if that fails, parses again with the diagnosing engine — for a par_fold rule, restarting from the item the fast pass stopped in rather than from the beginning. The mode is a field of the parse context and defaults to replay.

Nikaia emits no choice here. The default is right for every program the lowering produces: full speed on the accepted path, a full message on the rejected one, and the second pass paid only by inputs that were going to fail anyway.

A build switch that picked a diagnostics mode would cross ADR-296 D19’s line, since turning diagnostics off changes an error’s content — a switch that changes what a program means. If a program ever needs that, it is a property of the program and belongs in its source.

D2 — Three migrations, and one is Nikaia’s own bug.

D3 — The measurement, re-taken.

On 8 million rows, the generated parser runs at about 660 instructions per row, below hand-tuned Rust’s 690, and the all-negative file now parses at the same speed per byte as the mixed one — the half is gone, which is what says this was the right fix rather than a general speed-up that happened to help. Output is byte-identical at either setting and against the naive Rust program.

D4 — What this settles, and what it does not.

A parser generated from a declarative grammar is below hand-tuned Rust on instructions per row for this workload, and ahead of both hand-written programs on the clock before any parallelism is switched on. That is the claim ADR-296 makes, supported by measurement.

It is not a claim about the benchmark’s leaderboard. The top entries there are not parsers: SWAR over raw bytes, custom hash tables sized to the key count, no UTF-8 validation. That distance is architectural. What changed here is that the normal path is no longer the thing standing in the way.

The comparison is one workload. It is a good stress for scannerless parsing and zero-copy slices, and says nothing about a grammar with backtracking across long alternatives.

This site is open source. Improve this page.