Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ mod tests {
args: Vec::new(),
type_args: Vec::new(),
byte_offset: 0,
cap_args_appended: 0,
}),
}
}
Expand Down
15 changes: 11 additions & 4 deletions crates/perry-codegen/src/expr/new_dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
class_name,
args,
byte_offset,
cap_args_appended,
..
} => {
// #5253: under `--debug-symbols`, attach this `new`'s source
// `file:line` so a "X is not a constructor" throw from `lower_new`'s
// 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 <callee>(...spread)` — spread-bearing construction. Fold every
Expand Down Expand Up @@ -330,7 +331,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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);
}
}
}
Expand All @@ -340,7 +343,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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);
}
}
}
Expand Down Expand Up @@ -469,7 +474,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
"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);
}
}
}
Expand Down
50 changes: 28 additions & 22 deletions crates/perry-codegen/src/lower_call/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -101,11 +100,24 @@ pub(crate) use super::capture_writeback::emit_class_capture_writeback;
/// - Constructor cannot use `return <expr>` (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<String> {
// 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<String> {
// #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
Expand Down Expand Up @@ -302,21 +314,15 @@ fn lower_new_impl(
}
};

// #6530: the HIR bare-identifier `Expr::New` arm appends the class's
// captures as trailing `LocalGet(<cap_id>)` 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_<id>` (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(<cap_id>)` against the synthesized `__perry_cap_<id>` 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<String> = Vec::with_capacity(args.len());
Expand Down
46 changes: 5 additions & 41 deletions crates/perry-codegen/src/lower_call/new_ctor_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
//!
//! Holds the inline-ctor param binding/restore scope, the user-arg vs
//! synthesized `__perry_cap_<id>` 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
//! `<class>_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};
Expand Down Expand Up @@ -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_<id>` 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)]);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/type_analysis_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/tests/constructor_recursion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}],
Expand Down Expand Up @@ -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(),
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-codegen/tests/native_proof_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/tests/typed_shape_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-hir/src/analysis/value_types_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@ fn infers_common_constructed_runtime_values() {
args: vec![],
type_args: vec![],
byte_offset: 0,
cap_args_appended: 0,
},
&env,
),
Expand All @@ -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,
),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/dynamic_import/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ fn closed_chunk_registry() -> Expr {
],
type_args: Vec::new(),
byte_offset: 0,
cap_args_appended: 0,
}
}

Expand Down
17 changes: 17 additions & 0 deletions crates/perry-hir/src/ir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,23 @@ pub enum Expr {
/// location, falling back to `<anonymous>`. 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_<id>` 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(<id>)`
/// 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)
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/lower/expr_call/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/lower/expr_call/native_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,7 @@ pub(super) fn try_native_module_methods(
args: new_args,
type_args: vec![],
byte_offset: 0,
cap_args_appended: 0,
}));
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-hir/src/lower/expr_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}
Expand Down Expand Up @@ -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,
});
}

Expand Down Expand Up @@ -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,
});
}
}
Expand Down Expand Up @@ -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));
}
Expand All @@ -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()).
Expand Down
Loading
Loading