Skip to content

fix(transform): don't deforest producers called inside closures (#5136) - #5179

Merged
proggeramlug merged 2 commits into
mainfrom
worktree-fix-uuid-v5-segfault
Jun 15, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
worktree-fix-uuid-v5-segfault

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #5136 — uuid's v5() segfaults (SIGSEGV / exit 139) under perry.compilePackages.

Root cause

The interprocedural deforestation pass (crates/perry-transform/src/deforest) rewrites a "producer" function — const out = []; …push…; return out; — to take a trailing __deforest_out accumulator parameter, and rewrites its call sites to pass one.

Neither the producer-detection scans nor the phase-3 call-site rewriter descend into closure bodies: the shared walk_expr_children helper visits an Expr::Closure's parameter defaults but never its statement body. So when a producer's only caller lives inside a closure (e.g. one returned from a factory), the producer's signature gains the extra param while the in-closure call site keeps the original arity:

define double @…__helper(double %arg1, double %arg5) …      ; +1 param after rewrite
…
%r4 = call double @…__helper(double %r3)                     ; in-closure call: still 1 arg

That arity mismatch makes __deforest_out a garbage register, the function pushes into a junk pointer, and .length on the bogus result dereferences null → SIGSEGV.

In uuid this is exactly v5: v35() returns a generateUUID closure that calls the module-level stringToBytes producer.

The bug is general (not compile-package specific) — it reproduces in a plain .ts file:

function helper(str: string) { const b: number[] = []; for (let i=0;i<str.length;++i) b.push(str.charCodeAt(i)); return b; }
function factory() { function generate(v: string) { return helper(v).length; } return generate; }
const gen = factory();
console.log(gen("perry"));   // exit 139 before this fix

Fix

Add a fourth detection pass that bails on any producer referenced inside a closure body, so both inside- and outside-closure call sites keep the original signature. This is conservative — it only ever shrinks the candidate set (forgoes the optimization for that shape), and can never miscompile. Teaching the rewriter to descend into and correctly re-scope closure bodies is left as a follow-up, consistent with the other deferred closure-walking refinements already noted in detect.rs.

Verification

  • Exact issue repro (real uuid 9.0.1 source, compiled via compilePackages) now matches Node byte-for-byte: 6cb3836f-339d-52d8-acc6-8751229b61cf true 5.
  • New unit tests: rejects_producer_called_inside_closure (bails) and still_deforests_when_caller_is_not_a_closure (control — optimization still fires).
  • New end-to-end self-checking regression: test-files/test_deforest_closure_caller.ts.
  • cargo test -p perry-transform green (32 + 8 deforest tests).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an arity mismatch issue (issue #5136) where producer functions referenced inside closure bodies could be incorrectly selected for rewriting, leading to compilation failures or incorrect transformations. Producers used within closures are now safely excluded from transformation candidates.
  • Tests
    • Added new unit tests covering producers called only from within closures (no synthetic parameter added) and from non-closure contexts (synthetic parameter added).
    • Added a regression test for the closure-based caller scenario from #5136.

The interprocedural deforestation pass rewrites a "producer" function
(`const out = []; …push…; return out;`) to take a trailing accumulator
parameter and rewrites its call sites to pass one. Neither the
producer-detection scans nor the phase-3 call-site rewriter descended
into closure bodies — the shared `walk_expr_children` helper visits an
`Expr::Closure`'s parameter defaults but never its statement body.

So when a producer's only caller lived inside a closure (e.g. one
returned from a factory), the producer's signature gained the extra
param while the in-closure call site kept the original arity. That
arity mismatch lowered to a garbage accumulator pointer and a SIGSEGV.

Surfaced in the wild as `uuid`'s `v5()` crashing (exit 139) under
`perry.compilePackages`: its `stringToBytes` producer is called from
the `generateUUID` closure that `v35()` returns.

Fix: add a detection pass that bails on any producer referenced inside
a closure body, so both inside- and outside-closure call sites keep the
original signature. This is conservative — it only ever shrinks the
candidate set (forgoes an optimization), never miscompiles. Teaching
the rewriter to descend into and re-scope closure bodies is left as a
follow-up, consistent with the other deferred closure-walking
refinements noted in detect.rs.

Adds unit tests (in-closure caller bails; non-closure caller still
deforests) and an end-to-end regression in test-files/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f4d0739a-c5cf-4767-b30c-22c8ea7f2e1c

