Fix/string literal term - #907
Open
MattCozendey wants to merge 4 commits into
Open
MattCozendey wants to merge 4 commits into
MattCozendey wants to merge 4 commits into
Conversation
MattCozendey
force-pushed
the
fix/string-literal-term
branch
from
September 20, 2026 15:15
66cd582 to
104a26a
Compare
…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>
MattCozendey
force-pushed
the
fix/string-literal-term
branch
from
September 20, 2026 21:31
104a26a to
d816583
Compare
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A string literal checks as one node
Pull request for bendlang/bend from
fix/string-literal-term.What changes
A string literal now parses as one term node,
Lit, that holds its text.Its type is
String. It means the sameSConchain as before, and thechecker unfolds it one character at a time where a chain is read:
term_wnfreads"ab"asSCon{'a', "b"};term_comparecompares two literals by text, and unfolds a literal thatmeets a constructor, so
{"ab" == SCon{'a', "b"} : String}holds by{==};term_descendunfolds a literal argument against a constructor column;case "one":) is its chain's pattern, as before;term_checkaccepts a literal atStringwhen its head constructor is notpeeled off the type. For any other type it checks the first unfolding, so
the error message is the one the constructor gave.
Before this change the parser turned
"text"intoSCon{Chr{U32{WCon{...}}}, ...}, about 70 term nodes per character. Thechecker lifted, copied, checked and kept that chain. A literal cost about
40 KB of memory per character, and a literal past about 5000 characters
overflowed the host stack in
term_check. A Nat literal had the sameproblem before 2.0.7 (#779, #791).
The compiler sees no difference.
def_bodyin comp.ts expands every literalback into its chain (
lit_expand) before the compiler lifts a body. Theemitted C and JS are byte-identical to main for every test in
tests/. TheC, Metal and CUDA lanes run the same code as before.
A JS string holds Unicode scalar values only. A literal that spells a
surrogate or a code point past U+10FFFF still parses as its chain
(
tests/check/string_literal_surrogates.bend).Numbers
Checker peak memory and wall time with
--check-only, bun 1.4.2, WSL2 onx86_64. "before" is main at 6018e28.
Emission:
-o x.jsand-o x.cof a 4000-character literal overflowed onmain. They work now: 15.5 KB of JS, 255 KB of C. The emitters still walk a
literal as its chain, so one literal past about 8000 characters still
overflows in
-o. The checker has no limit.Files
Litnode: type,Lit,lit_of,lit_chain,lit_step,lit_expand; one case each interm_higher,term_lower,term_snf,term_show,parse_patt,match_flatten; the unfolding interm_wnf,term_compare,term_descend; thecheck-litrule; the string case of the parser;chr_show, shared by the char and string printersdef_bodyexpands literals before liftingString.length,String.append,String.eq, a match on literal patternsbend2/bend.ts: 41915 -> 43203 ttok (cl100k, counted with js-tiktoken; the
ttokbinary was not available here). bend2/comp.ts: 63466 -> 63471.bend2/bend.lean does not change. The core has no literal forms. Every
literal sugar already lives outside it and means its constructor chain.
Litfollows that convention, as a Nat literal above 256n does since 2.0.7.The core judges the chain. The checker computes the same judgment on the
compact form, and every read of a
Litunfolds it.AGENTS.md says bend2/bend.ts is human-written and not to be edited. This
change is in that file because the cost is in the checker's representation.
No other file can remove it. The diff is 130 lines. Every new function is
named
lit_*and sits in one section afteru32_to_term.Verified locally
tests/, 1382 files (1378 on main plus the four here): the JS-lane runoutput (stdout, stderr and exit code) of the 1273 tests outside tests/io is
identical before and after, except
string_literal_long, which overflowson main. This harness does not run tests/io, which waits on the gate's
harness. The emitted C (601 files) and JS (617 files) of every test are
byte-identical.
string_literal_longoverflows thestack, and the other three pass with the same output.
tsc -p bend2/pack/tsconfig.jsonreports the same two pre-existing errorsand no new one.
lanes are covered by the byte-identical emission only. Please run the gate.
Related
literal as
U32.to_nat(n).That report has the same root, on the emitter side. This change does not
touch the emitters. A
Litreachesdef_bodyintact, so an emitter thatwants the text has it.
Litis a directconstant for that representation.