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)
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.
Part I 2.4’s first half stays, and so does every program that rests on 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.
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.
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.
i64 outright, as Go’s and Swift’s
is. That is a larger change with a real argument behind it —
ADR-048 D1 already made a length and an index i64, so the most
common integer in a program is one — and it costs Part I 2.4 its i32 sentence
and every un-annotated number four more bytes. Not needed for the line this
record is about, and the two do not conflict: D2 is what a i64-default would
also have to say about a literal too large even for that.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.