Skip to content

Extend letrec pre-declare to ternary-wrapped self/mutual references - #56

Merged
revarbat merged 1 commit into
mainfrom
letrec-ternary-wrapped
Jul 31, 2026
Merged

Extend letrec pre-declare to ternary-wrapped self/mutual references#56
revarbat merged 1 commit into
mainfrom
letrec-ternary-wrapped

Conversation

@revarbat

Copy link
Copy Markdown
Member

Summary

  • The letrec pre-declare (self-reference: Let a self-referential let-bound closure resolve itself as an upvalue #54/v0.8.1, mutual recursion: Resolve mutual recursion between sibling let-bound closures #55/v0.9.0) only recognized a DIRECT name = function(...) ... RHS — name = cond ? function(...) ...name(...)... : function(...) ... still fell through to Op::LoadFree, staying correct but permanently interpreted.
  • Generalizes the eligibility check via collectLetrecCandidateLiterals: recursively determines whether an RHS is guaranteed to evaluate to a closure (a bare literal, or a ternary — or nested ternaries — whose branches all recursively qualify), returning every reachable FunctionLiteral node so each can be independently checked for a self/sibling reference after compiling (a ternary can produce two structurally distinct closures, not just one — this replaces the previous "was the last emitted instruction Op::MakeClosure" check with a proper per-candidate lookup by node identity).
  • Deliberately still gated on "unambiguously always a closure" — a mixed or uncertain RHS shape (a call, a non-ternary conditional, etc.) doesn't qualify, keeping let(x = x + 1)-style shadowing semantics untouched.

A separate finding, out of scope for this PR

While verifying at scale, I found (and ruled out as unrelated) a pre-existing crash: a non-tail-recursive function — closure or not, letrec or not — grows a real native C++ stack frame per call when compiled, with no depth guard, and segfaults around ~4000 levels deep. Confirmed present in stock v0.9.0 for a plain top-level function count_to(n) = n<=0?0:1+count_to(n-1); with zero closures involved; the interpreted path (OSCAD_BYTECODE_VM=0) handles the same script correctly via its own recursion guard. Not touched here — flagging for a possible follow-up.

Test plan

  • Full test suite (675 tests, up from 673) passes locally.
  • New TernaryWrappedSelfReferenceResolvesAsUpvalue (200,000-deep tail recursion, ~0.1s, proving it runs compiled) and TernaryWrappedMutualRecursionResolvesCorrectly.
  • Manually verified via the CLI: ternary-wrapped self-reference and mutual recursion both correct and fast; all previously-fixed patterns (count_to, f_1arg, reduce(), 2-way/3-way mutual recursion, let(x=x+1) shadowing, accumulate()/while()) unaffected.

🤖 Generated with Claude Code

The letrec pre-declare (self-reference: v0.8.1, mutual recursion:
v0.9.0) only recognized a DIRECT `name = function(...) ...` RHS --
`name = cond ? function(...) ...name(...)... : function(...) ...`
still fell through to Op::LoadFree, staying correct but interpreted.

Generalizes the eligibility check: collectLetrecCandidateLiterals
recursively determines whether an RHS is GUARANTEED to evaluate to a
closure (a bare literal, or a ternary -- or nested ternaries -- whose
branches all recursively qualify), returning every reachable
FunctionLiteral node so each can be independently checked for a
self/sibling reference after compiling. A ternary can produce two
structurally distinct closures, not just one, so this replaces the
single "was the last emitted instruction Op::MakeClosure" check with
a proper per-candidate lookup into chunk_.closureSites by node
identity.

Deliberately still gated on "unambiguously always a closure" -- a
mixed or uncertain RHS shape (a call, a non-ternary conditional, etc.)
doesn't qualify, keeping `let(x = x + 1)`-style shadowing semantics
untouched.

Verified: ternary-wrapped self-reference and mutual recursion both
resolve correctly and run compiled (200,000-deep tail recursion in
~0.1s). While investigating scale, found and ruled out a pre-existing,
unrelated crash: a NON-tail-recursive function (closure or not, letrec
or not) grows a real native C++ stack frame per call when compiled --
confirmed present in stock v0.9.0 for a plain top-level
FunctionDeclaration with zero closures involved, well outside the
scope of this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@revarbat
revarbat merged commit 36477f0 into main Jul 31, 2026
3 checks passed
@revarbat
revarbat deleted the letrec-ternary-wrapped branch July 31, 2026 00:14
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.

1 participant