i64, and the Numeric Surface Is the One the Page NamesStatus: Superseded by ADR-285 — its D1-D2 are D1-D2 there
Related: ADR-043 D4 and D7 (the checked conversion and
truncating_, whose usize sources this removes), ADR-028 D5 (an
entry exists because a program asked for it), ADR-011 D2
(name-for-name lowering, which is why a conversion has to be emitted rather
than resolved), ADR-024 (the checker that reads the ledger’s
signatures).
Two questions were in one, and answering the second alone leaves the first standing.
What len() hands back. A usize — Rust’s machine-width type — so every
loop, sum and comparison that met a length carried an as i64 the user wrote.
And what the numeric surface actually is. Part I 2.2 offered i32, i64 and
f64, and more than three were reachable: fs::read hands back a Vec[u8],
std.contracts said in as many words that “the compiler accepts u32 and the
rest, but the specification does not offer them”, and usize was writable.
i64, and so is an index.Vec::len, String::len, str::len and HashMap::len hand back an i64 —
every entry in std.contracts that returns a length. The machine-width type
leaves the surface a program can write, and usize::truncating_i32 and
usize::truncating_i64 go with it: nothing narrows out of a type nobody can hold.
The two conversions are not symmetric, and that is the whole argument.
Out of a length — t.len() as i64 — cannot fail. A usize exceeds an
i64 only above 9,223,372,036,854,775,807 elements; at one byte each that is
eight exabytes of memory. Every one of those conversions is ceremony with nothing
behind it, and the user writes it in every loop, sum and comparison that meets a
length.
Into an index — v[i] where i is an i64 — can fail, for a negative
number, which pos - 1 at pos == 0 produces. But a negative index is an
access out of bounds, and Part III A.2 already aborts on those. It is not a new
failure mode; it is the same one, reached a step earlier.
So the trade is a great deal of visible ceremony that cannot fail, against one invisible abort that already exists. And the user writes no conversion at all, in either direction — the compiler emits both.
One requirement comes with it. The emitted conversion at an index must report as an access out of bounds, not as a failed conversion. Otherwise a user gets a diagnostic about a conversion they never wrote, for a mistake they understand as a bad index.
The field agrees, with one dissenter. Go, Java, C# and Swift all hand back a
signed integer for a length and index with it — Swift explicitly, on the ground
that unsigned arithmetic causes more bugs than it prevents. Rust is the outlier,
and the conversions its usize demands are among the most commonly named
papercuts in it.
u8 is named in Part I 2.2. The rest of the unsigned family is not.fs::read hands back a Vec[u8], so a program that reads a file already meets
one; what was missing was the specification admitting it. It gets the same
conversion and arithmetic names as every other numeric type.
u32, u64 and isize stay out until a program asks
(ADR-028 D5). Why not a signed byte instead: Java has no
unsigned type and pays for it in every byte-handling program, where a masking
dance undoes the sign at each step. Go, Rust, C# and Swift all carry a byte type.
What it costs: four numeric types where the front page promised three, and a beginner meets an unsigned one the first time they read a file. What it buys is that the list of types is the list of types.
check’s NUMERIC list is the writable surface, and it is now
i32, i64, f64, u8. That list decides which conversions are checked at run time
(ADR-043 D4); the machine-width types were in it because len handed one back,
and that is why they leave with it. contracts::send’s plain-type list is
untouched: it answers whether a type may cross a thread, which is a different
question and not about what a program may write.
Two truncating_ entries go. Narrowing a length is still possible and still
said by name — xs.len().truncating_i32() is now i64::truncating_i32, which is
the entry that was always there.
A std function whose parameter is a Rust usize still needs the written
conversion. " ".repeat(indent as usize) in examples/json/src/main.nika is the one
in this repository. D1’s scope is what a length returns, deliberately — the
four signatures, not a neighbourhood — and this is the edge of it, written down
rather than left to be discovered.
Whether the parameters above should follow. It is the same shape of question and a different set of signatures, and no program has yet been written that makes the answer obvious.
All of it.
std.contracts has the four i64 lengths and no usize entry at all. NUMERIC
is the four writable types. Part I 2.2 names u8 and says which types the
compiler accepts without offering.
Two emitted conversions, and the shape of the second is where the work was:
xs.len() becomes xs.len() as i64.xs[nikaia_std::index::at(i)], and at chooses on the
type of what is in the brackets: an integer becomes a usize, anything
else — a map’s key, most of all — goes through untouched. It has to be a trait
and not a function, because this emitter does not know types
(ADR-011 D2) and counts[path] is an index too.as usize would not do: -3 as usize is
18446744073709551613, and the message would then be about a number nothing in
the program contains.xs[0] and &text[1..3]
need no conversion — Rust’s own inference gives a literal the usize a sequence
wants — and they cannot take one, because at(0) has nothing to infer its
argument type from and cannot infer type about a generated file is what
Part III C.1 forbids.And the parentheses around a conversion are now decided in one place
(Emitter::emits_as_cast), which this change forced and ADR-043 D7 needed
already. Three things come out as a Rust as: a written cast, truncating_i32,
and a length. Rust reads such an expression wrongly in exactly two positions —
beside a unary operator (-x as i64 is (-x) as i64) and as the left of a
comparison (x as i64 < k is read as i64<k>, and the program does not parse) —
and nowhere else, so parentheses go in those two and nowhere else. truncating_
used to parenthesise always, which is the other half of C.1: let n = (big as
i32); is a Rust warning about a file nobody wrote.