Skip to content

jit(wasm): drop the wasm32 arm of the self-recursive root-bridge inline - #1106

Merged
youknowone merged 4 commits into
mainfrom
fib_recursive
Aug 8, 2026
Merged

jit(wasm): drop the wasm32 arm of the self-recursive root-bridge inline#1106
youknowone merged 4 commits into
mainfrom
fib_recursive

Conversation

@youknowone

@youknowone youknowone commented Aug 7, 2026

Copy link
Copy Markdown
Owner

bridge_rec_root_selfrec (inline_call.rs) was gated on cfg!(not(target_arch = "wasm32")), so a guard-failure bridge reaching a self-recursive callee's CALL took the root-bridge admission on dynasm and cranelift and residualized on wasm. This drops the cfg term, leaving the last policy-level wasm32 cfg in the JIT frontend behind.

The claim did not survive an audit

The cfg's stated reason — "the wasm always-portal path type-confuses the self-recursive inline (setintbound: got Ref)" — is phrasing carried over from a different site:

Both mechanisms that could make such a type confusion wasm-specific are refuted:

fbw_inline_callee_hazardous's own self-recursive decline is deliberately unchanged — it is all-backend and rests on the CA-trampoline reason. Only the unsupported wasm clause is removed from its doc.

Coverage the slice was missing

bridge_recursion_overflow already exercised this admission, but only in its easiest form: tail recursion whose live set is two machine integers. synth/selfrec_bridge_nontail_promote adds the three ingredients a "Ref reached an int operation" actually needs —

  • non-tail recursion (inner + len(tag)), so the guard's resume stream is multi-frame;
  • a Ref local live across the recursive CALL beside the int;
  • an accumulator crossing 2**63 at level 13 of 24, so it genuinely promotes to a long — a Ref — partway down and the overflow guard fires inside the recursive frame.
loops / bridges / aborted / guard_failures
wasm, decline kept 2 / 4 / 1 / 606
wasm, decline removed 2 / 6 / 0 / 806
dynasm = cranelift 2 / 6 / 0 / 806 — byte-identical

806 − 606 = 200 = exactly one trace_eagerness bucket.

Verification

check.py with this commit alone: dynasm 405/406, cranelift 405/406, wasm 401/402. All five wasm fixtures the admission moves are re-recorded and now carry the native values, except recursion_memo_branch at one guard failure apart — previously twenty-one.

The one failure, synth/pypy_type_surface, is not this change:

  • it reports the same bridges_compiled 5 -> 102, guard_failures 1011 -> 20497 with both touched files restored to origin/main;
  • it fails identically on all three backends, while this change is a no-op wherever the cfg already read true;
  • the fixture contains no self-recursive function for code_is_self_recursive to answer.

It is a red at the main tip; the second commit below diagnoses and fixes it, after which all three backends are fully green.

Performance

wasm user+sys CPU, min/median over 31 interleaved samples, decline removed vs kept (all arms OUTPUT-MATCH):

fixture min median
wasm_ca_trampoline_decline −20.1% −20.7%
selfrec_bridge_nontail_promote −12.9% −11.9%
recursion_memo_branch −1.2% −1.6%
ca_bridge_multiframe_resume_double_call +1.2% +2.7%
foriter_call_resume_drops_iteration +2.4% +4.0%

The two that get slower now report exactly what the dynasm baseline records — 16 bridges / 0 aborts and 27 bridges / 1 abort — where the decline left them at 16/1 and 26/2. The cost is wasm taking the native decision and paying its compile toll on a short bench, not a shortcut being removed.


Second commit: a main-tip red this branch had to clear first

synth/pypy_type_surface fails on all three backends at origin/main
(bridges_compiled 5 -> 102, guard_failures 1011 -> 20497). Verified not to be
the first commit's doing: it reports identically with both touched files
restored to origin/main, it fails where this branch is a no-op (the cfg
already read true on the natives), and the fixture holds no self-recursive
function. So it had to be diagnosed rather than worked around.

