Feature/nat literal term - #924
MattCozendey wants to merge 8 commits into
Conversation
…ck frame per character, and the emitted code is the same
The parser turned "text" into SCon{Chr{U32{WCon{..}}}, ..}, about 70 nodes per character; the checker lifted, copied, checked and kept the chain, so a literal cost about 40 KB per character and one past about 5000 characters overflowed the host stack in term_check, as a Nat literal did before 2.0.7. A string literal now parses as one Lit node holding its text, typed String, that unfolds a character at a time where a chain is read: a match frame in term_wnf, term_compare against a constructor, term_descend against a constructor column, a literal pattern, and check-lit's fallback for any type but String. comp.ts expands every literal back into its chain in def_body, so the emitted C and JS are byte-identical to main for every test. 50 defs of a 1000-character literal: 2.60 GB and 4.00 s to 74 MB and 0.14 s; a 200,000-character literal checks in 0.17 s. A literal that spells a surrogate or a code point past U+10FFFF still parses as its chain. bend2/bend.ts may reach 43500 ttok (41915 to 43203). Four tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e surrogate
tests/check/string_literal_surrogates.bend ran String.length("\u{d83d}\u{de00}")
in main, and every test with a main and Base runs in the C and JS lanes;
the JS runtime aborts on a lone surrogate (as on main), so the gate would
fail. The same check is now the law surrogates_two in
tests/proof/string_literal_unfolds.bend, which the checker verifies and the
compiler never emits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ompiler expands a literal where it reads a constructor lit_expand walked a whole first-order term to turn every Lit into its chain before compilation, so comp.ts saw no Lit. A Lit has no sub-terms, so parse_patt calls lit_full, the chain of one literal. The compiler matches constructors in term_const, ty_peel and the Mat scrutinee of emit_unfold, so term_lit, memoized per node, hands them a Lit's chain there, and def_body no longer pre-expands. The emitted code is unchanged: the JS of the 164 runnable tests with a string literal is byte-identical, and the checker's output is the same on every non-IO test. bend2/bend.ts may reach 43000 ttok (43203 to 42853). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…a module's own String
Both hold on main and had no test. A literal argument that is a strict
tail of its case's literal pattern descends: case "ab" may call f("b"),
which needs term_descend to read the literal as its chain. A literal is a
String only where String declares SCon and SNil: a module that declares its
own type String is Data: Foo{} rejects "abc" with main's exact error, which
needs check-lit's ctrs_find guard.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…to_nat(0)
The test proves a literal pattern is refused like Zero{} is when Zero
carries a field. It wrote the pattern as U32.to_nat(0), the form the parser
gave a nat literal, which reads as a test of that longhand. It now writes
0n, the literal it is about, and prints the same error.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… and the emitted code is the same A nat literal up to 256 was a Succ chain and a larger one U32.to_nat(n), an application, so two forms meant one literal: the parser, the pattern reader and the printer each knew both, and a self-call on a literal past 256 failed the termination check. A Lit now holds a number as it holds a text: it unfolds a Succ at a time where a chain is read (a match frame, a comparison, a descent, the check rule's fallback), checks in O(1) against Nat, infers through its first step (so an undeclared Nat still reports "a declared constructor"), prints as its number (a Succ chain that ends in a literal prints as one number), and "1n+0n" parses as 1n so template keys agree. NAT_LITERAL_MAX stays for "n + T", which is Nat.add(n, T) past it, and for the compiler, whose term_lit emits U32.to_nat(n) past it and whose term_kids and term_any read a literal through it, so the C and JS of every test are byte-identical. The checker's output is the same on every non-IO test. 2000 defs of 200n: 1.06 s and 366 MB to 0.18 s and 83 MB. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… how it prints
nat_literal_unfolds: 300n is Succ{299n} and 0n is Zero{} to a comparison, a
match reads a literal pattern past 256 and a literal scrutinee, case 300n
may call down(299n) (the termination check reads the literal), and a
literal at the u32 bound passes through Nat.add unforced. The patterns past
256 live in laws: a compiled match on one rebuilds its scrutinee 300 deep,
past clang's bracket nesting, on main as well. nat_literal_own_nat: a
module's own type Nat with other constructors rejects 3n with main's exact
error. nat_literal_print: a literal prints back as a literal, "n+T" past 256
is Nat.add, and a Succ chain that ends in a literal prints as one number.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
This should only be considered if(and only if) #907 is merged as well, since it builds from it. |
A Succ emitted nat_chk(e, $0 + 1) around its field, so a Succ on a Succ nested the checks: a literal pattern past 256 whose default arm uses the scrutinee rebuilt it 300 levels deep, and clang refused the bracket nesting (256), on main as well. tpl_nat now reads a field that is a check of a sum, nat_chk(e, x + k) or nat_chk(x + kn), and bumps its constant: one call for the run, in C and JS. The result is the same, as nat_chk saturates (C) or throws (JS) past NAT_IMM, and x + k passes it whenever x + 1 does. Of 540 runnable tests, 10 emit differently and all print their lines. One test. bend2/comp.ts is 64998 ttok against a cap of 65000. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A literal no longer costs a constructor chain per character or per Succ: the parser makes one Lit, the checker unfolds it one step where a match, a comparison, a descent or the check rule needs a constructor, and the compiler expands it where it reads one, so the emitted C and JS are the same. A self-call on a literal past 256 now passes the termination check, and "1n+0n" parses as 1n (PRs #907 and #924 by MattCozendey).
… one function
Every read in comp.ts goes through term_force or term_strip, which now expand a
Lit once (a nat past NAT_LITERAL_MAX to U32.to_nat); term_lit and its five call
sites go. facts_packed, which read a Lit raw, sees the chain again: "" at a
borrowed parameter is packed, so the parameter stays borrowed, as on 2.0.23;
under the six-call design it owned the parameter and heated String.
ctr_adt and mat_adt each looked a datatype up and refused an open Array element
with a copy of lay_el's check (arr_open); js_expr held a third copy. adt_of
does the lookup and asks lay_el, where the rule lives.
comp.ts 65011 -> 64936 ttok. Emitted C and JS byte-identical to the Lit
branch over tests/{run,compile,rfc,reg,cost,io,base} and bench/runtime;
tests/compile/literal_reads pins the borrowed "" (#907, #924)
|
Merged, thank you very much. Both PRs landed on main as one commit under your authorship, 942573f "A string or nat literal is one Lit node in the checker", and bend.ts's cap moved to 43000 for it. The design is the right one: the core keeps no literal form, the checker reads a compact node and unfolds it one step where a constructor is needed, and the emitters see the chain they always saw. The numbers reproduced here, and a self-call on a literal past 256 passing the termination check is a real fix on its own. It ships in the next release. Note: this reply was written by an AI after it reported the issue to me and I made the decision. If anything here is wrong, reply and I will review it myself. |
A nat literal is one Lit node in the checker
A nat literal up to 256 parsed as a Succ chain, and a larger one as
U32.to_nat(n), an application. Two forms meant one literal: the parser, the
pattern reader and the printer each knew both, and a self-call on a literal
past 256 failed the termination check, since the check reads constructors
and saw a function call. A Lit now holds a number as it holds a text, and
the same peel reads it: 0n is Zero{}, n is Succ{n-1}.
What changes
read: a match frame, a comparison, a descent, and the check rule's
fallback. Against Nat it checks in O(1), with the same guard the string
rule has: the Nat in scope must declare Succ and Zero.
"a declared constructor".
in a literal as one number.
stays for two uses: "n + T" past it is Nat.add(n, T), and the compiler
emits U32.to_nat(n) past it. U32.to_nat is an intrinsic ($0 in C,
BigInt($0) in JS), so this costs nothing at run time.
the reference to U32.to_nat counts for reachability.
Emitted code and behavior
Tests
Succ{299n} and 0n as Zero{}, a match reads a literal pattern past 256
and a literal scrutinee, a self-call on a smaller literal descends, a
literal at the u32 bound passes through Nat.add unforced.
constructors rejects 3n with the base's exact error.
literal, "n+T" past 256 is Nat.add, a chain that ends in a literal
prints as one number.
wrote U32.to_nat(0), the form the parser gave a literal, which is no
pattern anymore. It prints the same error.
Size: bend2/bend.ts 42981 and bend2/comp.ts 64977 ttok (cl100k), under
the caps of 43000 and 65000 with no room to spare. If your ttok reads
higher, the caps need to move.
🤖 Generated with Claude Code