Skip to content

fix(check): a constructor's fields grow as a list, without copying - #1966

Merged
nicolas-abril merged 1 commit into
mainfrom
ai/1956-list-copies
Nov 2, 2026
Merged

nicolas-abril merged 1 commit into
mainfrom
ai/1956-list-copies

Conversation

@nicolas-abril

@nicolas-abril nicolas-abril commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1956. #1967 did the call spine and body_flatten, and this does the constructor fields, the last copies #1956 names.

What

lhs_ext and term_check_mat_goal built a constructor's fields with [...xs, x] and xs.concat([x]), so a constructor with n fields copied n(n-1)/2 entries. They now cons onto the existing List and build the array once with list_to_array. The list is persistent because these closures can be entered more than once.

The Ref case from #1967 now reads a call's pending arguments with the same helper, instead of its own loop.

Output is unchanged: the fields come out in source order, and tests/check/list_order.bend pins that.

Numbers

--check-only on a match over one constructor with 4,000 fields, best of 9, macOS: 257 ms on main, 220 ms here. bend.ts goes from 45,100 to 45,150 ttok (cap 48,000).

The gain only shows on very large constructors. The point of this PR is finishing #1956.

Dropped from the earlier version

This PR was +151/-54. It no longer has:

A review suggested recording dead binders in ctx_bind instead. I didn't do that: it would have to reduce every binder's type when it's bound, and main only does that when an empty match needs it. def f(x: loop(0n)) -> Nat finishes on main today, so that would add hangs.

Testing

  • This PR on main e73e9f98, gates/_run.ts: repo 54/54, perf 124/124, test 1817/1817.
  • One commit earlier, on 01fca262: four more side-by-side runs and three solo test runs all passed. An earlier side-by-side run printed test 1816/1817. I didn't capture which test that was, and it didn't come back.
  • ping was skipped, since there's no site repo at the path it expects.
  • gates/safe.ts and Metal and CUDA weren't run on this version.

list_order.bend also covers template arguments in a call and the empty-match search on both sides of the scope tree, which #1967 and #1965 change. I removed its skip case, which only guarded the batched var columns.

🤖 Generated with Claude Code

@nicolas-abril nicolas-abril changed the title fix(check): grow calls, fields and binders without copying the list fix(check): grow calls, fields, binders and match columns without copying Oct 30, 2026
@nicolas-abril nicolas-abril self-assigned this Oct 30, 2026
@Lorenzobattistela Lorenzobattistela self-assigned this Oct 30, 2026
@nicolas-abril

Copy link
Copy Markdown
Collaborator Author

Overlap with #1965 and #1967

#1965 and #1967 each do a smaller version of part of this PR. Suggestion: merge those two and cut this PR down to what they don't cover, rather than closing either of them. All three touch the same code, and each of the two conflicts with this PR if this one lands first, so the order needs deciding up front.

--check-only, best of 7, in seconds. Every version was merged onto main at f1273d30 and run locally, interleaved. Every run exited 0.

Program main #1965 #1967 both #1966
8000 empty matches, Empty 1st 4.80 0.75 4.69 0.76 0.76
8000 empty matches, Empty 3rd 5.30 1.23 5.30 1.19 0.77
8000 empty matches, Empty 7th 5.43 1.39 5.42 1.40 0.79
8000 empty matches, Empty 101st 5.49 1.35 5.55 1.37 0.83
Call with 8000 arguments 0.22 0.21 0.06 0.07 0.07
Def with 8000 parameters 0.08 0.08 0.05 0.05 0.05
Constructor with 4000 fields 0.34 0.35 0.33 0.33 0.21
Match on last of 4000 binders 0.25 0.24 0.24 0.24 0.22
Match on 2000 var columns 0.14 0.14 0.14 0.13 0.11
bend.ts ttok added +73 +39 +112 +315

bench/checker, median of 15 interleaved runs, in seconds:

Bench main #1965 #1967 both #1966
defs_12800 0.319 0.319 0.318 0.317 0.319
trees_400 0.649 0.650 0.646 0.648 0.651
compute_1600 0.533 0.535 0.531 0.534 0.534
generics_3200 0.408 0.406 0.402 0.404 0.401
proofs_3200 0.848 0.855 0.845 0.849 0.837

Where each one is better

Suggested plan

  1. Merge perf(check): ctx_dead searches the scope in place instead of copying it first #1965 and perf(check): a call's pending arguments are a cons list, and parameters flatten in one loop #1967. They merge cleanly together on main.
  2. Reduce this PR to level order, as a change to pmap_any only, and the constructor-field lists. It would still close [complexity, flattening, checker] O(n^2) — Checking a call copies its growing argument list each time #1956.
  3. Drop the batched var-column run. It's the most complex part of this PR and saves about 0.02s at these sizes.
  4. Revert the pmap_to_array change, unless a measurement justifies keeping it. With perf(check): ctx_dead searches the scope in place instead of copying it first #1965 in, ctx_dead no longer calls it.

That should keep almost all of the speedups for a much smaller diff. It also leaves less AI-written code in bend.ts, which matters given the AGENTS.md rule on that file.

Not verified

🤖 Generated with Claude Code

@YidaWeng

YidaWeng commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Verified against current main (00d29f44): tests/check/list_order.bend passes --check-only, interpreter, C and JS (prints 3040460226), so the ordering coverage is good. Token count is fine (bend.ts 45,265 / 48,000).

The issue is overlap: this conflicts with both #1965 and #1967, and your own comment already suggests landing those two first and cutting this down. I agree.

Two review points beyond the overlap:

  1. The level-order ctx_dead change alters which dead binder is found first, which can change which looping @unsafe binder hangs first. Please call that out explicitly as intended semantics, not just a performance change.
  2. The new optional out mutation in pmap_to_array works, but it makes the helper stateful. Consider an internal helper or a clearer signature.

bend.ts is human-written, so I'm not proposing code edits here — just review.

@nicolas-abril nicolas-abril changed the title fix(check): grow calls, fields, binders and match columns without copying fix(check): a constructor's fields grow as a list, without copying Nov 2, 2026
lhs_ext and term_check_mat_goal built a constructor's fields with
[...xs, x] and xs.concat([x]), so n fields copied n(n-1)/2 entries. They
cons onto the existing List and build the array once with list_to_array.
The list is persistent because these closures can be entered more than
once. The Ref case reads a call's pending arguments with the same helper.

Fixes #1956.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nicolas-abril
nicolas-abril merged commit d9c0b7e into main Nov 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants