Skip to content

Feature/u32 to nat - #936

Open
MattCozendey wants to merge 2 commits into
bendlang:mainfrom
MattCozendey:feature/U32-to-Nat
Open

MattCozendey wants to merge 2 commits into
bendlang:mainfrom
MattCozendey:feature/U32-to-Nat

Conversation

@MattCozendey

Copy link
Copy Markdown
Contributor

A U32 literal is U32.from_nat over a nat literal

Follow-up to #907 and #924, which made a string or nat literal one Lit
node. A bare number now parses as U32.from_nat(3n): one App over
one Lit, where before the parser built a 33-word U32{WCon{..}} chain
of about 100 nodes per number. The compiler no longer expands a nat past
256 into U32.to_nat over a word either.

In bend.ts the parser emits App(Ref("U32.from_nat"), Lit(n)) and
u32_lit reads that shape back. term_wnf takes U32.from_nat on a
nat literal straight to its word (u32_word, one shared term per
number), so a proof over 4294967295 costs what it did before. That
shortcut is a compressed run of the def's own unfolding, and
definitional equality is unchanged. lit_ctr reads any literal as its
constructor, a U32 literal as its word and a string or nat literal one
step, and the termination check, the match frame, term_compare and
check-lit call it where each used to step a Lit by hand. Patterns and
the printer read the shape too: case 0: still matches, and an error
spells 3 rather than U32.from_nat(3n).

In comp.ts a nat Lit is a leaf. lit_word gives the immediate for
either literal and feeds term_const and both emitters, so no
U32.from_nat call and no U32.to_nat chain reaches the output. The
compile-time fold still reads a literal scrutinee as its constructor, so
a match on a literal argument folds as it did.

In base.bend, Word.zero, Word.inc, U32.inc and U32.from_nat
move under def Word(n). Forward references are refused and IO.fork
writes 1 early in the file. The base case is U32{Word.zero(32n)}.

Measured with both trees on tmpfs:

before after
2000 defs, each a wide U32 literal 1.03 s, 217 MB 0.26 s, 81 MB
demos/app_slash_boss_3d --check-only 1.80 s, 420 MB 1.43 s, 319 MB
demos/proof_numerics --check-only 0.21 s, 74 MB 0.18 s, 70 MB

The gain scales with how many numbers a program writes. Emitted C over
tests/ and bench/runtime is byte-identical apart from 0 spelled 0ull
and one literal list that now lives in the static image. JS inlines
3000n where it had BigInt(3000). The bench binaries print the same
results.

Two things a user can observe. A literal now infers its own type, since
it is an application of a typed def, where a bare constructor needed a
goal: x = 5 with no annotation types as U32, and
tests/parse/rewrite_non_equation is repinned because the miss is now
the equation rather than the literal. And a file without import Base
that writes a bare number reports an undefined U32.from_nat instead of
an unknown constructor.

New tests: check/u32_literal_proof (wide and wrapping proofs, literal
patterns), check/u32_literal_show (a literal prints as its number) and
compile/u32_literal_word (immediates on all three lanes). Locally the
interpret lane over all 1415 tests has the same pass set as main plus
those three, and the C and JS lanes were built and run on the 118 tests
whose emitted code changed. The cluster gates were not run from here.
Caps move to 43400 for bend.ts (at 43338) and 65300 for comp.ts (at
65193).

bend.ts is edited, in the parser, term_wnf, the printer and the
pattern reader, since the change lives there by nature. The term_wnf
shortcut is keyed on the def name U32.from_nat in three places: the
parser, u32_lit and the guard. A rename in base.bend would fall back
to the slow unfolding without an error. bend.lean says literals are base
constructors, not calculus, and may want a line about this reduction.

🤖 Generated with Claude Code

Matt Pereira and others added 2 commits September 21, 2026 20:41
The parser reads "3" as U32.from_nat(3n), one App over one Lit node,
not a 33-word WCon chain: term_wnf takes U32.from_nat on a nat literal
to its word in one step (a proof over 4294967295 stays instant), the
pattern reader, the printer and the termination check read the same
shape. The compiler keeps a nat literal as a leaf and emits either
literal as an immediate (no U32.to_nat over a word past 256, no
U32.from_nat call); lit_ctr still folds a match on a literal. Base's
Word.zero, Word.inc, U32.inc and U32.from_nat move under def Word(n),
before the first bare number.

2000 defs with a wide literal: 1.03 s / 217 MB -> 0.26 s / 81 MB;
app_slash_boss_3d check 1.80 s / 420 MB -> 1.43 s / 319 MB. Emitted C
over tests and bench/runtime differs only in 0 -> 0ull and one list
now in the static image; JS inlines the nat. A literal now infers, so
rewrite_non_equation reports the equation miss. Caps: bend.ts 43400,
comp.ts 65300.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lit_ctr takes a U32 literal to its word and a string or nat literal one
step: term_descend, the match frame, term_compare and check-lit call it
where each stepped a Lit by hand, and comp.ts's fold delegates to it
past its NAT_LITERAL_MAX guard. Same tests, same emitted code; bend.ts
43338 ttok, comp.ts 65193.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant