ADR-060: A Literal No Use Constrains Takes the First Type That Holds It

Status: Superseded by ADR-285 — its D1-D2 are D20-D21 there; its D3 is not carried, displaced by ADR-249 D1-D3, which are D23-D25 there Related: ADR-043 D5 (NK1116, the out-of-range constant beside a type, and the constant fold both share), ADR-048 D1 (a length is an i64), Part I 2.4 (the rule this completes)

1. Context

let big = 3000000000

is refused, in the backend’s words, about a type the program never wrote: “literal out of range for i32“. It looks like a mistake and is not one — and that is the whole reason to change it. Nothing about the line is wrong; the number is a number and the program has said nothing about its size.

The rule it runs into is Part I 2.4’s: “a number takes the type its use asks for, and i32 where nothing asks”. The first half is Rust’s inference, inherited and working — measured:

let small = 42
println(f"{wide(small)}")   // fn wide(n: i64) -> i64

compiles and runs, because wide’s parameter decides. The second half is Rust’s default, inherited without being chosen, and it is the half that refuses big.

What the field does. Most languages let this line work, in two families. Go and Swift give the default a size big enough — int is the machine word — so 3000000000 fits it. C, C# and Kotlin let the literal take the first type from a fixed list that holds it, so 42 is a 32-bit type and 3000000000 a 64-bit one. Java refuses without an L, and Rust refuses by default. This language is in the smaller camp by inheritance rather than by choice.

Kotlin’s rule, taken whole, would cost the first half. Kotlin has no backward inference, so its literal settles where it stands — and let small = 42 followed by wide(small) stops compiling. Part I 2.4’s second example is exactly that program.

2. Decisions

D1 — Where a use constrains the literal, the use decides. Unchanged.

Part I 2.4’s first half stays, and so does every program that rests on it.

D2 — Where nothing constrains it, the literal takes the first type that holds it: i32, else i64.

let count = 42            // i32, as today
let big   = 3000000000    // i64, because i32 does not hold it

Kotlin’s rule as a fallback rather than as the rule: it applies only after the use has said nothing.

D3 — And it needs no analysis, which is why it is this and not Kotlin’s.

The reason to fear D2 was the walk it seems to need — deciding whether anything constrains the literal, over a binding’s whole scope, which nothing else in this compiler needs. It does not need one.

A literal that does not hold in an i32 can only be an i64. The types a program may write are i32, i64, u8 and f64 (ADR-048); an integer literal is never a floating-point one, u8 is narrower still, and there is no wider integer. So there is no second answer for a use to ask for, and nothing to find out: if the value fits an i32, change nothing and the use decides as it does today; if it does not, emit it as an i64.

One rule in the emitter, keyed on a fold this compiler already performs — the same one NK1116 reads, in an i128, over a literal, a folded immutable let, + - * / % and a negation (ADR-043 D5). The negation matters and is not an edge case to discover later: -2147483648 holds in an i32 although 2147483648 does not, so the fold is asked about the value and never about the digits.

3. Consequences

Nothing that compiles today stops compiling. A literal that fits an i32 is emitted exactly as now, so the use keeps deciding and both of Part I 2.4’s examples stay legal. That is the property (a) — “an un-annotated literal is an i32, full stop” — could not have.

Where a use does ask for an i32 and the value does not fit, the message moves to the use. NK1116 already takes that case wherever a type stands beside the literal — an annotated let, a return against a declared result, an argument whose parameter says what it takes — so what is left over is the case where the type is pinned indirectly, and there the backend speaks. Narrower than today and in the same direction.

A literal no type holds at all — beyond an i64 — is not this record’s and is still the backend’s to refuse. It is a different message about a different mistake, and this one does not make it better or worse.

4. What this does not decide

5. What is built

Both decisions, and it was the one rule in the emitter D3 said it would be: emit::integer_literal writes a literal an i32 does not hold as an i64, and writes every other one exactly as it wrote it before. let big = 3000000000 compiles and prints, and let small = 42 handed to a parameter taking an i64 still compiles — D1 is untouched because the literal that fits carries no type of its own, so the use still decides.

The negation is folded at the Unary arm rather than inside that rule, which is D3’s sentence about the value and not the digits: -2147483648 reaches the emitter as a negation of 2147483648, one past i32::MAX, and a rule reading the literal alone would widen the one number where widening is wrong. One further out, -2147483649, is an i64 and keeps its sign.

Nothing else was needed, which is what D3 predicted: no use-site walk, no second inference, and no change to a literal that fits. Three tests in tests/literals.rs hold the three cases.

This site is open source. Improve this page.