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
16 changes: 8 additions & 8 deletions benchmarks/repsel_census/fixtures/fixture_alloc_buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ const boxed = new Boxed(new Point(1, 2));
// with the line above.
const nested = { inner: { a: 1, b: 2 }, k: 3 };

// Bucket 4 — a returned expression OPERAND. What this function returns is the
// conditional, not either allocation, so #7107's return-shape fact covers
// neither and `pickPoint` gets no fact at all (`producer_return_class` admits
// only a bare `Expr::New` or a proven local as a return). Before the #7176
// review both arms inherited the `return` label from `Stmt::Return` and were
// counted as return positions — which is what over-stated the `return` bucket
// published on #7170 as R1's ceiling.
// Buckets 4 and 5 — returned expression OPERANDS on opposite sides of the R2
// boundary. The allocation in the conditional's condition is evaluated but
// can never become the returned value, so it remains an unserved rule-1 wall.
// The two fresh, agreeing result arms do feed the return-shape fact and must be
// reported as served. All three retain the operand position rather than being
// mislabeled as direct returns, which is the #7176 distinction this fixture
// originally introduced.
export function pickPoint(flag: boolean): Point {
return flag ? new Point(5, 6) : new Point(7, 8);
return (flag && new Point(0, 0)) ? new Point(5, 6) : new Point(7, 8);
}

const p = makePoint(3);
Expand Down
8 changes: 8 additions & 0 deletions changelog.d/8086-authoritative-shape-guards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### Fixed

- Make `ShapeId` descriptors authoritative for runtime and generated object
guards, including moving keys, exact logical/live slot facts, semantic
transitions, agent-local installation, and fail-stop exhaustion. Class
objects now carry their kind in the authoritative descriptor, and RegExp
values use a dedicated GC kind with relocation-safe side tables, while the
object header size remains unchanged.
13 changes: 5 additions & 8 deletions crates/perry-codegen/src/collectors/proven_this.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,11 @@
//!
//! * `method_direct.fast` (`lower_call/method_override.rs`) sits behind
//! `js_method_direct_shape_guard` / `js_typed_feedback_method_direct_call_guard`,
//! whose contract includes `receiver.keys_array == expected_keys` — a raw
//! POINTER compare (`typed_feedback/guards.rs`). The only code path
//! `js_object_delete_field` has for a `GC_TYPE_OBJECT` instance with a
//! keys array clones a FRESH keys array and repoints `keys_array` at it
//! (`perry-runtime/src/object/delete_rest.rs`; `Reflect.deleteProperty`
//! shares the same function) — for ANY key, declared or not, from ANY
//! module. The pointer compare can therefore never pass on a post-delete
//! instance, regardless of what this admission check saw.
//! whose contract includes `receiver.ShapeId == expected_shape_id`
//! (`typed_feedback/guards.rs`). `js_object_delete_field` publishes a
//! semantic successor ShapeId (`perry-runtime/src/object/delete_rest.rs`;
//! `Reflect.deleteProperty` shares the same function), so the guard can
//! never pass on a post-delete instance regardless of module boundaries.
//! * The Phase 3b guard-free `Ptr<Shape>` receiver arm needs no runtime
//! check at all, because rule 2's containment already rules out the alias
//! existing in the first place: creating one — `let other = o`, passing
Expand Down
88 changes: 32 additions & 56 deletions crates/perry-codegen/src/collectors/proven_this_routing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
//! Both routing sites are covered:
//!
//! * `lower_call/method_override.rs` — the guarded `method_direct.fast` arm,
//! dominated by the class-id + keys-token guard.
//! dominated by the class-id + ShapeId guard.
//! * `lower_call/property_get/dynamic_dispatch.rs` — the Phase 3b guard-free
//! `Ptr<Shape>` receiver arm.
//!
Expand Down Expand Up @@ -403,7 +403,7 @@ fn ptr_shape_local_module() -> Module {
/// A module whose `probe(c: Counter)` calls both methods on a statically-typed
/// parameter. A typed parameter is not a Phase 3b shape-proven local (no
/// provenance, no containment), so both calls go through the *guarded* site:
/// `emit_guarded_direct_method_call`, behind the class-id + keys-token guard.
/// `emit_guarded_direct_method_call`, behind the class-id + ShapeId guard.
fn guarded_site_module() -> Module {
let mut m = Module::new("pshape_guarded.ts");
m.classes = vec![counter_class()];
Expand Down Expand Up @@ -495,7 +495,7 @@ fn function_body(ir: &str, name_contains: &str) -> String {
}

/// Soundness ratchet (#7143): `method_direct.fast`'s `$pshape` call site is
/// preceded, in its own function, by the keys-token shape guard.
/// preceded, in its own function, by the ShapeId guard.
///
/// `guarded_site_module`'s `probe(c: Counter)` receiver (`c`) is a plain
/// typed PARAMETER — proven-`this`'s aliased-by-construction case, not a
Expand All @@ -511,25 +511,24 @@ fn function_body(ir: &str, name_contains: &str) -> String {
/// (`lower_call/method_override.rs`) unconditionally emits
/// `js_typed_feedback_method_direct_call_guard` (or, under `shape_only_guard`,
/// `js_method_direct_shape_guard`) BEFORE any block that can reach the clone
/// — both compare the receiver's live `keys_array` pointer against the
/// class's canonical `@perry_class_keys_*` token, and `delete`'s only code
/// path for a class instance always repoints `keys_array` at a freshly
/// cloned array (`perry-runtime/src/object/delete_rest.rs`), from ANY
/// — both compare the receiver's live ShapeId against the class's canonical
/// `@perry_class_shape_id_*` value, and `delete` publishes a semantic
/// successor descriptor (`perry-runtime/src/object/delete_rest.rs`), from ANY
/// module. See `collectors/proven_this.rs`'s "`delete` is aliased across
/// modules by construction" section for the full argument; this pins the
/// IR shape it depends on, the same way
/// `tower_route_is_guarded_by_the_class_keys_token` pins it for the #7142
/// `tower_route_is_guarded_by_the_class_shape_id` pins it for the #7142
/// tower site.
///
/// This checks TEXTUAL precedence within `probe`'s body rather than walking
/// block dominance (`tower_route_is_guarded_by_the_class_keys_token`'s
/// block dominance (`tower_route_is_guarded_by_the_class_shape_id`'s
/// approach) because `probe` calls two methods sequentially and the typed-f64
/// arm nests the clone's call another level deep behind its OWN per-argument
/// guard — precedence is the invariant that survives that nesting, and
/// `probe` is this fixture's only method-dispatching function, so it is also
/// the only place a `$pshape` callee name can appear in a `call` line.
#[test]
fn guarded_pshape_call_site_is_preceded_by_a_keys_token_guard() {
fn guarded_pshape_call_site_is_preceded_by_a_shape_id_guard() {
let ir = emit(&guarded_site_module(), false);
let calls = pshape_call_targets(&ir);
assert!(
Expand All @@ -546,7 +545,7 @@ fn guarded_pshape_call_site_is_preceded_by_a_keys_token_guard() {
|| prefix.contains("call i32 @js_method_direct_shape_guard(");
assert!(
guarded,
"{target}: no keys-token guard call precedes it in `probe` — a \
"{target}: no ShapeId guard call precedes it in `probe` — a \
post-`delete` receiver (deleted from through an alias in another \
module, #7143) would reach this clone's stale fixed-slot loads \
unguarded:\n{probe}"
Expand Down Expand Up @@ -770,16 +769,16 @@ fn tower_case_routes_to_proven_this_clone() {
}

/// Soundness ratchet (#7142): the routed call is dominated by a compare of the
/// receiver's live `keys_array` against the class's `@perry_class_keys_*` token.
/// receiver's authoritative ShapeId against `@perry_class_shape_id_*`.
///
/// A `class_id` match alone is NOT a layout proof — `delete inst.f` compacts
/// the packed slots while preserving `class_id`
/// (`object/delete_rest.rs`), which is why #7141 refused to route this site at
/// all. The compare below is the entire difference between sound and unsound,
/// so it is traced end to end: global → entry-hoisted slot → reload in the
/// guard block → `icmp eq i64` → the branch that enters the clone's block.
/// so it is traced end to end: global → entry-hoisted scalar slot → reload in
/// the guard block → `icmp eq i32` → the branch that enters the clone's block.
#[test]
fn tower_route_is_guarded_by_the_class_keys_token() {
fn tower_route_is_guarded_by_the_class_shape_id() {
let _shadow = crate::codegen::helpers::NativeRootsPin::shadow();
let ir = emit(&tower_site_module(), false);
let bs = blocks(&ir);
Expand All @@ -801,51 +800,40 @@ fn tower_route_is_guarded_by_the_class_keys_token() {
panic!("nothing conditionally branches to {clone_block} — the clone is reached unguarded:\n{ir}")
});

// 1. the class keys global is loaded once at function entry …
// 1. the class ShapeId global is loaded once at function entry …
let global_load = ir
.lines()
.find(|l| l.contains("= load i64, ptr @perry_class_keys_"))
.unwrap_or_else(|| panic!("the class keys token is never read:\n{ir}"));
.find(|l| l.contains("= load i32, ptr @perry_class_shape_id_"))
.unwrap_or_else(|| panic!("the class ShapeId is never read:\n{ir}"));
let global_reg = global_load.trim().split(' ').next().expect("ssa name");
// 2. … and parked in an entry slot …
let store = ir
.lines()
.find(|l| l.contains(&format!("store i64 {}, ptr ", global_reg)))
.unwrap_or_else(|| panic!("the hoisted keys token is never stored:\n{ir}"));
.find(|l| l.contains(&format!("store i32 {}, ptr ", global_reg)))
.unwrap_or_else(|| panic!("the hoisted ShapeId is never stored:\n{ir}"));
let slot = store.rsplit(' ').next().expect("slot name");
let store_pos = ir
.find(store)
.expect("the hoisted class-keys store should be in the function");
let bind_pos = ir
.lines()
.find(|line| line.contains("call void @js_shadow_slot_bind") && line.contains(slot))
.and_then(|line| ir.find(line))
.unwrap_or_else(|| {
panic!(
"the cached class-keys pointer is not a mutable shadow root; old-page moves would leave this copy stale:\n{ir}"
)
});
assert!(
store_pos < bind_pos,
"the class-keys slot must be initialized before the root scanner can read it:\n{ir}"
!ir.lines()
.any(|line| line.contains("call void @js_shadow_slot_bind") && line.contains(slot)),
"a ShapeId scalar must not be registered as a moving GC root:\n{ir}"
);
// 3. … which the guard block reloads …
let expected = guard_body
.iter()
.find_map(|l| {
let l = l.trim();
l.ends_with(&format!("load i64, ptr {}", slot))
l.ends_with(&format!("load i32, ptr {}", slot))
.then(|| l.split(' ').next().expect("ssa name").to_string())
})
.unwrap_or_else(|| {
panic!("the guard block never reads the hoisted keys token:\n{guard_body:#?}")
panic!("the guard block never reads the hoisted ShapeId:\n{guard_body:#?}")
});
// 4. … and compares against the receiver's live keys_array.
// 4. … and compares against the receiver's live ShapeId.
assert!(
guard_body
.iter()
.any(|l| l.contains("icmp eq i64") && l.contains(&expected)),
"the routed call is not dominated by a keys-token compare — a class_id \
.any(|l| l.contains("icmp eq i32") && l.contains(&expected)),
"the routed call is not dominated by a ShapeId compare — a class_id \
match alone does not prove the packed layout (`delete` compacts slots \
while preserving class_id):\n{guard_body:#?}"
);
Expand All @@ -861,30 +849,18 @@ fn tower_route_is_guarded_by_the_class_keys_token() {
}

#[test]
fn tower_class_keys_cache_is_a_native_mutable_root() {
fn tower_class_shape_id_cache_is_a_native_scalar() {
let _native = crate::codegen::helpers::NativeRootsPin::native();
let ir = emit(&tower_site_module(), false);
let global_load = ir
.lines()
.find(|line| line.contains("= load i64, ptr @perry_class_keys_"))
.unwrap_or_else(|| panic!("the class keys token is never read:\n{ir}"));
.find(|line| line.contains("= load i32, ptr @perry_class_shape_id_"))
.unwrap_or_else(|| panic!("the class ShapeId is never read:\n{ir}"));
let global_reg = global_load.trim().split(' ').next().expect("ssa name");
let cast = ir
.lines()
.find(|line| line.contains(&format!("= inttoptr i64 {global_reg} to ptr addrspace(1)")))
.unwrap_or_else(|| {
panic!("the cached class-keys pointer never enters a native GC root slot:\n{ir}")
});
let cast_reg = cast.trim().split(' ').next().expect("cast ssa name");
let root_store = ir
.lines()
.find(|line| line.contains(&format!("store ptr addrspace(1) {cast_reg}, ptr ")))
.unwrap_or_else(|| panic!("the native class-keys root is never stored:\n{ir}"));
let slot = root_store.rsplit(' ').next().expect("root slot name");
assert!(
ir.lines()
.any(|line| line.contains(&format!("{slot} = alloca ptr addrspace(1)"))),
"the class-keys cache alloca must be in the collector address space:\n{ir}"
.any(|line| line.contains(&format!("store i32 {global_reg}, ptr "))),
"the class ShapeId must be cached as an i32 scalar:\n{ir}"
);
}

Expand Down
Loading
Loading