ADR-173: What a Parse Fails With Has a Name

Status: Superseded by ADR-296 — its D1-D3 are D38-D40 there Date: September 20, 2026 Target Version: Nikaia 0.0.103 Related: ADR-023 D1 (throws is a set), ADR-158 (std names its error type — the shape this copies), ADR-159 D1 (a member a ledger describes), ADR-160 D1 (two or more named members are a generated sum), ADR-082 D1 (a pub rule is a ledger entry), ADR-101 (what waits on the last "?" going), Part III C.1

1. Context

A public grammar rule’s ledger entry was written throws = ["?"] — ADR-024 D1’s absence of a claim — because a parse fails with a rendered string:

Config::parse_file().parse_next(&mut stream).map_err(|error| error.render(source))

A string is not a type, so there was nothing to name. It was the last "?" written anywhere in this tree: std.contracts has nine throws lines and not one of them is ["?"], and every other producer of that spelling is an absence — a call this compiler could not resolve — rather than a decision to leave something unnamed.

What it cost is a channel. ADR-160 D1 makes a set of two or more named members a generated sum a program can match on. A set with a "?" in it is the boxed channel instead, so a program that opens a file and parses it could not tell the file was not there from the file was not the shape the grammar says.

2. Decisions

D1 — A parse fails with ParseError, and a rule’s entry says so.

throws = ["ParseError"] on every pub rule’s ledger entry. The type is nikaia-std’s, in the prelude, keyed in std.contracts with no module in front — which is Overtaken’s shape (ADR-039’s lock error): a program never writes a path to it, because it arrives in a catch.

The lowering is one call wider: ParseError::of(error.render(_source)). The text is unchanged, so a program that prints the error prints exactly what it printed before; what changes is the channel it travels in.

D2 — It carries the message and nothing else.

The backend’s own error has an offset, a list of expectations, what was found and the rule stack — and what it renders out of them is a whole diagnostic: the headline, the line with a caret under it, and what else was possible there. That is the thing a program already prints.

Fields a program could read — the line, the column, the expectation — are a second question, and nobody has asked it: a handler today prints the message or falls back to a default. Adding them now would mean fields on a std type in the ledger, which nothing there has; io::IoError is an enum taken apart by its variants, which is the shape that exists. The smaller answer is the one that can be taken back.

D3 — A callee the throws graph does not hold still has a set.

The fixpoint that grows each function’s throws builds its graph from Item::Fn. A grammar’s entry rule is not one: its contract is written straight into the ledger (ADR-082 D1). So the lookup found nothing, and what a parse threw reached no caller at all.

A callee with no node in the graph is read from the ledger instead. That is one line and it is the whole of this decision, but it is not bookkeeping — see §3.

3. Consequences

D3 was a miscompilation, and naming the error is what made it visible. While a rule threw "?", a caller’s set came out empty and the ledger wrote ["?"] for it — the boxed channel, and a box takes a String. The moment the caller’s set had a named member from somewhere else, the channel was inferred without the parse’s member in it:

async fn both(path: &str) -> Result<i64, io::IoError> {
    let data = fs::read_to_string(path).await?;
    let n = { … .map_err(|error| error.render(_source)) }?;   // a String
    Ok(n)
}

io::IoError has no From<String>, so rustc refused the generated file — the one thing Part III C.1 says may not happen. No program in the corpus had that shape, which is why nothing was red.

And what it buys is the sum. The same function is Result<i64, __NikaiaThrows_ParseError__io_IoError> now, and a program can tell the two apart.

4. What this does not decide

5. What is built

All of D1, D2 and D3.

This site is open source. Improve this page.