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
32 changes: 32 additions & 0 deletions changelog.d/8833-argument-route-guard-invariant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Argument-shape clone routes keep their runtime class+ShapeId guard whenever the caller's proof came
from the barrier-bypassing route-only pass, and a clone that publishes its parameter no longer keeps
a caller-side containment fact at all.

The route-only proof deliberately bypasses rule 5's module-wide §5.2 shape-barrier kill
(`collectors/ptr_shape.rs`), which is the belt-and-braces backstop against blind spots in the
containment walk. Pairing that bypass with an elided entry guard, and simultaneously dropping the
requirement that the callee preserve containment, removed every net that could observe a reshaped
argument: a module carrying an unattributable `Object.defineProperty`/`delete`/`Proxy` site, a
method that publishes its parameter after its licensed read, and two call sites on the same caller
local produced two unguarded direct calls into `…$pshape_args`, the second reading declared fields
at fixed offsets from an object the compiler itself had recorded as published to an alias it cannot
see.

`PrefixContainedParamUse` proves a temporal property — the licensed reads happen *before* the body
publishes the parameter — but the fact map that carries a caller-side route is keyed by local id and
is therefore flow-insensitive, so a fact kept past a publishing call is consulted again at every
later route site for that local. A per-local map cannot express "before", so route admission now
requires the clone to preserve containment for the parameter's whole lifetime, and the
`require_post_call_containment` knob whose only other mode was unsound is deleted rather than left
selectable.

Guard elision is retained for exactly the case that justifies it: a caller holding the broad
`Ptr<Shape>` representation fact, which by construction was proven in a barrier-free module under
full containment, where the caller is already licensed to read the same object's declared fields at
fixed offsets and the guard is tautological. The measured `perform-ecs` and Wolf routes are
unaffected — they are all fresh contained locals in barrier-free modules — and the #8774 slice
carried no speed claim to begin with.

Pinned by `published_argument_in_a_barrier_module_never_reaches_an_unguarded_clone`, which asserts
its own subject is live (the clone must still be emitted) and fails with "2 clone calls, 0 guard
blocks" against the pre-fix code.
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.
197 changes: 194 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,194 @@ 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))])),
))
}

/// An unrelated §5.2 barrier does not suppress the route, but it DOES make the
/// runtime guard load-bearing.
///
/// The route-only proof reaches this module by bypassing rule 5's module-wide
/// barrier kill. That kill is the belt-and-braces backstop against blind spots
/// in the containment walk (`ptr_shape.rs` rule 5), so with it bypassed the
/// entry guard is the only thing left that can observe a reshaped argument.
/// Eliding it here would leave the clone's fixed-offset reads with no check at
/// all in exactly the modules whose barriers the analysis refuses to attribute.
#[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("),
"a route that bypassed the module-wide barrier kill must keep its \
runtime guard and its generic fallback:\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}"
);
}

/// A clone that publishes its parameter gets NO caller-side route.
///
/// `PrefixContainedParamUse` proves a temporal property — the licensed field
/// reads happen before the body's first bare use of the parameter. The fact
/// map that would carry a caller-side route is keyed by local id and is
/// therefore flow-INSENSITIVE: a fact kept past a publishing call is consulted
/// again at every later route site for the same local, including sites that
/// run once the alias exists. A per-local map cannot express "before", so the
/// only sound reading is that no caller-side containment fact survives such a
/// call at all.
#[test]
fn publishing_clone_gets_no_caller_side_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 clone that publishes its parameter must not be routed from a \
caller-side containment fact:\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:#?}"
);
}

/// #8833 regression: the three widenings must not compose into an unguarded
/// fixed-offset read of a published object.
///
/// Fixture: a §5.2 barrier the analysis cannot attribute, a callee that
/// publishes its parameter after its licensed read, and TWO route sites on the
/// same caller local — so the second one executes after the alias exists. Every
/// safety net that could catch a reshape here had been removed at once: rule
/// 5's module kill (bypassed by the route-only proof), the caller's post-call
/// containment requirement, and the runtime class+ShapeId guard.
#[test]
fn published_argument_in_a_barrier_module_never_reaches_an_unguarded_clone() {
let mut module = fixture();
let param_id = module.classes[1].methods[0].params[0].id;
// The callee reads the declared field, then publishes the parameter.
module.classes[1].methods[0]
.body
.push(Stmt::Return(Some(Expr::LocalGet(param_id))));
// A module-wide §5.2 barrier whose target the containment walk cannot
// attribute to any tracked local.
module.init.insert(0, define_property(Expr::Object(vec![])));
// A second route site on the same local, after the first published it.
let second_call = module.init.last().expect("fixture call").clone();
module.init.push(second_call);

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";
// Subject-liveness: the argument-clone machinery must actually be engaged
// by this fixture, or the assertion below would pass for the wrong reason.
assert!(
ir.contains(&format!("@{clone_name}(")),
"fixture must still emit the argument clone, or this test is vacuous:\n{ir}"
);
let unguarded_calls = ir.matches(&format!("call double @{clone_name}(")).count();
let guard_blocks = ir.matches("pshape_arg.fallback").count();

assert!(
unguarded_calls == 0 || guard_blocks > 0,
"a published argument in a barrier-carrying module reached the \
argument clone with no runtime class+ShapeId guard \
({unguarded_calls} clone calls, {guard_blocks} guard blocks):\n{ir}"
);
}

#[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