From 881e98e6ee48ef21aeb212ac4753e7f818ec0cf3 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 6 Aug 2026 05:09:04 +0900 Subject: [PATCH 1/8] jit: stamp the qmut abort's own subwalk coordinate, and re-seed a live-NULL operand slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A walk that executed residual side effects and then fails to commit its end state falls back to the legacy replay from the traced region's entry, which runs those residuals a second time. Two shapes reached that fallback; both show up under `PYRE_FBW_CENSUS=1` as `committed=false effects>0`. `WalkSession::abort_in_subwalk` is sticky — `claim_abort_coordinate` only ever sets it — so an inline sub-walk abort the walk recovered from left it true for every later abort in the same trace attempt, and `flush_qmut_abort_state`'s gate then read a root-frame abort as a callee coordinate. The `ForceQuasiImmutable` raise in `dispatch_residual_call_iRd_kind` now stamps it from `fbw_mode.inline_subwalk` at the raise point, as the two kept-stack branch-guard raises already do. `reseed_vstack_from_shadow` rejected a NULL const-ptr shadow slot outright, because a NULL there can also mean a slot the portal never wrote. It now accepts one carrying the `virtualizable_live_null_slots` marker, which records that the last executed store into that slot wrote a NULL. PUSH_NULL's `self_or_null` sentinel is such a slot and stays live across the whole callable/args/kwargs build ahead of a CALL; the reorder region re-seeds the mirror in the middle of that build, and the rejected slot made `capture_vstack_mirror_image` refuse the image, leaving an escape inside the call with no blackhole resume. `capture_vstack_mirror_image`'s decline line gains the Python pc and the mirror boxes. The LoadName cell-fold gate comment is rewritten to the measured state: with the gate lifted the `bench/synth` corpus is output-correct, and what fails is `exception_reraise_tb_depth_jitstress` at 13.0x against its 4x pypy gate plus four benches' jit-stats. Measured with the gate lifted, in-place arms: `iter57/real_exception` 100003 -> 100000, `exception_reentry_guard_finally_residual` `leaked 4 reentry 2` -> `leaked 0 reentry 0`. Assisted-by: Claude --- .../src/jitcode_dispatch/residual_call.rs | 44 +++++++++++++------ .../src/jitcode_dispatch/vstack_mirror.rs | 20 ++++++++- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs index d1fe14c0fc2..22f0f859716 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs @@ -277,9 +277,12 @@ fn capture_vstack_mirror_image( } other => { latchdbg!( - "origin={origin} mirror-slot {}/{} unresolved opref={opref:?} concrete={other:?}", + "origin={origin} mirror-slot {}/{} unresolved opref={opref:?} \ + concrete={other:?} pypc={} boxes={:?}", slots.len(), ctx.vstack_boxes.len(), + ctx.vstack_cur_pypc, + ctx.vstack_boxes, ); return None; } @@ -4746,6 +4749,17 @@ pub(crate) fn dispatch_residual_call_iRd_kind( // Without it the abort lands in the `Generic` catch-all and the // `abort: force quasi-immut` counter stays at 0. crate::state::note_force_quasi_immut_abort(); + // Stamp the abort coordinate at the raise point, the way the two + // kept-stack branch-guard raises do, so the flush gate cannot observe + // an UNRELATED prior abort. `abort_in_subwalk` is sticky for the whole + // trace attempt (`claim_abort_coordinate` only ever sets it), so an + // earlier inline sub-walk abort the walk RECOVERED from — the attempt + // discarded, the call residualized, the walk continued — leaves it true + // for every later abort. `flush_qmut_abort_state`'s gate then declines + // a root-frame qmut abort as though its pc named a callee jitcode, and + // the legacy replay re-runs the region on top of the residuals the walk + // already executed. + ctx.session.borrow_mut().abort_in_subwalk = ctx.fbw_mode.inline_subwalk; return Err(DispatchError::ForceQuasiImmutable { pc: op.pc }); } @@ -5870,19 +5884,21 @@ pub(crate) fn dispatch_residual_call_iIRd_kind( // `GUARD_NOT_INVALIDATED`, so a rebind or a delete fails the loop instead of // reaching a NameError, and a successful fold provably cannot raise. // - // The gate is load-bearing because the fold INSTALLS the module dict's - // `version?` watcher, and that watcher is what makes a later bumping write - // in the same program abandon the walk with `ForceQuasiImmutable`. That - // abort resumes mid-expression off a latched operand mirror, and when one - // latched slot is unbound the flush declines to the legacy replay, which - // then REFUSES to re-deliver an in-flight FOR_ITER item once a body effect - // has committed — the iteration is dropped and its accumulator increment is - // silently lost. Lifting the gate to `DELETE_NAME`/`DELETE_GLOBAL` only - // (the implicit `del e` an `except X as e:` emits) reaches exactly that: - // `bench/synth/pickle_terminal_raise_resume` then prints 214 under the JIT - // against 216 interpreted, off one dropped iteration. Both halves of that - // chain — the unbound mirror slot and the silent drop the decline falls - // back to — have to be closed before the handler shape stops standing in. + // The gate stands on compile behaviour, not on correctness. The fold + // INSTALLS the module dict's `version?` watcher, and that watcher is what + // makes a later bumping write in the same program abandon the walk with + // `ForceQuasiImmutable` — so lifting the gate multiplies those aborts in + // handler-bearing module bodies. The three wrong-code shapes that used to + // ride on that (a dropped FOR_ITER iteration in + // `pickle_terminal_raise_resume`, and the two double-applies in + // `iter57/real_exception` and `exception_reentry_guard_finally_residual`) + // are closed: the first no longer reproduces, and the other two were the + // stale `abort_in_subwalk` this file now stamps at its own qmut raise plus + // the live-NULL mirror slot `reseed_vstack_from_shadow` now accepts. With + // the gate lifted the whole `bench/synth` corpus is output-correct; what + // still fails is `exception_reraise_tb_depth_jitstress` at 13.0x against + // its 4x pypy gate, and four benches' jit-stats move (most visibly + // `exception_reraise_tb_depth_hot`, `loops_aborted 0 -> 63`). // // The scan is whole-body, so one `try` anywhere in a module also charges // every name access in it a live dict lookup (~83ns each, linear in the diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs index 6dbce7071e2..e26bdd5e1f8 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs @@ -725,6 +725,17 @@ pub(crate) fn reseed_vstack_from_shadow( // portal never wrote through) fails the whole re-seed so the caller // leaves the slot NONE; `stack_sync` then omits it (resume // re-materializes). + // + // A NULL const-ptr is rejected because it cannot be told apart from a slot + // the portal never wrote — EXCEPT where the slot carries the live-NULL + // marker, which says the last executed store into it wrote a NULL on + // purpose. That is PUSH_NULL's `self_or_null` sentinel: it stays live + // across the whole callable/args/kwargs build ahead of a CALL, and the + // reorder region reseeds the mirror from the shadow in the middle of that + // build. Rejecting it left slot NONE, `capture_vstack_mirror_image` + // refuses an image with any unresolved slot, and an escape inside the call + // then had no blackhole image at all and fell back to the legacy entry + // replay — which re-runs the residuals the walk already executed. if ctx.vstack_boxes.len() < new_depth { ctx.vstack_boxes.resize(new_depth, OpRef::NONE); } @@ -733,8 +744,13 @@ pub(crate) fn reseed_vstack_from_shadow( if ctx.vstack_boxes[s] != OpRef::NONE { continue; } - match ctx.trace_ctx.virtualizable_box_at(nvs + nlocals + s) { - Some(b) if b != OpRef::NONE && !opref_is_null_const_ptr(b) => { + let flat = nvs + nlocals + s; + match ctx.trace_ctx.virtualizable_box_at(flat) { + Some(b) + if b != OpRef::NONE + && (!opref_is_null_const_ptr(b) + || ctx.trace_ctx.virtualizable_slot_stored_live_null(flat)) => + { ctx.vstack_boxes[s] = b; } // Fill what we can; an unsourceable hole (NONE / NULL const-ptr — From 264bd3f6c42de27bd7df22ea07bf28177241ccec Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 6 Aug 2026 07:11:50 +0900 Subject: [PATCH 2/8] jit: record why reseed_vstack_from_callee_shadow keeps its NULL const-ptr rejection The callee-shadow reseed is the structural twin of `reseed_vstack_from_shadow` and rejects a NULL const-ptr the same way, but its source is a sparse `HashMap`, where a present key is already the write-witness the dense virtualizable array needed a per-slot side table to supply. So the clause discards a proven write whose value happens to be PUSH_NULL's `self_or_null`. Measured before writing this: dropping the clause leaves `check.py --backend dynasm` at 386/386 with no jit-stats movement and no baseline change, so the corpus does not distinguish the two behaviours. Behaviour unchanged; the comment records the asymmetry and the measurement. Assisted-by: Claude --- .../src/jitcode_dispatch/vstack_mirror.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs index e26bdd5e1f8..7199190dc65 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs @@ -686,6 +686,18 @@ fn reseed_vstack_from_callee_shadow( if *slot != OpRef::NONE { continue; } + // The NULL const-ptr rejection here is stricter than the shadow can + // justify: `opref` is a sparse map, so a PRESENT key is already the + // proof that this walk wrote the slot, and a present key holding + // CONST_NULL is a deliberately written NULL — PUSH_NULL's `self_or_null` + // ahead of a call inside the inlined callee. `reseed_vstack_from_shadow` + // needed a per-slot live-NULL side table to draw that same distinction + // only because its source is a dense array, where absent and NULL are + // the same word. Kept as-is regardless: dropping the clause leaves the + // whole dynasm corpus at 386/386 with NO jit-stats movement, so nothing + // measures the difference, and an unwitnessed widening of what counts as + // a resolved mirror slot is the direction that turns a decline into a + // wrong answer. match shadow.opref.get(&((stack_base + s) as i64)).copied() { Some(value) if value != OpRef::NONE && !opref_is_null_const_ptr(value) => { *slot = value; From 596583d21c96497315141131b25ccebb08b84af9 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 6 Aug 2026 11:11:09 +0900 Subject: [PATCH 3/8] rework.md: refresh the audit against the current tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The findings were measured on `pc-map` on 2026-07-05. Re-measured on `ec-wiring` at base 58fcd373e05, thirteen of the fifteen issues the document tracks are closed and the priority order has inverted. F1: gh#366/367/368/369 closed; `metadata.pc_map` and `resume_jitcode_pc_for` have zero hits and `resume::SnapshotFrame.pc` is the JitCode byte offset. The surviving `pc_map` matches are the compile-time exit-recovery `Vec` in jit/codewriter.rs and jit/flatten.rs, a different thing. Residue recorded: recorder.rs's SnapshotFrame doc still describes the deleted translation, py_pc is stored rather than derived, and build_state_field_snapshot stamps the JitCode offset into py_pc (unproven, needs a repro). F2: verified done — `is_full_body_walk`, `PYRE_FULL_BODY_WALK` and `OpcodeHandler for MIFrame` have zero hits each. F3: regressed to 15 registrations against MAX_EXTRA_ROOT_WALKERS = 16; the 16th caller hits `panic!("capacity exceeded")` at startup. F4: gh#346 and gh#373 closed, coverage still landing (#1065); abort_permanent unchanged in scale, but the exit criterion is the census, not a match count. F5: gate-triage.md now exists but the population grew from 119 matches to 245 distinct PYRE_* identifiers. Sequencing amended to WS3 > WS2 > WS1-residue > WS4. Assisted-by: Claude --- pyre/rework.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/pyre/rework.md b/pyre/rework.md index 672147dd251..324e57a2c81 100644 --- a/pyre/rework.md +++ b/pyre/rework.md @@ -14,6 +14,24 @@ the anti-roadmap (§3.5) items have been rebuilt. The structural defects are concentrated in four places, all inside trace/resume/translate/GC-roots — exactly the Phase A territory the charter says outranks everything. +## Audit refresh — 2026-08-06 (`ec-wiring`, base `58fcd373e05`) + +The findings below were written against `pc-map` on 2026-07-05. Re-measured a +month later, **the priority order has inverted**: F1 and F2, the two the program +called critical path, are substantially done; F3, filed as the least urgent of +the four, has quietly moved to one slot from a hard failure. + +| finding | 2026-07-05 | 2026-08-06 | +|---|---|---| +| F1 resume coordinates | two systems + lossy `pc_map` | **largely resolved** — gh#366/367/368/369 all closed, both named artifacts gone from the tree; residue is comment rot and a `py_pc` that is still stored rather than derived | +| F2 three executors | trait twin alive | **done, verified** — `is_full_body_walk`, `PYRE_FULL_BODY_WALK`, `OpcodeHandler for MIFrame`: 0 hits each | +| F3 root-walker registry | 14 of 16 slots | **regressed — 15 of 16**; the 16th registration is a startup `panic!` | +| F4 translate coverage | ~124-graph gap | gh#346 and gh#373 closed, work still landing (#1065); `abort_permanent` unchanged in scale | +| F5 gate debt | 119 `PYRE_*` matches | 245 distinct `PYRE_*` identifiers; `gate-triage.md` refreshed 2026-08-02 | + +Of the fifteen issues this document tracks, **thirteen are closed**. Only gh#376 +(Phase C decision document) and gh#126 (fib_recursive residual) remain open. + --- ## 1. Findings @@ -57,6 +75,38 @@ slot-vs-color); groundwork gh#73 + PR#365 (closed, M3 milestone). Related: gh#343 (multi-frame virtual-PyFrame rematerialization on deopt), gh#371 (compile-time walker coalescing residual). +**Status (2026-08-06) — largely resolved.** gh#366, gh#367, gh#368, gh#369 and +gh#343 are all closed; `3ebf2f9280c` deleted the py_pc↔JitCode resume +translation and `4e53480018d` cleaned up the `pc_map` dead names. Measured on +the tree: `metadata.pc_map` and `resume_jitcode_pc_for` have **zero** hits, and +`resume::SnapshotFrame` now carries `jitcode_index` plus a `pc` documented and +written as the JitCode byte offset (`resume.py:250` parity). The ~56 surviving +`pc_map` matches are an unrelated compile-time `Vec` in +`jit/codewriter.rs` and `jit/flatten.rs` that drives exit-recovery construction; +it is not the resume translation table and is not F1. + +Three pieces of residue remain, none of them the original defect: + +1. **Comment rot.** `majit-metainterp/src/recorder.rs`'s `SnapshotFrame::pc` + still documents "Pyre's tracer populates this slot with the Python bytecode + PC because pyre traces Python bytecode rather than JitCode … the runtime + translates `py_pc` through `pc_map` at resume time until pyre's + walker-as-tracer epic lands." All three claims are now false — the tracer + interprets JitCode, the translation is deleted, and the epic landed. Its + writers (`pyjitpl/dispatch.rs build_state_field_snapshot`, + `history.rs`) stamp the JitCode offset. +2. **`py_pc` is stored, not derived.** WS1 increment 2 called for the + Python-level PC to be derived the way PyPy derives it. It is instead + forward-carried in every snapshot frame and serialized into + `rd_numb`/`numb_state`, then read back as a Python coordinate by + `pyre-jit/src/eval.rs` for frame and traceback reconstruction. That is the + §4 falsifier "keep derived, not stored" outcome — it should be recorded as + the deliberate adaptation it now is, or finished. +3. **Unproven: a coordinate mixed at one writer.** `build_state_field_snapshot` + stamps `py_pc: frame.pc`, i.e. the same JitCode offset it writes to `pc`, + into the field whose readers treat it as a Python pc. The corpus is green, + so if this is wrong it is latent; it needs a repro before it is a finding. + ### F2 — Trace time has a hand-written interpreter twin (three executors) **What exists.** PyPy has two executors: metainterp (trace time) and @@ -125,6 +175,23 @@ retirement (taxonomy of the 14 walkers, prebuilt-protocol absorption of the immortal populations, shrinking `MAX_EXTRA_ROOT_WALKERS` as the progress metric) is now recorded there as a scope supplement (2026-07-05 comment). +**Status (2026-08-06) — REGRESSED, and now the live risk.** gh#355 is closed, +but the registry retirement it was supplemented with did not happen. The +registry grew instead: **15 registrations against `MAX_EXTRA_ROOT_WALKERS = 16`** +(`shadow_stack.rs`), so exactly one slot is left and the 16th caller dies on +`panic!("register_extra_root_walker: capacity exceeded")` — at startup, on every +platform, for whoever adds the next walker. The current population is 11 from +`pyre-jit/src/eval.rs` (jit/bh/guard exc values, rbigint parts cache, immortal +exception singletons, last CA exception, sre patterns, w_globals-stamped code, +mapdict method cache, …), 3 from `pyre-interpreter/src/eval.rs` (global +prebuilt roots, thread roots, …) and 1 from `majit-gc/src/gcreftracer.rs`. + +This inverts the program's sequencing: F3 is no longer the low-urgency +workstream. Raising the constant is the accretion the finding condemns, so the +next walker to be added should instead be the trigger for the class-(a)/(b) +absorption in WS3 — with the caveat that the shrinking constant can no longer +serve as the progress metric until it first stops growing. + ### F4 — majit-translate coverage is sustained by per-case seams **What exists.** Rust idioms still lack systematic lowering: the #346 @@ -152,6 +219,16 @@ successor of the closed gh#131) with per-gap instance issues gh#336 NewWithVtable), gh#181 (box_value Void), gh#176 (phi threading), gh#180 (gctransform), gh#139 (EffectInfo). The cliff symptom is gh#373. +**Status (2026-08-06) — epic closed, work continuing under it.** gh#346 and +gh#373 are both closed, and coverage is still landing against the #346 line +(most recently #1065, the `ll_alloc_and_set` list-allocation family and the +full `newlist_clear` compound jitcode opcode). `abort_permanent` has not +shrunk in scale — ~214 matches, concentrated in `jit/codewriter.rs`, +`pyre-jit-trace/src/trace.rs` and `jit/flatten.rs` — but the exit criterion was +never "few matches", it was "zero UNLISTED sources", which the census answers +and a raw match count does not. Re-run the census before treating this +finding as open or closed. + ### F5 — Deficiencies (right design, unfinished) — the debt list - **Compilation cliffs**: unported opcode classes (CallIntrinsic2, GetLen, @@ -161,7 +238,12 @@ NewWithVtable), gh#181 (box_value Void), gh#176 (phi threading), gh#180 gh#343; the closed gh#215 was the umbrella). - **Gate debt**: **119 distinct `PYRE_*` env gates** in the tree (28 in the FBW family alone). Charter §3.6: a gate is a staging area, not a home. - No triage table exists. *No tracking issue.* + No triage table exists. *No tracking issue.* **2026-08-06: the table now + exists (`gate-triage.md`, refreshed 2026-08-02), but the population kept + growing — 245 distinct `PYRE_*` identifiers in the tree against the 119 + matches this audit counted. The triage is a snapshot, not a brake; nothing + makes a new gate enter the table at birth, which is what WS4 item 1 asked + for.** - **Documentation rot (N7)**: `majit/README.md` documented the deleted majit-analyze era (crates majit-opt/meta/codegen/runtime/analyze vs the actual majit-translate/metainterp/backend-* tree). **Resolved 2026-07-05: @@ -306,6 +388,15 @@ should become deletable — that is the real exit test). Priority under contention (charter §5 order): **WS1 > WS2 > WS3 > WS4**, with the qualifications: +> **Amended 2026-08-06.** This ordering was written when WS1 held the shipped +> miscompile class. It no longer does: WS1's increments 1–3 landed and its +> increment 4 is done pyre-side, while WS3's registry moved to 15 of 16 slots +> with a startup panic behind the last one. Under contention today the order is +> **WS3 > WS2 > WS1-residue > WS4** — WS3 because the next walker registration +> is an immediate hard failure, WS1-residue last because what remains of it is +> comment rot plus one policy decision (store-vs-derive `py_pc`), not a +> correctness risk. + - WS1 increments 1–3 are the critical path — they root out the shipped miscompile class. Increment 4 (W4) is the largest single piece; its prerequisite work (per-opcode entry_py_pc advance, concrete seeding) is From 425ecfa457b66d3cc6e9a504447a026a0561be21 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 6 Aug 2026 18:24:21 +0900 Subject: [PATCH 4/8] rework.md: correct the F5 gate count to a reproducible measurement The refresh recorded 245 distinct `PYRE_*` identifiers against the audit's original 119. That figure does not reproduce: tracked `*.rs` holds 131 distinct identifiers, all tracked files 174, and 548 raw matches. The quantity comparable to the original "distinct `PYRE_*` env gates" is the set of names actually read from the environment, which is 126. The command is now stated in the document so the number can be re-derived, along with the three other counts it is easy to confuse it with. Assisted-by: Claude --- pyre/rework.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyre/rework.md b/pyre/rework.md index 324e57a2c81..1e6a5746906 100644 --- a/pyre/rework.md +++ b/pyre/rework.md @@ -27,7 +27,7 @@ the four, has quietly moved to one slot from a hard failure. | F2 three executors | trait twin alive | **done, verified** — `is_full_body_walk`, `PYRE_FULL_BODY_WALK`, `OpcodeHandler for MIFrame`: 0 hits each | | F3 root-walker registry | 14 of 16 slots | **regressed — 15 of 16**; the 16th registration is a startup `panic!` | | F4 translate coverage | ~124-graph gap | gh#346 and gh#373 closed, work still landing (#1065); `abort_permanent` unchanged in scale | -| F5 gate debt | 119 `PYRE_*` matches | 245 distinct `PYRE_*` identifiers; `gate-triage.md` refreshed 2026-08-02 | +| F5 gate debt | 119 `PYRE_*` matches | 126 `PYRE_*` names read from the environment; `gate-triage.md` refreshed 2026-08-02 | Of the fifteen issues this document tracks, **thirteen are closed**. Only gh#376 (Phase C decision document) and gh#126 (fib_recursive residual) remain open. @@ -239,11 +239,19 @@ finding as open or closed. - **Gate debt**: **119 distinct `PYRE_*` env gates** in the tree (28 in the FBW family alone). Charter §3.6: a gate is a staging area, not a home. No triage table exists. *No tracking issue.* **2026-08-06: the table now - exists (`gate-triage.md`, refreshed 2026-08-02), but the population kept - growing — 245 distinct `PYRE_*` identifiers in the tree against the 119 - matches this audit counted. The triage is a snapshot, not a brake; nothing - makes a new gate enter the table at birth, which is what WS4 item 1 asked - for.** + exists (`gate-triage.md`, refreshed 2026-08-02), and the population has grown + only modestly — 126 distinct `PYRE_*` names are read from the environment, + against the 119 this audit counted:** + + ``` + rg -o 'env::var[_a-z]*\("(PYRE_[A-Z0-9_]+)"' -r '$1' --glob '*.rs' | sort -u | wc -l + ``` + + **Counting identifiers rather than environment reads inflates this — 131 + distinct `PYRE_*` identifiers appear in tracked `*.rs`, 174 across all tracked + files, 548 raw matches — so state which of the four the number is. The triage + is still a snapshot, not a brake: nothing makes a new gate enter the table at + birth, which is what WS4 item 1 asked for.** - **Documentation rot (N7)**: `majit/README.md` documented the deleted majit-analyze era (crates majit-opt/meta/codegen/runtime/analyze vs the actual majit-translate/metainterp/backend-* tree). **Resolved 2026-07-05: From 487c7ff1557daaa6d0fc4ba49ddc76c5a9ad2810 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 6 Aug 2026 19:46:11 +0900 Subject: [PATCH 5/8] check.py: do not fail a ratio gate whose baseline is clamped to the floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_exec_time` clamps a startup-subtracted time to `EXEC_TIME_FLOOR_S` so ratios cannot divide by ~0. When the pypy baseline lands there, the ratio is `pyre_exec / EXEC_TIME_FLOOR_S` and the ceiling it is compared against is an absolute wall-clock budget of `ceiling * EXEC_TIME_FLOOR_S` seconds, fitted on whichever host wrote the header. The comparison table already marks those ratios `~` and prints "ratio is not a measurement"; the gate failed the run on them anyway. `failed_bound` now returns None whenever the baseline is clamped, instead of requiring the backend to be at the floor as well. Only the ceiling changes behaviour: the floor arms at `exec_baseline >= FLOOR_GATE_MIN_BASELINE_S`, which a clamped baseline is always under. The gate can therefore only pass more than before, never fail more. The `[... clamped to floor; ratio not a measurement]` suffix in `_gate_fail_detail` is unreachable once a clamped baseline returns no bound, and is removed; the `~` legend states the consequence instead. Three consecutive `main` runs failed this way on three different fixtures across two runners: global_cell_shortpreamble_hot 24.1x > 19x and class_reassign_hot 49.2x > 47x on ubuntu-24.04, reentrant_key_eq_mutation 10.3x > 5x on macos-latest (runs 31079972573, 31080288895). Discriminator, cranelift, `class_reassign_hot` with its ceiling temporarily set to 1: the previous check.py reports SLOWER "exec 0.13s > pypy 0.01s ratio 27.0x > gate 1x [pypy exec clamped to floor; ratio not a measurement]", this one reports PASS. With the same ceiling of 1 on seqiter_tuple_error_parity, whose pypy exec is a measurement, this check.py still reports SLOWER at 18.3x — the ceiling is untouched wherever the baseline is real. The three fixtures above pass with their own ceilings restored. Assisted-by: Claude --- pyre/check.py | 53 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/pyre/check.py b/pyre/check.py index a55dcf06144..edd1828dc36 100644 --- a/pyre/check.py +++ b/pyre/check.py @@ -1891,21 +1891,36 @@ def _performance_gate_passed( def failed_bound(measured, baseline_value): exec_measured = self._exec_time(backend, measured) exec_baseline = self._exec_time(baseline_key, baseline_value) - # A perf ratio is dominated by timer resolution only when BOTH - # sides are: a baseline pinned to EXEC_TIME_FLOOR_S by startup - # subtraction over-estimates its real (sub-floor) work, so the - # reported ratio measured/floor is a LOWER bound on the true ratio. - # Exempting on a clamped baseline alone would therefore hide a - # provable slowdown -- if measured/floor already clears the gate, - # the true ratio clears it by even more. Require the backend to be - # at the floor too, so the exemption fires only when neither side - # has measurable work; a backend above the floor is a real - # measurement the gate still applies to (this is the actual-clamping - # test the 100ms absolute threshold failed to be). - if ( - self._baseline_exec_time_clamped(baseline_key, baseline_value) - and exec_measured <= EXEC_TIME_FLOOR_S - ): + # A baseline pinned to EXEC_TIME_FLOOR_S by startup subtraction is + # not a measurement of the baseline, it is the floor constant. The + # ratio built on it is `exec_measured / EXEC_TIME_FLOOR_S`, so the + # recorded ceiling it is compared against is an absolute wall-clock + # budget of `limit * EXEC_TIME_FLOOR_S` seconds -- and that budget + # was fitted on whichever host last wrote the header. Applying it + # elsewhere compares two hosts' wall clocks with no baseline + # standing between them, which is what the printed line already + # says ("ratio not a measurement") while failing the run on it. + # Measured: `class_reassign_hot` read 27.0x and 27.3x on one host + # and 49.2x on a CI runner against the same code, because only the + # numerator moves. + # + # The earlier reading kept the ceiling armed here because + # measured/floor is a LOWER bound on the true ratio, so a failure + # does prove the fixture is at least that many times slower than + # pypy. It is still not a bound this ceiling can judge: the + # recorded number carries the same clamp, so no pypy measurement + # enters the comparison on either side. Three consecutive `main` + # runs failed exactly this way on three different fixtures and two + # runners -- global_cell_shortpreamble_hot 24.1x > 19x, + # class_reassign_hot 49.2x > 47x, reentrant_key_eq_mutation 10.3x > + # 5x -- which is a population, not three regressions. + # + # Only the ceiling relaxes: the floor declines to arm below + # FLOOR_GATE_MIN_BASELINE_S for the neighbouring reason, which a + # clamped baseline is always under. The number stays visible either + # way -- the comparison table prints it with a `~`. A fixture that + # wants a ratio gate has to give pypy enough work to measure. + if self._baseline_exec_time_clamped(baseline_key, baseline_value): return None if exec_measured > exec_baseline * limit + compare_buffer: return "ceiling" @@ -1953,7 +1968,6 @@ def _gate_fail_detail( ratio = "-" else: ratio = f"{float(exec_m) / float(exec_b):.1f}x" - clamped = self._baseline_exec_time_clamped(baseline, baseline_time) if bound == "floor": detail = ( f"exec {exec_m:.2f}s vs {baseline} {exec_b:.2f}s " @@ -1965,8 +1979,6 @@ def _gate_fail_detail( f"exec {exec_m:.2f}s > {baseline} {exec_b:.2f}s " f"ratio {ratio} > gate {float(limit):g}x" ) - if clamped: - detail += f" [{baseline} exec clamped to floor; ratio not a measurement]" return detail def _run_backend_bench( @@ -2420,7 +2432,10 @@ def print_comparison_table(self): header += "".join(f" {b:>18s}" for b in cols) print(header) if any("~" in c[b] for c in self.comparisons for b in cols): - print(" ~ pypy exec clamped to floor; ratio is not a measurement") + print( + " ~ pypy exec clamped to floor; ratio is not a measurement, " + "and no ratio gate is applied to it" + ) print(" " + "─" * (54 + 19 * len(cols))) for c in self.comparisons: row = f" {c['name']:<35s} {c['cpython']:>8s} {c['pypy']:>8s}" From 863383e9e52d47d184eb744e44f8245d1a9a68be Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 6 Aug 2026 23:51:33 +0900 Subject: [PATCH 6/8] posix: correct which stat rejection precedes the platform's dir_fd check `stat_entry` consults `HAVE_FSTATAT` while unwrapping `dir_fd`, above the descriptor branch, so the descriptor+`dir_fd` conflict is unreachable where `fstatat` does not exist. The comment claimed both fd-conflict rejections come first. #1081 corrected the same claim in `extra_tests/parity_tests/os_stat_file_descriptor.py` and cites `_DirFD_Unavailable` (`interp_posix.py:285-292`) for it; this is the statement of it that sits next to the code. Assisted-by: Claude --- pyre/pyre-interpreter/src/module/posix/interp_posix.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs index 74bfa664135..c9e4742e7d8 100644 --- a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs +++ b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs @@ -3326,8 +3326,13 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { None => default_follow, }; // interp_posix.py:634-644 `do_stat` tests the descriptor first: with one - // in hand neither other argument has anything to apply to, and both - // rejections precede the platform's dir_fd availability. + // in hand neither other argument has anything to apply to. Only the + // `follow_symlinks` rejection precedes the platform's dir_fd + // availability, though — `_DirFD_Unavailable` (`interp_posix.py:285-292`, + // the `!HAVE_FSTATAT` arm above) turns the argument away while + // unwrapping it, a step earlier than this, so where `fstatat` does not + // exist a descriptor passed with `dir_fd` reports the platform rather + // than the conflict. if path.as_fd != -1 { if dir_fd.is_some() { // 3.14 words this "can't specify dir_fd without matching From a8673ce33a43facb16fb74e016d7fe599e11a753 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 7 Aug 2026 10:06:21 +0900 Subject: [PATCH 7/8] bench: re-record the wasm jit-stats for exception_reused_object_tb_not_doubled `fbw_blackhole_adopted_single_frame` reads 3 where the baseline had no entry for it. `loops_compiled=4` and `bridges_compiled=3` are unchanged, so the trace shape is the same and what moved is that the walk now adopts the blackhole resume image instead of falling back to the replay from the traced region's entry. Attributed by measuring both arms with the same command, `check.py --backend wasm --synthetic-only --synthetic-pattern exception_reused_object_tb_not_doubled`: with `ff503b5d746` reverse-applied in place the bench reports ALL PASSED against the existing baseline, and with it restored it reports the 0 -> 3 change. The control arm took 2m32s against the treatment arm's 4s, which is the wasm module being relinked rather than reused. The counter arrived with #1064 and this bench's baselines were last recorded at `da5e6fb38c7` (#1059), so absence from the baseline did not by itself say which of the two it was. No CI job runs `--backend wasm`, so the wasm baselines are not gated there either. The other four keys the re-record adds -- fbw_blackhole_adopted_multi_frame, fbw_store_journal_rollback_failed, field_pos_attached_misplaced, field_pos_spec_misplaced -- are counters that did not exist at #1059 and are pinned at 0 here for the first time. The dynasm and cranelift baselines are not re-recorded: both backends still report ALL PASSED for this bench. Assisted-by: Claude --- .../synth/exception_reused_object_tb_not_doubled.wasm.jitstats | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyre/bench/synth/exception_reused_object_tb_not_doubled.wasm.jitstats b/pyre/bench/synth/exception_reused_object_tb_not_doubled.wasm.jitstats index 04452df1061..cc58b9621ab 100644 --- a/pyre/bench/synth/exception_reused_object_tb_not_doubled.wasm.jitstats +++ b/pyre/bench/synth/exception_reused_object_tb_not_doubled.wasm.jitstats @@ -2,7 +2,10 @@ bridges_compiled=3 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 +fbw_blackhole_adopted_multi_frame=0 +fbw_blackhole_adopted_single_frame=3 fbw_rolled_back_with_effects=0 +fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 guard_failures=600 From 5e66b71a4f13e2b4f6937b10a837bb3bbe83cd7f Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 7 Aug 2026 11:56:29 +0900 Subject: [PATCH 8/8] bench: restore bridges_compiled and guard_failures on three synth baselines `9d2fff92649` (#1063) re-recorded 993 jit-stats baselines. All but four gained only the two new `field_pos_*_misplaced=0` keys; three changed a value: binary_int_overflow_local_resume bridges 5 -> 6 guards 647 -> 686 exc_bridge_entry_guard_not_removed bridges 4 -> 5 guards 809 -> 1009 list_append_write_barrier_gc bridges 5 -> 6 guards 1345 -> 1562 Five runs report the pre-#1063 values and none reports the recorded ones: dynasm, cranelift and wasm here, and `main`'s own CI on ubuntu-24.04 and macos-latest at 9d2fff92649 -- run 31139317566, jobs 92747505633 and 92748753166, on a tree carrying no commit from this branch. The three benches fail identically on all three backends in each of them. Only those two keys are restored; #1063's two added keys stay. The fourth bench it revalued, getattribute_override_no_bind, is left as recorded: it passes here and in that CI run, so its new values do reproduce. Assisted-by: Claude --- .../synth/binary_int_overflow_local_resume.cranelift.jitstats | 4 ++-- .../synth/binary_int_overflow_local_resume.dynasm.jitstats | 4 ++-- .../synth/binary_int_overflow_local_resume.wasm.jitstats | 4 ++-- .../exc_bridge_entry_guard_not_removed.cranelift.jitstats | 4 ++-- .../synth/exc_bridge_entry_guard_not_removed.dynasm.jitstats | 4 ++-- .../synth/exc_bridge_entry_guard_not_removed.wasm.jitstats | 4 ++-- .../synth/list_append_write_barrier_gc.cranelift.jitstats | 4 ++-- pyre/bench/synth/list_append_write_barrier_gc.dynasm.jitstats | 4 ++-- pyre/bench/synth/list_append_write_barrier_gc.wasm.jitstats | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyre/bench/synth/binary_int_overflow_local_resume.cranelift.jitstats b/pyre/bench/synth/binary_int_overflow_local_resume.cranelift.jitstats index d2a47c3998a..9df977d4495 100644 --- a/pyre/bench/synth/binary_int_overflow_local_resume.cranelift.jitstats +++ b/pyre/bench/synth/binary_int_overflow_local_resume.cranelift.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=6 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=686 +guard_failures=647 internal_compile_panics=0 loops_aborted=0 loops_compiled=5 diff --git a/pyre/bench/synth/binary_int_overflow_local_resume.dynasm.jitstats b/pyre/bench/synth/binary_int_overflow_local_resume.dynasm.jitstats index d2a47c3998a..9df977d4495 100644 --- a/pyre/bench/synth/binary_int_overflow_local_resume.dynasm.jitstats +++ b/pyre/bench/synth/binary_int_overflow_local_resume.dynasm.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=6 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=686 +guard_failures=647 internal_compile_panics=0 loops_aborted=0 loops_compiled=5 diff --git a/pyre/bench/synth/binary_int_overflow_local_resume.wasm.jitstats b/pyre/bench/synth/binary_int_overflow_local_resume.wasm.jitstats index d2a47c3998a..9df977d4495 100644 --- a/pyre/bench/synth/binary_int_overflow_local_resume.wasm.jitstats +++ b/pyre/bench/synth/binary_int_overflow_local_resume.wasm.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=6 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=686 +guard_failures=647 internal_compile_panics=0 loops_aborted=0 loops_compiled=5 diff --git a/pyre/bench/synth/exc_bridge_entry_guard_not_removed.cranelift.jitstats b/pyre/bench/synth/exc_bridge_entry_guard_not_removed.cranelift.jitstats index ba0fdfc0021..32fba6ef986 100644 --- a/pyre/bench/synth/exc_bridge_entry_guard_not_removed.cranelift.jitstats +++ b/pyre/bench/synth/exc_bridge_entry_guard_not_removed.cranelift.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=5 +bridges_compiled=4 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1009 +guard_failures=809 internal_compile_panics=0 loops_aborted=0 loops_compiled=4 diff --git a/pyre/bench/synth/exc_bridge_entry_guard_not_removed.dynasm.jitstats b/pyre/bench/synth/exc_bridge_entry_guard_not_removed.dynasm.jitstats index ba0fdfc0021..32fba6ef986 100644 --- a/pyre/bench/synth/exc_bridge_entry_guard_not_removed.dynasm.jitstats +++ b/pyre/bench/synth/exc_bridge_entry_guard_not_removed.dynasm.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=5 +bridges_compiled=4 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1009 +guard_failures=809 internal_compile_panics=0 loops_aborted=0 loops_compiled=4 diff --git a/pyre/bench/synth/exc_bridge_entry_guard_not_removed.wasm.jitstats b/pyre/bench/synth/exc_bridge_entry_guard_not_removed.wasm.jitstats index ba0fdfc0021..32fba6ef986 100644 --- a/pyre/bench/synth/exc_bridge_entry_guard_not_removed.wasm.jitstats +++ b/pyre/bench/synth/exc_bridge_entry_guard_not_removed.wasm.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=5 +bridges_compiled=4 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1009 +guard_failures=809 internal_compile_panics=0 loops_aborted=0 loops_compiled=4 diff --git a/pyre/bench/synth/list_append_write_barrier_gc.cranelift.jitstats b/pyre/bench/synth/list_append_write_barrier_gc.cranelift.jitstats index 600a782909f..b0b125c96eb 100644 --- a/pyre/bench/synth/list_append_write_barrier_gc.cranelift.jitstats +++ b/pyre/bench/synth/list_append_write_barrier_gc.cranelift.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=6 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1562 +guard_failures=1345 internal_compile_panics=0 loops_aborted=1 loops_compiled=12 diff --git a/pyre/bench/synth/list_append_write_barrier_gc.dynasm.jitstats b/pyre/bench/synth/list_append_write_barrier_gc.dynasm.jitstats index 600a782909f..b0b125c96eb 100644 --- a/pyre/bench/synth/list_append_write_barrier_gc.dynasm.jitstats +++ b/pyre/bench/synth/list_append_write_barrier_gc.dynasm.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=6 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1562 +guard_failures=1345 internal_compile_panics=0 loops_aborted=1 loops_compiled=12 diff --git a/pyre/bench/synth/list_append_write_barrier_gc.wasm.jitstats b/pyre/bench/synth/list_append_write_barrier_gc.wasm.jitstats index 600a782909f..b0b125c96eb 100644 --- a/pyre/bench/synth/list_append_write_barrier_gc.wasm.jitstats +++ b/pyre/bench/synth/list_append_write_barrier_gc.wasm.jitstats @@ -1,11 +1,11 @@ -bridges_compiled=6 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 fbw_rolled_back_with_effects=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1562 +guard_failures=1345 internal_compile_panics=0 loops_aborted=1 loops_compiled=12