Cause. try_walker_specialize_load_type_name_attr (#1097) takes the
metaclass from type_name_obj_fast_pathtypedef::type, which falls back to
gettypefor(ob_type) when the receiver's w_class slot is null. The fold then
guards the metaclass by reading the raw w_class field, so such a receiver
gets guard_value(NULL, type) — unfailable-to-discharge, and nothing writes the
slot afterwards, so it fails once per iteration forever.

One class reaches it: the getset_descriptor type object is built lazily inside
the init loop as a builder for other typedefs' descriptors, so it never enters
the registry whose post-loop sweep stamps w_class = type. typedef::type
still answers type via the fallback, so the null is invisible from Python.

Localisation. check_descriptor_kinds alone carries all 97 excess bridges;
the receiver value and the FOR_ITER are both irrelevant; int, str, list,
NoneType, object, type, method_descriptor, wrapper_descriptor,
builtin_function_or_method and a user class are clean; and
getattr(C, "__name__") — identical semantics, but the fold declines because
the site's name is not in the code's name table — is clean.

After. check.py dynasm 406/406, cranelift 406/406, wasm 402/402, fully
green. pypy_type_surface reads its recorded 11/5/0/1011 exactly and #1097's own
type_name_attr_fold still reads 5/0/1/4, so the fold keeps applying wherever
its guard holds.

Happy to split this into its own PR if you would rather review it separately —
it is a single self-contained commit.


Third commit: the Windows check.py red, also from the base

pyre/check.py (windows-latest) fails at the merge-base d936eb4be42 with
os_utime_pathconf_truncate.py cpython=FAIL dynasm=FAIL cranelift=FAIL — the
same row on #1102, whose merge is that base. Two halves of that script are
POSIX-only: the times= keyword check reused a time before the epoch, which
the block above it already excludes Windows from, and the pathconf section
reads os.pathconf_names, registered under #[cfg(unix)] here and absent from
CPython's os on Windows. The keyword spelling now uses a time the platform
holds and the pathconf section runs where the names table exists;
pyre/extra_tests/parity_tests/run.py is green on macOS.

Fourth commit: the test.test_re CPython-suite red, cherry-picked

CPython suite (gate) also fails at the base — test.test_re raises
TypeError: list indices must be integers or slices, not list_iterator from
_compiler.py:504 _get_charset_prefix, where p[0] inside for p in av[1]
received the loop's iterator as its subscript index.

reconstructed_all_ref_call_stack read the residual op's Ref var-list at a
fixed operand offset 1. That holds for the Ref-only shape iRd>r but not for
the mixed iIRd>r the method-form CALL helpers lower through, whose leading
Int list is variable-width — so the read landed on the Int list's length byte
and resolved its register indices through the Ref bank. The composed stack is
kept vstack prefix ++ this list, whose height is vstack_boxes.len() for any
list length, so the abort-flush's depth_at_py_pc check could not fail.

The fix derives the offset from the op's argcodes. It is 624d547875d,
authored on rbigint; cherry-picked here (-x) because that branch has no PR
yet and this one inherits the red. Drop this commit when rbigint lands.
The one conflict was in tests.rs, where the incoming hunk also carried a test
belonging to an earlier rbigint commit; only the new
ref_var_list_offset_follows_the_argcodes_not_a_fixed_byte was kept.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved JIT handling for self-recursive calls, mixed integer/reference arguments, and resumed multi-frame execution across native and WebAssembly builds.
    • Prevented invalid __name__ specialization guards and corrected operand reconstruction after FOR_ITER call aborts.
    • Improved platform-specific behavior for timestamp and configuration-limit tests on Windows.
  • Tests

    • Added coverage for recursive promotion, reference-list layouts, and iterator-call recovery.
    • Updated benchmark statistics to reflect current bridge, guard, loop, and recovery behavior.

`bridge_rec_root_selfrec` (inline_call.rs) was `cfg!(not(target_arch =
"wasm32"))`, so a guard-failure bridge reaching a self-recursive callee's CALL
took the root-bridge admission on the native backends and residualized on wasm.

The cfg's stated reason — "the wasm always-portal path type-confuses the
self-recursive inline (`setintbound: got Ref`)" — is phrasing carried over from
#643 `920367da965`, where it belonged to `fbw_inline_callee_hazardous`: a
decline that is not cfg-gated and rests on the CALL_ASSEMBLER trampoline frame.
#749 `71726847a10` introduced the cfg and reported dynasm + cranelift 294/294
for that slice with no wasm number. `always-portal` names nothing in the tree.

Neither mechanism that would make such a confusion wasm-specific holds:
`W_IntObject.intval` is i64 on every target, so there is no word-size Int→Ref
promotion, and general wasm CALL_ASSEMBLER support landed in #564
`c89254a6211` on 2026-07-15, before the claim.

Add `synth/selfrec_bridge_nontail_promote`, covering the admission in the form
`bridge_recursion_overflow` does not: non-tail recursion, so the guard's resume
stream is multi-frame; a Ref local live across the recursive CALL; and an
accumulator crossing 2**63 at level 13 of 24, so a long — a Ref — reaches the
int operations. wasm reads loops/bridges/aborted/gf 2/4/1/606 with the decline
and 2/6/0/806 without it, byte-identical to dynasm and cranelift.

wasm jit-stats re-recorded for the five fixtures the admission moves. All five
now carry the native values except `recursion_memo_branch`, one guard failure
apart, previously twenty-one.

check.py dynasm 405/406, cranelift 405/406, wasm 401/402. The one failure,
`synth/pypy_type_surface`, reports the same `bridges_compiled 5 -> 102,
guard_failures 1011 -> 20497` with this change's two files restored to
origin/main, and it fails identically on all three backends while the change is
a no-op wherever the cfg already read true. The fixture also holds no
self-recursive function for `code_is_self_recursive` to answer.

wasm user+sys CPU, min/median over 31 interleaved samples, decline removed vs
kept: `wasm_ca_trampoline_decline` -20.1%/-20.7%,
`selfrec_bridge_nontail_promote` -12.9%/-11.9%, `recursion_memo_branch`
-1.2%/-1.6%. Two get slower: `ca_bridge_multiframe_resume_double_call`
+1.2%/+2.7% and `foriter_call_resume_drops_iteration` +2.4%/+4.0%. Both now
report what the dynasm baseline records — 16 bridges / 0 aborts and 27 bridges
/ 1 abort — where the decline left them at 16/1 and 26/2, so the cost is wasm
taking the native decision and its compile toll.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change enables wasm self-recursive root-bridge admission, derives residual Ref offsets from encoded argcodes, adds specialization and abort-path regression tests, and adds a non-tail recursion promotion benchmark with updated JIT statistics.

Changes

JIT dispatch and validation

Layer / File(s) Summary
Dispatch reconstruction and admission
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
Residual-call Ref offsets now follow encoded argcodes. Self-recursive root-bridge admission now applies on wasm32 and native targets.
Hazard and specialization checks
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
FBW hazard comments describe residual trampoline frame-pointer retention. __name__ specialization checks metaclass consistency before emitting guards.
Regression coverage
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs, pyre/extra_tests/parity_tests/foriter_body_call_abort_operand_stack.py, pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
Tests cover Ref-list offsets, FOR_ITER abort operand reconstruction, and platform-specific os parity behavior.

Recursion benchmarks and statistics

Layer / File(s) Summary
Non-tail promotion benchmark
pyre/bench/synth/selfrec_bridge_nontail_promote.py
The benchmark exercises tagged non-tail self-recursion, accumulator promotion, and modular result checks across 50,000 cases.
JIT statistics baselines
pyre/bench/synth/selfrec_bridge_nontail_promote.*.jitstats, pyre/bench/synth/*wasm.jitstats
Statistics record bridge, loop, guard, blackhole, rollback, field-position, panic, and abort counters.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RootBridge
  participant InlineCall
  participant ArgcodeParser
  participant ResidualTrampoline
  participant Benchmark
  RootBridge->>InlineCall: evaluate self-recursive admission
  InlineCall->>ArgcodeParser: derive Ref-list offset
  ArgcodeParser->>ResidualTrampoline: provide reconstructed call layout
  ResidualTrampoline->>Benchmark: resume promoted recursive call
Loading

Possibly related issues

  • youknowone/pyre#466 — Covers wasm self-recursive CALL_ASSEMBLER and root-bridge admission.
  • youknowone/pyre#126 — Covers self-recursive bridge handling and recursive frame-resume behavior.

Possibly related PRs

Poem

A rabbit traces recursive flight,
Through wasm frames in moonlit light.
Ref offsets follow every code,
Benchmarks mark the bridge-road.
Guards and tests now safely run.
“Hop!” says Bun. “The work is done.”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: enabling self-recursive root-bridge inline admission on wasm32.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fib_recursive

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a62f004cd2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2911 to +2915
bridge_rec_root_selfrec = unsafe {
let raw = pyre_interpreter::w_code_get_ptr(w_code as pyre_object::PyObjectRef)
as *const pyre_interpreter::CodeObject;
!raw.is_null() && pyre_interpreter::code_is_self_recursive(&*raw)
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope the exemption to a proven recursive call

This removal newly enables the exemption on wasm whenever code_is_self_recursive returns true, but that predicate only looks for any LOAD_GLOBAL of the code object's own name; a non-recursive function that merely reads its own global name is therefore sufficient. If such a function reaches this root-bridge path and inlines a loop-bearing helper, bridge_rec_root_selfrec installs SELFREC_CA_FOLD_ACTIVE around the entire body walk, causing fbw_abort_nested_unjournaled_residual to skip the helper's loop-bearing hazard as well and allowing the documented iterator double-advance on replay. Restrict the exemption to the actual recursive CALL_ASSEMBLER site rather than trusting this coarse code-level predicate.

AGENTS.md reference: AGENTS.md:L14-L19

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/bench/synth/selfrec_bridge_nontail_promote.py`:
- Line 37: Preserve the explicit initialization loop around walk(24, (1 << 50) +
i) because the benchmark shape must remain unchanged, and add a localized Ruff
PERF401 suppression to that loop rather than converting it to a comprehension.
Keep the suppression scoped only to this intentional loop.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fcc04478-0ebc-4fc0-b44d-44caa4cb3ad1

📥 Commits

Reviewing files that changed from the base of the PR and between d936eb4 and a62f004.

📒 Files selected for processing (10)
  • pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats
  • pyre/bench/synth/foriter_call_resume_drops_iteration.wasm.jitstats
  • pyre/bench/synth/recursion_memo_branch.wasm.jitstats
  • pyre/bench/synth/selfrec_bridge_nontail_promote.cranelift.jitstats
  • pyre/bench/synth/selfrec_bridge_nontail_promote.dynasm.jitstats
  • pyre/bench/synth/selfrec_bridge_nontail_promote.py
  • pyre/bench/synth/selfrec_bridge_nontail_promote.wasm.jitstats
  • pyre/bench/synth/wasm_ca_trampoline_decline.wasm.jitstats
  • pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs


out = []
for i in range(50000):
out.append(walk(24, (1 << 50) + i) % 1000000007)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'pyre/bench/synth/selfrec_bridge_nontail_promote\.py|selfrec_bridge_nontail_promote|PERF401|PY'

echo "== target excerpt =="
cat -n pyre/bench/synth/selfrec_bridge_nontail_promote.py

echo "== related jitstats files =="
git ls-files | rg 'selfrec_bridge_nontail_promote'

echo "== ruff config references =="
rg -n "RUFF|ruff|PERF401|PY" -S --glob '!**/.git/**' --glob '!**/*.png' --glob '!**/*.jpg' . | head -200 || true

echo "== git diff stat/name-only =="
git diff --stat || true
git diff --name-only || true

Repository: youknowone/pyre

Length of output: 21136


🌐 Web query:

pyproject.toml ruff PERF401 preferred method build list comprehension

💡 Result:

The Ruff rule PERF401 (manual-list-comprehension) identifies for-loops that can be replaced with a list comprehension or a list extend operation to improve readability and performance [1][2]. The preferred method depends on the context of the code: 1. When initializing a new list: Ruff suggests replacing the loop with a list comprehension [1][3]. Example: # Original filtered = [] for i in original: if i % 2: filtered.append(i) # Preferred (List Comprehension) filtered = [i for i in original if i % 2] 2. When appending to an existing, already populated list: Ruff suggests using the list.extend method with a generator expression [1][4]. Example: # Original existing_list = [1][5] for i in original: if i % 2: existing_list.append(i) # Preferred (list.extend) existing_list.extend(i for i in original if i % 2) Key Considerations: - Purpose: The rule is a micro-optimization [1][2]. While list comprehensions are generally faster (typically 10-25% depending on the Python version), the performance impact is negligible in many real-world scenarios [1][2]. - Limitations: The automatic fix is not always applicable. Ruff applies this rule primarily to simple for-loops (e.g., those with a single non-if statement or a single if statement containing one nested operation) [6][4]. If a loop contains complex logic or multiple operations, it may not trigger the rule [6]. - False Positives: If the logic requires flattening (which.extend does) versus adding elements, or if the loop body is more complex than the rule's simple heuristic can analyze, you should manually evaluate if the suggested transformation maintains correct behavior [4].

Citations:


Resolve Ruff PERF401 without changing the benchmark shape.

This initialization loop triggers PERF401, but converting it to a list(...) construction may change the JIT trace and invalidate the .jitstats baselines. Add a local suppression if the explicit loop is intentional, or regenerate all three selfrec_bridge_nontail_promote JIT-stat files if the comprehension is kept.

Proposed localized suppression
-    out.append(walk(24, (1 << 50) + i) % 1000000007)
+    out.append(walk(24, (1 << 50) + i) % 1000000007)  # noqa: PERF401
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
out.append(walk(24, (1 << 50) + i) % 1000000007)
out.append(walk(24, (1 << 50) + i) % 1000000007) # noqa: PERF401
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 37-37: Use a list comprehension to create a transformed list

(PERF401)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/bench/synth/selfrec_bridge_nontail_promote.py` at line 37, Preserve the
explicit initialization loop around walk(24, (1 << 50) + i) because the
benchmark shape must remain unchanged, and add a localized Ruff PERF401
suppression to that loop rather than converting it to a comprehension. Keep the
suppression scoped only to this intentional loop.

Source: Linters/SAST tools

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 87c0439).
Updated: 2026-08-07T21:58:29.611Z

Files in the reviewed diff
pyre/bench/synth/selfrec_bridge_nontail_promote.py
pyre/extra_tests/parity_tests/foriter_body_call_abort_operand_stack.py
pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs

Codex did not produce a report (exit 1). Last log lines:

Assess by static analysis whether our changes in git diff upstream/main are equivalent
to the corresponding RPython/PyPy source code. The RPython and PyPy sources
are available locally (under `rpython/` and `pypy/` in this repository).

If anything was ported incorrectly, report every instance in detail. After
collecting all differences, organize the report into separate sections:

1. Cases where our patch regressed PyPy parity compared to main
2. Other mismatches introduced by our patch
3. Mismatches that already existed before this patch
4. Structural adaptations

Exceptions: some differences cannot be ported 1:1 because of Python 3.11 vs
3.14 differences, opcode mismatches caused by using a CPython-compatible
compiler, GIL/free-threading differences, and fundamental implementation-
language differences between RPython and Rust. Mark those separately under
"Structural adaptations".

Scope discipline: before writing the report, run
`git diff upstream/main --name-only -- . ':(exclude)*.jitstats'` and treat that
file list as the authoritative definition of "this patch" (when an authoritative
changed-file list is appended below, use that instead of re-deriving it). The
excluded `*.jitstats` files are `pyre/check.py`'s recorded jit-stats baselines —
generated golden data with no RPython/PyPy counterpart, so no parity finding can
cite one, and a bulk re-record of them is not a change to review. Findings under
sections 1 and 2 MUST cite our-side files from that list; a divergence in any
file NOT in the list is by definition not introduced by this patch — report
it under section 3 instead, or omit it. Verify every section-1/2 citation
against the list before finalizing the report.

---

Output format requirements (so the report can be parsed mechanically and
posted/triaged automatically). Use these four headings VERBATIM, in this
order, and nothing else at heading level 2:

## 1. Regressions to PyPy parity introduced by this patch
## 2. Other mismatches introduced by this patch
## 3. Pre-existing mismatches (already present before this patch)
## 4. Structural adaptations

Under each heading, list every finding as a bullet. For each finding cite the
concrete `our_file.rs:line ↔ rpython_or_pypy_file.py:line` pair and quote the
divergence concisely. If a section has no findings, still emit the heading
followed by a single line `None.` so all four sections are always present.
Do not modify any files; produce the report only.

Authoritative changed-file list for this patch (git diff upstream/main --name-only,
minus 7 generated `*.jitstats` baseline file(s)):
pyre/bench/synth/selfrec_bridge_nontail_promote.py
pyre/extra_tests/parity_tests/foriter_body_call_abort_operand_stack.py
pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
warning: Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the bundled bubblewrap in the meantime.
ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Aug 8th, 2026 3:32 AM.
ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Aug 8th, 2026 3:32 AM.

@youknowone

Copy link
Copy Markdown
Owner Author

Codex parity review — adjudication

§2 — reclassified, not fixed

inline_call.rs:2911 ↔ pyjitpl.py:1376 … The removed baseline documentation identifies this exact wasm path as setintbound: got Ref; no wasm typing/lowering repair is included in the patch.

The finding takes the deleted comment as authority, and that comment is exactly what this patch audits. Its provenance: setintbound: got Ref enters the tree in #643 920367da965 attached to fbw_inline_callee_hazardous — a different site, not cfg-gated, resting on the CALL_ASSEMBLER trampoline frame, and that slice did measure wasm. The cfg at inline_call.rs:2911 is #749 71726847a10, whose commit message reports dynasm + cranelift 294/294 and no wasm number at all. always-portal, the mechanism the sentence names, appears in three comments and nothing else in the tree.

The cited upstream line argues the other way. _opimpl_recursive_call (pyjitpl.py:1376-1422) decides purely from a green-key frame count against memmgr.max_unroll_recursion: below it, perform_call inlines the recursion; at or above it, dont_trace_here plus do_recursive_calldo_residual_call(assembler_call=True). There is no target-architecture term anywhere on that path, so a per-target policy term is the thing without upstream basis — keeping the cfg is the deviation, not removing it.

The typed-residual half of the finding is also unaffected: the non-inlined case still reaches fbw_inline_callee_hazardous, which this patch leaves in place on every backend and only edits the doc of.

Evidence that the named confusion does not occur, rather than that it merely was not hit: synth/selfrec_bridge_nontail_promote is built for it — non-tail recursion (multi-frame resume stream), a Ref local live across the recursive CALL, and an accumulator crossing 2**63 at level 13 of 24 so a long, a Ref, genuinely reaches the int operations. wasm reads 2/6/0/806 byte-identical to dynasm and cranelift, output matches, and internal_compile_panics=0. That counter is a real assert detector here: jit_strict_mode() is cfg!(debug_assertions) || env MAJIT_STRICT, and the guest has no environment, so an optimizer panic is caught and counted rather than re-raised.

§3 — both confirmed real, both deferred with a named blocker

Neither is "pre-existing, therefore skip" — both are genuine divergences worth a follow-up, and both exceed what this diff can safely carry.

  1. fbw_state.rs:1446 ↔ pyjitpl.py:1995 — confirmed. pyre aborts and permanently declines the nested residual; PyPy executes and records it. Upstream never aborts for recursion: it counts same-green-key frames and, past max_unroll_recursion, emits the residual CALL_ASSEMBLER. Convergence path: port the green-key recursion count and the recorded-residual shape into the walker so the decline becomes a bounded unroll limit instead of a permanent denial. Blocker: that replaces fbw_abort_nested_unjournaled_residual's whole contract, which the journal/rollback layer is currently written against.

  2. fbw_state.rs:1274 ↔ warmstate.py:679 — confirmed; upstream calls warmrunnerstate.dont_trace_here(greenboxes) at pyjitpl.py:1413, immediately beside the decision above. pyre leaves the declined callee on the normal threshold path, so it is traced later than upstream would trace it. Blocker: JC_DONT_TRACE_HERE lives in a JitCell keyed by green arguments; pyre's denial is a per-thread HashSet of code-object addresses (§4's own second item), so this needs the JitCell keying to land first.

§4 — both accurate, kept as documented adaptations.

commented by Claude

…hold

`try_walker_specialize_load_type_name_attr` takes the metaclass from
`baseobjspace::type_name_obj_fast_path`, which asks `typedef::type`. That falls
back to `gettypefor(ob_type)` when the receiver's `w_class` slot is null
(typedef.rs:234-243; the objects it covers sit in RODATA, so writing the slot
would SIGBUS). The fold then guards the metaclass by reading the raw `w_class`
field, so for such a receiver it emits `guard_value(NULL, type)` — a guard that
fails on every execution and that nothing can discharge, because no later write
fills the slot.

One class reaches it. The `getset_descriptor` type object is built lazily from
inside the init loop (typedef.rs:9501) as a builder for the descriptors other
typedefs install, so it never enters the registry the post-loop sweep walks to
stamp `w_class = type` (typedef.rs:1546-1553). `typedef::type` still answers
`type` through the fallback, so `type(x)` and `x.__class__` are correct and the
null slot is invisible from Python.

`synth/pypy_type_surface` reads `type(type.__dict__[name]).__name__`, so it paid
one guard failure per iteration on every backend: dynasm, cranelift and wasm all
reported `bridges_compiled 5 -> 102, guard_failures 1011 -> 20497` against the
figures #999 `dad2a722907` recorded — 19486 excess failures over 20000
iterations, and 19486/200 = 97 excess bridges, one per `trace_eagerness` bucket.
#1097 `d51ea7f32a0`, which added the fold, merged 83 seconds before #999, and
the recorded figures are the ones this decline reproduces.

Localized by taking the fixture apart: `check_descriptor_kinds` alone carries
all 97 bridges; the receiver's own value is irrelevant (a loop-invariant object
reads the same), the `FOR_ITER` is irrelevant (removing it reads the same), and
`int`, `str`, `list`, `NoneType`, `object`, `type`, `method_descriptor`,
`wrapper_descriptor`, `builtin_function_or_method` and a user class are all
clean. `getattr(C, "__name__")` — same semantics, but the fold declines because
the site's name is not in the code's name table — is clean too.

check.py dynasm 406/406, cranelift 406/406, wasm 402/402, all green.
`synth/pypy_type_surface` reads its recorded 11/5/0/1011 exactly and
`synth/type_name_attr_fold` still reads its own 5/0/1/4, so the fold keeps
applying wherever its guard holds.

The null slot itself is left alone. Stamping `w_class` on the
`getset_descriptor` type object would restore the invariant the sweep's comment
states and let the fold apply there as well, but it moves recorded baselines and
belongs in its own change.

Assisted-by: Claude
…Windows

Two parts of the script do not run there:

- the `times=` keyword check passed a time before the epoch, which the
  block above it already excludes Windows from, and the pyre backends
  answer it with `ValueError: utime: timestamp out of range`;
- `pathconf` and the `pathconf_names` table it resolves through are
  registered under `#[cfg(unix)]`, and CPython's `os` carries neither on
  Windows, so both runtimes raise `AttributeError`.

`pyre/check.py (windows-latest)` reports `os_utime_pathconf_truncate.py
cpython=FAIL dynasm=FAIL cranelift=FAIL` at the merge-base d936eb4.

The keyword spelling is now checked with a time the platform holds, and
the pathconf section runs where the names table exists.

Assisted-by: Claude

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 203cd0c9d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +73 to +75
# before the keyword-only marker. The pair is one the platform holds: Windows
# refuses times before the epoch, for the reason given above.
atime, mtime = (5.0, 6.0) if sys.platform == "win32" else (-5.0, -6.0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the Windows pre-epoch utime parity test

On Windows, -5.0 and -6.0 are representable because FILETIME starts in 1601; the upstream conversion explicitly adds the 1601-to-1970 offset in rpython/rlib/rwin32file.py:300-323. Pyre rejects these values only because utime_impl converts signed seconds to u64 at interp_posix.rs:2543-2555, so substituting positive timestamps removes the test's only Windows coverage of that user-visible incompatibility and lets the defect pass. Keep the negative pair and fix the Windows FILETIME conversion instead.

AGENTS.md reference: AGENTS.md:L252-L254

Useful? React with 👍 / 👎.

`reconstructed_all_ref_call_stack` read the residual op's Ref var-list at
operand offset 1.  That offset holds for the Ref-only residual shape
(`iRd>r`) but not for the mixed one (`iIRd>r`), where the leading Int list
is variable-width and `dispatch_residual_call_iIRd_kind` reads the Ref list
at `1 + i_width`.  On the mixed shape the read landed on the Int list's
length byte and resolved its register indices through the Ref bank.

The resulting list has an unrelated length, so the operand stack the
abort-flush composes from it (kept vstack prefix + this list) still matched
`depth_at_py_pc` at the CALL and was committed.  For `p[0]` inside a
`for p in ...` body the committed stack was `[iterator, p, iterator]`, and
the interpreter re-executed the subscript with the loop's iterator as the
index: `re.compile("|".join("%d" % x for x in range(2000)))` raised
`TypeError: list indices must be integers or slices, not list_iterator`
from `_compiler.py:504 _get_charset_prefix`, failing `test.test_re` in the
vendored CPython suite.

Derive the offset from the op's argcodes instead, walking the widths of
`blackhole.py:112-157`; an op with no Ref list declines.

Covered by `jitcode_dispatch::tests::ref_var_list_offset_follows_the_argcodes_not_a_fixed_byte`
and `parity_tests/foriter_body_call_abort_operand_stack.py`.

Assisted-by: Claude
(cherry picked from commit 624d547)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs`:
- Around line 3974-3980: Update ref_var_list_operand_offset to validate that
every skipped fixed-width operand, I/F length-prefixed list, and the complete R
operand (length byte plus register bytes) fits within code before advancing or
returning Some(cursor - first_operand_pc). Return None for any truncated
encoding, preserving normal offset calculation for complete sequences; add
regression cases covering truncated fixed-width, I/F, and R operands.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4d904f30-9d1a-4621-8e77-7a22f0651c66

📥 Commits

Reviewing files that changed from the base of the PR and between a62f004 and 87c0439.

📒 Files selected for processing (6)
  • pyre/extra_tests/parity_tests/foriter_body_call_abort_operand_stack.py
  • pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs

Comment on lines +3974 to +3980
'R' => return Some(cursor - first_operand_pc),
'i' | 'c' | 'r' | 'f' => cursor += 1,
'L' | 'd' | 'j' => cursor += 2,
'I' | 'F' => cursor += 1 + *code.get(cursor)? as usize,
'>' => {
chars.next()?;
cursor += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject incomplete operands before returning the offset.

ref_var_list_operand_offset returns Some when it reaches R, but it does not verify the R length byte or its register bytes. It also advances over prior fixed-width operands and I or F lists without verifying their complete encoded width.

For iIRd>r that ends after an I length byte of 3, this function returns Some(5). read_ref_var_list_concrete then indexes code[len_pc] and panics. This contradicts the documented None result for incomplete operand sequences.

Validate every skipped operand and the complete R list before returning its offset. Add truncated fixed-width, I or F, and R regression cases.

Proposed fix
-            'R' => return Some(cursor - first_operand_pc),
-            'i' | 'c' | 'r' | 'f' => cursor += 1,
-            'L' | 'd' | 'j' => cursor += 2,
-            'I' | 'F' => cursor += 1 + *code.get(cursor)? as usize,
+            'R' => {
+                let len = *code.get(cursor)? as usize;
+                let end = cursor.checked_add(1 + len)?;
+                code.get(end.checked_sub(1)?)?;
+                return Some(cursor - first_operand_pc);
+            }
+            'i' | 'c' | 'r' | 'f' => {
+                let end = cursor.checked_add(1)?;
+                code.get(end.checked_sub(1)?)?;
+                cursor = end;
+            }
+            'L' | 'd' | 'j' => {
+                let end = cursor.checked_add(2)?;
+                code.get(end.checked_sub(1)?)?;
+                cursor = end;
+            }
+            'I' | 'F' => {
+                let len = *code.get(cursor)? as usize;
+                let end = cursor.checked_add(1 + len)?;
+                code.get(end.checked_sub(1)?)?;
+                cursor = end;
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'R' => return Some(cursor - first_operand_pc),
'i' | 'c' | 'r' | 'f' => cursor += 1,
'L' | 'd' | 'j' => cursor += 2,
'I' | 'F' => cursor += 1 + *code.get(cursor)? as usize,
'>' => {
chars.next()?;
cursor += 1;
'R' => {
let len = *code.get(cursor)? as usize;
let end = cursor.checked_add(1 + len)?;
code.get(end.checked_sub(1)?)?;
return Some(cursor - first_operand_pc);
}
'i' | 'c' | 'r' | 'f' => {
let end = cursor.checked_add(1)?;
code.get(end.checked_sub(1)?)?;
cursor = end;
}
'L' | 'd' | 'j' => {
let end = cursor.checked_add(2)?;
code.get(end.checked_sub(1)?)?;
cursor = end;
}
'I' | 'F' => {
let len = *code.get(cursor)? as usize;
let end = cursor.checked_add(1 + len)?;
code.get(end.checked_sub(1)?)?;
cursor = end;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs` around lines 3974 - 3980,
Update ref_var_list_operand_offset to validate that every skipped fixed-width
operand, I/F length-prefixed list, and the complete R operand (length byte plus
register bytes) fits within code before advancing or returning Some(cursor -
first_operand_pc). Return None for any truncated encoding, preserving normal
offset calculation for complete sequences; add regression cases covering
truncated fixed-width, I/F, and R operands.

@youknowone
youknowone merged commit 4555e3d into main Aug 8, 2026
14 of 17 checks passed
@youknowone
youknowone deleted the fib_recursive branch August 8, 2026 01:10
youknowone added a commit that referenced this pull request Aug 8, 2026
`bridges_compiled 102 -> 5` and `guard_failures 20497 -> 1011`, byte-identical
on dynasm, cranelift and wasm.

The recorded 102/20497 are the counters the `Cls.__name__` fold produced while
its metaclass guard could not be discharged — it read the raw `w_class` slot
where the fast path answers through the `gettypefor(ob_type)` fallback, so a
receiver reached through that fallback got a `guard_value(NULL, type)` that
failed on every execution. #1106 narrowed the fold; the baseline still names
what the defect measured.

Read back rather than predicted: ubuntu-24.04, macos-latest and windows-latest
all report 5/1011 on main, and so does a local windows run. `retraces_compiled`
joins the file because the recorder now writes it; it is 0, which is what the
comparison already read for its absence.
youknowone added a commit that referenced this pull request Aug 8, 2026
`pypy_type_surface` reads bridges_compiled=5 and guard_failures=1011 on all
three backends, the figures #999 `dad2a722907` recorded. #1097 `d51ea7f32a0`
moved them to 102/20497 by folding `Cls.__name__` across a `getset_descriptor`
whose `w_class` slot is null, and #1107 `9614f37fb1e` declined that fold again;
#1107's own message records the fixture reading 11/5/0/1011 afterwards. #1086
`e5eff816849` sits between the two and wrote the regressed pair in as the
baseline, so the restored reading has failed the gate on every runner since.

The same commit reverted three wasm guard_failures counters to their pre-#1106
values while keeping #1106 `4555e3d76ba`'s other fields:
ca_bridge_multiframe_resume_double_call 2592 -> 2581, wasm_ca_trampoline_decline
601 -> 404, recursion_memo_branch 4704 -> 4724. The guest reads #1106's values
both here and on ubuntu-24.04, the only runner that runs wasm, and each
fixture's loops_compiled and bridges_compiled already agree with what both
observe.

Measured with `pyre/check.py --snapshot-diff` per backend and pattern: dynasm
18/18, cranelift 18/18, and wasm 15/15 for each of the three wasm fixtures.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 9, 2026
`bridges_compiled 102 -> 5` and `guard_failures 20497 -> 1011`, byte-identical
on dynasm, cranelift and wasm.

The recorded 102/20497 are the counters the `Cls.__name__` fold produced while
its metaclass guard could not be discharged — it read the raw `w_class` slot
where the fast path answers through the `gettypefor(ob_type)` fallback, so a
receiver reached through that fallback got a `guard_value(NULL, type)` that
failed on every execution. #1106 narrowed the fold; the baseline still names
what the defect measured.

Read back rather than predicted: ubuntu-24.04, macos-latest and windows-latest
all report 5/1011 on main, and so does a local windows run. `retraces_compiled`
joins the file because the recorder now writes it; it is 0, which is what the
comparison already read for its absence.
youknowone added a commit that referenced this pull request Aug 9, 2026
#1106 `4555e3d76b` dropped the wasm32 arm of the self-recursive root-bridge
inline and recorded what that moved: `ca_bridge_multiframe_resume_double_call`
2581 -> 2592, `wasm_ca_trampoline_decline` 404 -> 601 and
`recursion_memo_branch` 4724 -> 4704. #1086 `e5eff81684` wrote all three back
to their pre-#1106 values, and ubuntu has failed on them every run since.

#1086 resizes 130 fixtures so pypy's side clears the measurement floor, and
re-recording a resized fixture's counters is part of that. It moves 34 baseline
values; 26 sit in fixtures whose `.py` it also changed. These three — and
`pypy_type_surface`, restored in 958f66c — are among the eight it moved with
no workload change beside them, which is a snapshot taken on a base predating
#1106.

Measured rather than reverted: a local wasm run reads 2592, 601 and 4704, which
are #1106's figures and the ones ubuntu observes. The remaining two of those
eight, `closure_per_call` 418 and `recursive_call_frame_relocation` 638, read
what they already record and are left alone.

The lower counts are not the better state. #1106 measured removing the decline
at -20.1%/-20.7% wasm CPU on `wasm_ca_trampoline_decline`; the +197 guard
failures buy that. `ca_bridge_multiframe_resume_double_call` pays +1.2%/+2.7%
and in exchange reports the 16 bridges / 0 aborts the dynasm baseline records,
where the decline left wasm at 16/1.

`retraces_compiled=0` comes with the re-record: these were the only wasm
baselines missing the key, which `_parse_jit_stats` was defaulting.
youknowone added a commit that referenced this pull request Aug 9, 2026
…t was hiding (#1104)

* extra_tests: report the parity runner's failures where a CI log is read

The per-failure detail and the `N failure(s)` count went to stderr while the
213-row result table went to stdout.  A piped stdout is block-buffered and
stderr is not, so the whole report arrived in the log *above* the run's own
header, and the last thing before the runner's non-zero exit was a passing
row -- a failed job named none of what failed in it.

The report is printed last now, on stdout, and echoes each child's stderr
verbatim instead of a `repr` of the whole thing on one line: a traceback only
reads as one when its line breaks survive.  Each failing row also carries its
one-line verdict beneath it, and under `GITHUB_ACTIONS` every failure emits an
`::error file=` annotation carrying that verdict and the exception line, which
shows on the pull request without opening the log.

stdout is line-buffered so a run that dies mid-way names the scripts it got
through, and pinned to UTF-8 because the report echoes a child's stderr: these
scripts are largely about names no console codepage can spell, and printing
one of those raised `UnicodeEncodeError` out of the runner instead of the
failure it was in the middle of explaining.

`_run` returns the reason and the stderr as separate values, which `Failure`
carries.

* posix: the pre-epoch utime Windows refused, and six answers beside it

`os.utime` on Windows turned away every time before 1970.  The host call
takes its times as a `Duration`, which has no second below its epoch at all,
so `u64::try_from(sec)` was the whole pre-epoch range's refusal.  A FILETIME
counts 100ns ticks from 1601-01-01, so shifting the epoch is what makes such
a second a positive tick count; `SetFileTime` is called here now, over the
handle `host_env::fs::open_write_with_custom_flags` opens, with the same
wrapping `__int64` arithmetic `time_t_to_FILE_TIME` is written in.

Measured against CPython 3.14 on the same host, `os.utime(p, times=(-5.0,
-6.0))` now reads back as -6_000_000_000 where it raised, and `ns=(-1, -1)`
as the -100 a FILETIME's granularity leaves.

Six more answers along the same argument, each measured against 3.14:

  ('a', 'b')     ValueError: could not convert string to float
                   -> TypeError: argument must be int or float, not str
  (1e30, 0)      ValueError: utime: timestamp out of range
                   -> OverflowError: timestamp out of range for platform time_t
  (2**200, 0)    the same, and by the exact integer rather than through a
                   float that rounded the seconds it could not hold
  (nan, 0)       ValueError: utime: timestamp out of range
                   -> ValueError: Invalid value NaN (not a number)
  (1,)           utime: 'times' must be a tuple of two ints
                   -> ... must be either a tuple of two ints or None
  ns=(2**80, 0)  OverflowError -> written.  `split_py_long_to_s_and_ns`
                   splits with `divmod` BEFORE it narrows anything, so a
                   nanosecond count too wide for a `time_t` is refused only
                   when the second it names is; 2**80 ns is a second that
                   fits.  Dividing after the narrowing turned away the range.
                   `divmod` is also what answers for `ns=('a', 'b')`.

`os.truncate`/`os.ftruncate` on Windows read their length with a bare
`int_w`: no `__index__`, and `int too large to convert to int` where
`Py_off_t_converter` says `int too big to convert`.  Both now go through
`truncate_length_w`, which is hoisted out of the unix arm and names the C
type the platform's converter names.

`st_atime_ns` and its two siblings took `sec * 1_000_000_000` in `i64`, which
runs out in 2262 -- a file dated later, which every FILETIME up to the year
30828 can be, read back as the wrap.  The product is taken in `i128` and the
field is an int of whatever width it needs.

`parity_tests/os_utime_pathconf_truncate` covers all of it and no longer
skips its negative-time section on Windows; only the exact-nanosecond value
is platform-dependent there.  Its `pathconf` section is now gated on the name
existing, because neither CPython nor this build carries `pathconf` on
Windows and the reference failed the script before any backend could -- which
is what made the whole script red on every Windows runner.

* jit: assert `guard_exact_w_class` pins a `w_class` its operand carries

`is_exact_builtin_instance` (pyobject.rs:149-163) reads a null `w_class` as a
second spelling of "exact builtin", beside the one where the slot holds the
canonical type object, and `is_plain_int1` (listobject.rs:424-460) accepts both
for `int` and for a fits-int `W_LongObject`. `walker_guard_exact_w_class` reads
the slot and pins a single value, so an operand admitted under the null spelling
and pinned against the canonical gets a guard its own recorded operand fails —
and nothing writes the slot afterwards, so it fails on every execution without
converging, one bridge per `trace_eagerness` bucket. That is the shape
`try_walker_specialize_load_type_name_attr` shipped with, where the fold took
its metaclass from `typedef::type`'s `gettypefor(ob_type)` fallback while the
guard read the raw field.

The 40-odd other call sites establish the operand carries what they pin —
through `walker_exact_builtin_class`, which returns `None` on a null slot, or
through `is_plain_int1` / a local `is_exact_int` — but nothing checked that they
do. Measured across `bench/synth`, `bench` and `extra_tests/parity_tests`,
under a probe that reported the recorded slot against the pinned value at every
site: 1217 pins over 131 files, all carrying what they pin, and no site reaching
the guard without a concrete operand. The null spelling is reachable — `bool`,
`None`, functions, generators, iterators, sets and the itertools objects are all
built with a null slot, and `SMALL_INTS` is written that way behind
`WITHPREBUILTINT` — so what holds is that no admitting predicate currently pairs
one with a canonical pin, not that it could not.

`debug_assert!` rather than a decline: there is no live site to decline, release
codegen is unchanged, and the next occurrence fails loudly instead of costing a
20x jit-stats drift that takes a baseline diff to notice.

* sys, pyrex: the three Windows parity failures behind a job that named none

`pyre/check.py (windows-latest)` on #1109 ends on a passing row and exit code 1;
the three scripts it failed on are 200 lines up, in the stderr the neighbouring
commit here moves. They are main's, not that PR's, and independent of each
other.

**`keyboard_interrupt_exit_status`.** A Win32 process has no SIGINT to die of.
`app_main.py:1146-1151` restores `SIG_DFL` and calls `raise(SIGINT)`, saying the
MSVC runtime then exits with `STATUS_CONTROL_C_EXIT`; measured, that pair
returns through the CRT's default action and the process ends with status 3 —
`signal.signal(SIGINT, SIG_DFL); signal.raise_signal(SIGINT)` under CPython
exits 3 as well, while an uncaught `KeyboardInterrupt` there exits 0xC000013A.
`raise` never terminates, so `terminate_by_sigint` fell through to
`process::abort`, whose status 3 is what the fixture read. Windows exits with
`STATUS_CONTROL_C_EXIT` directly; both callers have already finalized.

**`builtin_module_loader_spec`.** Two missing names, both reached from
`test.support`'s import:
- `_sysconfig.config_vars()` answered an empty dict. `sysconfig._init_non_posix`
  SUBSCRIPTS `Py_GIL_DISABLED` and `Py_DEBUG` to spell `ABIFLAGS`, so under
  `os.name == 'nt'` an absent key is a `KeyError` out of the first
  `get_config_var` rather than the `None` the `.get()` readers take. Both are 0,
  which is what an empty `sys.abiflags` already says. `EXT_SUFFIX` and `SOABI`,
  the other two keys the call carries, name an extension ABI that
  `_imp.extension_suffixes()` says does not exist here, so they stay absent.
- `sys.getwindowsversion` was absent and `_init_config_vars` subscripts
  `sys._vpath` beside it. The version is a five-field sequence with five
  named-only fields over it, built the way `os.stat_result` carries its `st_*_ns`
  extras, off `host_env::windows::get_windows_version`. Every field matches
  CPython 3.14 on the same host except `build`: the sequence reports kernel32's
  file version, because `GetVersionEx` answers with the version an unmanifested
  binary is shimmed to, and `platform_version` — the field that exists because of
  that shimming — agrees with it here instead of correcting it.

**`frame_clear_finalization`** imported `resource` for one CPU-time bound at the
end. It is a POSIX module, absent from CPython on Windows too, so the import
took all six checks off the platform and left the reference failing beside the
backends. `time.process_time` is the same measurement and is everywhere.

extra_tests/parity_tests: 213/213 on both backends, bar
`builtin_module_loader_spec` under a local CPython with no `test` package
installed — the runner that CI uses has it and reported `cpython=OK`.

* display: spell a repr's address the way the platform's `%p` spells it

`PyUnicode_FromFormat`'s `%p` hands the pointer to the platform's own `printf`
and normalizes only the prefix — guaranteed to start with a literal `0x`
"regardless of what the platform's printf yields". The platforms disagree about
everything after it: the MSVC runtime pads to the pointer width and uppercases,
glibc does neither. So on Windows CPython reads
`<function f at 0x000001B7AF7FFCC0>` where it reads `<function f at
0x1b7af7ffcc0>` elsewhere, and Rust's `{:p}` — along with `{:?}` on a raw
pointer and a hand-written `0x{:x}` — is only ever the second spelling.

Every address-bearing repr was therefore wrong on Windows. Measured against
CPython 3.14 on the same host, fifteen kinds disagreed: function, object,
generator, coroutine, async_generator, bound and built-in method,
method-wrapper, cell, weakref, memoryview, ContextVar, Token, code, frame and
both `_thread` locks. They now go through one `display::repr_addr`, and the
shapes are identical. `_pickle`'s cyclic-object message names an address the
same way and is the one such site that is not a repr.

`surrogate_name_messages` asserted the glibc spelling against `id()`, so it was
the REFERENCE that failed it on every Windows runner — a script that fails on
CPython measures nothing, and the backends were being compared against a failing
oracle. It builds the platform's spelling now.

The frame repr's `file '...'` is left raw: `pyframe.py:849-853` interpolates
`'%s'`, and CPython's `%R` there escapes the backslashes a Windows path is full
of. That is a parity-source disagreement rather than this fix's business.

* bench/synth: re-record pypy_type_surface's jit-stats baseline

`bridges_compiled 102 -> 5` and `guard_failures 20497 -> 1011`, byte-identical
on dynasm, cranelift and wasm.

The recorded 102/20497 are the counters the `Cls.__name__` fold produced while
its metaclass guard could not be discharged — it read the raw `w_class` slot
where the fast path answers through the `gettypefor(ob_type)` fallback, so a
receiver reached through that fallback got a `guard_value(NULL, type)` that
failed on every execution. #1106 narrowed the fold; the baseline still names
what the defect measured.

Read back rather than predicted: ubuntu-24.04, macos-latest and windows-latest
all report 5/1011 on main, and so does a local windows run. `retraces_compiled`
joins the file because the recorder now writes it; it is 0, which is what the
comparison already read for its absence.

* sys, parity: a structseq type built once, a decoded timeout stderr, and two NTFS-only assertions

`sys.getwindowsversion` built its structseq type inside the call, so every
answer was an instance of a different class and
`type(sys.getwindowsversion()) is type(sys.getwindowsversion())` read False
where CPython reads True. It was the one of pyre's ten structseq types that did
not already cache its type in a `OnceLock` — `stat_result`, `terminal_size`,
`uname_result`, `statvfs_result`, `waitid_result`, `times_result`,
`struct_time`, `_ExceptHookArgs` and `UnraisableHookArgs` all do — and it now
does the same. Measured: a probe over every structseq both runtimes carry
reports the types equal for all nine others and for all of CPython's, so this
constructor was the whole divergence. That the cache is a bare `usize` the GC
cannot see is the established pattern rather than a new bet, and it holds: 200
`os.stat` answers dropped across forced collections plus 60MB of churn leave the
cached type identical and its fields readable.

`structseq_type_identity` pins the property for every structseq the host
carries, and fails on the binary built before this commit naming exactly
`sys.getwindowsversion`.

The parity runner decoded its child's stderr on the normal path only.
`subprocess.run(text=True)` decodes what `communicate()` returns; the timeout
path raises with the raw chunks it had joined, which is bytes on POSIX and str
on Windows. A timed-out script would have reported one `b'...\n...'` line — the
unreadable shape the rest of this branch exists to remove.

Two assertions in `os_utime_pathconf_truncate` could only ever have held on
NTFS. The step that runs them has not reached ubuntu or macos: `check.py` runs
first in that job and has been failing, so the parity suite never starts there.

* `ns=(2**62, 2**62)` read back `4611686018427387900`, which is 2**62 rounded
  down to a FILETIME's 100ns tick. ext4 and APFS keep the nanosecond and answer
  `...904`. Both spellings now come from one `storable()`, which also replaces
  the `-1`/`-100` conditional above it.
* `utime(p, (8.8e11, 8.8e11))` names the year 29880, which no filesystem but
  NTFS reaches — an APFS timestamp is itself an int64 of nanoseconds, so 2262
  bounds it too, and ext4 stores a 34-bit second and stops in 2446. What every
  platform is held to is the identity the `i128` widening buys,
  `st_mtime_ns == int(st_mtime) * 1_000_000_000`, wherever the write succeeds;
  the exact value is asserted only where the second survived the round trip.

The remaining review note asked for f-strings in `surrogate_name_messages`.
Left alone: the file builds every expected repr with `%` formatting, including
the assertions this branch did not touch, and nothing lints for it.

* cpython_tests: report the cases unittest named, the tail of a timed-out run, and decode the report

`classify` recorded `last_stderr_line` for a FAIL. This runner sets
`MAJIT_STATS`, so the last thing every process writes is the JIT summary:
every FAIL in the suite recorded `rc=1 Compilation time: <n>ms`, a line that
names no test. The nightly report carries 118 of them, all the same. A FAIL
now carries unittest's own account instead — the closing `FAILED (...)` and
the `FAIL:`/`ERROR:` case headers, up to four of them and a count of the rest.
An IMPORTERROR never reached unittest, so it keeps the tail.

`TimeoutExpired` was caught and discarded, leaving `timeout 120s`. It carries
the output the child had produced, which for a unittest module is the progress
dots — the record of which case it stopped in. `text=True` decodes what
`communicate()` returns, not what the timeout path raises, so the partial
arrives as bytes on POSIX and str on Windows and both are accepted.

The report's box-drawing killed the run with a `UnicodeEncodeError` on a
console whose codepage cannot spell it, before any test ran; stdout is
reconfigured the way the parity runner's is.

* _locale: the Windows category numbers, no LC_MESSAGES there, and a setlocale that reaches the CRT

The `LC_*` values came from libc under `cfg(unix)` and from a hardcoded POSIX
table everywhere else, so Windows published `LC_CTYPE=0, LC_ALL=6` where the
MSVC CRT numbers them `LC_ALL=0, LC_COLLATE=1, LC_CTYPE=2`. Every constant
named a different category than the one it was passed to. They now come from
libc there too, which carries the CRT's own numbering.

`LC_MESSAGES` is a POSIX category the MSVC CRT has no counterpart for, and
CPython does not define it on Windows; it is registered under `cfg(unix)`.

`setlocale` was gated on `all(unix, host_env)`, so on Windows it fell to the
no-libc arm and answered "C" for every name it was handed, reporting success
for a locale that was not installed. `host_env::locale::setlocale` is not
unix-only — it calls `libc::setlocale`, which Windows has. The gate now admits
Windows, and an uninstallable name raises `locale.Error` as CPython's does.

Measured against CPython 3.14 on Windows: the six category numbers agree,
neither has `LC_MESSAGES`, `en_US.iso88591` raises `locale.Error` on both and
`en_US.utf8` and `English_United States.1252` are accepted by both.
`locale_categories` pins all three properties and fails on the prior binary.

* bench/synth: restore the three wasm jit-stats baselines #1086 reverted

#1106 `4555e3d76b` dropped the wasm32 arm of the self-recursive root-bridge
inline and recorded what that moved: `ca_bridge_multiframe_resume_double_call`
2581 -> 2592, `wasm_ca_trampoline_decline` 404 -> 601 and
`recursion_memo_branch` 4724 -> 4704. #1086 `e5eff81684` wrote all three back
to their pre-#1106 values, and ubuntu has failed on them every run since.

#1086 resizes 130 fixtures so pypy's side clears the measurement floor, and
re-recording a resized fixture's counters is part of that. It moves 34 baseline
values; 26 sit in fixtures whose `.py` it also changed. These three — and
`pypy_type_surface`, restored in 958f66c — are among the eight it moved with
no workload change beside them, which is a snapshot taken on a base predating
#1106.

Measured rather than reverted: a local wasm run reads 2592, 601 and 4704, which
are #1106's figures and the ones ubuntu observes. The remaining two of those
eight, `closure_per_call` 418 and `recursive_call_frame_relocation` 638, read
what they already record and are left alone.

The lower counts are not the better state. #1106 measured removing the decline
at -20.1%/-20.7% wasm CPU on `wasm_ca_trampoline_decline`; the +197 guard
failures buy that. `ca_bridge_multiframe_resume_double_call` pays +1.2%/+2.7%
and in exchange reports the 16 bridges / 0 aborts the dynasm baseline records,
where the decline left wasm at 16/1.

`retraces_compiled=0` comes with the re-record: these were the only wasm
baselines missing the key, which `_parse_jit_stats` was defaulting.
youknowone added a commit that referenced this pull request Aug 9, 2026
Rebasing onto `a7eb493079f` moved these counters. Measured on the final tree
with all three backends rebuilt from a full `extract-llbc.py`.

  synth/pypy_type_surface (all 3)   bridges_compiled 102 -> 5,
                                    guard_failures 20497 -> 1011
  synth/mapdict_frozen_unboxing_fold (all 3)  guard_failures 8 -> 11
  synth/ca_bridge_multiframe_resume_double_call (wasm)
                                    guard_failures 2581 -> 2592
  synth/closure_per_call (wasm)     guard_failures 418 -> 426
  synth/wasm_ca_trampoline_decline (wasm)  guard_failures 404 -> 601
  synth/recursion_memo_branch (wasm)  guard_failures 4724 -> 4704

`pypy_type_surface` returns to the values #999 committed. #1086 had rewritten
the file to 102 / 20497 — the numbers the `Cls.__name__` metaclass fold
produced while it guarded the raw `w_class` slot its oracle's `gettypefor`
fallback never read — and #1086 landed before #1106 declined that fold, so
the file has named a defect since. The fixed fold gives 5 / 1011 again.

`pypy_type_surface`, `ca_bridge_multiframe_resume_double_call` and
`wasm_ca_trampoline_decline` are also red on main's own CI at `b925c3e5ead`
(run 31283765874, ubuntu leg), so those three do not originate here.

An in-place control arm reverting only this branch's vable shadow write-back
reproduces every one of these numbers, so none of them is that change.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 9, 2026
Rebasing onto `a7eb493079f` moved these counters. Measured on the final tree
with all three backends rebuilt from a full `extract-llbc.py`.

  synth/pypy_type_surface (all 3)   bridges_compiled 102 -> 5,
                                    guard_failures 20497 -> 1011
  synth/mapdict_frozen_unboxing_fold (all 3)  guard_failures 8 -> 11
  synth/ca_bridge_multiframe_resume_double_call (wasm)
                                    guard_failures 2581 -> 2592
  synth/closure_per_call (wasm)     guard_failures 418 -> 426
  synth/wasm_ca_trampoline_decline (wasm)  guard_failures 404 -> 601
  synth/recursion_memo_branch (wasm)  guard_failures 4724 -> 4704

`pypy_type_surface` returns to the values #999 committed. #1086 had rewritten
the file to 102 / 20497 — the numbers the `Cls.__name__` metaclass fold
produced while it guarded the raw `w_class` slot its oracle's `gettypefor`
fallback never read — and #1086 landed before #1106 declined that fold, so
the file has named a defect since. The fixed fold gives 5 / 1011 again.

`pypy_type_surface`, `ca_bridge_multiframe_resume_double_call` and
`wasm_ca_trampoline_decline` are also red on main's own CI at `b925c3e5ead`
(run 31283765874, ubuntu leg), so those three do not originate here.

An in-place control arm reverting only this branch's vable shadow write-back
reproduces every one of these numbers, so none of them is that change.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 9, 2026
Rebasing onto `a7eb493079f` moved these counters. Measured on the final tree
with all three backends rebuilt from a full `extract-llbc.py`.

  synth/pypy_type_surface (all 3)   bridges_compiled 102 -> 5,
                                    guard_failures 20497 -> 1011
  synth/mapdict_frozen_unboxing_fold (all 3)  guard_failures 8 -> 11
  synth/ca_bridge_multiframe_resume_double_call (wasm)
                                    guard_failures 2581 -> 2592
  synth/closure_per_call (wasm)     guard_failures 418 -> 426
  synth/wasm_ca_trampoline_decline (wasm)  guard_failures 404 -> 601
  synth/recursion_memo_branch (wasm)  guard_failures 4724 -> 4704

`pypy_type_surface` returns to the values #999 committed. #1086 had rewritten
the file to 102 / 20497 — the numbers the `Cls.__name__` metaclass fold
produced while it guarded the raw `w_class` slot its oracle's `gettypefor`
fallback never read — and #1086 landed before #1106 declined that fold, so
the file has named a defect since. The fixed fold gives 5 / 1011 again.

`pypy_type_surface`, `ca_bridge_multiframe_resume_double_call` and
`wasm_ca_trampoline_decline` are also red on main's own CI at `b925c3e5ead`
(run 31283765874, ubuntu leg), so those three do not originate here.

An in-place control arm reverting only this branch's vable shadow write-back
reproduces every one of these numbers, so none of them is that change.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 10, 2026
Rebasing onto `a7eb493079f` moved these counters. Measured on the final tree
with all three backends rebuilt from a full `extract-llbc.py`.

  synth/pypy_type_surface (all 3)   bridges_compiled 102 -> 5,
                                    guard_failures 20497 -> 1011
  synth/mapdict_frozen_unboxing_fold (all 3)  guard_failures 8 -> 11
  synth/ca_bridge_multiframe_resume_double_call (wasm)
                                    guard_failures 2581 -> 2592
  synth/closure_per_call (wasm)     guard_failures 418 -> 426
  synth/wasm_ca_trampoline_decline (wasm)  guard_failures 404 -> 601
  synth/recursion_memo_branch (wasm)  guard_failures 4724 -> 4704

`pypy_type_surface` returns to the values #999 committed. #1086 had rewritten
the file to 102 / 20497 — the numbers the `Cls.__name__` metaclass fold
produced while it guarded the raw `w_class` slot its oracle's `gettypefor`
fallback never read — and #1086 landed before #1106 declined that fold, so
the file has named a defect since. The fixed fold gives 5 / 1011 again.

`pypy_type_surface`, `ca_bridge_multiframe_resume_double_call` and
`wasm_ca_trampoline_decline` are also red on main's own CI at `b925c3e5ead`
(run 31283765874, ubuntu leg), so those three do not originate here.

An in-place control arm reverting only this branch's vable shadow write-back
reproduces every one of these numbers, so none of them is that change.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 10, 2026
…ts, and a measured FOR_ITER gate widening (#1103)

* list: give the unused typed strategy an empty array, not a block

`build_list_storage` called `IntArray::from_vec` and `FloatArray::from_vec`
unconditionally, and `try_alloc_typed_items_block` clamps `cap` to 1 into the
old-gen `try_gc_alloc_stable_raw`, so every list allocated two blocks whose
strategy never reads them. The trace emitters leave those fields null:
`emit_empty_list_inline` and `emit_object_list_inline` set only `length` /
`items` / `strategy`, and `emit_typed_list_inline` writes one typed pair.

Add `IntArray::empty()` / `FloatArray::empty()` and use them where emptiness is
statically known — `build_list_storage`'s non-matching arms,
`switch_to_object_strategy`, `w_list_clear`. `switch_to_correct_strategy` keeps
`from_vec`, since its twin `emit_promote_empty_list_inline` emits a capacity-1
block and seeds the capacity getfield cache with 1.

`base()` takes `wrapping_add`, so the null block yields the items offset — a
non-null, 8-aligned address `from_raw_parts` accepts at length zero.

`list_object_custom_trace` skips the ownership query on a null typed block.

Assisted-by: Claude

* optimizeopt: answer an unwritten field of a virtual with its typed zero

virtualize.py:184-190 optimize_GETFIELD_GC_* resolves a field the virtual has
never been written to through optimizer.new_const(fielddescr).  Pyre carried
only the written-field arm, so such a read fell through to OptEarlyForce,
which forces every argument of a non-exempt operation and materialised the
struct along with everything its fields reach.

The array counterpart was already in place: NEW_ARRAY_CLEAR seeds every slot
with the typed zero at creation (virtualize.py:27-35, info.py:507-514).

typeptr keeps its own arm.  heaptracker.py:66 excludes it from the virtual
field set and the block above answers it from the descr vtable, so a struct
whose descr carries no vtable must not fold its class pointer to null.

Assisted-by: Claude

* mapdict: keep builtin storage on user subclasses

Restore exact int and bool objects to 24 bytes, Unicode objects to 64 bytes, and tuple objects to 40 bytes. Add distinct user-subclass layouts carrying mapdict map and storage fields, with their own GC types and traces.

Select the wider layouts from builtin subclass constructors and resolve mapdict field descriptors from each concrete carrier layout. Keep the specialized attribute load guarded by the subclass map and storage descriptors.

Record the wasm guard-count changes caused by the restored exact-object heap trajectory.

Assisted-by: Claude

* jit: record integer zero-divisor raising arms

Assisted-by: Claude

* mapdict: harden builtin subclass carriers

Guard the live Python class before native mapdict field access. Size map descriptors to the target word and exclude specialised tuple layouts. Mark private user layouts as GC objects without adding duplicate subclass-range peers. Re-root every mapdict carrier on class reassignment and allocate hasdict structseq values with tuple-user storage. Extend parity coverage for exact-value exits, descriptors, slots, GC inspection, and structseq extras.

Assisted-by: Claude

* jit: initialize inline allocation scalar fields

Assisted-by: Claude

* jit: record range zero-step raising arms

Assisted-by: Claude

* jit: record float zero-divisor raising arms

Assisted-by: Claude

* jit: record bigint zero-divisor raising arms

Assisted-by: Claude

* jit: record negative bigint shift raising arms

Assisted-by: Claude

* jit: scope FOR_ITER safety to escaping range loops

Assisted-by: Claude

* bench: re-record seven wasm jitstats baselines on the rebased base

Five of them (`exception_traceback_loop_forms`,
`gc_bug_bridge_flavor_traceback_names`, `loops_comprehension`,
`newslice_step_hot`, `unpack_ex_hot`) return to the values already committed on
the base; the rebase conflict resolution had kept this branch's older
measurements over them. Their only remaining difference from the base is added
counter keys.

`range_ctor_in_loop` compiles and enters its loop for the first time, so
loops_compiled 1 -> 5, bridges_compiled 0 -> 3 and guard_failures 0 -> 1009: a
fixture that never entered compiled code reported zero guard failures
trivially. An in-place revert of the FOR_ITER admission reproduced the old
values.

`closure_per_call` guard_failures moves 420 -> 417. This one is not attributed
by a control arm.

Assisted-by: Claude

* jit: diagnose FOR_ITER gate opcode declines

Assisted-by: Claude

* jit: gate FOR_ITER decline census allocation

Assisted-by: Claude

* jit: gate FOR_ITER decline census collection

Assisted-by: Claude

* jit: guard numeric binary specialization classes

Assisted-by: Claude

* jit: retain context on specialized builtin raises

Assisted-by: Claude

* jit: skip redundant numeric class guards

Assisted-by: Claude

* test: drive the numeric subclass fixture through the specialized pc

The fixture fed its subclass operand to a tail expression at a different
BINARY_OP pc than the loop that went hot, so that site was never specialized
and the check passed on a binary without the class guards.  Iterate a list
whose tail holds the subclass instead, so it arrives at the pc under test.

Adds left-operand cases and a bool-driven case, which reaches the tagged and
bool path where walker_numeric_builtin_class returns null and no class guard
is emitted at all.

Assisted-by: Claude

* Grow FOR_ITER regions through handler rejoins

Assisted-by: Claude

* Tighten escaping range append recognition

Assisted-by: Claude

* Update range constructor loop jitstats

Assisted-by: Claude

* jit: admit LIST_EXTEND in FOR_ITER bodies

Assisted-by: Claude

* jit: admit call-bearing LIST_APPEND bodies in the FOR_ITER gate

A LIST_APPEND body was admitted only when the body performed no call, because
a mid-body abort after the append routed through fbw_foriter_inflight_take,
which refuses delivery and dropped the iteration's item.

range_ctor_in_loop goes from mc_entered=0 to 813.

The surrounding comment previously cited blackhole.py as authority for the
append being rolled back and replayed once.  It is not: blackhole.py:1712 is
setposition, which continues from the coordinate already reached, and upstream
places the resume coordinate past a residual call so the effect is never
re-executed.  State instead what pyre actually relies on, and record that a
non-committed walk exit still keeps the legacy entry replay whose delivery can
be refused.

Assisted-by: Claude

* jit: census in-flight FOR_ITER delivery outcomes

Assisted-by: Claude

* bench: re-record nineteen jit-stats baselines

Five fixtures enter compiled code where they previously did not, so their
zero counters were zero trivially:

  exception_group_type      loops_compiled 0 -> 1, guard_failures 0 -> 1
  list_append_virtual_payload  loops_compiled 0 -> 2, bridges 0 -> 8,
                            guard_failures 0 -> 1603
  minmax_key_rooting        loops_compiled 1 -> 2, bridges 0 -> 2,
                            guard_failures 5 -> 409
  range_ctor_in_loop        loops_compiled 1 -> 3, bridges 0 -> 3,
                            guard_failures 0 -> 811 (812 on wasm)
  global_store_plain_dict_globals (wasm)  loops_compiled 5 -> 6,
                            loops_aborted 1 -> 2, guard_failures 1 -> 18
  pickle_terminal_raise_resume (wasm)  loops_compiled 67 -> 68,
                            loops_aborted 13 -> 14, guard_failures 339 -> 356

mapdict_frozen_unboxing_fold takes guard_failures 2 -> 8 with
loops_compiled unchanged at 3. An A/B across the call-bearing LIST_APPEND
admission alone gives mc_entered 2 -> 8 on the same fixture, so the counter
tracks compiled-code entries one for one: its `[C(i) for i in range(n)]`
comprehension is a call-bearing LIST_APPEND body.

gc_bug_bridge_flavor_traceback_names (wasm) improves guard_failures
2027 -> 1670.

exception_reused_object_tb_not_doubled (wasm) loses
fbw_blackhole_adopted_single_frame 3 -> 0. A binary built from
128590c with no branch commits applied reports 0 on the same fixture,
so the fall is the base's; the baseline was last recorded at 779da08.
Every other counter on that fixture is unchanged (loops 4, bridges 3,
aborted 3, guard_failures 600) and its traceback-shape oracle passes.

The recorder also writes the fbw_* and field_pos_* keys that were absent
from baselines recorded before those counters existed.

Assisted-by: Claude

* Trace traceback escape marking in exception attribute fold

Assisted-by: Claude

* Trace fresh container allocations in FOR_ITER callees

Admit replay-safe fresh tuple and list allocation helpers during nested callee tracing. Specialize len() for empty-list storage and add a cross-backend parity fixture for the admitted shape.

Assisted-by: Claude

* Admit tuple copies from exact lists during replay

Assisted-by: Claude

* Identify traceback walk bridge training

The 603 guard failures comprise three 200-hit bridge thresholds and three one-off transition failures. The final bridge reconnects the traceback walk to its compiled inner-loop token, so no resume-semantics change is required.

Assisted-by: Claude

* bench: add a synthetic fixture for the subscript inline's index operand

`Seq.__getitem__` reached as `p[len(EMPTY)]`, so the index arrives on the
operand stack rather than from a constant or a local, with an
`isinstance(index, slice)` branch in the body so the inline has a residual to
abort on. Prints 276000 under cpython, pypy3 and pyre.

The defect the shape covers — the FOR_ITER deferred admission reading
`arg_class_guard.is_none()` as a proxy for "the entry opcode is a CALL", which
admitted the BINARY_OP-entered subscript inline and let the flush resume one
operand short — is fixed in #1082, which names the property directly and
carries its own parity test. This holds the shape under the jit-stats gate too.

Assisted-by: Claude

* bench: re-record the wasm pickle terminal-raise baseline

`loops_compiled` 66 -> 67, `loops_aborted` 14 -> 15 and `guard_failures`
339 -> 356 on the wasm leg of `synth/pickle_terminal_raise_resume`.  The file
already carried the 356 from an earlier recording; the two loop counters did
not.  `retraces_compiled=0` joins the recorded set.

Bisected to `jit: admit call-bearing LIST_APPEND bodies in the FOR_ITER gate`
by in-place whole-tree control arms at four points of this branch: the base and
the trees at the three commits below it read 66 / 14 / 339, the tree at that
commit reads 67 / 15 / 356, and every counter moves there together.  A base
control arm reproduces main's committed 66 / 14 / 339 on this host, so the move
is this branch's and not the host's.

The dynasm and cranelift baselines for the same fixture are byte-identical to
main's (30 compiled, 1 aborted, 338 guard failures) and are unchanged here: the
loop the widened gate admits is one only the guest reaches, which compiles 66
loops in this fixture where the native backends compile 30.  The extra abort is
one more attempt at a loop the gate now allows, recorded beside the compile it
gained.

Not the collection schedule: `PYPY_GC_MIN` at 256MB, 384MB and 512MB gives
identical counters, and three repeats agree exactly.

Assisted-by: Claude

* jit: pair the vable static shadow write with a heap write-back

`mirror_vable_static_to_boxes` wrote `virtualizable_boxes` without the
`synchronize_virtualizable()` half `_opimpl_setfield_vable` performs
(pyjitpl.py:1188-1199). `walker_capture_snapshot_for_last_guard_impl`
publishes `last_instr = py_pc - 1` through it, and the walk never runs the
interpreter's own `frame.last_instr = pc` store, so the live frame stayed one
opcode behind the shadow and `check_synchronized_virtualizable`
(pyjitpl.py:3463-3468) failed under `debug_assertions` in
`gc_stress::module_dict_move_to_end_reentrant_survives_python_callbacks`.

Add `TraceCtx::synchronize_virtualizable_static`, a single-static
`write_boxes` that keeps `synchronize_virtualizable`'s guards and its
`VableArrayStorage::RustVec` carve-out. The full `write_all_boxes` is not
usable here: the shadow's array half holds NULL for the operand slots a
mid-opcode guard resumes before, and writing it back would stamp those NULLs
into the live frame. Call it from `mirror_vable_static_to_boxes`.

`try_execute_residual_call_via_executor` saves the `last_instr` shadow entry
before publishing the executing pc and restores it after the residual
returns, matching `LiveLastInstrGuard`'s save/restore of the heap half. The
restore is skipped when the callee forced the virtualizable.

Assisted-by: Claude

* docs: list the two FOR_ITER gate diagnostics in gate-triage

`PYRE_FOR_ITER_GATE_DIAG` (pyre-jit-trace/src/jitcode_dispatch/mod.rs,
pyre-jit/src/eval.rs) and `PYRE_FORITER_INFLIGHT_CENSUS`
(pyre-jit-trace/src/jitcode_dispatch/mod.rs) are read through
`env::var_os(..).is_some()`, so both are default-OFF diagnostics and belong in
§6c. `pyre/pyrex/tests/gate_triage_complete.rs
::every_live_pyre_gate_has_a_gate_triage_entry` failed on their absence.

Assisted-by: Claude

* bench: re-record ten jit-stats baselines after the rebase

Rebasing onto `a7eb493079f` moved these counters. Measured on the final tree
with all three backends rebuilt from a full `extract-llbc.py`.

  synth/pypy_type_surface (all 3)   bridges_compiled 102 -> 5,
                                    guard_failures 20497 -> 1011
  synth/mapdict_frozen_unboxing_fold (all 3)  guard_failures 8 -> 11
  synth/ca_bridge_multiframe_resume_double_call (wasm)
                                    guard_failures 2581 -> 2592
  synth/closure_per_call (wasm)     guard_failures 418 -> 426
  synth/wasm_ca_trampoline_decline (wasm)  guard_failures 404 -> 601
  synth/recursion_memo_branch (wasm)  guard_failures 4724 -> 4704

`pypy_type_surface` returns to the values #999 committed. #1086 had rewritten
the file to 102 / 20497 — the numbers the `Cls.__name__` metaclass fold
produced while it guarded the raw `w_class` slot its oracle's `gettypefor`
fallback never read — and #1086 landed before #1106 declined that fold, so
the file has named a defect since. The fixed fold gives 5 / 1011 again.

`pypy_type_surface`, `ca_bridge_multiframe_resume_double_call` and
`wasm_ca_trampoline_decline` are also red on main's own CI at `b925c3e5ead`
(run 31283765874, ubuntu leg), so those three do not originate here.

An in-place control arm reverting only this branch's vable shadow write-back
reproduces every one of these numbers, so none of them is that change.

Assisted-by: Claude

* jit: follow the InflightForiterBody field rename in the census

`InflightForiterBody::Jit` carries `jitcode_index: i32` since #1111, which
also made the identity negative when unresolvable. The census `code_ptr`
resolution still destructured the former `outer_jitcode_index: u32` and cast
it, so the crate stopped compiling once both sides met.

`raw_code_for_jitcode_index` indexes with the value, so a negative index
misses and the census keeps the live frame's code.

Assisted-by: Claude

* mapdict: split the layout predicate from the storage predicate

`has_mapdict_layout` answers the physical question — the allocation
carries the `MapdictStorageMixin` slots — and no longer consults
`w_type_get_hasdict` for the generated int/str/tuple user layouts.
`has_mapdict_storage` is that test plus the owning class's `hasdict`
flag, and `mapdict_carrier` now asserts the layout predicate, so a
`__slots__`-only native subclass no longer trips the assertion.

`is_generated_user_layout_family` carries the specialised-tuple
exclusion for the layout test, the storage test, and the carrier's
`W_TupleObjectUser` arm.

Assisted-by: Claude

* _structseq: re-read the pinned class after the tuple allocation

The array-backed tuple constructor can collect, so the class pointer
read before it can be stale when it is stored into the new object's
`w_class`. Re-read it from the shadow-stack slot after the allocation.

Assisted-by: Claude

* jit: gate the in-flight FOR_ITER census key lookup on the census

`raw_code_for_jitcode_index` runs `ensure_finish_setup` and borrows
`METAINTERP_SD`; `fbw_foriter_inflight_take` called it on every take
even though `census_record_foriter_inflight` returns immediately unless
`PYRE_FORITER_INFLIGHT_CENSUS` or `PYRE_FBW_DEBUG_ABORT` is set. The
enable check moves into `foriter_inflight_census_enabled`, which both
sites share.

Assisted-by: Claude

* jit: correct the exception descr group note on w_context

`w_context` is written by the raise lowering, not left zeroed by GC
pointer clearing.

Assisted-by: Claude

* test: scan the loop-region fixture in two passes

`loop_region_includes_out_of_line_handler_rejoining_mid_body` compared
each backward target against `outer_header` while still lowering it, so
a jump seen before the smallest target was missed. The scan now runs
twice over a shared target closure, each pass with its own `OpArgState`.

Assisted-by: Claude

* test: cover synchronize_virtualizable_static

Five cases: the single-field write-back, absent virtualizable state, an
out-of-range index, a RustVec-backed array field, and a shadow slot
holding no concrete.

Assisted-by: Claude

* majit: exclude the identity slot from the static write-back bound

`virtualizable_values`'s last slot holds the vable identity
(`virtualizable_boxes[-1]`), not a field value.
`synchronize_virtualizable_static` bounded `index` by the full vector
length, so a shadow shorter than the declared static count would have
written the identity ref into a static field. Bound by the data length.

Assisted-by: Claude

* jit: read PYRE_FOR_ITER_GATE_DIAG through one accessor

The per-opcode decline and the whole-region decline each owned a
function-local `OnceLock` for the same variable.

Assisted-by: Claude

* jit: admit builtin subclass carriers in the mapdict storage helpers

The seven mapdict residual wrappers tested their receiver with
`is_instance`, which is true only for an ordinary `W_ObjectObject`. The
generated int/str/tuple user layouts failed that test, and the wrappers
answer a value rather than declining: the unboxed reads returned 0 and
0.0, the boxed read returned PY_NULL, and all three writes returned
without storing.

`Flag.__or__` reads `other._value_`, so `Perm.R & Perm.R` computed
`4 & 0`; `test.test_enum`'s `OldTestIntFlag` test_and/test_or/test_xor/
test_type failed on that. Measured on the release dynasm build, an
unboxed int attribute read on an int/str/tuple subclass was wrong 1756/
2411/2498 times per run and correct under PYRE_NO_JIT=1.

The receiver test is now `has_mapdict_layout`, which is `mapdict_carrier`'s
own precondition, shared through `is_mapdict_carrier`.

The parity fixture gains loops that validate the loaded and stored values
for the unboxed int and float slots; the existing ones discard what they
load and so never observed this.

Assisted-by: Claude

* Revert "jit: admit call-bearing LIST_APPEND bodies in the FOR_ITER gate"

This reverts commit 2a0f91f.

The decline it removed is load-bearing. A comprehension whose body calls
a user Python function drops an element:
`[random.randrange(25) for i in range(size)]` returned 22 items for
size=23, and PYRE_FORITER_INFLIGHT_CENSUS reported DELIVERED=0 REFUSED=1
for that body pc on the same run. `test.test_heapq`'s test_heapsort
failed on the shortened list, raising IndexError from `heappop`.

The reverted commit argued the append always sits past the resume
coordinate; the census shows the refusal path is reachable, because the
call commits body effects that `fbw_foriter_inflight_take` sees as a
committed effect since the consume.

The parity fixture records the shape.

Assisted-by: Claude

* majit: drop the narrowed virtualizable static synchronizer

`mirror_vable_static_to_boxes` now calls `synchronize_virtualizable()`,
the shape `_opimpl_setfield_vable` uses (`pyjitpl.py:1188-1199`), so the
narrowed single-field variant and its tests have no caller.

Assisted-by: Claude

* bench: restore five jit-stats baselines the reverted gate had moved

The call-bearing LIST_APPEND admission raised loops_compiled and
bridges_compiled on exception_group_type, list_append_virtual_payload,
minmax_key_rooting, range_ctor_in_loop and subscr_user_getitem_stack_index;
reverting it returns them to what main records. mapdict_frozen_unboxing_fold's
guard_failures returns to 2, the value main carries — the branch's 11 was
recorded while the mapdict storage helpers answered zero.

dynasm only; the cranelift and wasm baselines follow.

Assisted-by: Claude

* bench: restore the cranelift jit-stats baselines to match

Same six benches as the dynasm pass, same direction and magnitude.

Assisted-by: Claude

* bench: restore the wasm jit-stats baselines to match

The same six benches as the dynasm and cranelift passes, plus
global_store_plain_dict_globals and pickle_terminal_raise_resume, whose
observed loops_compiled / loops_aborted / guard_failures all return to the
values main records.

closure_per_call keeps main's guard_failures: its loops_compiled and
bridges_compiled are unchanged, so the count drifts without a shape change.

Assisted-by: Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant