Skip to content

jit: fold Cls.__name__, and admit a type receiver into the FOR_ITER inline - #1097

Merged
youknowone merged 1 commit into
mainfrom
jit-type-name-attr-fold
Aug 7, 2026
Merged

jit: fold Cls.__name__, and admit a type receiver into the FOR_ITER inline#1097
youknowone merged 1 commit into
mainfrom
jit-type-name-attr-fold

Conversation

@youknowone

@youknowone youknowone commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Rebased onto origin/main (128590c). #1088 landed the instance half of the
FOR_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 fold
retires.

Cls.__name__ had no fold

try_walker_specialize_load_attr wants a mapdict instance, so a class receiver
fell through to the opaque getattr CALL_MAY_FORCE residual — 328 ns for a
top-level Derived.__name__, and far worse inside a callee, as below.

type.__name__ is a GetSetProperty on the metatype, i.e. a DATA descriptor,
so descr_getattribute (typeobject.py:814-819) hands the read to it before
the 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 pinned
to type itself — which is immutable, so the getter cannot be replaced — the
whole read is the w_name slot the getter returns:

guard_class(obj, W_TypeObject layout)      # is_type
guard_value(getfield(obj, w_class), type)  # the metaclass is `type`
getfield(obj, w_name) + guard_nonnull      # what the getter returns

Two deliberate differences from the classmethod fold beside it:

  • the class is not pinned. Nothing here depends on which class arrived, so
    a loop reading cls.__name__ over several classes keeps one trace.
  • the slot is read live, not baked. descr_set__name__ (typeobject.py:1046)
    replaces the name without going through mutated(), so the version tag does
    not move when a class is renamed — a version-pinned constant name would
    outlive the rename and be wrong.

w_type_peek_name_obj is the slot read without the lazy materialisation
w_type_get_name_obj performs: materialising means allocating, and the walker
is 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 the
receiver is not a type object. Its comment gives the reason: a type receiver's
read went through type.__getattribute__, reached the deferred abort path, and
that 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_supported and InlineBodyFacts::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-trace half reverted:

origin/main this branch
classmethod reading len(cls.__name__) 1349 ns 2.2
len(type(self).__name__) in a method 1379 ns 3.8
top-level len(Derived.__name__) 328 ns 136
bench/synth/classmethod_protocol_hot 1544 ns 99.2
def at(self, i): return self.v + i 6.4 ns 6.4 — #1088's

Wall-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_fold covers the fold's boundary rather than its speed: a
rename 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 plain type subclass — the last because the fold's
precondition is the metaclass, not what it happens to do).

parity_tests/type_name_attr_identity carries the name-object identity: a
str subclass assigned to __name__ comes back as that same object. CPython
preserves it and PyPy does not, and check.py compares synth output against
both, so it belongs on the CPython-only side.

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.

Verification

  • pyre/check.py --backend dynasm404/404
  • pyre/check.py --backend cranelift404/404
  • pyre/check.py --backend wasm — 399/400. The one failure,
    synth/exception_reused_object_tb_not_doubled
    (fbw_blackhole_adopted_single_frame 3 -> 0), is already failing on
    origin/main: reverting this branch's pyre-jit-trace half and rebuilding
    reproduces 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 -- --check
  • LLBC re-extracted before every measurement that a stale image could have
    fabricated

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ebcfe7ff-8c2f-4b01-83ae-2c555ab53867

📥 Commits

Reviewing files that changed from the base of the PR and between 128590c and bff00d8.

📒 Files selected for processing (18)
  • pyre/bench/synth/classmethod_protocol_hot.cranelift.jitstats
  • pyre/bench/synth/classmethod_protocol_hot.dynasm.jitstats
  • pyre/bench/synth/classmethod_protocol_hot.wasm.jitstats
  • pyre/bench/synth/type_metatype_method_call.cranelift.jitstats
  • pyre/bench/synth/type_metatype_method_call.dynasm.jitstats
  • pyre/bench/synth/type_metatype_method_call.wasm.jitstats
  • pyre/bench/synth/type_name_attr_fold.cranelift.jitstats
  • pyre/bench/synth/type_name_attr_fold.dynasm.jitstats
  • pyre/bench/synth/type_name_attr_fold.py
  • pyre/bench/synth/type_name_attr_fold.wasm.jitstats
  • pyre/extra_tests/parity_tests/type_name_attr_identity.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/pyjitcode.rs
  • pyre/pyre-object/src/typeobject.rs

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

