feature/js-plains - #974
Open
MattCozendey wants to merge 2 commits into
Open
MattCozendey wants to merge 2 commits into
MattCozendey wants to merge 2 commits into
Conversation
MattCozendey
force-pushed
the
feature/js-plains
branch
from
September 21, 2026 23:58
8ff7eab to
c03b845
Compare
MattCozendey
force-pushed
the
feature/js-plains
branch
from
September 22, 2026 23:35
c03b845 to
6d8bdf6
Compare
JS-only OPERATIONS entries for bool_and, bool_not, bool_pick, u32_min, u32_max, f32_min, f32_max, nat_min and nat_max: each mirrors its base.bend body exactly (pick c a b = c ? a : b; min = a < b ? a : b; max = a < b ? b : a, so F32's NaN answers follow is_lt, and Nat's Succ-peeling recursion becomes one BigInt compare). Before, each call cost a run_jump trampoline into the def and, for min/max, a second one into Bool.pick; now the ternary emits in place. The C lane is untouched: intr_of without js sees no JS-only entry, so fuse, spins and the borrow inference read the same book as before. Verified through WSL on the JS lane: a scratch program exercising every entry through variable and constant arguments prints the same value interpreted and emitted, and 14 tests (num_kit, nat_ops, list_ops, list_fold, list_kit, string_ops, text_ops, float_roundtrip, u32_divmod, rand, tuple_suite, bool_algebra, fold, readback_sugars) pass run and emitted-JS alike. The cluster gates did not run (no ssh from this machine). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A def is plain when its emitted JS returns no $JMP: a self call or a closure applied in tail position rides the trampoline, so its callers must run_loop; everything else returns through the machine stack, whose depth the def DAG bounds, since defs never call forward. plain_of reads that off the body with term_any's tail flag (a closure body counts as tail, which only errs toward the trampoline). js_call then calls a plain callee directly, tail or not, so the JIT can inline it; a self tail call keeps run_jump, and a tail call to a trampolined callee absorbs its $JMPs with run_loop in place, one bounded frame. The C lane and the device read none of this. Verified through WSL on the JS lane: 73 tests across base, eval and io pass run and emitted-JS alike (three tests/run fails are the @unsafe stderr note the local runner merges into stdout; they fail identically on the parent commit), and a 10M-iteration loop over a three-def chain runs 1.14-1.42 s before, 0.45-0.54 s after, same output and RSS. The cluster gates did not run (no ssh from this machine). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MattCozendey
force-pushed
the
feature/js-plains
branch
from
September 23, 2026 11:01
6d8bdf6 to
8953224
Compare
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.
The JS lane stops paying for calls: selector intrinsics + plain defs off the trampoline
Two changes to
bend2/comp.ts, both invisible to the C emitter, the deviceand the checker (no base.bend, bend.ts or Lean surface is touched).
1. Selector intrinsics (b4ebc8b)
JS-only
OPERATIONSentries forbool_and,bool_not,bool_pick,u32_min,u32_max,f32_min,f32_max,nat_min,nat_max.Before,
U32.min(a, b)emitted a trampolined call into$U32$min$, whichtrampolined again into
$Bool$pick$— three frames and two$JMPallocations for one compare. Now the ternary emits in place.
Each template mirrors its base.bend body exactly:
pick c a b = c ? a : b,min = a < b ? a : b,max = a < b ? b : ais_lt, asBool.pickofF32.is_ltdoesNat.min/max's Succ-peeling recursion (O(min(a,b)) BigInt steps) becomesone BigInt compare
bool_pickstays JS-only because itsAis polymorphic; on native lanesno single word shape fits
2. Plain defs are called directly (8ff7eab)
A def is plain when its emitted JS can never return a
$JMP: no tailself-call and no closure applied in tail position. Every other return comes
back through the machine stack, whose depth the def call DAG bounds — Bend
has no mutual recursion and no forward calls.
js_callnow calls a plain callee directly (tail or not), so the JIT caninline it. A self tail call keeps
run_jump; a tail call into a trampolinedcallee absorbs its
$JMPs withrun_loopin place, costing one boundedframe.
plain_ofreads the property off the body withterm_any's tailflag; a closure body counts as tail, which only errs toward keeping the
trampoline.
Measurements (WSL, bun, medians of three, identical outputs and RSS)
The control row is expected: a def that never calls anything gains nothing;
its costs (per-iteration
run_jumpallocation, BigInt Nat arithmetic) arefollow-up work, not this PR.
Verification
variable arguments prints the same value interpreted and emitted.
tests/base,tests/evalandtests/iopass on boththe run path and the emitted-JS path.
perf pins and the device lane still need the usual gate pass.
🤖 Generated with Claude Code