+ Over Text Is a Call, and an Expression Gets a Span to Key It ByStatus: Accepted Date: September 15, 2026 Related: ADR-285 D1 (the trait that made one index rule work everywhere — this is that arrangement a third time), ADR-285 D5 (the overflow abort, which is what keeps arithmetic out of it), ADR-288 (the channel the checker hands answers over, which this gives its first expression-level key), ADR-296 D17 (the emitter knows no types)
Lowered as written, three of the four ways a program can join text are refused
by the language below. With s and s2 being String:
| written | what the language below said |
|---|---|
"a" + "b" |
error[E0369]: cannot add &str to &str |
"a" + s |
error[E0369]: cannot add String to &str |
s + "b" |
compiled |
s + s2 |
error[E0308]: mismatched types |
Rust’s own rule — String + &str and nothing else — would reach the user unchanged,
about a file nobody wrote, which is Part III C.1’s
class. This language’s + says nothing about which side is owned and should not
have to.
And a checker that types a concatenation as a view is worse. If
"User: " + self.username came to a view, a function declaring -> String
would be refused as NK1104 — a false refusal, and
Part III C.4 says this compiler never refuses a
correct program. Part I 4.7 writes that line as its own example.
Expr::Binary carries a span, and it is the operator’s.The first expression-level span in this AST, because an answer has to be keyed by which + rather than by the statement
around it — a + b + c is two of them, and every other answer in ADR-288’s
channel is keyed by a statement because every other answer is about one thing per
statement.
The tail’s span — the operator and what stands to its right — rather than the whole expression’s, because that is what the grammar has in hand where the node is built, and uniqueness is the only property a key needs.
One variant and not every expression, because one variant needed it. A field nobody reads is a field that goes stale, and the next one that needs a span will know where to put it.
+ where either side is text lowers to nikaia_std::concat::plus, and its result is a String.A trait with an impl per shape, and the language below picks — which is ADR-285 D1’s own sentence a third time: this emitter does not know types, and a rule applied everywhere cannot be applied to the wrong thing.
What that buys is not merely working. Each shape keeps the lowering that
suits it. The obvious single rule — lower every concatenation to a format! —
measured six times slower on the one form Rust already accepts (250 ms
against 42 ms for three million concatenations); the trait costs that form
nothing. The rule is the table of four cases, written once in std instead of
four times in the emitter.
And the result is a String. That is a decision and not an inference: a
concatenation makes a new value, so there is nothing else it could be, and saying
it is a view is what would make -> String a false refusal.
+ does not move, and that is what makes D2 narrow.A trait over + would take numbers too, and a number’s + may not leave this
language’s own crates. ADR-285 D5 turns overflow-checks on per
Nikaia crate while the profile turns them off, because a foreign crate’s hash
function wraps on purpose — and nikaia_std is on the foreign side of that
split. Measured, with the same profile shape:
a + b in the checked crate → aborts
the same a + b through an #[inline] helper → -9223372036854775808, silently
in the unchecked crate
Inlining does not carry the check across: it is decided where the code is
written. So any arithmetic moved into a std helper silently loses the abort
this language promises. index::at escapes it only because its conversion is an
explicit try_from, and index::set because it does no arithmetic at all. That
is a trap for the whole family of these decisions and it is written down here
rather than left to be met again.
Part I 4.7’s "User: " + self.username is accepted by the checker, and
Part I 6’s println(prefix + "System started") compiles.
All four shapes are programs, and the one Rust accepts directly keeps its speed and what it allocates.
The channel has an expression-level key, which the next answer about a sub-expression can use instead of inventing a positional one.
+= over text. s += "x" is a compound assignment and goes through a
different arm; it is not read here.String and ref String, and deliberately not a T?:
maybe + "x" is a member reached off a nullable, which Part I 2.3 answers and
this must not quietly paper over.+ where neither side is known. It still says nothing and is emitted as
it was written, which is what stops a Vec’s length from being silently
turned into a string.