📥 Commits

Reviewing files that changed from the base of the PR and between 45888c9 and ce74fce.

📒 Files selected for processing (1)
  • crates/perry-transform/src/deforest/scan.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-transform/src/deforest/scan.rs

📝 Walkthrough

Walkthrough

Adds a new scan pass that walks module statement trees to find producer FuncIds referenced inside any closure body. detect_producers uses this as a fourth filtering stage to exclude such producers from deforestation, preventing arity mismatches and SIGSEGV when a producer is referenced in a closure context.

Changes

Closure-safe deforestation guard

Layer / File(s) Summary
Closure-body producer-reference scanner
crates/perry-transform/src/deforest/scan.rs
Adds scan_producers_used_in_closures plus scan_stmt_for_closures, scan_expr_for_closures, ClosureFuncRefCollector, and StmtRefAllWalker, which together walk all statement/expression trees (including nested closures) and collect candidate producer FuncIds found inside closure bodies.
Fourth-pass filter in detect_producers and re-export wiring
crates/perry-transform/src/deforest/detect.rs, crates/perry-transform/src/deforest/mod.rs
Calls scan_producers_used_in_closures across all function, init, and class method/constructor bodies, then removes any candidate whose FuncId appears in the closure-use set; touches the scan re-export line.
Unit tests and TypeScript regression test
crates/perry-transform/src/deforest/tests.rs, test-files/test_deforest_closure_caller.ts
Adds rejects_producer_called_inside_closure and still_deforests_when_caller_is_not_a_closure unit tests, plus a TypeScript end-to-end regression reproducing the prior SIGSEGV scenario (checksum assertion of 562).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A closure once hid a producer inside,
And the deforester changed its arity with pride —
But SIGSEGV struck with a terrible thud!
Now a fourth pass checks, deep in the bud:
"No closures shall trick me!" the rabbit declared,
And the encoder ran safely, the checksum compared. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: preventing deforestation of producers called inside closures, directly addressing issue #5136.
Description check ✅ Passed The description comprehensively covers the root cause, the fix, verification steps, and includes detailed technical context and test results.
Linked Issues check ✅ Passed The code changes fully address #5136 by implementing a fourth detection pass that prevents deforestation when producers are referenced inside closure bodies, fixing the arity mismatch that caused SIGSEGV.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the deforestation closure-walking bug: new closure detection logic, tests for the fix, and a regression test file.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-uuid-v5-segfault

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-transform/src/deforest/scan.rs`:
- Around line 186-257: The wildcard pattern `_ => {}` in the match statement for
`scan_stmt_for_closures` (ending around line 256) silently skips five statement
variants that should be explicitly handled. Replace the catch-all pattern with
explicit match arms for the `Break`, `Continue`, `LabeledBreak`,
`LabeledContinue`, and `PreallocateBoxes` statement variants, each with `{}`
bodies, or replace the catch-all with a single comment explaining why these
variants intentionally require no processing. Apply the same fix to the
`StmtRefAllWalker::visit_stmt` method at the corresponding location (line 419),
ensuring both functions consistently handle all statement variants explicitly
rather than silently skipping them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 01b77818-a55d-432a-a15a-2e1044dc28e2

📥 Commits

Reviewing files that changed from the base of the PR and between 7695ff5 and 45888c9.

📒 Files selected for processing (5)
  • crates/perry-transform/src/deforest/detect.rs
  • crates/perry-transform/src/deforest/mod.rs
  • crates/perry-transform/src/deforest/scan.rs
  • crates/perry-transform/src/deforest/tests.rs
  • test-files/test_deforest_closure_caller.ts

…eRabbit)

Replace the `_ => {}` catch-alls in `scan_stmt_for_closures` and
`StmtRefAllWalker::visit_stmt` with explicit leaf arms (Break, Continue,
LabeledBreak, LabeledContinue, PreallocateBoxes). These variants carry
no nested expressions or statements so behavior is unchanged, but
dropping the wildcard means a future `Stmt` variant that does carry a
closure forces a compile error here instead of being silently skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug
proggeramlug merged commit 817ed77 into main Jun 15, 2026
15 checks passed
@proggeramlug
proggeramlug deleted the worktree-fix-uuid-v5-segfault branch June 15, 2026 08:52
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.

compilePackages: uuid v5() segfaults (SIGSEGV)

1 participant