Skip to content

Commit 40ee8e8

Browse files
author
Ralph Küpper
committed
perf: static method literals, captured closure reuse, argument shape facts
Lands #8793, #8792 and #8787. #8793 lowers static method literals directly; #8792 indexes captured closure reuse; #8787 propagates shape facts into argument positions. All three were showing pr-gate red before #8791 landed, because main itself was failing `cargo-test` on a Web Streams test. Re-gated against the fixed baseline, all three are clean. No version bump.
1 parent 98ecdc5 commit 40ee8e8

32 files changed

Lines changed: 2484 additions & 307 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Guarded direct method calls now propagate exact class and shape facts into
2+
eligible object arguments, including unannotated JavaScript parameters with a
3+
unique declared-field signature. The internal tagged-ABI clones use direct
4+
field offsets while exact runtime guards, shadow roots, and the ordinary method
5+
fallback preserve behavior for subclasses, proxies, mutated shapes, and other
6+
dynamic values.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Captured-closure singleton reuse now fingerprints each exact capture tuple and maintains a direct-mapped, collision-safe hint into its bounded LRU. Hits no longer move vector entries, hash or hint collisions still require bit-exact capture equality, and copying GC recomputes fingerprints while discarding stale hints after rewriting pointer-bearing captures.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Static-key object literals containing methods now use the ordinary final-shape object lowering when every value is independent of the hidden home object. This removes the synthetic builder closure and property-by-property mutation while preserving evaluation order, dynamic `this`, capture semantics, and inferred method names; `super`, computed keys, spreads, accessors, prototype setters, and other source-ordered forms retain the fail-closed builder path.
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
//! #8774 exact-shape ordinary-argument clone ratchets.
2+
3+
use crate::{compile_module, AppMetadata, CompileOptions};
4+
use perry_hir::types::Type;
5+
use perry_hir::{Class, ClassField, Expr, Function, Module, Param, Stmt};
6+
7+
fn opts() -> CompileOptions {
8+
CompileOptions {
9+
emit_ir_only: true,
10+
is_entry_module: true,
11+
output_type: "executable".to_string(),
12+
app_metadata: AppMetadata::default(),
13+
..CompileOptions::default()
14+
}
15+
}
16+
17+
fn param(id: u32, name: &str, ty: Type) -> Param {
18+
Param {
19+
id,
20+
name: name.to_string(),
21+
ty,
22+
default: None,
23+
decorators: Vec::new(),
24+
is_rest: false,
25+
arguments_object: None,
26+
}
27+
}
28+
29+
fn function(id: u32, name: &str, params: Vec<Param>, body: Vec<Stmt>) -> Function {
30+
Function {
31+
id,
32+
name: name.to_string(),
33+
type_params: Vec::new(),
34+
params,
35+
return_type: Type::Void,
36+
body,
37+
is_async: false,
38+
is_generator: false,
39+
is_strict: true,
40+
is_exported: false,
41+
captures: Vec::new(),
42+
decorators: Vec::new(),
43+
was_plain_async: false,
44+
was_unrolled: false,
45+
}
46+
}
47+
48+
fn class(id: u32, name: &str, fields: Vec<&str>, methods: Vec<Function>) -> Class {
49+
Class {
50+
id,
51+
name: name.to_string(),
52+
type_params: Vec::new(),
53+
extends: None,
54+
extends_name: None,
55+
native_extends: None,
56+
extends_expr: None,
57+
heritage_lexically_shadowed: false,
58+
fields: fields
59+
.into_iter()
60+
.map(|name| ClassField {
61+
name: name.to_string(),
62+
key_expr: None,
63+
ty: Type::Any,
64+
init: None,
65+
is_private: false,
66+
is_readonly: false,
67+
decorators: Vec::new(),
68+
})
69+
.collect(),
70+
constructor: None,
71+
methods,
72+
getters: Vec::new(),
73+
setters: Vec::new(),
74+
static_accessor_names: Vec::new(),
75+
static_accessor_fn_ids: Vec::new(),
76+
static_fields: Vec::new(),
77+
static_methods: Vec::new(),
78+
computed_members: Vec::new(),
79+
decorators: Vec::new(),
80+
is_exported: false,
81+
is_nested: false,
82+
alloc_width_hint: 0,
83+
specialized_from: None,
84+
aliases: Vec::new(),
85+
}
86+
}
87+
88+
fn field_get(local: u32, property: &str) -> Expr {
89+
Expr::PropertyGet {
90+
object: Box::new(Expr::LocalGet(local)),
91+
property: property.to_string(),
92+
byte_offset: 0,
93+
}
94+
}
95+
96+
fn fixture() -> Module {
97+
let entity_param = 20;
98+
let read = function(
99+
200,
100+
"read",
101+
vec![param(
102+
entity_param,
103+
"entity",
104+
Type::Named("Entity".to_string()),
105+
)],
106+
vec![Stmt::Expr(field_get(entity_param, "id"))],
107+
);
108+
let mut module = Module::new("argument_shape_clone.ts");
109+
module.classes.push(class(1, "Entity", vec!["id"], vec![]));
110+
module
111+
.classes
112+
.push(class(2, "Registry", vec![], vec![read]));
113+
module.init.extend([
114+
Stmt::Let {
115+
id: 10,
116+
name: "registry".to_string(),
117+
ty: Type::Named("Registry".to_string()),
118+
mutable: false,
119+
init: Some(Expr::New {
120+
class_name: "Registry".to_string(),
121+
args: Vec::new(),
122+
type_args: Vec::new(),
123+
byte_offset: 0,
124+
cap_args_appended: 0,
125+
}),
126+
},
127+
Stmt::Let {
128+
id: 11,
129+
name: "entity".to_string(),
130+
ty: Type::Named("Entity".to_string()),
131+
mutable: false,
132+
init: Some(Expr::New {
133+
class_name: "Entity".to_string(),
134+
args: Vec::new(),
135+
type_args: Vec::new(),
136+
byte_offset: 0,
137+
cap_args_appended: 0,
138+
}),
139+
},
140+
Stmt::Expr(Expr::Call {
141+
callee: Box::new(Expr::PropertyGet {
142+
object: Box::new(Expr::LocalGet(10)),
143+
property: "read".to_string(),
144+
byte_offset: 0,
145+
}),
146+
args: vec![Expr::LocalGet(11)],
147+
type_args: Vec::new(),
148+
byte_offset: 0,
149+
}),
150+
]);
151+
module
152+
}
153+
154+
fn function_ir<'a>(ir: &'a str, marker: &str) -> &'a str {
155+
let start = ir
156+
.match_indices("define ")
157+
.find(|(index, _)| {
158+
let end = ir[*index..]
159+
.find('\n')
160+
.map(|offset| index + offset)
161+
.unwrap_or(ir.len());
162+
ir[*index..end].contains(marker)
163+
})
164+
.map(|(index, _)| index)
165+
.unwrap_or_else(|| panic!("missing function {marker}:\n{ir}"));
166+
let end = ir[start..]
167+
.find("\n}")
168+
.map(|offset| start + offset)
169+
.expect("function terminator");
170+
&ir[start..end]
171+
}
172+
173+
#[test]
174+
fn guarded_call_routes_to_shadow_rooted_direct_field_clone() {
175+
// Native statepoint roots are the host default. Pin the shadow-stack
176+
// lowering because this assertion specifically ratchets the portable
177+
// tagged-slot fallback required by the clone ABI.
178+
let _shadow = crate::codegen::helpers::NativeRootsPin::shadow();
179+
let ir = String::from_utf8(compile_module(&fixture(), opts()).expect("module compiles"))
180+
.expect("LLVM IR is UTF-8");
181+
let clone_name = "perry_method_argument_shape_clone_ts__Registry__read$pshape_args";
182+
let clone = function_ir(&ir, &format!("@{clone_name}("));
183+
let generic = function_ir(
184+
&ir,
185+
"@perry_method_argument_shape_clone_ts__Registry__read(",
186+
);
187+
188+
assert!(
189+
ir.contains(&format!("call double @{clone_name}(")),
190+
"the guarded call site must route to the argument clone:\n{ir}"
191+
);
192+
assert!(
193+
ir.contains("pshape_arg.fallback")
194+
&& ir.contains("call double @perry_method_argument_shape_clone_ts__Registry__read("),
195+
"guard failure must retain the ordinary method body:\n{ir}"
196+
);
197+
assert!(
198+
clone.contains("@js_shadow_slot_bind(")
199+
&& clone.find("@js_shadow_slot_bind(")
200+
< clone
201+
.find("getelementptr double")
202+
.or_else(|| clone.find("inttoptr i64")),
203+
"the tagged parameter slot must be bound before fixed-offset access:\n{clone}"
204+
);
205+
assert!(
206+
clone.contains("inttoptr i64") && clone.contains("getelementptr double"),
207+
"the clone must use direct declared-field addressing:\n{clone}"
208+
);
209+
assert!(
210+
!clone.contains("js_typed_feedback_class_field_get_guard")
211+
&& !clone.contains("shape_descriptor_by_id"),
212+
"the clone fast body must not rebuild the field IC diamond:\n{clone}"
213+
);
214+
assert!(
215+
generic.contains("js_typed_feedback_class_field_get_guard")
216+
|| generic.contains("js_object_get_field"),
217+
"the generic fallback must retain guarded field semantics:\n{generic}"
218+
);
219+
}
220+
221+
#[test]
222+
fn routed_argument_is_a_contained_ptr_shape_win_in_the_opt_report() {
223+
let session = crate::opt_report::test_support::Session::start();
224+
compile_module(&fixture(), opts()).expect("module compiles");
225+
let entries = session.entries();
226+
let entity_entries: Vec<_> = entries
227+
.iter()
228+
.filter(|entry| entry.name == "entity" && entry.local_id == Some(11))
229+
.collect();
230+
231+
assert!(
232+
entity_entries
233+
.iter()
234+
.any(|entry| entry.outcome == crate::opt_report::Outcome::Selected),
235+
"the guarded argument route must preserve the caller's Ptr<Shape> fact: {entries:#?}"
236+
);
237+
assert!(
238+
entity_entries.iter().all(|entry| {
239+
entry.outcome != crate::opt_report::Outcome::Denied
240+
|| !entry
241+
.reason
242+
.as_deref()
243+
.unwrap_or("")
244+
.contains("passed as a call argument")
245+
}),
246+
"the retired call-argument denial must not survive a selected clone route: {entries:#?}"
247+
);
248+
}
249+
250+
#[test]
251+
fn unannotated_parameter_uses_the_runtime_validated_class_overlay() {
252+
let mut module = fixture();
253+
module.classes[1].methods[0].params[0].ty = Type::Any;
254+
let ir = String::from_utf8(compile_module(&module, opts()).expect("module compiles"))
255+
.expect("LLVM IR is UTF-8");
256+
let clone = function_ir(&ir, "Registry__read$pshape_args(");
257+
assert!(
258+
clone.contains("getelementptr double") && clone.contains("inttoptr i64"),
259+
"the exact runtime guard must recover the class layout for an unannotated parameter:\n{clone}"
260+
);
261+
assert!(
262+
!clone.contains("js_typed_feedback_class_field_get_guard")
263+
&& !clone.contains("shape_descriptor_by_id"),
264+
"the unannotated clone must not rebuild the field IC diamond:\n{clone}"
265+
);
266+
}
267+
268+
#[test]
269+
fn ambiguous_unannotated_field_signature_stays_generic() {
270+
let mut module = fixture();
271+
module.classes[1].methods[0].params[0].ty = Type::Any;
272+
module
273+
.classes
274+
.push(class(3, "OtherEntity", vec!["id"], vec![]));
275+
let ir = String::from_utf8(compile_module(&module, opts()).expect("module compiles"))
276+
.expect("LLVM IR is UTF-8");
277+
assert!(
278+
!ir.contains("Registry__read$pshape_args"),
279+
"an unannotated field signature matching multiple classes must not nominate either:\n{ir}"
280+
);
281+
}
282+
283+
#[test]
284+
fn aliased_or_reassigned_parameter_does_not_get_a_clone() {
285+
let mut module = fixture();
286+
let method = &mut module.classes[1].methods[0];
287+
method.body.insert(
288+
0,
289+
Stmt::Expr(Expr::LocalSet(
290+
method.params[0].id,
291+
Box::new(Expr::Undefined),
292+
)),
293+
);
294+
let ir = String::from_utf8(compile_module(&module, opts()).expect("module compiles"))
295+
.expect("LLVM IR is UTF-8");
296+
assert!(
297+
!ir.contains("Registry__read$pshape_args"),
298+
"a reassigned parameter must keep only generic semantics:\n{ir}"
299+
);
300+
}

0 commit comments

Comments
 (0)