jit(wasm): drop the wasm32 arm of the self-recursive root-bridge inline - #1106
Conversation
`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
WalkthroughThe 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. ChangesJIT dispatch and validation
Recursion benchmarks and statistics
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
Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
| 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) | ||
| }; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
📒 Files selected for processing (10)
pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstatspyre/bench/synth/foriter_call_resume_drops_iteration.wasm.jitstatspyre/bench/synth/recursion_memo_branch.wasm.jitstatspyre/bench/synth/selfrec_bridge_nontail_promote.cranelift.jitstatspyre/bench/synth/selfrec_bridge_nontail_promote.dynasm.jitstatspyre/bench/synth/selfrec_bridge_nontail_promote.pypyre/bench/synth/selfrec_bridge_nontail_promote.wasm.jitstatspyre/bench/synth/wasm_ca_trampoline_decline.wasm.jitstatspyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rspyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
|
|
||
| out = [] | ||
| for i in range(50000): | ||
| out.append(walk(24, (1 << 50) + i) % 1000000007) |
There was a problem hiding this comment.
📐 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 || trueRepository: 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:
- 1: https://docs.astral.sh/ruff/rules/manual-list-comprehension/
- 2: https://github.com/astral-sh/ruff/blob/main/crates%2Fruff_linter%2Fsrc%2Frules%2Fperflint%2Frules%2Fmanual_list_comprehension.rs
- 3: [
perflint] Allow list function calls to be replaced with a comprehension (PERF401) astral-sh/ruff#17519 - 4: PERF401 rule confusion astral-sh/ruff#20350
- 5: [
perflint] AddPERF401andPERF402rules astral-sh/ruff#5298 - 6: Where is the cutoff for list comprehension suggestion from PERF401 astral-sh/ruff#18290
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.
| 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
🤖 Codex parity reviewStatic analysis of this diff vs the local RPython/PyPy sources (commit 87c0439). Files in the reviewed diffCodex did not produce a report (exit 1). Last log lines: |
Codex parity review — adjudication§2 — reclassified, not fixed
The finding takes the deleted comment as authority, and that comment is exactly what this patch audits. Its provenance: The cited upstream line argues the other way. The typed-residual half of the finding is also unaffected: the non-inlined case still reaches Evidence that the named confusion does not occur, rather than that it merely was not hit: §3 — both confirmed real, both deferred with a named blockerNeither is "pre-existing, therefore skip" — both are genuine divergences worth a follow-up, and both exceed what this diff can safely carry.
§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
There was a problem hiding this comment.
💡 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".
| # 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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
pyre/extra_tests/parity_tests/foriter_body_call_abort_operand_stack.pypyre/extra_tests/parity_tests/os_utime_pathconf_truncate.pypyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rspyre/pyre-jit-trace/src/jitcode_dispatch/mod.rspyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rspyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
| '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; |
There was a problem hiding this comment.
🩺 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.
| '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.
`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.
`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
`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.
#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.
…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.
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
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
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
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
…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
bridge_rec_root_selfrec(inline_call.rs) was gated oncfg!(not(target_arch = "wasm32")), so a guard-failure bridge reaching a self-recursive callee'sCALLtook the root-bridge admission on dynasm and cranelift and residualized on wasm. This drops the cfg term, leaving the last policy-levelwasm32cfg 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:setintbound: got Refenters the tree in jit: nsvable multi-frame resume + callee-locals shadow + exc-edge bridge + portal-runner descr (gated); gc: register deque iterators; vstack NONE-hole #643920367da965, attached tofbw_inline_callee_hazardous— a decline that is not cfg-gated and rests on theCALL_ASSEMBLERmoving-nursery trampoline frame. That slice did measure wasm.71726847a10, whose commit message reportsdynasm + cranelift 294/294and no wasm number at all.always-portalappears in three comments and names nothing in the tree.Both mechanisms that could make such a type confusion wasm-specific are refuted:
W_IntObject { ob_header, intval: i64 }is i64 on every target.ALLOW_UNBOXING_INTS = LONG_BIT == 64gates the mapdict unboxed attribute slot, which is pointer-sized — not the integer's width.CALL_ASSEMBLERcapability. General wasm CA landed in jit(wasm): general CALL_ASSEMBLER + float residual/local perf gaps (nbody/fannkuch/float_loop/spectral) #564c89254a6211on 2026-07-15 and was hardened through jit(wasm): fix latent CALL_ASSEMBLER terminal-decline pointer miscast #575/jit: descr-identity cutover for CALL_ASSEMBLER + default-on recursive-call portal cutover #609 by 07-17 — before both jit: nsvable multi-frame resume + callee-locals shadow + exc-edge bridge + portal-runner descr (gated); gc: register deque iterators; vstack NONE-hole #643 and jit: single-authoritative bridge-carrier walk + flip multiframe chain-inline depth to 7 #749. Unlike Hazard 3, this is not a capability that arrived after the claim.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_overflowalready exercised this admission, but only in its easiest form: tail recursion whose live set is two machine integers.synth/selfrec_bridge_nontail_promoteadds the three ingredients a "Ref reached an int operation" actually needs —inner + len(tag)), so the guard's resume stream is multi-frame;2**63at level 13 of 24, so it genuinely promotes to a long — a Ref — partway down and the overflow guard fires inside the recursive frame.806 − 606 = 200 = exactly one
trace_eagernessbucket.Verification
check.pywith 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, exceptrecursion_memo_branchat one guard failure apart — previously twenty-one.The one failure,
synth/pypy_type_surface, is not this change:bridges_compiled 5 -> 102, guard_failures 1011 -> 20497with both touched files restored toorigin/main;true;code_is_self_recursiveto 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):
wasm_ca_trampoline_declineselfrec_bridge_nontail_promoterecursion_memo_branchca_bridge_multiframe_resume_double_callforiter_call_resume_drops_iterationThe 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_surfacefails on all three backends atorigin/main(
bridges_compiled 5 -> 102, guard_failures 1011 -> 20497). Verified not to bethe 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 cfgalready read
trueon the natives), and the fixture holds no self-recursivefunction. So it had to be diagnosed rather than worked around.
Cause.
try_walker_specialize_load_type_name_attr(#1097) takes themetaclass from
type_name_obj_fast_path→typedef::type, which falls back togettypefor(ob_type)when the receiver'sw_classslot is null. The fold thenguards the metaclass by reading the raw
w_classfield, so such a receivergets
guard_value(NULL, type)— unfailable-to-discharge, and nothing writes theslot afterwards, so it fails once per iteration forever.
One class reaches it: the
getset_descriptortype object is built lazily insidethe init loop as a builder for other typedefs' descriptors, so it never enters
the registry whose post-loop sweep stamps
w_class = type.typedef::typestill answers
typevia the fallback, so the null is invisible from Python.Localisation.
check_descriptor_kindsalone carries all 97 excess bridges;the receiver value and the
FOR_ITERare both irrelevant;int,str,list,NoneType,object,type,method_descriptor,wrapper_descriptor,builtin_function_or_methodand a user class are clean; andgetattr(C, "__name__")— identical semantics, but the fold declines becausethe 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_surfacereads its recorded 11/5/0/1011 exactly and #1097's owntype_name_attr_foldstill reads 5/0/1/4, so the fold keeps applying whereverits 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-based936eb4be42withos_utime_pathconf_truncate.py cpython=FAIL dynasm=FAIL cranelift=FAIL— thesame 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, whichthe block above it already excludes Windows from, and the
pathconfsectionreads
os.pathconf_names, registered under#[cfg(unix)]here and absent fromCPython's
oson Windows. The keyword spelling now uses a time the platformholds and the
pathconfsection runs where the names table exists;pyre/extra_tests/parity_tests/run.pyis green on macOS.Fourth commit: the
test.test_reCPython-suite red, cherry-pickedCPython suite (gate)also fails at the base —test.test_reraisesTypeError: list indices must be integers or slices, not list_iteratorfrom_compiler.py:504 _get_charset_prefix, wherep[0]insidefor p in av[1]received the loop's iterator as its subscript index.
reconstructed_all_ref_call_stackread the residual op's Ref var-list at afixed operand offset 1. That holds for the Ref-only shape
iRd>rbut not forthe mixed
iIRd>rthe method-form CALL helpers lower through, whose leadingInt 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 isvstack_boxes.len()for anylist length, so the abort-flush's
depth_at_py_pccheck 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 PRyet and this one inherits the red. Drop this commit when
rbigintlands.The one conflict was in
tests.rs, where the incoming hunk also carried a testbelonging to an earlier
rbigintcommit; only the newref_var_list_offset_follows_the_argcodes_not_a_fixed_bytewas kept.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
__name__specialization guards and corrected operand reconstruction afterFOR_ITERcall aborts.Tests