Skip to content

graph_close indexes the edges once, so a closure is linear in the graph instead of the reached set times the edges - #1298

Merged
nicolas-abril merged 1 commit into
bendlang:mainfrom
moorbrook:graph-close-linear
Oct 4, 2026
Merged

nicolas-abril merged 1 commit into
bendlang:mainfrom
moorbrook:graph-close-linear

Conversation

@moorbrook

Copy link
Copy Markdown
Contributor

graph_close rescanned the whole edge list for every node it reached, so a closure cost (nodes reached) × (edges). It now groups the edges by source once and walks only each reached node's own edges. The C emitter calls it three times per build (the lend fixpoint, live/device reachability, and forky), so large programs paid the quadratic term on every emission.

The result is unchanged: the same Set comes back with the same members in the same insertion order (Set.prototype.forEach visits values added during the walk, as the old for..of did). All three callers pass string keys, so map lookup matches the old ===.

Same output

  • Emitting C for every .bend under tests/, demos/ and bench/ (1,626 files) with main and with this branch: the same exit status for all 1,626, the same stderr for all 1,626, and all 737 emitted .c files byte-identical.
  • A randomized check of old against new on 20,000 string graphs (cycles, self loops, duplicate edges, unreachable nodes; the closure grew in 12,503 of them) compares members and order. Planting either a dropped edge or a non-transitive walk in the new version makes it fail.
  • comp.ts is 64,000 ttok with this change (main: 63,980), at the cap, not over it. tsc -p bend2/pack reports the same 12 errors as main.

Speed

C emission time (bend chain.bend -o chain.c, one run each, Apple M-series) for a chain of N defs where each calls the next:

N main this PR
1,000 0.22 s 0.23 s
4,000 1.06 s 0.75 s
8,000 2.33 s 1.20 s
16,000 12.82 s 2.22 s

The repository's own demos are too small to show it: app_slash_boss_3d (3.2 MB of C) emits in about 1.3 s either way, within run-to-run noise.

n=16000; { echo "import Base"; echo; printf 'def f%d(x: U32) -> U32:\n  x\n\n' $n
  for i in $(seq $((n-1)) -1 0); do printf 'def f%d(x: U32) -> U32:\n  f%d((x + 1 : U32))\n\n' $i $((i+1)); done
  printf 'def main() -> IO(Unit):\n  IO.print(U32.show(f0(0)))\n'; } > chain.bend
time bun bend2/main.ts chain.bend -o chain.c

🤖 Generated with Claude Code

…ph instead of the reached set times the edges

graph_close rescanned every edge for every node it reached. It now groups
the edges by source once and walks only each reached node's own edges.
The returned set gets the same members in the same insertion order, so
emission is unchanged; all three callers pass string keys.

Co-Authored-By: Claude Opus 5.5 <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.

2 participants