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/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 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 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}" 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 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..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; @@ -725,6 +737,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 +756,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 — diff --git a/pyre/rework.md b/pyre/rework.md index 672147dd251..1e6a5746906 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 | 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. + --- ## 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,20 @@ 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), 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: @@ -306,6 +396,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