Skip to content
Closed
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
9 changes: 9 additions & 0 deletions changelog.d/8833-ecs-integration-followthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The ECS benchmark specializations now cover their real cross-module integration paths: short
packed spread and exact argument-shape calls receive producer metadata, imported object-literal
methods retain exact own-method capabilities through adapter parameters, and closure-captured
packed loops version nested arrays derived from guarded indexed reads. Every path keeps a guarded
generic side exit. The audit also fixes mixed fixed/spread `Math` calls incorrectly treating their
tail array as a scalar argument and restores iterator-protocol fallback for proxy and Array-subclass
spread tails. On the controlled M1 cohort, the corrected full Wolf workload is 1.73x faster and the
imported-method `perform-ecs` workload is 11.05% faster; the short-spread and argument-clone slices
activate correctly but remain performance-neutral.
132 changes: 129 additions & 3 deletions crates/perry-codegen/src/codegen/argument_shape_clone_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,12 @@ fn guarded_call_routes_to_shadow_rooted_direct_field_clone() {

assert!(
ir.contains(&format!("call double @{clone_name}(")),
"the guarded call site must route to the argument clone:\n{ir}"
"the exact contained call site must route to the argument clone:\n{ir}"
);
assert!(
ir.contains("pshape_arg.fallback")
!ir.contains("pshape_arg.fallback")
&& ir.contains("call double @perry_method_argument_shape_clone_ts__Registry__read("),
"guard failure must retain the ordinary method body:\n{ir}"
"fresh provenance must elide the redundant argument guard while other receiver routes retain the ordinary body:\n{ir}"
);
assert!(
clone.contains("@js_shadow_slot_bind(")
Expand Down Expand Up @@ -302,3 +302,129 @@ fn aliased_or_reassigned_parameter_does_not_get_a_clone() {
"a reassigned parameter must keep only generic semantics:\n{ir}"
);
}

fn define_property(target: Expr) -> Stmt {
Stmt::Expr(Expr::ObjectDefineProperty(
Box::new(target),
Box::new(Expr::String("unrelated".to_string())),
Box::new(Expr::Object(vec![("value".to_string(), Expr::Number(1.0))])),
))
}

#[test]
fn unrelated_module_shape_barrier_keeps_guarded_argument_route() {
let mut module = fixture();
module.init.insert(0, define_property(Expr::Object(vec![])));
let ir = String::from_utf8(compile_module(&module, opts()).expect("module compiles"))
.expect("LLVM IR is UTF-8");
let clone_name = "perry_method_argument_shape_clone_ts__Registry__read$pshape_args";

assert!(
ir.contains(&format!("call double @{clone_name}(")),
"an unrelated barrier must not suppress the exact guarded route:\n{ir}"
);
assert!(
!ir.contains("pshape_arg.fallback")
&& ir.contains("call double @perry_method_argument_shape_clone_ts__Registry__read("),
"an unrelated barrier must not reintroduce a redundant argument guard:\n{ir}"
);
}

#[test]
fn barrier_targeting_argument_stays_on_generic_route() {
let mut module = fixture();
module.init.insert(2, define_property(Expr::LocalGet(11)));
let ir = String::from_utf8(compile_module(&module, opts()).expect("module compiles"))
.expect("LLVM IR is UTF-8");
let clone_name = "perry_method_argument_shape_clone_ts__Registry__read$pshape_args";

assert!(
!ir.contains(&format!("call double @{clone_name}(")),
"a value reshaped before the call must not receive a containment route:\n{ir}"
);
}

#[test]
fn field_read_before_terminal_publication_gets_only_the_guarded_route() {
let mut module = fixture();
let param_id = module.classes[1].methods[0].params[0].id;
module.classes[1].methods[0]
.body
.push(Stmt::Return(Some(Expr::LocalGet(param_id))));

let session = crate::opt_report::test_support::Session::start();
let ir = String::from_utf8(compile_module(&module, opts()).expect("module compiles"))
.expect("LLVM IR is UTF-8");
let clone_name = "perry_method_argument_shape_clone_ts__Registry__read$pshape_args";
assert!(
ir.contains(&format!("call double @{clone_name}(")),
"a direct read performed before publication should use the guarded clone:\n{ir}"
);
let entries = session.entries();
assert!(
!entries.iter().any(|entry| {
entry.name == "entity"
&& entry.local_id == Some(11)
&& entry.outcome == crate::opt_report::Outcome::Selected
}),
"a publishing clone must not preserve the caller's broad Ptr<Shape> fact: {entries:#?}"
);
}

#[test]
fn field_read_after_publication_does_not_get_a_clone() {
let mut module = fixture();
let method = &mut module.classes[1].methods[0];
method
.body
.insert(0, Stmt::Expr(Expr::LocalGet(method.params[0].id)));
let ir = String::from_utf8(compile_module(&module, opts()).expect("module compiles"))
.expect("LLVM IR is UTF-8");
assert!(
!ir.contains("Registry__read$pshape_args"),
"an entry shape proof cannot license a field read after publication:\n{ir}"
);
}

#[test]
fn forwarded_clone_parameter_retains_runtime_guard_and_fallback() {
let mut module = fixture();
let entity_param = 21;
let forward = function(
201,
"forward",
vec![param(
entity_param,
"entity",
Type::Named("Entity".to_string()),
)],
vec![
Stmt::Expr(field_get(entity_param, "id")),
Stmt::Expr(Expr::Call {
callee: Box::new(Expr::PropertyGet {
object: Box::new(Expr::This),
property: "read".to_string(),
byte_offset: 0,
}),
args: vec![Expr::LocalGet(entity_param)],
type_args: Vec::new(),
byte_offset: 0,
}),
],
);
module.classes[1].methods.push(forward);
let ir = String::from_utf8(compile_module(&module, opts()).expect("module compiles"))
.expect("LLVM IR is UTF-8");

assert!(
ir.contains("Registry__forward$pshape_args")
&& ir.contains(
"call double @perry_method_argument_shape_clone_ts__Registry__read$pshape_args("
),
"the forwarding clone must route its selected parameter onward:\n{ir}"
);
assert!(
ir.contains("pshape_arg.fallback"),
"a fact inherited from a dynamic clone boundary must retain an exact guard and generic fallback:\n{ir}"
);
}
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,8 @@ pub(super) fn compile_closure(
local_imported_object_aliases: HashMap::new(),
imported_vars: &cross_module.imported_vars,
imported_object_literals: &cross_module.imported_object_literals,
short_spread_method_candidates: &cross_module.short_spread_method_candidates,
object_literal_method_candidates: &cross_module.object_literal_method_candidates,
compile_time_constants: native_facts.compile_time_constants(),
target_triple: &cross_module.target_triple,
app_metadata: &cross_module.app_metadata,
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/emission_order_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ fn ir_opts() -> CompileOptions {
namespace_imports: Vec::new(),
namespace_member_nested: Vec::new(),
imported_classes: Vec::new(),
short_spread_method_candidates: std::sync::Arc::default(),
object_literal_method_candidates: std::sync::Arc::default(),
imported_enums: Vec::new(),
imported_async_funcs: std::collections::HashSet::new(),
type_aliases: std::collections::HashMap::new(),
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,8 @@ pub(super) fn compile_module_entry(
local_imported_object_aliases: HashMap::new(),
imported_vars: &cross_module.imported_vars,
imported_object_literals: &cross_module.imported_object_literals,
short_spread_method_candidates: &cross_module.short_spread_method_candidates,
object_literal_method_candidates: &cross_module.object_literal_method_candidates,
compile_time_constants: main_native_facts.compile_time_constants(),
target_triple: &cross_module.target_triple,
app_metadata: &cross_module.app_metadata,
Expand Down Expand Up @@ -1628,6 +1630,8 @@ pub(super) fn compile_module_entry(
local_imported_object_aliases: HashMap::new(),
imported_vars: &cross_module.imported_vars,
imported_object_literals: &cross_module.imported_object_literals,
short_spread_method_candidates: &cross_module.short_spread_method_candidates,
object_literal_method_candidates: &cross_module.object_literal_method_candidates,
compile_time_constants: init_native_facts.compile_time_constants(),
target_triple: &cross_module.target_triple,
app_metadata: &cross_module.app_metadata,
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/entry/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ fn entry_opts(output_type: &str) -> CompileOptions {
namespace_imports: Vec::new(),
namespace_member_nested: Vec::new(),
imported_classes: Vec::new(),
short_spread_method_candidates: std::sync::Arc::default(),
object_literal_method_candidates: std::sync::Arc::default(),
imported_enums: Vec::new(),
imported_async_funcs: std::collections::HashSet::new(),
type_aliases: std::collections::HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,8 @@ pub(super) fn compile_function(
local_imported_object_aliases: HashMap::new(),
imported_vars: &cross_module.imported_vars,
imported_object_literals: &cross_module.imported_object_literals,
short_spread_method_candidates: &cross_module.short_spread_method_candidates,
object_literal_method_candidates: &cross_module.object_literal_method_candidates,
compile_time_constants: native_facts.compile_time_constants(),
target_triple: &cross_module.target_triple,
app_metadata: &cross_module.app_metadata,
Expand Down
29 changes: 28 additions & 1 deletion crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ pub(super) fn scoped_method_name(
/// changing it desyncs cross-module symbol references (a module's prefix is
/// `sanitize(module_name)` at the definition site and must match the prefix the
/// importing module re-derives).
pub(super) fn sanitize(name: &str) -> String {
pub(crate) fn sanitize(name: &str) -> String {
let mut s: String = name
.chars()
.map(|c| {
Expand Down Expand Up @@ -1029,6 +1029,33 @@ pub(super) fn sanitize_member(name: &str) -> String {
s
}

/// Reserve the keys-global symbol for one source class. Keep this single
/// implementation shared by capability harvesting and module emission: both
/// passes must assign collision suffixes in the same source order or a
/// harvested ShapeId external can name a global the producer never defines.
pub(crate) fn unique_class_keys_global(
module_prefix: &str,
class_name: &str,
used: &mut std::collections::HashSet<String>,
) -> String {
let base = format!(
"perry_class_keys_{}__{}",
module_prefix,
sanitize(class_name)
);
if used.insert(base.clone()) {
return base;
}
let mut suffix = 1u32;
loop {
let candidate = format!("{base}_{suffix}");
if used.insert(candidate.clone()) {
return candidate;
}
suffix += 1;
}
}

/// Host default triple.
/// Host-default LLVM target triple. Used when `CompileOptions.target`
/// is `None`. Also re-exposed via `pub(crate)` so `linker.rs` can pin
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@ pub(super) fn compile_method(
local_imported_object_aliases: HashMap::new(),
imported_vars: &cross_module.imported_vars,
imported_object_literals: &cross_module.imported_object_literals,
short_spread_method_candidates: &cross_module.short_spread_method_candidates,
object_literal_method_candidates: &cross_module.object_literal_method_candidates,
compile_time_constants: native_facts.compile_time_constants(),
target_triple: &cross_module.target_triple,
app_metadata: &cross_module.app_metadata,
Expand Down Expand Up @@ -1794,6 +1796,8 @@ pub(super) fn compile_static_method(
local_imported_object_aliases: HashMap::new(),
imported_vars: &cross_module.imported_vars,
imported_object_literals: &cross_module.imported_object_literals,
short_spread_method_candidates: &cross_module.short_spread_method_candidates,
object_literal_method_candidates: &cross_module.object_literal_method_candidates,
compile_time_constants: native_facts.compile_time_constants(),
target_triple: &cross_module.target_triple,
app_metadata: &cross_module.app_metadata,
Expand Down
Loading
Loading