From 0b954e659cdacb599648527ea75286db43b87a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 11 Aug 2026 00:17:45 +0200 Subject: [PATCH] test(codegen): assert the guard-failure edge's property, not a symbol name (#7506) --- .../7807-guard-failure-edge-property.md | 23 ++++++++++ .../tests/native_proof_regressions.rs | 46 ++++++++++++++++++- scripts/ci_e2e_scope.py | 14 +++--- 3 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 changelog.d/7807-guard-failure-edge-property.md diff --git a/changelog.d/7807-guard-failure-edge-property.md b/changelog.d/7807-guard-failure-edge-property.md new file mode 100644 index 0000000000..ab6449f2ae --- /dev/null +++ b/changelog.d/7807-guard-failure-edge-property.md @@ -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 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. diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 01b7ae8df8..7675add11b 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -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); @@ -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 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 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}" diff --git a/scripts/ci_e2e_scope.py b/scripts/ci_e2e_scope.py index 68b5ce9bdc..d52efe3375 100755 --- a/scripts/ci_e2e_scope.py +++ b/scripts/ci_e2e_scope.py @@ -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", @@ -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