From c0575142416609bae9c5cc555d182e38a2d94971 Mon Sep 17 00:00:00 2001 From: Ralph Date: Sat, 18 Jul 2026 00:05:53 -0700 Subject: [PATCH] refactor(codegen): explicit HIR marker for appended class-capture forwards (#6538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the arg-shape inference that identified compiler-appended class-capture forwards at bare-identifier `new C(...)` sites with an explicit provenance marker. The old `new_site_args_carry_appended_caps` (new_ctor_args.rs) checked that the trailing `new`-site args were each `Expr::LocalGet(id)` matching the id embedded in the class's synthesized `__perry_cap_` ctor params. As #6538 notes, that heuristic has 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, yet the shape check would strip those user args as cap fallbacks. - Add `cap_args_appended: u32` to `Expr::New` recording how many trailing args are appended cap forwards (excluded from stable-hashing, like `byte_offset`). - Set it to `class_captures.len()` at the two append sites (`expr_new.rs`, `expr_new/non_ident.rs`); `0` at every other construction site. `substitute_expr` preserves it through type substitution. - Thread it through the `Expr::New` codegen dispatch into `lower_new` / `lower_new_impl` as `caps_absent_from_args = cap_args_appended == 0`, and delete `new_site_args_carry_appended_caps`. Chose a field on `Expr::New` over a dedicated `Expr` variant to keep the blast radius to construction sites (a new variant would ripple through ~100+ exhaustive `Expr` matches). Behavior-preserving on today's suite: the sibling-method construction case (#6530) sets `count == 0` because `lookup_class_captures` is scope-local, and `defaults.rs` arg padding binds identically under the count (user params from the head, cap params from the decl-site snapshot). The false-positive shape is currently unreachable end-to-end (fails module init before the site runs). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/collectors/scalar_method_dispatch.rs | 1 + crates/perry-codegen/src/expr/new_dynamic.rs | 15 ++++-- crates/perry-codegen/src/lower_call/new.rs | 50 +++++++++++-------- .../src/lower_call/new_ctor_args.rs | 46 ++--------------- .../perry-codegen/src/type_analysis_tests.rs | 1 + .../tests/constructor_recursion.rs | 2 + .../tests/native_proof_regressions.rs | 8 +++ .../tests/typed_shape_descriptor.rs | 1 + .../src/analysis/value_types_tests.rs | 2 + crates/perry-hir/src/dynamic_import/tests.rs | 1 + crates/perry-hir/src/ir/expr.rs | 17 +++++++ .../perry-hir/src/lower/expr_call/globals.rs | 1 + .../src/lower/expr_call/native_module.rs | 1 + crates/perry-hir/src/lower/expr_new.rs | 8 +++ crates/perry-hir/src/lower/expr_new/member.rs | 4 ++ .../perry-hir/src/lower/expr_new/non_ident.rs | 6 +++ crates/perry-hir/src/lower/expr_object.rs | 1 + crates/perry-hir/src/lower_decl/block.rs | 1 + .../src/monomorph/substitute_expr.rs | 4 ++ .../perry-transform/src/generator/id_scan.rs | 1 + crates/perry-transform/src/inline/mod.rs | 1 + 21 files changed, 105 insertions(+), 67 deletions(-) diff --git a/crates/perry-codegen/src/collectors/scalar_method_dispatch.rs b/crates/perry-codegen/src/collectors/scalar_method_dispatch.rs index 28a3e89379..103f9339e6 100644 --- a/crates/perry-codegen/src/collectors/scalar_method_dispatch.rs +++ b/crates/perry-codegen/src/collectors/scalar_method_dispatch.rs @@ -474,6 +474,7 @@ mod tests { args: Vec::new(), type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), } } diff --git a/crates/perry-codegen/src/expr/new_dynamic.rs b/crates/perry-codegen/src/expr/new_dynamic.rs index 28151744df..9156c48392 100644 --- a/crates/perry-codegen/src/expr/new_dynamic.rs +++ b/crates/perry-codegen/src/expr/new_dynamic.rs @@ -74,6 +74,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { class_name, args, byte_offset, + cap_args_appended, .. } => { // #5253: under `--debug-symbols`, attach this `new`'s source @@ -81,7 +82,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // runtime-construct fallback (or a built-in non-constructor) renders // a location. No-op for resolved user classes (no throw fires). crate::expr::calls::emit_call_location_at(ctx, *byte_offset); - lower_new(ctx, class_name, args) + lower_new(ctx, class_name, args, *cap_args_appended) } // `new (...spread)` — spread-bearing construction. Fold every @@ -330,7 +331,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if matches!(property.as_str(), "BlockList" | "SocketAddress") { if let Expr::NativeModuleRef(mod_name) = object.as_ref() { if mod_name == "net" || mod_name == "node:net" { - return lower_new(ctx, property, args); + // NewDynamic reroute of a native-module builtin ctor + // export: no HIR cap forwards are appended here. + return lower_new(ctx, property, args, 0); } } } @@ -340,7 +343,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if property == "WebSocket" { if let Expr::NativeModuleRef(mod_name) = object.as_ref() { if mod_name == "http" || mod_name == "node:http" { - return lower_new(ctx, property, args); + // NewDynamic reroute of a native-module builtin ctor + // export: no HIR cap forwards are appended here. + return lower_new(ctx, property, args, 0); } } } @@ -469,7 +474,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" ) { - return lower_new(ctx, property, args); + // NewDynamic reroute of a native-module stream ctor + // export: no HIR cap forwards are appended here. + return lower_new(ctx, property, args, 0); } } } diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index ed5f0de953..46d07b1dcf 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -13,8 +13,7 @@ use super::field_init::{apply_field_initializers_recursive, FieldInitMode}; use super::lower_builtin_new; use super::new_ctor_args::{ bind_inline_constructor_params, call_local_constructor_symbol, lower_constructor_arg, - marshal_imported_ctor_args, new_site_args_carry_appended_caps, - restore_inline_constructor_scope, CaptureFill, + marshal_imported_ctor_args, restore_inline_constructor_scope, CaptureFill, }; use super::new_helpers::{ collect_decl_local_ids, ctor_body_calls_super, ctor_body_closure_calls_super, @@ -101,11 +100,24 @@ pub(crate) use super::capture_writeback::emit_class_capture_writeback; /// - Constructor cannot use `return ` (would terminate the /// enclosing function, not the constructor body) /// - No method dispatch or vtables — those land in Phase C.2/C.3 -pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> Result { - // Bare-identifier `new C(...)` path: the HIR `Expr::New` arm appended the - // class captures as trailing `LocalGet` args, so caps are PRESENT in - // `args`. - lower_new_impl(ctx, class_name, args, false) +pub(crate) fn lower_new( + ctx: &mut FnCtx<'_>, + class_name: &str, + args: &[Expr], + cap_args_appended: u32, +) -> Result { + // #6538: the HIR bare-identifier / anonymous-class `Expr::New` arms append + // the class's captures as trailing `LocalGet` args ONLY where the captured + // locals are in scope (the declaring function), recording the count in + // `Expr::New::cap_args_appended`. Zero means no cap forwards were appended + // here — a non-capturing class, or a bare `new C(...)` reached from a + // sibling scope (bundled zod's `ZodType.transform() { new ZodEffects(...) }`) + // where the trailing args are USER args, NOT caps. The provenance is now + // explicit, so the codegen no longer infers it from the arg shape (the old + // `new_site_args_carry_appended_caps` heuristic, which could misfire on a + // forward-referenced capture class whose user args happened to equal its + // captured locals). + lower_new_impl(ctx, class_name, args, cap_args_appended == 0) } /// Member-callee `new ns.C(...)` construct (#5437): the captures were NOT @@ -302,21 +314,15 @@ fn lower_new_impl( } }; - // #6530: the HIR bare-identifier `Expr::New` arm appends the class's - // captures as trailing `LocalGet()` args only where the captured - // locals are IN SCOPE (the class's declaring function). Inside a SIBLING - // class's method nothing is appended — bundled zod's - // `ZodType.transform() { return new ZodEffects({...}) }` — but this path - // assumed the bare form always carries them, so the tail-split treated - // the trailing USER args as cap fallbacks: the synthesized ctor received - // an empty rest array, `super(...[])` ran the parent ctor with no `def`, - // and every base-ctor field (`_def`, the bound methods) stayed - // undefined. The appended form is exactly `LocalGet(id)` paired with the - // synthesized trailing param `__perry_cap_` (same id, same order — - // `expr_new.rs` pushes `LocalGet(cid)` per captured id), so verify the - // tail matches before treating it as appended caps. - let caps_absent_from_args = - caps_absent_from_args || !new_site_args_carry_appended_caps(class, args); + // #6538: `caps_absent_from_args` is now authoritative. The bare-identifier + // path (`lower_new`) derives it from `Expr::New::cap_args_appended` — the + // explicit count of trailing cap forwards the HIR appended at THIS site — + // and the member-callee path (`lower_new_member_captured`) passes `true` + // unconditionally. This replaced the old `new_site_args_carry_appended_caps` + // shape check, which inferred presence from the arg tail matching + // `LocalGet()` against the synthesized `__perry_cap_` params + // (#6530) and could misfire on a forward-referenced capture class whose + // user args happened to equal its captured locals. // Lower the args first (constructor params). let mut lowered_args: Vec = Vec::with_capacity(args.len()); diff --git a/crates/perry-codegen/src/lower_call/new_ctor_args.rs b/crates/perry-codegen/src/lower_call/new_ctor_args.rs index 0ec8805e49..900eb466bf 100644 --- a/crates/perry-codegen/src/lower_call/new_ctor_args.rs +++ b/crates/perry-codegen/src/lower_call/new_ctor_args.rs @@ -3,10 +3,13 @@ //! //! Holds the inline-ctor param binding/restore scope, the user-arg vs //! synthesized `__perry_cap_` tail split (`CaptureFill`, -//! `inline_constructor_param_values_with_class`, -//! `new_site_args_carry_appended_caps` — #6530), rest/`arguments` packing, +//! `inline_constructor_param_values_with_class`), rest/`arguments` packing, //! imported-ctor arg marshaling, and the standalone //! `_constructor`-symbol call path. +//! +//! #6538: the presence of appended cap forwards is now carried explicitly by +//! `Expr::New::cap_args_appended` (consumed in `new.rs::lower_new`), replacing +//! the former `new_site_args_carry_appended_caps` arg-shape heuristic. use anyhow::Result; use perry_hir::{Expr, Param}; @@ -231,45 +234,6 @@ fn inline_constructor_param_values_with_class( out } -/// #6530: true when the trailing args of a bare-identifier `new C(...)` site -/// are the HIR-appended capture forwards for `class`'s synthesized -/// `__perry_cap_` constructor params. The HIR `Expr::New` arm appends -/// `LocalGet(cid)` per captured id, in cap-param order, ONLY where those -/// locals are in scope (the class's declaring function) — so each trailing -/// arg must be a `LocalGet` whose id equals the id embedded in the matching -/// param name. Any mismatch (a sibling-class method's `new ZodEffects({...})` -/// carries only user args) means the caps are absent and the tail-split must -/// not steal user args as cap fallbacks. -/// -/// Soundness of the id match: `LocalId`s come from a single MODULE-WIDE -/// counter (`LoweringContext::fresh_local` — never reset per function), so -/// `LocalGet(id)` anywhere in the module denotes the one local with that id. -/// A user expression can therefore only produce the cap-matching ids (all of -/// them, in declaration order) by referencing the captured locals themselves -/// — possible only in scopes where they are visible, which are exactly the -/// scopes where the HIR appends the caps anyway (and there the appended tail -/// follows the user args, so the tail-split still binds correctly). -pub(super) fn new_site_args_carry_appended_caps(class: &perry_hir::Class, args: &[Expr]) -> bool { - let Some(ctor) = class.constructor.as_ref() else { - return false; - }; - let cap_params: Vec<&Param> = ctor - .params - .iter() - .filter(|p| { - p.name.starts_with("__perry_cap_") && !p.is_rest && p.arguments_object.is_none() - }) - .collect(); - if cap_params.is_empty() || args.len() < cap_params.len() { - return false; - } - let tail = &args[args.len() - cap_params.len()..]; - tail.iter().zip(cap_params.iter()).all(|(arg, p)| { - matches!(arg, Expr::LocalGet(id) - if perry_hir::cap_fields::cap_field_outer_id(&p.name) == Some(*id)) - }) -} - fn pack_lowered_args_array(ctx: &mut FnCtx<'_>, args: &[String]) -> String { let cap = (args.len() as u32).to_string(); let mut current = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); diff --git a/crates/perry-codegen/src/type_analysis_tests.rs b/crates/perry-codegen/src/type_analysis_tests.rs index acb0c9dc4c..964d7b0192 100644 --- a/crates/perry-codegen/src/type_analysis_tests.rs +++ b/crates/perry-codegen/src/type_analysis_tests.rs @@ -70,6 +70,7 @@ fn hir_inferred_static_type_provides_codegen_fallback_facts() { args: vec![Expr::Integer(4)], type_args: vec![], byte_offset: 0, + cap_args_appended: 0, }, ), Some(HirType::Array(Box::new(HirType::Any))) diff --git a/crates/perry-codegen/tests/constructor_recursion.rs b/crates/perry-codegen/tests/constructor_recursion.rs index ed705a0163..815b32390a 100644 --- a/crates/perry-codegen/tests/constructor_recursion.rs +++ b/crates/perry-codegen/tests/constructor_recursion.rs @@ -81,6 +81,7 @@ fn module_with_recursive_constructor_return() -> Module { args: vec![Expr::Bool(false), Expr::LocalGet(11)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }))], else_branch: None, }], @@ -135,6 +136,7 @@ fn module_with_recursive_constructor_return() -> Module { args: vec![Expr::Bool(true), Expr::Undefined], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, })], exported_native_instances: Vec::new(), exported_func_return_native_instances: Vec::new(), diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 9d0a18b6a8..ce04ff2608 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -9268,6 +9268,7 @@ fn scalar_method_summary_module() -> Module { args: vec![number(1.25), number(2.75)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), }, Stmt::Return(Some(Expr::Call { @@ -9353,6 +9354,7 @@ fn scalar_method_numeric_local_temp_module(case: &str, mutable_temp: bool) -> Mo args: vec![number(1.25), number(2.75)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), }, Stmt::Return(Some(Expr::Call { @@ -9394,6 +9396,7 @@ fn scalar_method_boolean_predicate_module() -> Module { args: vec![number(4.0), number(2.0)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), }, Stmt::Return(Some(Expr::Call { @@ -9441,6 +9444,7 @@ fn scalar_method_boolean_public_numeric_arg_module(case: &str, arg_ty: Type) -> args: vec![number(4.0), number(2.0)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), }, Stmt::Return(Some(Expr::Call { @@ -9474,6 +9478,7 @@ fn scalar_method_boolean_public_numeric_expr_arg_module() -> Module { args: vec![number(4.0), number(2.0)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), }, Stmt::Return(Some(Expr::Call { @@ -9594,6 +9599,7 @@ fn scalar_method_int32_bitwise_module(case: &str, field_ty: Type, arg_ty: Type) args: vec![int(42), int(7)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), }, Stmt::Return(Some(Expr::Call { @@ -9805,6 +9811,7 @@ fn scalar_method_boolean_negative_module(case: &str) -> Module { args: vec![number(4.0), number(2.0)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), }, Stmt::Return(Some(Expr::Call { @@ -9831,6 +9838,7 @@ fn scalar_method_boolean_negative_module(case: &str) -> Module { args: vec![number(4.0), number(2.0)], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), }, Stmt::Return(Some(Expr::Call { diff --git a/crates/perry-codegen/tests/typed_shape_descriptor.rs b/crates/perry-codegen/tests/typed_shape_descriptor.rs index d3cd2dfdc0..6f2d1cb824 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptor.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptor.rs @@ -116,6 +116,7 @@ fn module_with_new(class: Class) -> Module { args: Vec::new(), type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }))], is_async: false, is_generator: false, diff --git a/crates/perry-hir/src/analysis/value_types_tests.rs b/crates/perry-hir/src/analysis/value_types_tests.rs index e49da4f860..1e6cbd1d3e 100644 --- a/crates/perry-hir/src/analysis/value_types_tests.rs +++ b/crates/perry-hir/src/analysis/value_types_tests.rs @@ -836,6 +836,7 @@ fn infers_common_constructed_runtime_values() { args: vec![], type_args: vec![], byte_offset: 0, + cap_args_appended: 0, }, &env, ), @@ -848,6 +849,7 @@ fn infers_common_constructed_runtime_values() { args: vec![Expr::Integer(4)], type_args: vec![], byte_offset: 0, + cap_args_appended: 0, }, &env, ), diff --git a/crates/perry-hir/src/dynamic_import/tests.rs b/crates/perry-hir/src/dynamic_import/tests.rs index 1e150f49d5..947567f154 100644 --- a/crates/perry-hir/src/dynamic_import/tests.rs +++ b/crates/perry-hir/src/dynamic_import/tests.rs @@ -157,6 +157,7 @@ fn closed_chunk_registry() -> Expr { ], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, } } diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index 84ec0fa151..aeee774d55 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -319,6 +319,23 @@ pub enum Expr { /// location, falling back to ``. Mirrors `Call.byte_offset` /// (#5247) and is excluded from stable-hashing. byte_offset: u32, + /// #6538: how many of the TRAILING `args` are compiler-appended + /// class-capture forwards, NOT user arguments. When a class nested in + /// a function captures enclosing-scope locals, `lower_class_decl` + /// synthesizes one `__perry_cap_` constructor param per captured + /// id, and the bare-identifier `new C(...)` / anonymous-class arms + /// (`expr_new.rs`, `expr_new/non_ident.rs`) push one `LocalGet()` + /// per captured id after the user args. This count records that + /// provenance EXPLICITLY so codegen no longer has to infer it from the + /// arg shape (the old `new_site_args_carry_appended_caps` heuristic, + /// which could misfire on a forward-referenced capture class whose + /// user args happened to be exactly its captured locals). `0` for + /// every other `new` site — non-capturing classes, member-callee + /// `new ns.C(...)` (caps filled from the decl-site snapshot instead), + /// synthesized options-object shapes, and transform-created nodes. + /// Excluded from stable-hashing (derived metadata, like `byte_offset`; + /// the appended `LocalGet` args it counts are themselves hashed). + cap_args_appended: u32, }, /// Dynamic new expression (new with non-identifier callee) diff --git a/crates/perry-hir/src/lower/expr_call/globals.rs b/crates/perry-hir/src/lower/expr_call/globals.rs index a7feb32017..f2d26c02b7 100644 --- a/crates/perry-hir/src/lower/expr_call/globals.rs +++ b/crates/perry-hir/src/lower/expr_call/globals.rs @@ -133,6 +133,7 @@ pub(super) fn try_global_builtins( args, type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, })); } // A missing argument to these is NOT an error in JS — the parameter is diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index 7c0f13b82d..976d08691d 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -1357,6 +1357,7 @@ pub(super) fn try_native_module_methods( args: new_args, type_args: vec![], byte_offset: 0, + cap_args_appended: 0, })); } } diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index 2f78329d59..296f6d71d1 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -120,6 +120,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), byte_offset: new_byte_offset, + cap_args_appended: 0, }); } } @@ -282,6 +283,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), byte_offset: new_byte_offset, + cap_args_appended: 0, }); } @@ -1142,6 +1144,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R args, type_args, byte_offset: new_byte_offset, + cap_args_appended: 0, }); } } @@ -1295,6 +1298,10 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R .lookup_class_captures(&lookup_name) .map(|c| c.to_vec()) .unwrap_or_default(); + // #6538: record how many trailing cap forwards we append so codegen + // reads the provenance explicitly instead of inferring it from the + // arg shape. + let cap_args_appended = class_captures.len() as u32; for cid in class_captures { args.push(Expr::LocalGet(cid)); } @@ -1303,6 +1310,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R args, type_args, byte_offset: new_byte_offset, + cap_args_appended, }) } // Non-identifier callee (e.g., new (condition ? A : B)() or new someVar()). diff --git a/crates/perry-hir/src/lower/expr_new/member.rs b/crates/perry-hir/src/lower/expr_new/member.rs index cdd50f37c4..4bbf81b872 100644 --- a/crates/perry-hir/src/lower/expr_new/member.rs +++ b/crates/perry-hir/src/lower/expr_new/member.rs @@ -50,6 +50,7 @@ pub(crate) fn lower_new_member_native( args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), byte_offset: new_byte_offset, + cap_args_appended: 0, })); } if let Some(expr) = @@ -68,6 +69,7 @@ pub(crate) fn lower_new_member_native( args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), byte_offset: new_byte_offset, + cap_args_appended: 0, })); } @@ -398,6 +400,7 @@ pub(crate) fn lower_new_member_native( args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), byte_offset: new_byte_offset, + cap_args_appended: 0, })); } if let Some((module_name, _)) = ctx.lookup_native_module(module_alias) { @@ -424,6 +427,7 @@ pub(crate) fn lower_new_member_native( args, type_args: Vec::new(), byte_offset: new_byte_offset, + cap_args_appended: 0, })); } } diff --git a/crates/perry-hir/src/lower/expr_new/non_ident.rs b/crates/perry-hir/src/lower/expr_new/non_ident.rs index 6370abfcc7..9a727310a0 100644 --- a/crates/perry-hir/src/lower/expr_new/non_ident.rs +++ b/crates/perry-hir/src/lower/expr_new/non_ident.rs @@ -179,6 +179,9 @@ pub(crate) fn lower_new_non_ident( .lookup_class_captures(&synthetic_name) .map(|c| c.to_vec()) .unwrap_or_default(); + // #6538: record the appended cap-forward count explicitly (see the + // named-class arm in `expr_new.rs` and the `Expr::New` docs). + let cap_args_appended = class_captures.len() as u32; for cid in class_captures { args.push(Expr::LocalGet(cid)); } @@ -197,6 +200,7 @@ pub(crate) fn lower_new_non_ident( args, type_args, byte_offset: new_byte_offset, + cap_args_appended, }; // The `Sequence` yields its LAST element, so the `new` site still sees // the constructed instance — the registration is pure side effect, @@ -244,6 +248,7 @@ pub(crate) fn lower_new_non_ident( args, type_args: Vec::new(), byte_offset: new_byte_offset, + cap_args_appended: 0, }); } if matches!(object.as_ref(), Expr::NativeModuleRef(module) @@ -256,6 +261,7 @@ pub(crate) fn lower_new_non_ident( args, type_args: Vec::new(), byte_offset: new_byte_offset, + cap_args_appended: 0, }); } } diff --git a/crates/perry-hir/src/lower/expr_object.rs b/crates/perry-hir/src/lower/expr_object.rs index 56cfa7e23a..413295dfcd 100644 --- a/crates/perry-hir/src/lower/expr_object.rs +++ b/crates/perry-hir/src/lower/expr_object.rs @@ -752,6 +752,7 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R args, type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }); } } diff --git a/crates/perry-hir/src/lower_decl/block.rs b/crates/perry-hir/src/lower_decl/block.rs index ba00552f81..19b199e3eb 100644 --- a/crates/perry-hir/src/lower_decl/block.rs +++ b/crates/perry-hir/src/lower_decl/block.rs @@ -1891,6 +1891,7 @@ fn lower_stmts_using_aware_inner( ], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }), ))], else_branch: Some(vec![ diff --git a/crates/perry-hir/src/monomorph/substitute_expr.rs b/crates/perry-hir/src/monomorph/substitute_expr.rs index 4dadb0fb84..a1a46f551d 100644 --- a/crates/perry-hir/src/monomorph/substitute_expr.rs +++ b/crates/perry-hir/src/monomorph/substitute_expr.rs @@ -260,6 +260,7 @@ pub(crate) fn substitute_expr(expr: &Expr, substitutions: &HashMap args, type_args, byte_offset, + cap_args_appended, } => Expr::New { class_name: class_name.clone(), args: args @@ -271,6 +272,9 @@ pub(crate) fn substitute_expr(expr: &Expr, substitutions: &HashMap .map(|t| substitute_type(t, substitutions)) .collect(), byte_offset: *byte_offset, + // Type substitution rewrites arg/type contents but preserves arg + // COUNT and order, so the appended cap forwards stay trailing. + cap_args_appended: *cap_args_appended, }, // Class/Enum references diff --git a/crates/perry-transform/src/generator/id_scan.rs b/crates/perry-transform/src/generator/id_scan.rs index a31f9cc33e..aed9c495dd 100644 --- a/crates/perry-transform/src/generator/id_scan.rs +++ b/crates/perry-transform/src/generator/id_scan.rs @@ -563,6 +563,7 @@ mod tests { args: vec![closure], type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }], }; diff --git a/crates/perry-transform/src/inline/mod.rs b/crates/perry-transform/src/inline/mod.rs index 9ad37d463c..92f1937cc0 100644 --- a/crates/perry-transform/src/inline/mod.rs +++ b/crates/perry-transform/src/inline/mod.rs @@ -709,6 +709,7 @@ mod tests { args: Vec::new(), type_args: Vec::new(), byte_offset: 0, + cap_args_appended: 0, }) }