Summary
Follow-up to #6530 / PR #6537 (CodeRabbit finding). The codegen currently identifies HIR-appended capture forwards at bare-identifier new C(...) sites by shape: the trailing args must each be Expr::LocalGet(id) whose id equals the one embedded in the matching synthesized __perry_cap_<id> ctor param (exact ids, declaration order, all-or-nothing) — see new_site_args_carry_appended_caps in crates/perry-codegen/src/lower_call/new.rs.
LocalIds are module-unique (single fresh_local counter), so a user expression can only produce the matching ids by referencing the captured locals themselves. That leaves one theoretical false positive: a FORWARD-referenced capture-class constructed inside an earlier closure of the declaring scope, passing exactly its captured locals (in capture order) as the final user args — there the HIR appends nothing (lookup_class_captures misses pre-declaration) and the codegen would strip those user args as cap fallbacks.
Why this is currently unreachable
Fixtures for that shape (CJS module function: const h = ...; const mk = () => new Later(h); class Later extends Base { /* captures h */ } ...) fail module init on main before the construction site ever runs — require() yields undefined exports (pre-existing, reproduces identically on a pre-#6537 main build; both the forward-arrow and even backward-arrow variants of this fixture fail init). So the ambiguity cannot currently be observed end-to-end.
Proposed fix
Make the provenance explicit instead of inferred: either
- a dedicated HIR marker for appended capture forwards (e.g. an
Expr::ClassCaptureForward(LocalId) variant, or a cap_args_appended: bool / count field on Expr::New), emitted at the two append sites in crates/perry-hir/src/lower/expr_new.rs (+ expr_new/non_ident.rs), consumed by lower_new_impl; then delete the shape check, or
- fix the init failure family above first, then extend the gap suite with the adversarial shapes and revisit.
Blast radius of the HIR variant is the main cost (many exhaustive Expr matches across perry-codegen collectors/transform), which is why it was split out of #6537.
Summary
Follow-up to #6530 / PR #6537 (CodeRabbit finding). The codegen currently identifies HIR-appended capture forwards at bare-identifier
new C(...)sites by shape: the trailing args must each beExpr::LocalGet(id)whose id equals the one embedded in the matching synthesized__perry_cap_<id>ctor param (exact ids, declaration order, all-or-nothing) — seenew_site_args_carry_appended_capsincrates/perry-codegen/src/lower_call/new.rs.LocalIds are module-unique (singlefresh_localcounter), so a user expression can only produce the matching ids by referencing the captured locals themselves. That leaves one theoretical false positive: a FORWARD-referenced capture-class constructed inside an earlier closure of the declaring scope, passing exactly its captured locals (in capture order) as the final user args — there the HIR appends nothing (lookup_class_capturesmisses pre-declaration) and the codegen would strip those user args as cap fallbacks.Why this is currently unreachable
Fixtures for that shape (CJS module function:
const h = ...; const mk = () => new Later(h); class Later extends Base { /* captures h */ } ...) fail module init on main before the construction site ever runs —require()yieldsundefinedexports (pre-existing, reproduces identically on a pre-#6537 main build; both the forward-arrow and even backward-arrow variants of this fixture fail init). So the ambiguity cannot currently be observed end-to-end.Proposed fix
Make the provenance explicit instead of inferred: either
Expr::ClassCaptureForward(LocalId)variant, or acap_args_appended: bool/ count field onExpr::New), emitted at the two append sites incrates/perry-hir/src/lower/expr_new.rs(+expr_new/non_ident.rs), consumed bylower_new_impl; then delete the shape check, orBlast radius of the HIR variant is the main cost (many exhaustive
Exprmatches across perry-codegen collectors/transform), which is why it was split out of #6537.