int or One of Five Fixed Widths, and Stops the Program Where It Does Not Fit; a Number Without an Annotation Is Typed by Its UsesStatus: Accepted
Date: October 3, 2026
History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached
Answers: what an arithmetic overflow does, which Part III A.2 did not say;
what len() hands back and which numeric types a program may write; what an
as may name, and how a std parameter counted in usize is written; what
type a literal no use constrains takes, and a constant sum
(#142); how a number
may be spelled (#135);
unsigned 64- and 32-bit integers and bit operators, which fixed.rs’s FNV and
CHD ask for; and who types an unannotated number when the language below
cannot (#176,
#177); where one
integer type takes another without as
(#439)
Related: ADR-300 (every abort names the Nikaia line);
ADR-002 D1 (the generated Cargo project D8 writes into);
ADR-021 D5 and ADR-037 D4 (the cache key, which D8
adds nothing to); ADR-292 D7 (a semantic default that can be
switched off - the precedent D8 does not follow);
ADR-286 D15 and D22 (a Nikaia dependency is
on the program’s side of D8’s split); ADR-081 D3 (the check is
on per Nikaia crate, so a number’s + stays in this language’s crates);
ADR-296 D17 and ADR-288 (the emitter knows no types,
which is why conversions are chosen by name and the checker hands answers
over); ADR-024 (the ledger the checker reads);
ADR-288 D11 (an entry is written and reviewed, not copied from Rust);
ADR-135 D2 (an empty list typed by its use, the walk D24 reuses);
ADR-311 and ADR-290 (the table fixed.nika
builds, and the route by which Rust moves into Nikaia);
ADR-306 (an overflow check a proof shows unneeded is not
emitted - D5’s meaning unchanged); ADR-258 (the four weights)
What is an integer in this language? Which types a program writes; what happens when a result, a conversion or a literal does not fit; how the bits are reached; how a number is spelled; and what type a number gets when the program writes none.
Without an answer, each part reaches the user in rustc’s words or as two
programs: an overflow that aborts in a debug build and wraps in --release; a
len() in usize that puts an as i64 in every loop; an as that makes
-3 as usize 18,446,744,073,709,551,613 in a language where a conversion that
does not fit aborts; let big = 3000000000 refused as literal out of range
for i32, about a type the program never wrote; 1_000_000 read as 1
beside an undeclared name; fixed.rs, which cannot be written without ^,
>> and a u64; and let i = 0 then v[i].push(x), which leaves rustc no
type to settle on.
Each answer leans on the others - the literal rule on the type list, the conversion rule on the overflow rule, the use-site typing on all of them - so they are decided together.
| Question | Language / tool | Its answer | See |
|---|---|---|---|
| Overflow | Rust | aborts in a debug build, wraps in --release |
measured |
| Wrapping on purpose | Swift | &+, &* operators |
D6 |
| A length | Go, Java, C#, Swift | a signed integer; Swift on the ground that unsigned arithmetic causes more bugs than it prevents | D1 |
| Rust | usize, and the conversions it demands are a well-known papercut |
D1 | |
| A byte | Go, Rust, C#, Swift | an unsigned byte type | D2 |
| Java | signed only; a masking dance in every byte-handling program, >>>, remainderUnsigned |
D2 | |
| An unconstrained literal | Go, Swift | the default is the machine word, so 3000000000 fits |
— |
| C, C#, Kotlin | the first type from a fixed list that holds it | — | |
| Java, Rust | refused (Java without an L) |
— | |
| Kotlin | no backward inference, so the literal settles where it stands | — | |
| Bit operator precedence | C | & below ==: a & mask == 0 is a & (mask == 0) |
D10 |
| Rust, Go | shifts, then &, ^, \|, then comparisons |
D10 | |
| Number spelling | Rust and its neighbours | 1_000, 0xFF, 0b1010, 0o17; 0xFF is 255 |
— |
What does not transfer. Rust’s as is defined to truncate and has no
failure case to switch on - 5000000000 as i32 is 705032704 under
-C overflow-checks=on and -C debug-assertions=on alike (measured) - so a conversion cannot ride on the overflow switch. And this
language promises one meaning in every build (Part I 1.2), which is the
promise Rust’s debug/release split breaks.
Every option rejected, with the reason it lost. The choices themselves are §4.
| Option | Example | Why it lost | See |
|---|---|---|---|
| Wrapping operators | a &+ b |
& is borrowing here, and a &+ b collides with &a to the eye; if saturating earns no symbol, neither does wrapping |
D6 |
| A checked helper per arithmetic operation | add(a, b) in the generated Rust |
the same arithmetic, but nobody comparing benchmarks believes a + that became a call |
D8 |
| An overflow check a build can turn off | [build] overflow = off |
it changes what a program computes, which no build option may (Part I 1.2) - unlike ADR-292 D7’s ordering |
— |
A length as usize |
xs.len() as i64 in every loop |
ceremony that cannot fail (a usize exceeds an i64 only past 9,223,372,036,854,775,807 elements - eight exabytes at one byte each) against one abort that already exists |
D1 |
| A signed byte only | b & 0xff at each step |
Java’s masking dance | D2 |
as naming any type the language below has |
n as u128, -3 as usize |
an escape hatch nobody decided, and the one conversion that does not abort | D14 |
| A negative count reported as an index | index out of bounds: -1 for repeat(-1) |
it names an access the program does not make | D15 |
| Kotlin’s first-type rule as the whole rule | let small = 42 then wide(small) refused |
it costs backward inference, and Part I 2.4’s own example | — |
| Refuse a constant sum instead of widening it | 3000000000 + 1 refused |
breaks a program that compiles and that the page calls correct | — |
| Let an overflowing sum widen the name it reads | let a = 2000000000 widened because of a + a |
a value never decides a type through an operation; the uses do (D24-D26) | D25, D26 |
| A second constant fold in the emitter | literals only, no lookup | it works, and two folds drift | — |
| A hexadecimal literal carries its width | 0xFF a u8, 0x0FF a u16 |
a type that depends on how many zeroes somebody typed | D17 |
| Named bit operations | hash.bit_xor(byte) |
says exactly what ^ says; a wrapping name says this may overflow, on purpose, which an operator does not |
D9 |
| Unsigned questions on signed types | >>>, remainderUnsigned |
Java’s cost, decided against once already for the byte | — |
| No parenthesis rule for bits beside a comparison | a & mask == 0 |
the precedence is right, and a reader who learned C reads it the other way; two characters end the question | D10 |
| An index pinned at its one site | index::at::<i64>(i) |
mends #177 there and collides with the first use that asks for anything else | — |
| An index as a use like any other | let i = 0 handed to an i32 and used as an index: a conflict |
the language below converts either | — |
int whose width follows the caller |
fn add(a: int) -> int an u8 for one caller, an i64 for another |
the width changes what a function computes - where it aborts, whether it takes a negative number, which bits a hash gives - so the signature would no longer say what the function does; a function that is meant for several widths says so with a type parameter | D36 |
int as wide as the machine |
C’s int, Go’s and Swift’s Int |
one program, other results on another machine (Part I 1.2) | D36 |
int as a second spelling of i64 |
int and i64 the same promise |
two names for one promise; the name is worth having only because it promises less | D37 |
| A narrower default type to keep room for optimisation | let n = 0 an i32 so that it may take 32-bit lanes |
with an aborting overflow, a narrower type changes what the program computes; the room belongs in the representation | D37 |
| Fixed widths only at the C and Rust interfaces, a warning elsewhere | let h: u64 warned |
about 160 widths outside an extern are deliberate - spans, hashes, a solver’s layout, bytes - and no language surveyed warns about a written width |
D39 |
The weights pulled apart in one place. Reading every use of a number (D23-D25) is work the compiler could have left to the language below; it is paid so that the developer writes no annotation that the uses already answer (ADR-258 D2), and it costs one list per open number, read once per function body - the cost ADR-135 D2 already pays for an empty list.
The integer types
i64, and so is an index.Every std entry that hands back a length or a count - Vec::len,
String::len, a map’s and a set’s len, Seq::count - returns an i64, and
an index takes one. The program writes no conversion in either direction; the
compiler emits both. Out of a length the conversion cannot fail. Into an index
it fails only for a negative number, and that failure reports as an access
out of bounds (“the index is -1”), because it is one - not as a failed
conversion the program never wrote. An index written only in literals is left
bare for the language below’s own inference.
i64, i32, u8, u64 and u32; no machine-width type is among them.u8 is named because fs::read hands one back; u64 and u32 because a
hash over bytes asks for them (D3). usize and isize stay out: a length is an i64 (D1), and nothing a
program writes needs a machine-width type. f64 is the floating-point type.
Beside the five fixed widths stands int (D36): the values of an i64, with
no width promised.
u64 and u32 have every rule the other integers have, and no integer value is lost silently.A result that does not fit aborts (D5); wrapping_, saturating_ and
truncating_ are their names (D6, D13). A wider type takes a narrower one
without as wherever no value can be lost (D32); every other conversion is
as, checked where it may not fit (D12). u64 + i64 has no type that holds
both and is refused where both sides are known (NK1199), the message naming
both types and the help handing over the as.
Amended October 6, 2026 (#439).
text.bytes() is the text’s UTF-8, one u8 at a time.A sequence, as chars() is, so a hash over it is the hash other languages
compute over the same text.
Overflow
The same program computes the same thing under every build, and a benchmark measures what is shipped. Part III A.2’s list of unrecoverable errors carries the overflow beside a division by zero. A check the compiler proves unneeded is not emitted (ADR-306), which changes no result.
h.wrapping_mul(31).wrapping_add(c), level.saturating_add(increase). The
names are Rust’s, one for one, so the emitted code stays idiomatic.
wrapping_add, _sub, _mul, _div, _neg, _abs, _shl, _shr, and
saturating_ wherever Rust has it. -x and x.abs() on a signed type’s
smallest value overflow, and so does that value divided by -1; a shift by
the width or more is an abort of the same category (D11).
The emitted code says a + b; the generated Cargo project turns
overflow-checks on for every crate of this language - the program and every
Nikaia dependency - and off for foreign packages, whose hash functions wrap
on purpose. The known limit: whoever rebuilds the
generated project with other settings gets other numbers, as whoever edits
the generated code by hand does.
Bits
&, |, ^, << and >> are operators, and ! on an integer flips every bit.On integers only, both sides one type, a shift’s count excepted (any
integer). The compound forms &=, |=, ^=, <<=, >>= stand beside
+=. On a bool they are refused (NK1198), naming &&, || or !=: a
& that does not short-circuit would be a second spelling of one thing.
* / %, + -, << >>, &, ^, |, the comparisons, && and || -
Rust’s and Go’s order. A bit operator written beside a comparison without
parentheses is NK1197, the help handing over (a & mask) == 0.
>> fills with zeros on an unsigned type and with the sign on a signed one.
Conversions
as only where it is shown to fit.A conversion that can lose a value - a wider integer to a narrower one, a
signed one to an unsigned one or back, an f64 to an integer or to an f32 -
is written as only where the compiler shows it fits:
NK1116 (D29);as is NK1241. The program says by name what it means:
checked_<T>() hands back a T?, null where the value does not fit, and
the program handles it; truncating_<T>() is the backend’s conversion (D13).Which conversions narrow is the checker’s answer, since as i32 narrows or
widens by what it is given, and the list is written as the conversions that
always fit, so an unforeseen pair is treated as narrowing.
Why not an abort. A value lost in a conversion is a failure the program cannot see where it is written. Where a proof can find it, it is found while building; where none can, the program decides, at the line, between handling it and keeping what the backend makes of it. A stop at run time is what is left when neither was asked for, and a conversion is a place where asking costs one name. Arithmetic overflow (D5) is not changed by this: it has no spelling a program writes in its place on every operator.
Changed October 8, 2026 (#552): a narrowing as aborted where it did not fit.
Changed by D42: truncating_ converts out of a wider type only; the same
width with the other sign is bits_as_.
truncating_i32, truncating_i64, truncating_u32 and truncating_u64
(and truncating_u8, D41) lower to Rust’s as, from the sources a program can narrow out of: a wider
integer and an f64. There is no name out of a machine-width type, because
no program can hold one (D1, D2). Out of an f64 the backend’s as makes
1e20 as i32 2147483647 and NaN 0; the name is how a program says that
is what it wants. checked_i32, checked_i64, checked_u32, checked_u64
and checked_f32 hand back null where the value does not fit, out of an
integer and out of an f64, where a value that is not a number does not fit
an integer. An integer to an f64 loses digits at large
values without overflowing (9007199254740993 comes back 9007199254740992);
every language does this, there is no sensible point to stop, and it is a
stated limit, not a check. The f64 helper goes through i128, because
i64::MAX as f64 rounds up and the obvious comparison accepts one value
too many.
as names a type this language offers, and nothing else.i32, i64, u8, u32, u64, f64, bool, char, String and its
view. Anything else - n as usize, n as u128 - is NK1122, naming the
type and saying that the compiler writes the machine’s conversions itself.
This list answers which types a program may write; the narrower list of D12
answers which conversions can lose something.
std parameter the language below counts in usize is written as an i64, and the conversion is emitted.str::repeat(&str, n: i64), and the emitter writes nikaia_std::count::of
around the argument, so " ".repeat(indent) needs no conversion. A negative
count aborts with a message of its own - “a count cannot be negative: the
count is -1” - because it is not an access out of bounds. The list is chosen
by name, because it is a fact about Rust’s library and the emitter knows no
types; a test keeps the ledger and the emitter from drifting. A count written
only in literals is left bare, as an index is. No ledger signature names
usize.
How a number is spelled
1_000_000, 0xFF, 0b1010 or 0o17.Changed by ADR-322: a decimal whole number may end in a scale (1G, 4Gi), and an exponent is written e.
An underscore stands between digits and nowhere else. The prefixes are
lower-case; hexadecimal digits either case. A float takes the separator
(1_000.5) and no prefix. A digit the radix does not have (0b1210) is
refused. A - written directly against the digits is part of the literal, so
-9223372036854775808 is i64::MIN and -0xFF is -255; - 5 and -x are
the unary operator.
0xFF is 255 and takes its type exactly as 255 does (D20-D25):
let mask = 0xFF is an i32, let mask: u8 = 0xFF a u8.
1_000 is 1000 to the checker, the ledger, the fold, and a diagnostic’s
text.
u64 holds.Up to 18446744073709551615. A literal above i64::MAX is a u64’s and
nothing else’s; one wider than a u64 is refused by name. With no u64
asking, it is refused against i64 (NK1116), the help naming u64.
What type a number takes
Part I 2.4’s first half: let small = 42 handed to fn wide(n: i64) is an
i64. This compiler, not the language below, reads the use (D24).
i32, else i64.let count = 42 is an i32, let big = 3000000000 an i64. The question is
about the value: -2147483648 is an i32. A fallback, not Kotlin’s whole
rule, so backward inference survives.
The unit is the expression, decided once on the outermost expression that
folds and reaching every literal under it: 2000000000 + 2000000000 is
2000000000i64 + 2000000000i64. A value no type holds is refused in this
language’s words. A sequence index and a repeat count are left bare (D1, D15).
A value that fits is untouched.
let without an annotation whose value is only numbers is an open number.A literal, a sum of literals, or an operation over other open numbers. One
operation has one type, so a + b, a < b and c = a + b make one number of
all of them. A shift’s count joins nothing.
A use asks where a type stands beside the number: a parameter, a return or
last expression, an annotated let, a field, a typed operand or comparison
(i < xs.len() asks for an i64), an assignment either way. An index asks
for an i64 only where nothing else asks - a preference, never a conflict.
Two uses asking two types are NK1200, naming both, with the annotation to
write.
The first type that holds all of its values - its own and every one assigned
to it - i32, else i64 (D21 over every value): let mut n = 1 then
n = 3000000000 is an i64. A value the decided type does not hold is
NK1116 at that value, with a note that the type came from the uses.
What an operation over a name comes to is held to the name’s type and never
decides it. let a = 2000000000 then let c = a + a is refused where nothing
asks (“this comes to 4000000000, which does not fit in an i32“, the way
out let a: i64), and is an i64 where a use asks for one.
let.let i: i64 = 0;, so the language below infers nothing this compiler
decided; a literal that would get an i64 suffix carries the decided type’s.
A number no use asked for is written bare.
crate::fold holds the arithmetic - in an i128, every step checked, over a
literal, an immutable let whose value folded, + - * / % and a negation -
and what a name is worth is a parameter. The checker passes its scope; the
emitter, which has none, passes nothing_is_known, and gets exactly D22’s
subset.
Refused at compile time
let x: i32 = 3000000000 is NK1116, at an annotated let, a return
against a declared result, an argument whose parameter says what it takes,
and where an operand’s declaration pins the type (let b = a + 1). A fold
that stops - a mut local, a parameter, a for binding, a cast - claims
nothing, and an expression that does not fold is never refused.
In an f"…" hole, a condition, a list, a receiver - once, at the innermost
operation that overflows. An operation over an open number is measured once
that number’s type is decided.
NK1118, by the fold D29 uses. Every other division by zero stays where Part
III A.2 has it: unrecoverable at run time, naming the Nikaia line. A divisor
the fold cannot evaluate is one nothing is claimed about.
Added October 6, 2026 (#439).
fn area(x: i32, y: i32) -> i64 {
x * y // computed in i64
}
let n: u64 = xs.len() // a length is never negative
let total = count + offset // u32 + i64 is an i64
if small < big { … } // u64 against i64, compared as numbers
1. The common type. Two integer types have a common type when one type holds every value of both:
u8 |
u32 |
u64 |
i32 |
i64 |
|
|---|---|---|---|---|---|
u8 |
u8 |
u32 |
u64 |
i32 |
i64 |
u32 |
u32 |
u64 |
i64 |
i64 |
|
u64 |
u64 |
none | none | ||
i32 |
i32 |
i64 |
|||
i64 |
i64 |
A typed slot - a let with an annotation, an argument for a parameter of
a stated type, a return or a body’s last expression, a field in a struct
literal, an assignment, an element of a list whose element type is stated -
takes a value of a narrower type that its type holds. An operator whose
two sides are two integer types computes in their common type, and a list
literal whose elements are two integer types holds their common type,
whichever comes first. Where there is none, NK1199 and NK1154 stand.
2. A comparison of two integer types compares the numbers, for every
pair, u64 against i64 included.
3. Inference first, widening after. A number without an annotation
(D23-D25) and a generic parameter are typed by their uses, each use asking
for exactly its type, as before. Widening is a check between two types already
decided and never one of a use’s answers: let n = 5 handed to an i64 and
to a u32 is NK1200, and pick(a, b) for fn pick[T](a: T, b: T) over an
i32 and an i64 is refused with a as i64 as the help.
4. A slot’s type reaches through +, -, *, /, % and a negation.
let t: i64 = x * y over two i32 computes in i64: each operand is widened
before the operation. It does not reach into what a name was given, into a
call’s arguments, a method’s receiver or an index, and it stops at the
operators whose result depends on the width - &, |, ^, !, <<, >> -
and at the wrapping_, saturating_ and truncating_ names. There, a computation in a narrower type is refused
(NK1215), the help naming both readings: (a as i64) << 3 and
(a << 3) as i64.
5. A value shown non-negative goes into an unsigned type without as.
let n: u64 = xs.len() and, after if k >= 0, let n: u64 = k are taken
where the proof says k >= 0 (ADR-306 D12 and D16; a callee’s postcondition,
ADR-269 D17). Where it does not, the slot is NK1103, NK1102 or its
neighbour, with as u64 and assert k >= 0 as the help.
Why a lost value and not a mix. Every refusal D3 made for a mix that loses
nothing was answered with an as that could not fail; it marked no place a
value might not fit, which is what D3 asked of it, and taught that as is
noise. Java, Kotlin and C# widen at the same places; C widens too, but to a
type that does not hold both (u32 + i32 is a u32 there), which is the
mistake D3 was written against and the common-type table rules out.
Why inference does not widen. With widening among a use’s answers, let n
= 5 handed to an i64 and a u32 has two types that work and no rule that
picks one, and a generic T over an i32 and an i64 has two as well. C#
picks the wider one for a generic; Swift admits no implicit widening at all,
in part because its inference is already costly with literals and overloads.
Between two types already decided the common type is unique, so keeping
widening out of inference keeps both.
Why the slot’s type reaches the operands. Overflow aborts (D5), so +,
-, *, / and % in a narrow type either give the number the wide
computation gives or stop the program; computing wider changes no value a
program prints and only removes a stop. That is untrue of the bit operators:
!a for a: u32 = 0 is 4294967295 as a u32 and -1 as an i64, and
x << 31 for x: i32 = 1 is -2147483648 and 2147483648. So those stop the
slot’s type, and an expression of them is refused rather than read either
way. Computing the narrow type and widening the result (the rule for a name)
was the other choice for arithmetic: it is the long t = x * y; mistake C is
known for, kept as an abort.
Why a proof may admit a value. The proof a build uses is the committed
nikaia.proofs entry, replayed from its certificate (ADR-270 D4, D21), so the
answer does not depend on the machine or on the search; a change that removes
the fact is a refusal with a help, never a wrong value. Facts about std
calls are their postconditions and D12’s length bound, so nothing is added to
the ledger.
Rejected. A ledger column never negative for std results: a second
place for what a postcondition already says, and blind to if k >= 0.
Refusing a narrow computation in a wide slot: safe, but let p = x * y then
let t: i64 = p is allowed, and the rule would forbid the spelling that
means what the annotation says. Computing a whole expression in its widest
operand’s type wherever it stands: one x * y would compute in three types
in three places. Converting a finished list (Vec[u32] into Vec[i64]): a
new allocation and a copy, and no longer the same list.
What it costs. A reader meets an expression whose type is the slot’s,
not its operands’: x * y as an i64 and, moved into its own let, as an
i32 that may stop. A computation into a wide slot handles half as many
elements per vector register (not measured). No mainstream language passes a
slot’s type to its operands.
f32 is a type of the language, beside f64.Half the size per value is what a picture, a model’s weights, a sensor’s samples and most wire formats are made of, and every language this one is measured against offers it (C, Rust, Go, Swift, Java, C#). It follows the rules already set for numbers, so it adds no rule of its own:
f64, unless its use asks for an f32
(let w: f32 = 0.5), as an integer literal takes the type its use asks for.f32 and an f64 do not mix without as.f32 as f64 always fits.f64 as f32 is written only where it is shown to fit. A constant beyond
the f32 range is refused where it stands (NK1116, as D29 for integers).
A value the prover shows to be within the f32 range, or infinite, or not a
number, converts, rounding to the nearest f32. Any other is refused with
NK1241, and the program says what it wants by name:
x.checked_f32() is an f32?, null where a finite value is beyond the
range, and the program handles it (?? …);x.rounding_f32() is the backend’s conversion: the nearest f32, and an
infinity where a finite value is beyond the range.Losing digits is not a failure: every f64 within the range has a nearest
f32. Only leaving the range is.
This is D12’s rule for every narrowing conversion. The prover reasons about
integers and not yet about floats (ADR-270 D11), so until it does, a
non-constant f64 as f32 is NK1241 and a program writes one of the two
names.
Decided October 8, 2026 (#470, #552).
[12, 14, 13] is a Vec[T] whose T is decided as D23-D25 decide a let’s:
the uses ask - a parameter Vec[u8], an annotated let, an element read into
a typed place (let x: u8 = xs[0]), a push of a typed value, an element in
a typed operation - and two uses asking two types are NK1200. Where no use
asks, the first type that holds every element, and every value written into
the list, is the element type: i32, else i64 (D21, D25). D26 holds for an
element as for a name: xs[0] + xs[1] over two 2000000000s is NK1116
where nothing asks for an i64.
Why. A list of literals is the same question as a let of one, one level
down, as ADR-135 D2 already answers it for []. A fixed
Vec[i64] would refuse total([1, 2, 3]) against fn total(xs: Vec[u8]), and
a list left untyped stops at run time where the same sum over a name is refused
while building.
Decided October 8, 2026 (#513).
text::parse_i32, parse_i64, parse_u8, parse_u32, parse_u64,
parse_f32 and parse_f64 each hand back a T?: the number the whole text
writes, or null where it writes none or one the type does not hold. An
integer is an optional sign and decimal digits (an unsigned type takes no
-); a float is an optional sign, digits, an optional . and digits, and an
optional exponent (e or E), or one of inf, -inf and nan in any
case (NaN, -Inf), which a float holds and a data file writes. Nothing else: no space, no _. A program that has to say why a text is not a number reads it with a
grammar (dec[T](p), Part II 10.8).
Why. The name carries the type, as checked_i32 and truncating_i32 do,
so no signature has to take its type from the place its result goes, and no
conversion follows the parse: text::parse_i32(s) ?? 1000 instead of an
i64 narrowed by checked_i32 (D12). Java, C# and Go name the type the same
way; Rust’s and Swift’s generic parse would be the one function in std
whose result is typed by its use. Rust’s str::parse is not offered
(ADR-286 D36).
Decided October 8, 2026 (#504).
The ordinary integer
int holds the values of an i64 and stops the program where an i64 would.int is an integer type of the language, beside the five fixed widths. Its
values are an i64’s; its arithmetic, its bit operators, its conversions, its
literals and the places an overflow aborts are an i64’s (D5-D19). An int
and an i64 take each other without as, in both directions: no value can
be lost. Against the other widths an int widens and narrows as an i64
does (D12, D32).
Why. A programmer who writes i64 means 64 bits, and the language keeps
that promise; one who only means an integer has had no way to say so. The
values are fixed, not the machine’s: a C int that is 16, 32 or 64 bits wide
gives one program other results on another machine, which Part I 1.2 does
not allow. And the width does not follow the caller: an int whose width a
call decided would let one function abort for one caller and not for another,
with nothing in its signature to say so.
int is the compiler’s choice; a fixed width is exactly that width.The compiler may hold and compute an int in fewer bits where it shows that no
value, no result and no abort changes - an index below the length of a short
list, a list of literals whose sums cannot leave 32 bits. Where it does not
show it, an int is an i64. A written i64, i32, u8, u32 or u64
is exactly that width, in memory and in arithmetic.
Why. A narrower type was kept as a fallback (D21) so that 32-bit targets,
SIMD lanes and smaller lists stay within reach. Under an overflow that aborts
(D5), a narrower type changes what the program computes; a narrower
representation that is shown not to does not. It is the split String already
has: the program says what the value is, the compiler chooses how it is held.
ADR-306 shows a check unneeded by the same kind of proof.
An extern "C" signature and a described Rust contract take a fixed width; an
int there is refused with a code in Part III’s table, the help naming
i64.
Why. At an interface the other side reads the bits, so a width chosen by the compiler would be a width nobody decided.
int is the integer ordinary code writes; a fixed width says that the width is the point.Part I 2.2 recommends int for a number and a fixed width for a layout, a
byte, a hash and an interface. Neither is a warning anywhere.
Why. The width that is written then carries information: a u32 field or
a u64 hash is read as a decision, not a habit. A warning on a fixed width was
weighed and rejected (§3).
Decided October 17, 2026 (#630).
+ and * are.D22 covers << and >>: 1 << 40 is the value 1099511627776 and takes the
first type that holds it, an i64; 1 << 31 is 2147483648, also an i64;
1 << 63 no type holds, and it is refused. A shift loses the bits it moves out
(Part I 2.2) only in a type, so where a use asks for one (let m: i32 = 1 << 31,
an i32 parameter or field), the shift is that type’s and nothing changes. An
open let given a shift of literals (D23, D25) is typed by the folded value.
Why. 1 << 40, 1099511627776 and 1024 * 1024 * 1024 * 1024 are one
number and get one type; an exception for shifts would make a shift the only
operator whose literal result is cut to an i32 where nothing asked for one.
Go, Swift and Zig give 1 << 40 its exact value. Rejected: a shift of literals
as an i32 (Rust’s choice, 1 << 40 refused and 1 << 31 negative), because
it splits D22 by operator, and Java’s masking of the count (1 << 40 is 256),
which is a wrong value without a word.
Decided October 19, 2026 (#661).
u8 has every name the other fixed widths have.wrapping_, saturating_ and checked_ arithmetic (ADR-315 D1) exist for
u8 as for u32, without _neg and _abs; truncating_u8 and checked_u8
convert into it out of a wider integer and out of an f64, as D13 and ADR-315
D4 say for the other destinations. c.wrapping_sub(b'0') is a digit’s value
read with no check, n.truncating_u8() a byte’s low digits.
Why. u8 is one of the five integer types (D2) and the element of every
Bytes; reading bytes - digits, ASCII ranges, a checksum - is where a program
most often means to wrap or to stop. The families are the same on every width,
as Rust’s are (D6, D7); a width that lacks them forces a widening the program
did not mean ((b as i32) - 48). Rejected: leaving u8 without them, which no
record decided and nothing argues for.
Decided October 19, 2026 (#729).
bits_as_; truncating_ only narrows.u.bits_as_i32() out of a u32, i.bits_as_u32() out of an i32,
bits_as_i64() out of a u64 and bits_as_u64() out of an i64 keep every
bit and read it as the other type: 4000000000 as a u32 comes out
-294967296, -1 as an i64 comes out 18446744073709551615. There is no
bits_as_ between different widths and none for u8, which has no signed
partner.
truncating_T converts out of every integer type wider than T, whatever its
sign, and out of an f64 (D13): the low bits of T’s width, read as T.
u64::truncating_i32 joins the existing names, and f64::truncating_u32 and
f64::truncating_u64 round toward zero and clamp as the signed ones do. Out of
a type of the same width there is no truncating_ name: u64::truncating_i64
and i64::truncating_u64 go, and a call is refused (NK1210) with help naming
bits_as_i64 or bits_as_u64. A widening with the other sign is written in
two steps, each named for what it does: (x as i64).bits_as_u64().
Why. Truncating says that digits are lost, and between types of the same
width none are: what changes is how the top bit is read. A program that
reinterprets a hash or a signed word read from a file means something else than
one that cuts a counter down, and a reader should see which one it is, as Zig
separates @truncate from @bitCast and Rust 1.87 added cast_signed and
cast_unsigned beside as. A name that does both hides a sign error behind a
width change, the mistake C’s and Java’s implicit conversions make easy. The
name carries the destination, as the other families do (D13), and stays apart
from as, which in this language never changes a value.
Rejected: same-width conversions under truncating_ (as u64::truncating_i64
was), which keeps one name for two meanings; a direction without a type
(cast_signed), against the naming of every other conversion here; leaving
the 32-bit pair out, so that the rule would differ by width.
Decided October 20, 2026 (#742).
collections::HashMap() without an annotation takes its key type and its value
type from the first write that says them: m.insert("a", 1), m["a"] = 1 or
m.entry("a").or_insert(1) make a HashMap[String, N], where N is one
number without a type. That number is decided as D34 decides a list’s
elements: by the uses of the values and of the keys (a value read into a typed
place, handed to a typed parameter, written from a typed value), two uses
asking two types are NK1200, and where none asks it is i32, or i64 where
a written value does not fit. Keys written in number literals are one open
number in the same way. A key written as a view of text (a line of a kept
text, a part split cut) is a text position like any other, and its tier is
the one ADR-282 D13 decides: a map of views of a kept text holds
views. A map that nothing ever writes into and nothing types
is refused with NK1153, as [] is.
Why. A map filled with insert("a", 1) is not a map of unknown types: its
key is text and its value is an integer, and only the integer’s width is
open. Leaving both open (HashMap[?, ?]) loses what the program already
says, and what is lost reaches rustc as an error the checker never sees,
such as += on an &mut {integer} in an and_modify. A map’s value is the
same question as a list’s element (D34) one level further, which is a let’s
one level down; answering it the same way keeps one rule for every place a
number literal is stored.
Rejected: requiring an annotation on every map, against D34 and
ADR-135 D2 for lists; fixing the value to i64 at the first
write, which refuses fn total(m: HashMap[String, u8]) for a map of small
literals, as a fixed Vec[i64] would.
Decided October 20, 2026 (#700).
as and
its checks, and the compile-time refusals; Part I 2.4 says what a use is
and what a number takes where none asks; Part III A.2 lists the overflow
and the conversion that does not fit among the unrecoverable errors; Part
III’s table carries NK1116, NK1118, NK1122, NK1197, NK1198,
NK1199, NK1200 and NK1215.rustc’s overflow lint, its literal out of
range, its type annotations needed or its mismatched types: each is
answered in this language’s words first.int instead of i32 (D21).
With D37 the i32 fallback no longer buys an optimisation that int could
not; it still decides where an unannotated counter aborts.total += b
over text.bytes() makes total an u8 today.fn f[T: Integer]), for a
function meant for several widths.len() and an index become int (D1), and a hint for an i64
written where an int would do.Vec[i32] against Vec[i64], a SIMD kernel).std entries join D15’s list; an entry that cannot be described without one
is the argument for reopening.1u8), a separator in a char escape or a string, and a
hexadecimal float.