ADR-063: A Constant Written Only in Literals Takes the First Type That Holds It, and a Name Pins What Its Value Took

Status: Superseded by ADR-285 — its D1 is D22, D2 is D26 (as narrowed by ADR-249 D3), D3 is D28 there Extends: ADR-060 D2, which gave that to a literal and did not reach a sum Related: ADR-043 D5 (the constant fold this uses, and the two messages it already decides), ADR-028 (the emitter has no types, which is what shapes D3), ADR-048 (the numeric surface: two integer types a program may write)

1. Context

ADR-060 D2 gives an unconstrained literal the first type that holds it, so let big = 3000000000 compiles. Its D3 says why that needs no analysis, and the argument is exact — and it is about a literal. A sum is not a literal, and the rule did not reach it. Built and run:

let b = 3000000000 + 1                        →  3000000001
let c = 2000000000 + 2000000000               →  refused
println(f"{2000000000 + 2000000000}")         →  refused
let a = 2000000000 … a + a                    →  refused

And what “refused” means is the point. Not a message of ours:

error: src/main.nika:2:5: this arithmetic operation will overflow
   2 |     let c = 2000000000 + 2000000000

That is the backend’s own lint, in Rust’s words, on the Nikaia line that wrote it — Part III C.1’s class, which this compiler calls a bug of its own. ADR-043 D5 built NK1116 and NK1118 for exactly that reason: what it takes back is the message. The sum with nothing beside it was the case those two did not cover, because neither a type beside it nor an operand’s declaration said what it should fit.

Two passes already exist, and the fold already runs in the first one. constant_of folds in an i128 with every step checked_, follows an immutable let to its value, knows the five operators and the negation, and records the type an operand’s declaration pinned. At a bare let it is called twice already — once to ask whether the value fits, once to carry it to the next name. The folded value is in hand at the place where the decision falls.

The compile-time interpreter is not what answers this. It is a 143-line stub that prints Bind: x = <evaluated> and has no store for values, and what a program may run at compile time is itself an open question. Hanging a language rule on it would block a small answer on a large undecided one.

2. Decisions

D1 — A constant expression written only in literals takes the first type that holds it.

ADR-060’s rule, one step wider: the unit is the expression and not the token. i32 if it holds the value, else i64; a value neither holds is refused in this language’s words rather than widened, because there is no third type to move to.

Decided once, on the outermost expression that folds, and reaching every literal under it. Rust computes in the type of the operands, so one suffix on one half would be two types meeting across a +. What is emitted is 2000000000i64 + 2000000000i64.

Not where the position decides the type. A sequence index and a repeat count are usize, and both are deliberately left bare for Rust’s own inference — index::at(0) and count::of(2) have nothing to infer from (ADR-048, ADR-054). A suffix written into one would pin the type the position is there to give, and what came back would be a message about the generated file.

A value that fits is untouched, which is what keeps every program that compiles today compiling: a suffix pins what a use is supposed to answer (Part I 2.4).

D2 — A name pins the type its own value took.

let a = 2000000000 is an i32 — the first type that holds it — so a + a is an i32 sum, and it does not fit. It is refused, and the refusal is ours: “this comes to 4000000000, which does not fit in an i32“, with the way out in one word, let a: i64 = 2000000000.

Why it is not widened. Widening a + a would mean widening a’s declaration — a walk from a use back to a binding. That is exactly the analysis ADR-060 D3 was cheap for not needing, and it is the expensive half in every form it takes. It is also the answer every language with two integer widths gives: arithmetic happens in the type of its operands.

So the rule reaches exactly as far as the expression does, and a name is a step outside it. That is predictable from the page: a number takes the first type that holds it, and a name is a number that already took one.

D3 — One fold, and the lookup is what separates the two callers.

The arithmetic moves to crate::fold, and what a name is worth becomes a parameter. The checker passes what it knows; the emitter, which has no scope and no types (ADR-028), passes nothing_is_known — and thereby gets exactly the subset of expressions that can be answered without looking anything up, which is D1’s subset and no more.

It is a function and not an absent argument so the difference is visible at the call: the emitter is not skipping the lookup, it has none. Two constant folds in one compiler would be one fold and one liability.

3. Consequences

The class is closed. Every constant integer expression now gets an answer from this compiler: widened where an i64 holds it, refused in this language’s words where no type does, and arithmetic in the pinned type where a declaration or a name says so. No arrangement of them reaches rustc’s overflow lint any more.

What it costs is one sentence of ADR-060 D3, restated honestly. The emitter reads an expression where it read a token — but an expression containing nothing but tokens. It still has no types, consults no scope and asks no use site. “No analysis” becomes “no lookup”, which was always the expensive half and is the half that is still not there.

And the emitter already had this distinction. It asks “is this written only in literals?” for an index, for precisely the reason that the answer is then fixed without types. D1 is the second user of that idea rather than the first.

4. What was weighed

5. What is built

All three, and the four programs in §1 now answer this way:

let c = 2000000000 + 2000000000        →  4000000000
println(f"{2000000000 + 2000000000}")  →  4000000000
let a = 2000000000 … a + a             →  NK1116, "this comes to 4000000000,
                                          which does not fit in an `i32`"
9000000000000000000 + 9000000000000000000
                                       →  NK1116, "does not fit in an `i64`"

crate::fold holds the arithmetic and wants_widening; check::Checker::constant_of is the lookup over locals; the emitter folds at the outermost Expr::Binary, carries the answer inward on Flow::widen, and refuses to start one under Flow::inferred — an index and a count. let small = 2 + 3 and let small = 42 are written exactly as before.

This site is open source. Improve this page.