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
23 changes: 23 additions & 0 deletions changelog.d/7807-guard-failure-edge-property.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**`typed_f64_receiver_method_clone_raw_loads_after_composed_guards` is green again, and now asserts the property instead of a symbol name** (#7506). `native_proof_regressions` goes from 261/262 to **262/262** and moves into the per-PR map.

#7506 posed the question directly: the guard-failure edge stopped calling `$generic`, so either the assertion is stale or a receiver whose fields are not raw f64 takes the typed clone's raw loads anyway. Answered by reading the emitted IR rather than by choosing.

The composition still has **three** outcomes, not two:

1. method-direct guard fails → `js_native_call_method_by_id` (fully dynamic)
2. raw-f64 field guard passes → `$typed_f64_recv`, which raw-loads the receiver's slots with no coercion
3. raw-f64 field guard **fails** → `$pshape`, a Ptr<Shape> clone

So (3) changed callee, not existence. And `$pshape` is sound for that edge for a reason the old assertion could not express: it *does* emit `inttoptr` + `getelementptr` + `load double` — the shape guarantees the slot OFFSETS — but routes every loaded slot through `js_number_coerce`, which is exactly the right handling for a slot that may hold a NaN-boxed value. It is a strictly better target than the generic body, and it is still correct when the guard that just failed said nothing about the slots' representation.

The assertion is therefore re-pointed at that property, following #7492's worked example:

* the failure edge must reach a clone that does not assume raw-f64 slots (`$generic` **or** `$pshape`);
* it must **not** reach `$typed_f64_recv`, whose whole premise is the guard that just failed;
* and when it reaches `$pshape`, that clone must contain `js_number_coerce` — without which it would be the typed clone under another name.

Both new assertions were **sabotage-verified**: breaking the coercion check and narrowing the accepted callee set each make the test fail with its own message; restoring passes.

Because the suite is now fully green, its `SUITE_EXCLUSIONS` entry is deleted and `native_proof_regressions` joins `SOURCE_SUITE_MAP`. That is not optional bookkeeping — `e2e-scoped` fails the job when an excluded test PASSES, and `ci_e2e_scope.py --self-test` refuses a suite that is in neither list, which is what caught the half-done state while I was making this change. Self-test passes.

Worth recording alongside #7245: of the 16 tests that issue reported red, **15 had already been fixed by unrelated work** with nothing recording it. This was the only real one.
46 changes: 44 additions & 2 deletions crates/perry-codegen/tests/native_proof_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11687,6 +11687,7 @@ fn typed_f64_receiver_method_clone_raw_loads_after_composed_guards() {
let public = "perry_method_typed_f64_receiver_method_ts__Point__score";
let generic_body = "perry_method_typed_f64_receiver_method_ts__Point__score$generic";
let typed = "perry_method_typed_f64_receiver_method_ts__Point__score$typed_f64_recv";
let pshape_body = "perry_method_typed_f64_receiver_method_ts__Point__score$pshape";
let caller = "perry_fn_typed_f64_receiver_method_ts__probe";
let typed_ir = defined_function_ir_section(&ir, typed);
let caller_ir = defined_function_ir_section(&ir, caller);
Expand Down Expand Up @@ -11724,10 +11725,51 @@ fn typed_f64_receiver_method_clone_raw_loads_after_composed_guards() {
method_guard < field_guard && field_guard < typed_call,
"receiver clone must run only after method-direct and raw-f64 field guards:\n{caller_ir}"
);
// #7506: this used to assert the guard-failure edge calls `$generic` BY
// NAME, and it drifted — the edge now calls `$pshape`, a Ptr<Shape> clone.
// That is a refinement, not a miscompile, but only for a reason a
// symbol-presence check cannot express, so the assertion is re-pointed at
// the PROPERTY the composition has to preserve (the #7492 shape).
//
// The three outcomes must stay three:
//
// 1. method-direct guard fails -> fully dynamic `js_native_call_method_by_id`
// 2. raw-f64 field guard passes -> `$typed_f64_recv`, which raw-loads the
// receiver's slots with NO coercion
// 3. raw-f64 field guard FAILS -> a clone that may use the shape's slot
// offsets but must NOT assume the slots hold canonical raw f64
//
// What makes (3) sound is not which symbol it is, it is that the callee
// coerces what it loads. `$pshape` does: it emits `inttoptr` +
// `getelementptr` + `load double` — the shape guarantees the OFFSETS — and
// then routes every loaded slot through `js_number_coerce`, which is
// exactly the right handling for a slot that may hold a NaN-boxed value.
// `$generic` is also acceptable here; what must never appear on this edge
// is `$typed_f64_recv`, whose whole premise is the guard that just failed.
let failure_edge_callee = [generic_body, pshape_body]
.into_iter()
.find(|sym| caller_ir.contains(&format!("call double @{sym}(")))
.unwrap_or_else(|| {
panic!(
"raw-f64 field guard failure must reach a clone that does not \
assume raw-f64 slots (`$generic` or `$pshape`):\n{caller_ir}"
)
});
assert!(
caller_ir.contains(&format!("call double @{generic_body}(")),
"receiver field or numeric arg guard failure should call the generic method body:\n{caller_ir}"
!caller_ir.contains(&format!("call double @{typed}(double ")),
"the guard-failure edge must not reach the raw-f64 receiver clone \
(that clone is only valid when the guard PASSED):\n{caller_ir}"
);
if failure_edge_callee == pshape_body {
let pshape_ir = defined_function_ir_section(&ir, pshape_body);
assert!(
pshape_ir.contains("call double @js_number_coerce("),
"the Ptr<Shape> clone reached on raw-f64 guard FAILURE must coerce \
every slot it loads — without that it is the typed clone under \
another name, and a receiver whose fields are not raw f64 would \
take raw loads anyway:\n{pshape_ir}"
);
}
assert!(
caller_ir.contains("call double @js_native_call_method_by_id"),
"method-direct guard failure should retain dynamic method fallback:\n{caller_ir}"
Expand Down
14 changes: 7 additions & 7 deletions scripts/ci_e2e_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@
"macos_bundle_chdir_gate",
"manifest_consistency",
"native_proof_buffer_views",
# #7506/#7245: held out until its one failing test was triaged. The
# composition it guards had drifted from three named callees to three
# PROPERTIES (the guard-failure edge now reaches `$pshape`, which coerces
# what it loads, rather than `$generic`), so the test asserted a symbol that
# no longer had to be there. Re-pointed at the property; 262/262 green, and
# the suite belongs in the per-PR map rather than in the exclusions.
"native_proof_regressions",
"node_test_mock_property_presence",
"perry_builtin_name_collision",
"private_guard_declaring_class",
Expand Down Expand Up @@ -168,13 +175,6 @@
"large_local_array_push_inbounds_store_emits_precise_slot_barrier",
"#7708 — red on main; the other 2 tests in this suite pass.",
),
(
"perry-codegen",
"native_proof_regressions",
"typed_f64_receiver_method_clone_raw_loads_after_composed_guards",
"#7506 — the guard-failure edge no longer calls $generic; miscompile or "
"intentional collapse is an open question. The other 261 tests pass.",
),
]

# Suites reached through `SOURCE_SUITE_MAP` are exempt from `--cap` and carry a
Expand Down
Loading