ADR-188: The Escape Set Is The Language Below’s, Written Down Once And Refused Here

Status: Accepted Date: September 22, 2026 Closes: #197 (the escape set is Rust’s, and no page says so) Related: ADR-300 (the source map that puts a backend refusal on the right .nika line), ADR-017 (escaping in a template, a different question one word away), ADR-287 D6 (the build-time decoder that reads these escapes)

1. Context

The parser’s STR_CHAR takes \ and any character and keeps both; the emitter writes a .nika string literal into the generated Rust verbatim. So what a \ means is decided by rustc.

The set itself is not the problem. A literal that goes into the generated file as it stands has the language below’s escapes whether anybody writes that down or not, and writing it down is describing — the same bargain Part I 2.2 already makes about numbers, where the widths are the machine’s and the page says so.

What an unknown escape is told is the problem. Left to rustc, println("a\qb") is refused on the right line — ADR-300’s source map holds — but in rustc’s vocabulary, ending for more information, visit doc.rust-lang.org/reference/tokens.html#literals. A Nikaia program sent to the Rust reference to find out what it may write is Part III C.2’s in the compiler’s own words not met, and C.1’s class besides.

2. Decisions

D1 — The set is Rust’s, and Part I 2.5 says which.

\n \r \t \0 \\ \' \" \xNN \u{…}, in a "…", an f"…", a '…' and a template alike. \xNN names a byte below \x80, because that is what the language below allows inside a string; a character above it is \u{…}.

Describing and not deciding. Borrowing the backend’s escapes costs one sentence and keeps the two readings of a literal — this compiler’s and the generated file’s — from ever parting company. Deciding a set of our own would mean decoding every literal and re-encoding it, which is ADR-287 D6’s work done twice for no reader’s benefit.

D2 — One table, with two readers.

build_time::one_escape is the table. decoded reads it to turn a literal into a value while the program is built; an_escape_nothing_names reads it to say which escape a literal has that the set does not name.

Two walks would be two sets. A second copy of the table would drift — and the one that drifted open would refuse a literal the other decodes, which is a correct program refused (C.4). The refusal is safe precisely because every escape it refuses is one the decoder cannot read and rustc refuses one file later: what moves is who says it, and in whose vocabulary.

D3 — The refusal is NK1184, in the checker, naming the set.

At every literal that keeps its body as written — Expr::LitStr, Expr::LitInterpolated and Expr::LitChar. The message names the escape as written, one clause saying what is wrong with it, and the set.

In the checker and not in the parser. A fail arm inside a lexical rule is not fatal in this parser — an error that got further still wins, progress before priority — so where the rest of the file parses, as it does here, the message would be discarded with the attempt. The literal reaches the checker with its body intact, which is all this question needs.

D4 — The way out is read off the escape.

A malformed \x or \u{…} was meant as a character, so the help names the form it should have had: \u{80}, \u{1F600}. Anything else is a backslash that was meant literally, and the help writes the literal back with its backslashes doubled.

C.2’s rule, applied rather than quoted: a way out that cannot be taken is not one, and telling someone who wrote \x80 to double the backslash would be one.

3. Consequences

4. What this does not decide

This site is open source. Improve this page.