❤️ Share

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/c8065595e83ce4ca82b1d18852cdbf5d01ee822f/pyre-jit-trace/src/jitcode_dispatch/specialize.rs#L3106
P1 Badge 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".

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit bff00d8).
Updated: 2026-08-07T08:56:39.123Z

Files in the reviewed diff
pyre/bench/synth/type_name_attr_fold.py
pyre/extra_tests/parity_tests/type_name_attr_identity.py
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-jit-trace/src/descr.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-jit-trace/src/pyjitcode.rs
pyre/pyre-object/src/typeobject.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs:3319 ↔ pypy/objspace/std/typeobject.py:1042 — the new JIT fold returns the cached w_name object directly. PyPy executes space.newtext(w_type.getname(space)), producing a fresh exact str; the fold therefore preserves object identity and a str subclass where PyPy does neither.

  • pyre/extra_tests/parity_tests/type_name_attr_identity.py:33 ↔ pypy/objspace/std/typeobject.py:1042 — the new test asserts Heap.__name__ is tagged and that its type remains Name. PyPy’s setter stores only space.text_w(w_value) (typeobject.py:1054-1058) and its getter returns space.newtext(...), so both assertions contradict PyPy.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-object/src/typeobject.rs:1359 ↔ pypy/objspace/std/typeobject.py:1042w_type_get_name_obj caches and repeatedly returns one object; PyPy allocates a new exact text object on each __name__ read.

  • pyre/pyre-object/src/typeobject.rs:1389 ↔ pypy/objspace/std/typeobject.py:1046 — pyre stores the assigned object in t.w_name; PyPy converts it to text and stores only w_type.name, discarding subclass identity and attached attributes.

  • pyre/pyre-interpreter/src/baseobjspace.rs:7498 ↔ pypy/objspace/std/typeobject.py:811 — pyre bypasses descriptor dispatch for every type object’s __name__. PyPy first dispatches through the metaclass, so a custom metaclass’s __getattribute__ or __name__ data descriptor can override the result.

4. Structural adaptations

None.

`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.
@youknowone
youknowone force-pushed the jit-type-name-attr-fold branch from c806559 to bff00d8 Compare August 7, 2026 07:53

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/bff00d8976904292eea979ea209d8e3c551aa9ec/pyre-interpreter/src/baseobjspace.rs#L9606-L9607
P2 Badge 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".

@youknowone youknowone changed the title jit: fold Cls.__name__, and inline a method body that reads an attribute jit: fold Cls.__name__, and admit a type receiver into the FOR_ITER inline Aug 7, 2026
@youknowone
youknowone merged commit d51ea7f into main Aug 7, 2026
15 of 17 checks passed
@youknowone
youknowone deleted the jit-type-name-attr-fold branch August 7, 2026 12:35
youknowone added a commit that referenced this pull request Aug 7, 2026
…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
youknowone added a commit that referenced this pull request Aug 7, 2026
…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)
youknowone added a commit that referenced this pull request Aug 7, 2026
…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
youknowone added a commit that referenced this pull request Aug 8, 2026
…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)
youknowone added a commit that referenced this pull request Aug 8, 2026
…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
youknowone added a commit that referenced this pull request Aug 8, 2026
`pypy_type_surface` reads bridges_compiled=5 and guard_failures=1011 on all
three backends, the figures #999 `dad2a722907` recorded. #1097 `d51ea7f32a0`
moved them to 102/20497 by folding `Cls.__name__` across a `getset_descriptor`
whose `w_class` slot is null, and #1107 `9614f37fb1e` declined that fold again;
#1107's own message records the fixture reading 11/5/0/1011 afterwards. #1086
`e5eff816849` sits between the two and wrote the regressed pair in as the
baseline, so the restored reading has failed the gate on every runner since.

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

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant