Skip to content

Commit 28cf11b

Browse files
author
Ralph Kuepper
committed
fix(hir): compose imported methods with static literals
1 parent cd175db commit 28cf11b

5 files changed

Lines changed: 144 additions & 6 deletions

File tree

‎crates/perry-hir/src/lower/context.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ impl LoweringContext {
199199
mixin_funcs: HashMap::new(),
200200
anon_shape_classes: HashMap::new(),
201201
anon_shape_fields: HashMap::new(),
202+
prefer_exported_method_shape_seed: false,
202203
forward_class_names: std::collections::HashSet::new(),
203204
forward_class_decl_depth: std::collections::HashMap::new(),
204205
class_renames: std::collections::HashMap::new(),

‎crates/perry-hir/src/lower/expr_object.rs‎

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,11 @@ fn accessor_key_expr(key: MethodKeyKind) -> Expr {
632632
}
633633

634634
pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> Result<Expr> {
635+
// A directly exported object is the producer boundary for #8775. Consume
636+
// the marker here so nested literals continue through their ordinary
637+
// lowering paths.
638+
let prefer_exported_method_shape_seed =
639+
std::mem::take(&mut ctx.prefer_exported_method_shape_seed);
635640
// Phase 3: closed-shape object literals lower to `new __AnonShape_N()`
636641
// so downstream field access hits the direct-GEP fast path. The
637642
// anon class is synthesized as a shape-only class with constructor
@@ -1074,7 +1079,8 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R
10741079
collect_local_refs_expr(value, &mut refs, &mut visited_closures);
10751080
!refs.contains(&param_id)
10761081
};
1077-
let can_emit_static_object = has_method
1082+
let can_emit_static_object = !prefer_exported_method_shape_seed
1083+
&& has_method
10781084
&& !has_spread
10791085
&& !has_accessor
10801086
&& !has_computed
@@ -1105,8 +1111,58 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R
11051111
return Ok(Expr::Object(props));
11061112
}
11071113

1108-
// Pass 2: build the IIFE wrapper. `__o` starts as an empty object
1109-
// and each op mutates it in source order.
1114+
// Imported-object capabilities need a producer-authoritative non-zero
1115+
// class and stable slot order. Keep directly exported, static-key
1116+
// method literals in the source-ordered IIFE, but seed it with a
1117+
// shape-only anonymous class instead of `{}`. This composes with the
1118+
// direct-object optimization above: non-exported literals still skip
1119+
// the IIFE, while exported literals retain the metadata required by a
1120+
// consumer's guarded direct call.
1121+
let static_shape_seed = if prefer_exported_method_shape_seed
1122+
&& !has_spread
1123+
&& !has_accessor
1124+
&& !has_computed
1125+
&& !has_proto_setter
1126+
{
1127+
let mut names = Vec::new();
1128+
let mut seen = std::collections::HashSet::new();
1129+
let mut eligible = true;
1130+
for op in &ops {
1131+
let name = match op {
1132+
SpreadOp::Set {
1133+
key: Expr::String(name),
1134+
infer_name: false,
1135+
..
1136+
}
1137+
| SpreadOp::MethodByName { key: name, .. } => name,
1138+
_ => {
1139+
eligible = false;
1140+
break;
1141+
}
1142+
};
1143+
if seen.insert(name.clone()) {
1144+
names.push(name.clone());
1145+
}
1146+
}
1147+
eligible.then(|| {
1148+
let fields: Vec<(String, Type)> =
1149+
names.iter().map(|name| (name.clone(), Type::Any)).collect();
1150+
let class_name = ctx.synthesize_anon_shape_class(&fields);
1151+
Expr::New {
1152+
class_name,
1153+
args: names.iter().map(|_| Expr::Undefined).collect(),
1154+
type_args: Vec::new(),
1155+
byte_offset: 0,
1156+
cap_args_appended: 0,
1157+
}
1158+
})
1159+
} else {
1160+
None
1161+
};
1162+
1163+
// Pass 2: build the IIFE wrapper. `__o` starts as the exported stable
1164+
// shape seed when eligible, otherwise as an empty object, and each op
1165+
// mutates it in source order.
11101166
let extern_call = |name: &str, args: Vec<Expr>| Expr::Call {
11111167
callee: Box::new(Expr::ExternFuncRef {
11121168
name: name.to_string(),
@@ -1250,7 +1306,7 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R
12501306
};
12511307
return Ok(Expr::Call {
12521308
callee: Box::new(closure),
1253-
args: vec![Expr::Object(Vec::new())],
1309+
args: vec![static_shape_seed.unwrap_or_else(|| Expr::Object(Vec::new()))],
12541310
type_args: vec![],
12551311
byte_offset: 0,
12561312
});

‎crates/perry-hir/src/lower/lowering_context.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,11 @@ pub struct LoweringContext {
767767
/// (e.g. recognizing a bundled mysql2 `createPool(config)` by its option
768768
/// names) recovers them here.
769769
pub(crate) anon_shape_fields: HashMap<String, Vec<String>>,
770+
/// Set while lowering a directly exported binding/default expression.
771+
/// Eligible method literals consume this flag and retain the seeded IIFE
772+
/// representation needed to publish an exact cross-module own-method
773+
/// capability. Ordinary local method literals keep the direct-object path.
774+
pub(crate) prefer_exported_method_shape_seed: bool,
770775
/// Class DECLARATION names at the top level of the function body
771776
/// currently being lowered. JS resolves a method-body reference to a
772777
/// sibling class declared LATER in the same function at call time

‎crates/perry-hir/src/lower/module_decl.rs‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ use native_default_import::{
2222
node_submodule_default_export_key,
2323
};
2424

25+
fn is_direct_object_literal(expr: &ast::Expr) -> bool {
26+
let mut current = expr;
27+
loop {
28+
match current {
29+
ast::Expr::TsAs(wrapper) => current = &wrapper.expr,
30+
ast::Expr::TsNonNull(wrapper) => current = &wrapper.expr,
31+
ast::Expr::TsSatisfies(wrapper) => current = &wrapper.expr,
32+
ast::Expr::TsTypeAssertion(wrapper) => current = &wrapper.expr,
33+
ast::Expr::TsConstAssertion(wrapper) => current = &wrapper.expr,
34+
ast::Expr::Paren(wrapper) => current = &wrapper.expr,
35+
ast::Expr::Object(_) => return true,
36+
_ => return false,
37+
}
38+
}
39+
}
40+
2541
pub(crate) fn lower_module_decl(
2642
ctx: &mut LoweringContext,
2743
module: &mut Module,
@@ -1179,7 +1195,13 @@ pub(crate) fn lower_module_decl(
11791195
}
11801196
}
11811197

1182-
let expr = lower_expr(ctx, init)?;
1198+
let previous_shape_seed = ctx.prefer_exported_method_shape_seed;
1199+
ctx.prefer_exported_method_shape_seed = var_decl.kind
1200+
== ast::VarDeclKind::Const
1201+
&& is_direct_object_literal(init);
1202+
let expr = lower_expr(ctx, init);
1203+
ctx.prefer_exported_method_shape_seed = previous_shape_seed;
1204+
let expr = expr?;
11831205
let id = if ctx.pre_registered_module_vars.remove(&name) {
11841206
ctx.pre_registered_module_var_decls.remove(&name);
11851207
let id = ctx.lookup_local(&name).unwrap();
@@ -1904,7 +1926,12 @@ pub(crate) fn lower_module_decl(
19041926
}
19051927
ast::ModuleDecl::ExportDefaultExpr(export_default_expr) => {
19061928
// export default <expr>
1907-
let lowered = lower_expr(ctx, &export_default_expr.expr)?;
1929+
let previous_shape_seed = ctx.prefer_exported_method_shape_seed;
1930+
ctx.prefer_exported_method_shape_seed =
1931+
is_direct_object_literal(&export_default_expr.expr);
1932+
let lowered = lower_expr(ctx, &export_default_expr.expr);
1933+
ctx.prefer_exported_method_shape_seed = previous_shape_seed;
1934+
let lowered = lowered?;
19081935

19091936
// If the expression is a FuncRef, add to exported_functions for proper wrapper generation
19101937
if let Expr::FuncRef(func_id) = &lowered {

‎crates/perry-hir/src/lower/tests.rs‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,55 @@ const computed = { [key]() { return 1; } };
155155
}
156156
}
157157

158+
#[test]
159+
fn exported_static_method_literals_keep_a_stable_shape_seed() {
160+
let source = r#"
161+
export const named = {
162+
value: 1,
163+
read() { return this.value; },
164+
};
165+
export default {
166+
value: 2,
167+
read() { return this.value; },
168+
};
169+
"#;
170+
let module =
171+
perry_parser::parse_typescript(source, "exported-method-object.ts").expect("source parses");
172+
let hir = super::lower_module(
173+
&module,
174+
"exported-method-object",
175+
"exported-method-object.ts",
176+
)
177+
.expect("source lowers");
178+
179+
for name in ["named", "default"] {
180+
let init = hir
181+
.init
182+
.iter()
183+
.find_map(|stmt| match stmt {
184+
Stmt::Let {
185+
name: local_name,
186+
init: Some(init),
187+
..
188+
} if local_name == name => Some(init),
189+
_ => None,
190+
})
191+
.unwrap_or_else(|| panic!("missing init for {name}"));
192+
let Expr::Call { callee, args, .. } = init else {
193+
panic!("exported method object must retain its seeded IIFE: {init:#?}");
194+
};
195+
assert!(matches!(
196+
callee.as_ref(),
197+
Expr::Closure { params, .. }
198+
if params.first().is_some_and(|param| param.name == "__perry_obj_iife")
199+
));
200+
assert!(matches!(
201+
args.as_slice(),
202+
[Expr::New { class_name, .. }] if class_name.starts_with("__AnonShape_")
203+
));
204+
}
205+
}
206+
158207
#[test]
159208
fn test_lower_function_registration() {
160209
let mut ctx = make_ctx();

0 commit comments

Comments
 (0)