1_000_000, 0xFF, 0b1010 or 0o17, and the Digits It Was Written In Are a SpellingStatus: Superseded by ADR-285 — its D1-D3 are D16-D18 there
Date: September 19, 2026
Target Version: Nikaia 0.0.18
Related: ADR-060 (a literal no use constrains takes the first type that holds it), ADR-043 D5 and ADR-063 (a constant that fits, NK1116), ADR-048 (the numeric surface), Part I 2.2 and 2.4
let n = 1_000_000 is two statements: the number 1 and a name _000_000
that nothing declares, which is NK1117. 0xFF is the number 0 beside a
name xFF. The grammar is scannerless, so a separator and a radix prefix are
not syntax errors — they are a misparse, which is the class
Part III C.1 is about and the class NK1117 was
built to report rather than hand to rustc.
The forms are Rust’s and the lowering is verbatim, so nothing here is expensive. One question is the owner’s.
Does a hexadecimal literal carry a width? 0xFF is eight bits written
out, and a reader may take that as a u8. Or the radix is a spelling and
0xFF is 255, which takes the first type that holds it exactly as 255
does (ADR-060).
1_000_000, 0xFF, 0b1010 and 0o17.An underscore may stand between digits and nowhere else: not first, not last,
not beside the radix prefix. 0x, 0b and 0o are lower-case, and the
digits after 0x may be either case. A float takes the separator too —
1_000.5 — and no radix prefix, because a hexadecimal float is a shape
nobody writes.
0xFF is 255. It takes the first type that holds it, which is
ADR-060’s rule unchanged, so let mask = 0xFF is an i32 and
let mask: u8 = 0xFF is a u8. A literal that fits nothing is
NK1116, which is the refusal it already gets in decimal.
Because the alternative makes the width invisible. If 0xFF were a u8,
then 0x0FF would be a u16 and 0xFF + 1 would overflow where 255 + 1
does not — a type that depends on how many zeroes somebody typed. What a
reader wants from 0xFF is the bits; what they want from the type is the
declaration, and this language has one place for that.
1_000 is 1000 to the checker, to the ledger and to the constant fold. It
does not survive into a diagnostic’s text either: a message about
let n = 1_000 names 1000, because that is the number, and naming the
spelling back at a reader who wrote it says nothing.
NK1117’s standing example — the _000 of 1_000 is a name — stops
being true, which is the point. Its help loses that clause.0xFF and 0b1010 mean what every neighbouring language means by them.1u8 or 1i64. A literal takes its type from its use and
the declaration is where a type is written; a suffix would be a second
place.char escape or in a string, which is not a
number.All three steps.
_000 is a name, in every
language that has both, and a rule that read it as part of a number would
take let _1 = 5 away. A float takes the separator (1_000.5) and no
prefix, which it has to: without it 1_000 stands beside .5, and .5 on
a number is a tuple part.NK1116 and NK1118 were already answering about it.
D3 falls out of the same thing — the separator is not in the value, so it is
not in a message either.NK1117’s help lost the clause. It explained that 1_000 is 1 beside
the name _000, which stopped being true here; a help text that explains a
form the language has is worse than no help at all.Written in Rust rather than in the grammar, and for two reasons the DSL
gives rather than a preference. Its character classes are digit and any, so
a binary digit and a hexadecimal digit have no spelling in it; and an
action cannot fail, so a number too wide for an i64 could only be
parse().unwrap() — which is what it was, and 99999999999999999999 took this
compiler down rather than the program. extern rule number_lit -> i64; is the
declaration and the parser stands beside the grammar.
Two things beyond what the record asked for, both of them the same misparse
one step along. A digit the radix does not have — 0b1210, 0o19 — is refused,
because otherwise the number ends at the bad digit and what follows is a second
number nobody wrote. And the refusals put the caret on the character that is
wrong rather than at the start of the literal: a diagnostic is ranked by how far
the parse got, so one built at the start loses to the float rule’s failure
further along, which is a true sentence about this parser and no help to a
reader.
And one number the record left without a spelling, since given one.
-9223372036854775808 is i64::MIN and belongs to the type; as a negation of
a positive literal its digits are 9223372036854775808, which no i64 holds,
so the parser refused a number that is in the language. Every other number in
the range parsed either way, which is what made it one number missing rather
than a hole. A - written directly against the digits is now part of the
literal, and the sign goes into the text the radix parser reads rather than
being applied to what came out — so -0xFF is -255 for the same reason 0xFF
is 255 (D2).
Only where it is against them, which is the whole of the rule: - 5 and -x
are the unary operator they always were, a binary - is matched by the rule
that wrote it rather than by its operand’s, and a . or an exponent after the
digits hands the - back — -1.16e+00 is a float and -2..<2 is a range, and
examples/n-body/src/main.nika is what said so the first time the rule ran.
#135 carried both.