jit: fold Cls.__name__, and admit a type receiver into the FOR_ITER inline - #1097
Conversation
|
Warning Review limit reached
Next review available in: 1 minute You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (18)
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
https://github.com/youknowone/pyre/blob/c8065595e83ce4ca82b1d18852cdbf5d01ee822f/pyre-jit-trace/src/jitcode_dispatch/specialize.rs#L3106
Trace
type.__name__ through the translated interpreter
Replace this walker-only __name__ branch with translation of the existing W_TypeObject.descr_getattribute/descr_get__name__ path. This function implements descriptor selection, metaclass restrictions, slot access, and guards a second time inside the JIT, so future interpreter changes will not automatically propagate here and other type descriptors remain opaque; that defeats the repository's required line-by-line RPython structure even though the benchmark currently passes.
AGENTS.md reference: AGENTS.md:L231-L233
ℹ️ 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".
🤖 Codex parity reviewStatic analysis of this diff vs the local RPython/PyPy sources (commit bff00d8). Files in the reviewed diff1. Regressions to PyPy parity introduced by this patchNone. 2. Other mismatches introduced by this patch
3. Pre-existing mismatches (already present before this patch)
4. Structural adaptationsNone. |
`type.__name__` is a GetSetProperty on the metatype, so `descr_getattribute` selects it ahead of the class's own MRO and its getter returns the class's `w_name` slot. A `LOAD_ATTR` of `__name__` on a class whose metaclass is exactly `type` now folds to that slot read -- guard_class for the W_TypeObject layout, guard_value pinning `w_class` to `type`, then getfield(w_name) + guard_nonnull -- instead of the opaque `getattr` CALL_MAY_FORCE residual. The class is not pinned, so several classes share one trace, and the slot is read live rather than baked because `descr_set__name__` replaces it without calling `mutated()`. Supporting pieces: `w_type_peek_name_obj` reads the slot without the lazy materialisation `w_type_get_name_obj` performs (a null slot declines rather than allocating inside the walker), `type_name_obj_fast_path` is the safety oracle, and `type_name_obj_descr` the field descr. The FOR_ITER deferred-admit gate declined an unbound method-form callee whose body carried a LoadAttr residual, because admitting one cost an abort before the callee-deny took effect. Those attribute reads fold now -- the mapdict one for an instance, this one for `cls.__name__` -- so the gate admits them; a body whose read does not fold still takes the one abort and is denied. `method_form_callee_body_supported` and `InlineBodyFacts::method_form_supported` had no other caller and are removed. Marginal cost per iteration, dynasm, differencing 200k against 2M: def at(self, i): return self.v + i 1091 ns -> 5.9 classmethod reading len(cls.__name__) 1082 ns -> 1.6 bench/synth/classmethod_protocol_hot 1508 ns -> 96.2 `synth/type_name_attr_fold` covers the fold's boundary: a rename seen through an inlined read, two classes on one trace, a class dict entry of the same name losing to the metatype descriptor, and the three metaclass shapes that must decline. `parity_tests/type_name_attr_identity` carries the name-object identity, which CPython preserves and PyPy does not. `classmethod_protocol_hot` and `type_metatype_method_call` go `loops_compiled` 2 -> 1 on all three backends: their callee is inlined instead of compiled as a separate function-entry trace.
c806559 to
bff00d8
Compare
There was a problem hiding this comment.
💡 Codex Review
https://github.com/youknowone/pyre/blob/bff00d8976904292eea979ea209d8e3c551aa9ec/pyre-interpreter/src/baseobjspace.rs#L9606-L9607
Materialize the name before declining the fold
When a class's first __name__ access occurs while recording a trace—for example, on a bridge after a loop condition first becomes true—w_name is still null, so this returns None and try_walker_specialize_load_type_name_attr records the opaque residual permanently. No GUARD_NONNULL is emitted in this case, contrary to the specialization's stated side-exit/re-entry design, so materializing the slot does not let the compiled path adopt the fold. Arrange safe materialization before recording, or emit a path that side-exits on the null slot, rather than declining the specialization outright.
AGENTS.md reference: AGENTS.md:L231-L233
ℹ️ 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".
…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
…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 (cherry picked from commit 95ec7f2)
…nes" This reverts commit b6f90ad. The re-record was wrong. It read "no machine reproduces 5 / 1011" as "the baseline was never measured", but an in-place control arm on another branch shows the numbers move with #1097's `Cls.__name__` fold (`try_walker_specialize_load_type_name_attr`): fold off gives 5 / 1012 against the committed 5 / 1011, fold on gives 102 / 20498. #999 measured honestly on a tree that predated the fold, which merged directly before it. So `bridges_compiled 5 -> 102` is a live regression inherited from main, not a recording that matches no build, and adopting its numbers here would erase the evidence. It is bisected to `type(type.__dict__[name]).__name__` and is specific to a `getset_descriptor` receiver; the failing guard is not the fold's own and the mechanism is still open. Assisted-by: Claude
…ne (#1106) * jit(wasm): drop the wasm32 arm of the self-recursive root-bridge inline `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 * jit: decline the `Cls.__name__` fold when its metaclass guard cannot 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 * parity_tests: gate os_utime_pathconf_truncate's POSIX-only halves on 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 * jit: read an aborted inline's Ref operand list at its argcode offset `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)
…ry (#68) (#1107) * bench/synth: re-record jitstats baselines for three synthetics binary_int_overflow_local_resume, exc_bridge_entry_guard_not_removed, and list_append_write_barrier_gc across dynasm/cranelift/wasm: bridges_compiled drops by one and guard_failures decreases after the trace-time unary int specialization removes deopts (loops_compiled and loops_aborted unchanged, internal_compile_panics=0). Adds the fbw_blackhole_adopted_multi_frame, fbw_blackhole_adopted_single_frame, and fbw_store_journal_rollback_failed counters (all 0). Values are identical across the three backends. Assisted-by: Claude * posix: correct the DirEntry.stat caching citation to the object-cache model dir_entry_stat caches and returns the built stat_result object (`entry.stat() is entry.stat()`), which is `posixmodule.c DirEntry_get_stat`, not `interp_scandir.py descr_stat` (which caches raw stat data and rebuilds a fresh result per call). The doc comment cited the latter; correct it and note the object-caching is the 3.14 behavior. Assisted-by: Claude * posix: DirEntry.inode() returns the inode captured at scandir enumeration W_DirEntry gains a native enum_ino field. The name-based scandir path captures the readdir inode (DirEntryExt::ino(), never a stat) and dir_entry_inode returns it directly, the way descr_inode returns self.inode; it is -1 when unavailable (the scandir(fd) path and non-unix hosts), which falls back to the existing stat. A primitive field is GC-safe — the pyre_class macro emits PTR_OFFSETS only for PyObjectRef fields. Assisted-by: Claude * posix: answer DirEntry type/inode predicates from the enumeration dirent Capture the d_type and d_ino readdir reports at scandir enumeration into W_DirEntry.enum_type / enum_ino. The name path enumerates through opendir/readdir instead of std::fs::read_dir so it reads the dirent directly, and the descriptor path captures the same fields through fd_readdir. is_dir/is_file/is_symlink answer from enum_type unless it is DT_UNKNOWN or a followed symlink, and inode() answers from enum_ino on the descriptor path too, both without a stat. A known DT_DIR/DT_REG therefore answers is_dir()/is_file() from the cached type even after the entry is removed, as descr_is_dir/is_file do. The wasm, sandbox, and non-host_env paths keep the std fallback with enum_type DT_UNKNOWN and stat. Extract readdir_collect and fd_readdir out of fdlistdir, and add join_dir_name for the enumerated full path. Assisted-by: Claude * jit: decline the `Cls.__name__` fold when its metaclass guard cannot 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 (cherry picked from commit 95ec7f2) * majit-gc: serialize gcreftracer registry tests against concurrent walks walk_all_gc_tables is reachable from any collector test's collection through the globally-registered gc_table_extra_root_walker once any table has existed. Under the parallel test harness a collector test's collection can upgrade a Weak the dropping registry test expects dead, transiently resurrecting the table and failing "freed table must not be walked". Add a test-only RwLock: every walk takes the read side, a registry test takes the write side across its whole drop/observe window. Split the walk into a locked outer and an unlocked inner so a registry test holding the write side observes the registry without re-entering the lock. Compiled out in production, where the STW collector already prevents a concurrent drop. Assisted-by: Claude * posix: write Windows utime through SetFileTime so pre-epoch times round-trip The Windows utime path carried its timestamps as an epoch-based unsigned Duration, so u64::try_from turned away every time before 1970. A FILETIME counts 100-ns intervals from 1601-01-01, which holds a pre-epoch time as an ordinary positive count, so convert the seconds/nanoseconds to a FILETIME and write them with CreateFileW + SetFileTime, the write rposix.win32_utime makes. Failures go through fs_err_with_filename so the OSError carries .winerror, as the module's other Windows path calls do. Enable the Win32_Security feature CreateFileW's signature needs. Assisted-by: Claude * extra_tests: skip the POSIX-only pathconf section of os_utime_pathconf_truncate on Windows pathconf and pathconf_names are POSIX-only; Windows CPython has neither, so the section raised AttributeError there under the oracle. Guard it with sys.platform, matching the file's existing utime/truncate Windows guards, and refresh the block comment now that SetFileTime writes pre-epoch times. Assisted-by: Claude
`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
Rebased onto
origin/main(128590c). #1088 landed the instance half of theFOR_ITER method-form inline in the meantime, so what is left here is the type
receiver: the fold that makes
Cls.__name__cheap, and the decline that foldretires.
Cls.__name__had no foldtry_walker_specialize_load_attrwants a mapdict instance, so a class receiverfell through to the opaque
getattrCALL_MAY_FORCEresidual — 328 ns for atop-level
Derived.__name__, and far worse inside a callee, as below.type.__name__is aGetSetPropertyon the metatype, i.e. a DATA descriptor,so
descr_getattribute(typeobject.py:814-819) hands the read to it beforethe class's own MRO. The MRO does not have to be walked at all, and no
__name__entry in any base's dict can shadow it. So with the metaclass pinnedto
typeitself — which is immutable, so the getter cannot be replaced — thewhole read is the
w_nameslot the getter returns:Two deliberate differences from the classmethod fold beside it:
a loop reading
cls.__name__over several classes keeps one trace.descr_set__name__(typeobject.py:1046)replaces the name without going through
mutated(), so the version tag doesnot move when a class is renamed — a version-pinned constant name would
outlive the rename and be wrong.
w_type_peek_name_objis the slot read without the lazy materialisationw_type_get_name_objperforms: materialising means allocating, and the walkeris holding raw heap pointers, so a null slot declines instead.
The FOR_ITER gate's "receiver is not a type" proof
#1088 admits a widened method-form callee — an unbound one whose body reads
self.attr— into the FOR_ITER deferred path, but only after proving thereceiver is not a type object. Its comment gives the reason: a type receiver's
read went through
type.__getattribute__, reached the deferred abort path, andthat first abort retires the enclosing loop before the deny helps.
That read folds now, so a type receiver reaches no residual to abort on either
and the proof is no longer what admits it. It goes, and with it
method_form_callee_body_supportedandInlineBodyFacts::method_form_supported,which had no other caller. The three conditions #1088 added alongside it —
owns_loop_header,code_has_for_iter,has_exception_table— are untouched.A body whose attribute read does not fold — any metaclass other than
type— still takes the one abort and is denied, which is what this arm's promise has
always rested on.
Measured
Marginal cost per iteration, dynasm, min-of-3 at 200k and 2M, against a binary
built from this branch with only the
pyre-jit-tracehalf reverted:origin/mainlen(cls.__name__)len(type(self).__name__)in a methodlen(Derived.__name__)bench/synth/classmethod_protocol_hotdef at(self, i): return self.v + iWall-clock on the shipped fixture barely moves, and that is not a
contradiction: at its 20001 iterations the exec time is ~0.04 s of compile
time and the loop itself is under 2 ms. The count is left alone — it still puts
the fixture's own side well above the timer floor its header is about.
Tests
synth/type_name_attr_foldcovers the fold's boundary rather than its speed: arename seen through an inlined read, two classes down one trace, a class dict
entry of the same name losing to the metatype descriptor, and the three
metaclass shapes that must decline (a
__getattribute__override, a metaclass__name__property, and a plaintypesubclass — the last because the fold'sprecondition is the metaclass, not what it happens to do).
parity_tests/type_name_attr_identitycarries the name-object identity: astrsubclass assigned to__name__comes back as that same object. CPythonpreserves it and PyPy does not, and
check.pycompares synth output againstboth, so it belongs on the CPython-only side.
classmethod_protocol_hotandtype_metatype_method_callgoloops_compiled2 -> 1 on all three backends — their callee is inlined instead of compiled as a
separate function-entry trace.
Verification
pyre/check.py --backend dynasm— 404/404pyre/check.py --backend cranelift— 404/404pyre/check.py --backend wasm— 399/400. The one failure,synth/exception_reused_object_tb_not_doubled(
fbw_blackhole_adopted_single_frame3 -> 0), is already failing onorigin/main: reverting this branch'spyre-jit-tracehalf and rebuildingreproduces the identical numbers, with every other counter matching. Its
baseline was recorded by Two blackhole-resume double-apply shapes, and a ratio gate that fails on a non-measurement #1095. It needs a re-record, which is not this PR's
to make.
extra_tests/parity_tests— 212/213, the one failure(
os_utime_pathconf_truncate) failing on CPython too on this host.cargo test -p pyre-jit-trace,cargo fmt --all -- --checkfabricated