Skip to content

mat_lits compares a word match's default with its replay node by node, up to where they meet, not as two JSON strings - #1387

Merged
Lorenzobattistela merged 2 commits into
bendlang:mainfrom
Iann29:mat-lits-no-json
Oct 14, 2026
Merged

Lorenzobattistela merged 2 commits into
bendlang:mainfrom
Iann29:mat-lits-no-json

Conversation

@Iann29

@Iann29 Iann29 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

At every single-arm level of a U32 match's 32-level bit tree, mat_lits decides whether the level's default is the covering default replayed. It did this by comparing JSON.stringify(term_lower(·)) of both sides, which lowers and prints the rest of the match twice per level. In a multi-column match with literals inside constructor patterns, the rest of the match is large. On the AMAGE Eco demo (amage-si; 1,253 live defs, 7.7 MB of C), mat_lits took 8.1 s of the 17.9 s of C emission.

The two sides are nearly always equal, and they share most of their nodes as the same objects. same walks both the way term_lower would: λs opened with the same variable at the same depth, then applications, constructors and matches field by field. It returns true at the first shared object and false at the first difference. Any other node it compares by the old key, lowered at its depth. So it gives the key comparison's answer without printing the rest of the match.

Speed

bend lits.bend -o lits.c with the generator below. Apple M4 Pro, Bun 1.4.0, medians of 5 interleaved runs:

program main this PR
step, 8 arms 1.10 s 0.66 s
step, 16 arms 2.06 s 1.13 s
step, 32 arms 4.20 s 2.35 s
AMAGE Eco demo (Ryzen 7 5800H, Linux, author's run) 18.2 s 12.2 s

The repository's own programs have no match of this shape. Their perf gate results are unchanged (124 / 124).

n=32; { cat <<'EOF'
import Base

type Tag is Data:
  Tag{kind: U32, open: Bool, close: Bool, a: U32, b: U32, c: U32}

type Doc is Data:
  Doc{depth: U32, xs: +List<U32>, y: U32, w: U32, h: U32, z: U32, status: U32, count: U32}

def step(t: Tag, d: Doc) -> U32:
  match t d:
    case Tag{0,True{},False{},a,b,c} Doc{_,xs,y,w,h,z,1,count}:
      U32.add(a, count)
    case _ Doc{_,_,_,_,_,_,1,_}:
      1
EOF
for i in $(seq 1 $((n-1))); do
  echo "    case Tag{$i,True{},False{},a,b,c} Doc{_,x <> more,y,w,h,z,0,count}:"
  echo "      U32.add(U32.add(U32.add(a, b), U32.add(c, x)), U32.add(U32.add(y, w), U32.add(U32.add(h, z), U32.add(count, $i))))"
done
cat <<'EOF'
    case _ _:
      0

def main() -> IO(Unit):
  IO.print(U32.show(step(Tag{3,True{},False{},1,2,3}, Doc{0,[7, 8],2,3,4,5,0,6})))
EOF
} > lits.bend
time bun bend2/main.ts lits.bend -o lits.c

Same output

  • For all 1,706 .bend files under tests/, demos/ and bench/, main and this branch give the same exit status, stdout and stderr. Every emitted .c (803) and .js (833) is byte-identical. The generator's C is byte-identical at 8, 16 and 32 arms.
  • Every comparison mat_lits made while compiling all of the above was done both ways, with any disagreement logged. That covered 205,970 comparisons (201,496 equal, 4,474 not), and same agreed with the key on all of them.
  • New test tests/compile/word_match_cover_fields.bend has an odd arm and a default that differ only in a constructor, or only in a nested match's last arm. Before this, a same that skipped constructor names or a match's rest would pass every test.

Gates

comp.ts is at 63,968 ttok (cap 64,000). Results: repo 54 / 54, test 1624 / 1624, perf 124 / 124, ping 58 / 58. Not run: CUDA, Linux, Windows.

Found while building AMAGE Eco, a UI toolkit written in Bend 2.

@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

Thanks @Iann29! I rebased this onto current main (after #1385), and comp.ts is at 64,017 ttok, so gates/repo.ts fails (the earlier check ran on an older main). We can't merge until it fits under the 64k cap. Could you add the rewrites you mentioned? We'll review once the gate passes.

Iann29 added a commit to Iann29/bend that referenced this pull request Oct 7, 2026
…d too

bendlang#1385 brought main's comp.ts to 63,975 ttok, which put this branch at
64,015. These rewrites take 30 back, with the same output: the File,
Scope, Seg and Spine types list several fields per line; term_const,
fun_of and intr_of read lit_call's result, h and tld.i as truthiness,
since each is an object or an array when present. The branch is at
63,985, and merged with bendlang#1387 (which takes its own share) at 63,989.
@Iann29

Iann29 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @Lorenzobattistela! Added a5ac12d on top of your rebase: 38 more ttok of rewrites of the same kinds, same output.

@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

Thanks @Iann29! The mat_lits change (comparing node by node instead of as two JSON strings) is a good fix, but we can't merge the trimming commits (e516a63 and a5ac12d) as they are. They save tokens by writing comp.ts in a style the rest of the repo doesn't use. bend.ts is our reference for style, and these commits move away from it:

  • They inline named helpers into larger functions: js_match into js_func (about 40 lines), and js_key and js_host into the js_lib template.
  • They turn if/return blocks into nested ternaries, some with die() inside, and loops into flatMap/forEach chains.
  • They swap explicit === null / === undefined checks for truthiness (lay_box, val_box, val_unbox, and others). bend.ts always writes these checks out.
  • They squeeze the OPERATIONS entries onto shared lines.

Behavior is the same, so this is only about style.

Where things stand: main is now at 63,848 ttok after #1395. Your mat_lits commit (2583108) alone still applies cleanly and brings comp.ts to 64,112, so it needs about 112 tokens of room. Could you drop the two trimming commits, rebase onto current main, and find that room with real simplifications (dead code, duplicated logic, helpers that can be shared) instead of compressing code? If both this PR and #1386 go in, together they need about 430 tokens, so this is worth planning across both PRs.

We're also discussing whether to raise the token cap, so if you prefer, you can wait for that decision before doing the cuts. We'll review once the gate passes.

Iann29 and others added 2 commits October 14, 2026 08:22
…, not as two JSON strings

At each single-arm level of a U32 match's bit tree, mat_lits compared
JSON.stringify(term_lower(.)) of the level's default and of the covering
default replayed. On the AMAGE Eco demo that was 8.1 s of 17.9 s of C
emission. The two sides are nearly always equal and share most nodes as
the same objects, so same walks both as term_lower would (a lambda
opened with one variable at its depth, applications, constructors and
matches field by field), stops at the first shared object or difference,
and compares any other node by the old key at its depth: the key
comparison's answer, without printing the rest of the match.
…fault that differs from its replay only in a constructor or a nested match's arm
@Lorenzobattistela
Lorenzobattistela merged commit e78dc9c into bendlang:main Oct 14, 2026
1 check passed
nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 19, 2026
…ts did

c36d181 compared a default arm with its parent's by lowering both to
JSON, which on nested word matches lowers each level's whole subtree:
pong emitted in 1.6-2x main's time, the ray tracer 1.2-1.5x. term_same is
now main's walk (bendlang#1387): equal by identity, else node by node, by key only
where it does not open. Byte-identical; pong and the ray tracer back to
main's emit time or under.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Lorenzobattistela Lorenzobattistela mentioned this pull request Oct 20, 2026
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.

2 participants