Skip to content

fix(deforest): rewrite producer call sites in class member bodies - #5772

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:deforest-class-member-call-sites
Jun 28, 2026
Merged

fix(deforest): rewrite producer call sites in class member bodies#5772
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:deforest-class-member-call-sites

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Problem

Deforestation promotes an array-producer function

function f() { const out = []; /* ...out.push(x)... */ return out; }

to take the accumulator as a synthetic trailing __deforest_out parameter, then rewrites every call site to allocate the array and pass it in.

detect_producers scans free functions, module-init, and class member bodies (methods + constructors) when deciding whether a producer is safe to rewrite — a plain let v = f() inside a method counts as a supported call site, so the producer is admitted. But phase 3 only rewrote call sites in module-init and free functions.

So a producer whose call site lives in a class method had its signature rewritten (gaining the out-param) while the method's call kept its original arity. Codegen then passes undefined for the missing argument, and the body runs out.push(...) / return out on a non-array:

function build() { const out = []; out.push(1); return out; }   // becomes build(__deforest_out)
class C { m() { const v = build(); return v.length; } }         // call site NOT rewritten → build(undefined)
new C().m();   // SIGSEGV (push on undefined)

When the (now undefined) result is later spread / for…of'd, it surfaces instead as TypeError: is not iterable.

