f Goes on the String, Because the Mark Belongs on the ConstructStatus: Superseded by ADR-309 — its D1, D2, D3, D4, D5 are D1, D4, D2, D3, D15 there Supersedes: ADR-032 D5 — a brace deciding a literal’s type Related: ADR-032 (a hole is code), ADR-024 D5 (what the checker says a literal’s type is), ADR-017 (templates, which keep their bare braces)
Every string literal in Nikaia was a template. "hello {name}" interpolated, and so did
"{ margin: 0 }", "\d{3}" and every line of JSON a program ever printed — which is why
examples/json/src/main.nika, a program whose entire job is to emit braces, was written like this:
print("{{}}") // prints {}
print("{{\n") // prints {
print("\n{pad}}}") // one hole and one brace, side by side
and examples/1brc.nika ended on
println("{{{body}}}") // three braces open. Two of them are tax.
Measured over the corpus — 45 .nika files, 212 string literals:
| holds a hole | 56 | 26 % |
| plain text, unaffected | 154 | 72 % |
only {{ escapes — paying for a feature it does not use |
2 |
Three quarters of the literals in this repository were templates that interpolated nothing, and every string that wanted a brace had to double it whether or not anything in the file interpolated at all.
The distinction already existed everywhere except where it is written:
let plain = "nur text" → let plain = "nur text";
let woven = "text der {n} interpoliert" → let woven = format!("text der {} interpoliert", n);
The emitter branches. The type branches — ADR-024 D5 made "a" a view of static text and
"a {b}" a String, so adding a brace to a piece of text changed its type. And since
ADR-032, every analysis branches: literal_expressions has to open every string
literal, because only its contents say whether there is code in it.
Three places decide the same question by scanning text, and the one party who cannot see the answer is the person writing the string.
The mark belongs on the construct, and Nikaia already knows that. dsl html { <p>{x}</p> } eod
is marked; its holes are unambiguous because the construct said there would be holes. A grammar is
marked. A spawn is marked. The bare string literal is the one place the rule was forgotten — and
ADR-017 inherited that gap two ADRs later, which is how ADR-032’s blindness came to
cover templates as well.
| Language | Mark | A brace in plain text |
|---|---|---|
| Python | f"…" |
"{" just works |
| C# | $"…" |
"{" just works |
| Scala | s"…" |
"{" just works |
| JavaScript | a different delimiter (backtick) | "{" just works |
| Ruby | "#{}" vs '…' |
'{' just works |
| Swift | \(x) — an escape nobody types by accident |
"{" just works |
| Rust | the macro is the mark, not the literal | doubled only inside one |
| Kotlin | none — "$x" everywhere |
\$ required |
| Nikaia, before this | none — {} everywhere |
{{ always required |
Nikaia stood with Kotlin, and worse off than Kotlin: $ is rare in prose, { is not. Rust is the
instructive one — the mark sits on the construct, which is exactly what dsl html does here.
(a) Leave it, and teach {{. What the corpus says about that is §1: the two most ordinary
programs in the repository were the two hardest to read.
(b) A different delimiter for plain text (Ruby’s '…', JavaScript’s backtick). Rejected: it
marks the common case — three quarters of all literals — and leaves the rare, code-bearing one
looking ordinary. The mark should be on the thing that surprises.
(c) f"…" marks the interpolated string; "…" is inert. Chosen. It marks 26 % rather than
72 %, it is the majority design in languages people arrive from, and it gives the type, the
lowering and every analysis a syntactic answer to a question they were each deriving from text.
f"…" interpolates. "…" is text, and a brace is a brace.{{ and }} remain the escapes inside an f"…", where there is something to escape from. A
plain string needs no escape, so "{" is one brace and "{{" is two.
print is a macro below, and a brace in its format string is a hole of Rust’s own. The doubling is
a transcription detail (rust_format_escape), not a rule anybody writes — and an escape is copied
whole, \u{0041} included, or Rust’s lexer would be handed a \u with nothing after it.
"…" is a view of static text; f"…" is a String, hole or no hole. This retires
ADR-032 D5, which was the same fact stated as a consequence of the text: under that
rule, adding a brace to a message silently changed its type.
f and the quote are one token, and a template keeps its bare braces.f"x" interpolates; f "x" is a variable beside a string. Whitespace never decides what a program
means. And dsl html { … } needs no f on anything: the construct is the mark, which is the rule
in §2 read a second time.
f"…" is not reachable where a constant is required — a Kap 5.1 default, a match pattern, a
grammar’s literal. The grammar says so rather than a later pass discovering it.
A string written before this looks exactly like one that meant its braces, and the change is
invisible at compile time: println("hallo {name}") still compiles and now prints {name}. Part
III C.1 calls a silent change of meaning a bug in this compiler.
So NK1111, the first thing this checker has ever warned about rather than refused, in the two
shapes the change has: a plain string holding a brace group, and a plain string holding a doubled
brace. The help is the fixed string, ready to paste.
Why not an error. "{ margin: 0 }" is correct CSS and "\d{3}" a correct regular expression.
Refusing either would break the property the whole checker rests on — it never rejects a program
that is right (Part III C.4). So the test is narrow: the braces must hold something that parses as
an expression and resolves to something that is actually here — a variable in scope, or a
function some ledger knows. A name nobody declared is text, and text says nothing.
Why not permanent. After the corpus has moved, NK1111 fires only on code written against a
language that no longer exists, and a warning nobody can trigger is a warning nobody reads. It is
removed in the release after this one, together with Severity::Warning if nothing else has
claimed it by then.
examples/json/src/main.nika reads like what it is. print("{}") prints {}. That single line is the
clearest statement of what this buys.
The 56 literals were migrated mechanically and the corpus is clean — no file in this repository
raises NK1111. The transformation is meaning-preserving by construction: a literal with a real
hole gained an f and kept its text; a plain literal with {{ had its braces undoubled; everything
else was untouched. Comments, grammar patterns and template bodies were excluded, and the three
places where an automated pass got that wrong were caught by the test suite rather than by review.
Three quarters of all literals are now inert at a glance, which is an analysis property and not
only a reading one: sync, throws and provenance have to look inside a literal only where
somebody wrote an f. ADR-032 made every analysis open every string; this means they open the ones
that said there was something to find.
A diagnostic got better on the way. Folding f"…" into str_lit as a second alternative
silently dropped ` " ` from the parser’s “also possible here” list — a rule whose body is one
sequence reports what it can start with, and one built from alternatives did not. f_str_lit is
therefore its own rule, and the list now carries both ` " ` and ` f" `.
What this does not do: holes are still parsed in the emitter rather than living in the AST.
ADR-032 §3 said why that waits, and the f does not change the argument — it makes the eventual
change smaller, because the variant that would carry the parts already exists.