diff --git a/crates/perry-hir/src/lower/expr_object.rs b/crates/perry-hir/src/lower/expr_object.rs index dee4cf761f..dcd183560b 100644 --- a/crates/perry-hir/src/lower/expr_object.rs +++ b/crates/perry-hir/src/lower/expr_object.rs @@ -668,11 +668,58 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R } } ast::Prop::Shorthand(_) => {} + // Method shorthand `m() { … }` with a static key lowers exactly + // like `m: function () { … }` (dynamic `this`, a plain + // pointer-bearing field slot), so it joins the closed-shape + // record route instead of falling to the by-name/shape-cache + // `Expr::Object` path whose field READS degrade to by-name + // dispatch (114–461 ns per literal on a 3-prop object vs 44 ns + // for the function-expression spelling). What is NOT admitted: + // `super` (needs the object home slot the IIFE route + // provides), async/generator methods (their own lowering + // shapes), computed keys, and `__proto__`. + ast::Prop::Method(method) => { + if is_noncomputed_proto_key(&method.key) { + return false; + } + match &method.key { + ast::PropName::Ident(_) | ast::PropName::Str(_) | ast::PropName::Num(_) => {} + _ => return false, + } + if method.function.is_async + || method.function.is_generator + || function_body_uses_super(&method.function) + { + return false; + } + } _ => return false, } } true } + + /// Conservative: any `super.x` / `super[x]` / `super(...)` anywhere in the + /// body — nested functions and classes included — keeps the method on the + /// home-object-carrying route. + fn function_body_uses_super(function: &ast::Function) -> bool { + use swc_ecma_visit::{Visit, VisitWith}; + struct Finder(bool); + impl Visit for Finder { + fn visit_super_prop_expr(&mut self, _: &ast::SuperPropExpr) { + self.0 = true; + } + fn visit_callee(&mut self, callee: &ast::Callee) { + if matches!(callee, ast::Callee::Super(_)) { + self.0 = true; + } + callee.visit_children_with(self); + } + } + let mut finder = Finder(false); + function.visit_with(&mut finder); + finder.0 + } // #6812 (w16): `{}` — the builder-pattern seed — lowers to `new // __AnonShape_()`, a unique 0-field shape-only class per // source site, instead of the legacy class-0 empty object. See @@ -695,7 +742,15 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R cap_args_appended: 0, }); } - if is_closed_shape(obj) { + // Directly exported method literals stay on the seeded IIFE route below: + // a consumer module's guarded direct call on an imported object keys on + // the producer's shape-only seed class and slot order, and this routing + // is what that capability was built against. + let exported_method_literal = prefer_exported_method_shape_seed + && obj.props.iter().any(|prop| { + matches!(prop, ast::PropOrSpread::Prop(p) if matches!(p.as_ref(), ast::Prop::Method(_))) + }); + if is_closed_shape(obj) && !exported_method_literal { let mut fields: Vec<(String, Type, Expr)> = Vec::new(); let mut bail = false; let mut seen = std::collections::HashSet::new(); @@ -783,6 +838,61 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R }; fields.push((name, ty, value)); } + ast::Prop::Method(method) => { + // `is_closed_shape` admitted only static-key, non-async, + // non-generator, super-free methods. Lower the body with + // the shared method lowering, then take the closure as a + // dynamic-`this` value — identical to the function- + // expression spelling — so no post-construction `this` + // patch is needed and `t.m()` / `f.call(other)` both bind + // the call receiver, as the spec says. + let Some((mkey, value_expr, _uses_this)) = lower_method_prop(ctx, method)? + else { + bail = true; + break; + }; + let MethodKeyKind::Static(key) = mkey else { + bail = true; + break; + }; + if !seen.insert(key.clone()) { + bail = true; + break; + } + let value = match value_expr { + Expr::Closure { + func_id, + params, + return_type, + body, + captures, + mutable_captures, + captures_this: _, + captures_new_target, + enclosing_class: _, + is_arrow, + is_async, + is_generator, + is_strict, + } => Expr::Closure { + func_id, + params, + return_type, + body, + captures, + mutable_captures, + captures_this: false, + captures_new_target, + enclosing_class: None, + is_arrow, + is_async, + is_generator, + is_strict, + }, + other => other, + }; + fields.push((key, Type::Any, value)); + } _ => unreachable!(), } } diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index 9f29aacd44..a317c71543 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -163,23 +163,44 @@ const computed = { [key]() { return 1; } }; .unwrap_or_else(|| panic!("missing init for {name}")) }; - let Expr::Object(props) = local_init("fast") else { + // A static-key, super-free method literal is a closed-shape RECORD: it + // lowers to the anonymous-shape class allocation (fixed-slot reads, no + // builder IIFE), with each method carried as a dynamic-`this` closure + // argument — the function-expression spelling's semantics — so no + // post-construction `this` patch exists to get wrong. + let Expr::New { + class_name, args, .. + } = local_init("fast") + else { panic!( - "static method literal should be a direct object: {:#?}", + "static method literal should be a closed-shape record allocation: {:#?}", hir.init ); }; + assert!( + class_name.starts_with("__AnonShape_"), + "record class expected, got {class_name}" + ); + let record = hir + .classes + .iter() + .find(|class| &class.name == class_name) + .unwrap_or_else(|| panic!("record class {class_name} must be synthesized")); assert_eq!( - props + record + .fields .iter() - .map(|(key, _)| key.as_str()) + .map(|field| field.name.as_str()) .collect::>(), - ["plain", "captured", "dynamicThis"] + ["plain", "captured", "dynamicThis"], + "field order must follow source order" ); + assert_eq!(args.len(), 3); assert!(matches!( - &props[2].1, + &args[2], Expr::Closure { - captures_this: true, + captures_this: false, + enclosing_class: None, .. } ));