This is the same arity-mismatch class as the in-closure bail-out (#5136). There the fix was to drop the producer; method/ctor/accessor bodies are ordinary statement lists, so we can rewrite them rather than bail.

Fix

Make detection, fresh-id seeding, and the phase-3 rewrite cover the identical complete set of code bodies — module-init, free functions, constructors, methods, getters, setters, and static methods:

  • deforest/mod.rs (phase 3): rewrite producer call sites in every class member body, not just module-init + free functions.
  • deforest/detect.rs: extend the three safety scans (funcref misuse, unsafe call sites, in-closure usage) to getters/setters/static methods so admission and rewrite stay symmetric (previously they covered only methods + constructors).
  • deforest/walk.rs (max_local_id): include getter/setter/static-method locals so the synthetic-id seed can't collide with a local already living in those bodies.

Test

Adds deforests_producer_called_from_class_method: a producer called via let v = helper() inside a class method is deforested and its call site is rewritten to pass the accumulator (the surviving call's arity matches the rewritten producer). Full perry-transform suite green (40 passed).

Summary by CodeRabbit

  • Bug Fixes

    • Improved deforestation so call sites inside class methods, getters, setters, and static methods are rewritten correctly.
    • Fixed producer detection to consider these class member bodies when deciding whether a producer can be transformed.
    • Prevented identifier conflicts by including class accessors and static members in local ID tracking.
  • Tests

    • Added regression coverage for producer calls inside class methods.

Deforestation promotes an array-producer function (`function f(){ const
out = []; ...out.push(x)...; return out; }`) to take the accumulator as a
synthetic trailing `__deforest_out` parameter, then rewrites every call
site to allocate the array and pass it in.

`detect_producers` scans free functions, module-init, AND class
member bodies (methods + constructors) when deciding whether a producer
is safe to rewrite — a plain `let v = f()` inside a method is a
"supported" call site, so the producer is admitted. But phase 3 only
rewrote call sites in module-init and free functions. A producer whose
call site lived in a class method therefore had its signature rewritten
(gaining the out-param) while the method's call kept its original arity.
Codegen then passes `undefined` for the missing argument, so the body
runs `out.push(...)` / `return out` on a non-array — a SIGSEGV, or a
downstream `TypeError: is not iterable` when the returned value is later
spread / for-of'd.

This is the same arity-mismatch class as the in-closure bail-out
(PerryTS#5136); there the fix was to drop the producer, but method bodies are
ordinary statement lists we can rewrite rather than bail on.

Fix: make detection, fresh-id seeding, and the phase-3 rewrite all cover
the identical complete set of code bodies — module-init, free functions,
constructors, methods, getters, setters, and static methods:
- `deforest/mod.rs` (phase 3): rewrite call sites in every class member
  body, not just module-init + free functions.
- `deforest/detect.rs`: extend the three safety scans (funcref misuse,
  unsafe call sites, in-closure usage) to getters/setters/static methods
  so admission and rewrite stay symmetric.
- `deforest/walk.rs` (`max_local_id`): include getter/setter/static-
  method locals so the synthetic-id seed can't collide with a local
  already living in those bodies.

Adds a regression test: a producer called via `let v = helper()` inside
a class method is deforested AND its call site is rewritten to pass the
accumulator (arity matches the rewritten producer).
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Producer deforestation is extended to cover class getters, setters, and static_methods. Three validation passes in detect_producers, the max_local_id walk, and the phase-3 call-site rewriting step in run all gain iteration over these additional class member bodies. A regression test verifies detection and call-site arity rewriting for a producer called from a class method.

Changes

Deforest class member coverage

Layer / File(s) Summary
Producer validation and LocalId scanning
crates/perry-transform/src/deforest/detect.rs, crates/perry-transform/src/deforest/walk.rs
Three validation passes (FuncRef misuse, unsupported call-site shape, used-in-closure) each gain traversal of class getter/setter/static method bodies. max_local_id similarly extended to scan params and bodies of those members.
Call-site rewriting in class members
crates/perry-transform/src/deforest/mod.rs
run phase-3 now iterates constructor, methods, getters, setters, and static methods per class and calls rewrite_call_sites_in_stmts on each body.
Regression test
crates/perry-transform/src/deforest/tests.rs
deforests_producer_called_from_class_method builds a producer and a class method calling it, asserts detection, checks the producer gains the accumulator param, and verifies the call site is rewritten to arity 1.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • PerryTS/perry#5179: Added the "used inside closures" disqualification pass in detect.rs that this PR now extends to class getters/setters/static methods.

Poem

🐇 Hop through the getter, leap past the setter,
Static methods too — now coverage is better!
The deforester scans each nook of the class,
Rewrites call sites so no stale arity shall pass.
No local id left behind, no body unseen —
The warren is tidy, the transform is clean! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: rewriting producer call sites in class member bodies.
Description check ✅ Passed The description is mostly complete: it explains the problem, fix, and test, with only minor template sections omitted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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.

🧹 Nitpick comments (2)
crates/perry-transform/src/deforest/mod.rs (1)

177-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Centralize class-member traversal before it drifts again.

This member list now lives in detect.rs, walk.rs, and run(), which is the exact contract that got out of sync here. A shared helper over class-member Function bodies would make future member-kind additions much harder to miss.

🤖 Prompt for 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.

In `@crates/perry-transform/src/deforest/mod.rs` around lines 177 - 230, The
class-member traversal logic is duplicated across multiple places and is already
drifting out of sync; centralize it into a shared helper that walks all
class-member Function bodies. Refactor the member-body rewriting in the deforest
pipeline (the logic around module.classes, class.constructor, class.methods,
class.getters, class.setters, and class.static_methods) to call that helper so
future member-kind additions only need one update.
crates/perry-transform/src/deforest/tests.rs (1)

282-398: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an accessor/static-member regression case.

This only exercises class.methods, so the new getter/setter/static-method loops in detect_producers, max_local_id, and phase 3 can still regress without any test failing. Parameterizing the fixture by member kind would cover the new surface with little extra code.

🤖 Prompt for 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.

In `@crates/perry-transform/src/deforest/tests.rs` around lines 282 - 398, Add a
regression test that covers class accessor/static member bodies, not just
class.methods, because the new getter/setter/static-method handling in
detect_producers, max_local_id, and phase 3 can still miss call-site rewrites.
Reuse the existing deforestation fixture pattern in deforest/tests.rs by
parameterizing the member kind and asserting the producer still gains the
synthetic accumulator param and every FuncRef(1) call inside the member body is
rewritten to arity 1. Keep the same style as
deforests_producer_called_from_class_method so the new test exercises the same
run() pipeline across getter, setter, and static method cases.
🤖 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.

Nitpick comments:
In `@crates/perry-transform/src/deforest/mod.rs`:
- Around line 177-230: The class-member traversal logic is duplicated across
multiple places and is already drifting out of sync; centralize it into a shared
helper that walks all class-member Function bodies. Refactor the member-body
rewriting in the deforest pipeline (the logic around module.classes,
class.constructor, class.methods, class.getters, class.setters, and
class.static_methods) to call that helper so future member-kind additions only
need one update.

In `@crates/perry-transform/src/deforest/tests.rs`:
- Around line 282-398: Add a regression test that covers class accessor/static
member bodies, not just class.methods, because the new
getter/setter/static-method handling in detect_producers, max_local_id, and
phase 3 can still miss call-site rewrites. Reuse the existing deforestation
fixture pattern in deforest/tests.rs by parameterizing the member kind and
asserting the producer still gains the synthetic accumulator param and every
FuncRef(1) call inside the member body is rewritten to arity 1. Keep the same
style as deforests_producer_called_from_class_method so the new test exercises
the same run() pipeline across getter, setter, and static method cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9bb37e94-34dd-4068-9992-8e044f4b2b36

📥 Commits

Reviewing files that changed from the base of the PR and between 1f4a2c5 and 029f1ab.

📒 Files selected for processing (4)
  • crates/perry-transform/src/deforest/detect.rs
  • crates/perry-transform/src/deforest/mod.rs
  • crates/perry-transform/src/deforest/tests.rs
  • crates/perry-transform/src/deforest/walk.rs

@proggeramlug
proggeramlug merged commit 5d47364 into PerryTS:main Jun 28, 2026
15 checks passed
proggeramlug added a commit that referenced this pull request Jun 29, 2026
… rewrite (#5780 cluster A) (#5788)

Class methods / constructors / accessors that reference `super` now act
as bail-out bodies in the deforest pass.  Commit 5d47364 (#5772)
extended the DEFOREST phase-3 call-site rewriter to class member bodies
to fix the arity-mismatch SIGSEGV (#5136-class).  A member body that
also uses `super.x`, `super[e]`, or `super(…)` had its [[HomeObject]]
setup corrupted by the synthetic-local introduction, causing a
`TypeError: Cannot convert undefined or null to object` at runtime
(~24 test262 cases, cluster A of #5780).

Fix (detect.rs): `body_has_super` walks a member-body statement list
for any `Expr::SuperPropertyGet / SuperCall / SuperMethodCall /
ObjectSuper*` variant.  In the third (unsafe-call-site) detection pass
any producer called from a super-using body is added to the
`unsupported_call` exclusion set, so the producer is dropped from the
candidate map before the rewrite phase begins.

Belt-and-suspenders (mod.rs): the phase-3 rewriter skips any class
member body for which `body_has_super` returns true, ensuring the
[[HomeObject]] frame is never disturbed even if the detect exclusion
were somehow bypassed.

Super-free class methods continue to be deforested as before (#5772
fix preserved); only bodies that actually use `super` take the bail
path — a missed optimisation, not a correctness loss.

Tests: two new unit tests in deforest/tests.rs —
`rejects_deforest_when_class_method_uses_super` (the regression) and
`still_deforests_when_method_has_no_super` (control / #5772 guard).

Co-authored-by: Claude <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.

1 participant