Skip to content

typedef: method-wrapper and classmethod_descriptor types, plus jit-stats baselines - #970

Merged
youknowone merged 4 commits into
mainfrom
agent/stdlib-foundations
Aug 2, 2026
Merged

youknowone merged 4 commits into
mainfrom
agent/stdlib-foundations

Conversation

@youknowone

@youknowone youknowone commented Aug 2, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up work on top of #845.

jit: exempt the load-global namespace operand from the NULL-Ref walk abort

pickle_terminal_raise_resume produced wrong code. The residual-call walk
aborted on a NULL Ref operand that a LOAD_GLOBAL namespace slot legitimately
carries.

typedef: add method-wrapper and classmethod_descriptor types

method_descriptor_kind.py reported (list, '__len__', method_descriptor)
where the descriptor is a wrapper_descriptor. Gating the second retag sweep on
gateway::is_slot_wrapper needs a bound form for the retagged
wrapper_descriptor, so both missing types are added:

type payload published via
method-wrapper the Method a bound method descriptor uses w_method_set_public_class in function::method_wrapper_new
classmethod_descriptor the ClassMethod a @classmethod uses w_class write in function::classmethod_retag_descriptor

Neither writes ob_type, so py_type_check predicates (is_method,
is_classmethod) and every binding path are unaffected — only typedef::r#type,
which prefers w_class, sees the new type.

  • classmethod_descr_get picks builtin_bound_method_new for a
    classmethod_descriptor and w_method_new otherwise, so dict.fromkeys is a
    builtin_function_or_method while a user @classmethod binds to method.
  • is_bound_builtin_method widens from "wraps a method_descriptor" to
    "published as builtin_function_or_method" — the classmethod case wraps a
    plain builtin, so the old test rejected it and repr(dict.fromkeys) raised.
  • wrapper_descriptor and method_descriptor get __reduce__
    (getattr(objclass, name)); copyreg._reduce_ex refuses them otherwise.
    classmethod_descriptor deliberately gets none — pickling one raises
    "cannot pickle 'classmethod_descriptor' object".
  • builtins::is_native_exception_dunder guarded on is_function, which excludes
    SLOT_WRAPPER_TYPE; once __str__/__repr__ retag, the anti-recursion guard
    stopped firing and str(SyntaxError("bad")) overflowed the native stack. It
    now guards on is_function_carrier.

The classmethod_descriptor retag is two-phase: init_typeobjects runs the
sweeps inside TYPEOBJECT_CACHE.get_or_init, where gettypeobject returns
nothing, so the type is registered with set_instantiate at creation, the retag
reads get_instantiate and no-ops on null, and an idempotent closing pass over
reg.values() covers the namespaces built earlier (object, type, int,
dict, mappingproxy).

lib-python/3/inspect.py returns to pristine CPython 3.14 for ismethod,
isbuiltin, ismethodwrapper, ismethoddescriptor, _NonUserDefinedCallables
and _signature_get_user_defined_method. _slot_wrapper_type and the
_builtin_code_type block are deleted — dict.update.__code__ stops resolving
after the split, so the isfunction override it fed was already dead.

bench: record the seven missing jit-stats baselines

21 files for hot_loop_exit_then_class_stmt, inline_freevar_after_mayforce,
math_log_trig_hot, pickle_terminal_raise_resume, pypy_dict_primitives_nonbinding,
raise_reg_unbound_jitstress and tuple_unpack_array_backed_hot on all three
backends. All seven were inherited without a baseline.

Two nonzero badness values are pinned as-is: raise_reg_unbound_jitstress
loops_aborted=2 on every backend, and pickle_terminal_raise_resume
loops_aborted=54 on wasm against 0 on both native backends — unexplained, and
the baseline only pins it against a rise.

bench: re-record the four caught-in-callee jit-stats baselines

float_div_zero_caught_loop, mutate_then_raise_caught,
inline_subwalk_property_mutates and inline_subwalk_mutating_residual. The cause
is the try_walker_inline_resolved_user_call decline for a branchy callee with
its own exception handler when fbw_callee_body_replay_safety is not Clean —
reverting that hunk alone puts all four back on the old numbers exactly.

The re-record raises no gated badness field:

field movement gate class
guard_failures 402→611, 401→611, 401→611, 603→813 rise-bounded (what failed)
bridges_compiled +1 each ungated
loops_aborted 1→0, 2→1, 2→0, unchanged badness — falls
loops_compiled 1→2, 3→4 where it moves fall-gated — rises

Gates

pyre/check.py --backend dynasm: 5 failed, 361 passed (16 failed, 350 passed
before this branch).

The five are left red on purpose — closure_freevar_branch_resume,
exception_args_virtual, exception_multi_handler_warmup,
exception_reraise_tb_depth_jitstress and list_length_hint_validate raise
loops_aborted and lower loops_compiled with no attributed cause, and
re-recording them would disarm the floor.

pyre/check.py --backend cranelift: 7 failed, 359 passed — the same five plus
two perf-ratio rows that are the pypy-startup clamp, not a regression:

bench full suite isolated ×2 gate
jit_reg_const_pool_256_slot_decline 131.2x (exec 0.66s) 50.8x / 53.1x (exec 0.70s) 122x
recursion_memo_branch 24.8x (exec 0.12s) 10.7x / 11.4x (exec 0.19s) 20x

0.66/131.2 = 0.00503 and 0.12/24.8 = 0.00484 are both EXEC_TIME_FLOOR_S, so
the pypy denominator was clamped and the ratio is pyre_exec / 5ms. The isolated
pyre times are higher while the ratios fall.

extra_tests/parity_tests/run.py: green on cpython, dynasm and cranelift.
cpython_tests/run.py --backend dynasm: 46 pass, no regressions.
test_inspect: 362/362.

Descriptor kinds and bound/unbound type pairs were diffed against a CPython
3.14 oracle for object.__init_subclass__, object.__subclasshook__,
type.__prepare__, int.from_bytes, dict.fromkeys, dict.__class_getitem__,
float.fromhex, bytes.fromhex and set.__class_getitem__.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added CPython-compatible method-wrapper and classmethod-descriptor behavior, including binding, invocation, representation, comparison, hashing, and attribute access.
    • Improved handling of built-in slot wrappers and class methods.
  • Bug Fixes

    • Prevented recursive dispatch when formatting native exception representations.
    • Improved introspection of callable types and signatures.
    • Fixed nested global lookups involving nullable namespaces.
  • Tests

    • Expanded JIT benchmark coverage and refreshed performance statistics across multiple execution paths.

…abort

`bh_load_global_fn(namespace_ptr, w_code, frame, namei)` discards
`namespace_ptr` and resolves the namespace from the executing frame or the
callee's own promoted `w_code`; the operand survives only as the cell-fold
recogniser's hint.  A nested function's `LOAD_GLOBAL` folds it to the NULL
constant, and both NULL-Ref-arg refusals treated that as the broken baked-NULL
self-slot shape.

`walker_abort_if_mayforce_null_ref_arg` then aborted the walk after an
effectful residual had already run concretely, so the caller replayed the
region: a callee that stores a residual result into a local and then raises
(`value = self.stack.pop(); raise _Stop(value)`) ran its `pop` twice.

Add the arg-0 exemption to both refusals, which the comments already require
to match.

synth/pickle_terminal_raise_resume prints `checksum = 216` on dynasm,
cranelift, PYRE_JIT=0, pypy3 and cpython; it crashed with
`IndexError: pop from empty list` on all three pyre arms before.

Assisted-by: Claude
Registers two descriptor types the namespace sweeps did not model:

- `method-wrapper`, the bound form of a `wrapper_descriptor`, produced by
  `bind_slot_wrapper` and `baseobjspace::get`.  Both retag sweeps now route
  through `gateway::is_slot_wrapper`, so a slot dunder becomes a
  `wrapper_descriptor` and its `__get__` a `method-wrapper`, while a
  `tp_methods` entry stays a `method_descriptor` binding to
  `builtin_function_or_method`.
- `classmethod_descriptor`, a builtin type's `METH_CLASS` entry.  The payload
  stays the `ClassMethod` a `@classmethod` uses and only the published class
  differs; `classmethod_descr_get` binds it through `builtin_bound_method_new`.
  Types built before that type object exists are retagged by a closing pass of
  `init_typeobjects`.

`is_bound_builtin_method` now tests the published class, which both bound
forms share.  `wrapper_descriptor` and `method_descriptor` gain the
`__reduce__` that `copyreg._reduce_ex` needs.  `is_native_exception_dunder`
accepts every fixed-code carrier, since `__str__` and `__repr__` are now
`wrapper_descriptor`.

`lib-python/3/inspect.py` drops the adaptations these types replace:
`ismethod`, `isbuiltin`, `ismethodwrapper`, `ismethoddescriptor`,
`_NonUserDefinedCallables`, `_signature_get_user_defined_method`, and the
`_slot_wrapper_type` / `_builtin_code_type` discriminators.
`dict.update.__code__` no longer resolves, so the latter was already None.

Assisted-by: Claude
`check.py`'s floor hard-fails a synthetic benchmark that emits a
`[jit-stats]` line without a committed baseline.  Seven had none on any
backend: hot_loop_exit_then_class_stmt, inline_freevar_after_mayforce,
math_log_trig_hot, pickle_terminal_raise_resume,
pypy_dict_primitives_nonbinding, raise_reg_unbound_jitstress and
tuple_unpack_array_backed_hot.  Recorded with
`check.py --snapshot --synthetic-only --synthetic-pattern <name>.py
--backend dynasm,cranelift,wasm`; all three backends passed for all seven.

Two baselines pin a nonzero badness field:

- `raise_reg_unbound_jitstress` loops_aborted=2, identical on all three
  backends.
- `pickle_terminal_raise_resume` loops_aborted=54 on wasm against 0 on both
  native backends.  The value is what the wasm backend does today; the
  divergence is unexplained and the baseline only pins it against a rise.

Assisted-by: Claude
float_div_zero_caught_loop, mutate_then_raise_caught,
inline_subwalk_property_mutates and inline_subwalk_mutating_residual all
fail the `guard_failures` bounded-rise gate by a constant +209/210 with
exactly one extra bridge each.  Attributed to the
`try_walker_inline_resolved_user_call` arm in
`pyre-jit-trace/src/jitcode_dispatch/inline_call.rs` that declines to inline
a branchy callee owning an exception handler when
`fbw_callee_body_replay_safety` is not `Clean`: re-entering the opcode that
owns the abort jitcode pc would repeat its residual effect, because the
callee-rebuild payload cannot carry a post-op stack anchor.  Reverting that
hunk alone restores the old counters exactly, so the rise is the guard's
price and the guard stays.

Recorded on all three backends.  No `JITSTATS_BADNESS_FIELDS` value rises:
`loops_aborted` falls on every affected fixture (1 -> 0, 2 -> 1, 2 -> 0) and
`loops_compiled` rises where it moves.

The five remaining jit-stats failures are left alone —
closure_freevar_branch_resume, exception_args_virtual,
exception_multi_handler_warmup, exception_reraise_tb_depth_jitstress and
list_length_hint_validate raise `loops_aborted` and lower `loops_compiled`
with no attributed cause, and re-recording them would disarm the floor.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds CPython-compatible method-wrapper and classmethod-descriptor behavior, simplifies callable inspection, permits NULL LoadGlobal namespace hints during residual calls, and updates synthetic JIT statistics.

Changes

Descriptor compatibility

Layer / File(s) Summary
Descriptor types and retagging
pyre/pyre-interpreter/src/function.rs, pyre/pyre-interpreter/src/typedef.rs
Registers public method-wrapper and classmethod_descriptor types and retags builtin carriers.
Bound wrapper construction and invocation
pyre/pyre-interpreter/src/baseobjspace.rs, pyre/pyre-interpreter/src/function.rs, pyre/pyre-interpreter/src/typedef.rs, pyre/pyre-interpreter/src/builtins.rs
Adds bound wrapper construction, carrier classification, wrapper behavior, and native dunder handling.
Descriptor protocols and reduction
pyre/pyre-interpreter/src/typedef.rs
Implements descriptor access, calls, metadata, comparison, hashing, and reduction behavior.

Callable classification

Layer / File(s) Summary
Standard callable predicates
lib-python/3/inspect.py
Uses standard types predicates for method, descriptor, function, builtin, and method-wrapper checks.
Signature callable gate
lib-python/3/inspect.py
Uses one non-user-defined callable type gate for signature discovery.

Synthetic JIT statistics

Layer / File(s) Summary
Synthetic benchmark baselines
pyre/bench/synth/*.jitstats
Adds or updates Cranelift, DynASM, and WebAssembly JIT counters across synthetic benchmarks.

LoadGlobal NULL handling

Layer / File(s) Summary
LoadGlobal namespace exemption
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
Skips NULL validation for the first reference argument of LoadGlobal helpers during validation and executor dispatch.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BuiltinTypeSetup
  participant DescriptorCarriers
  participant BoundMethod
  BuiltinTypeSetup->>DescriptorCarriers: register and retag method-wrapper carriers
  DescriptorCarriers->>BoundMethod: construct bound slot-wrapper method
  BoundMethod->>BoundMethod: validate receiver and invoke wrapped slot
Loading

Possibly related PRs

Poem

A rabbit checked each wrapper’s name,
Then bounded through descriptors’ frame.
NULL hints passed without a fright,
JIT counters logged through the night.
“Hop,” said the hare, “the types align!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary descriptor-type additions and the related JIT-statistics baseline updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/stdlib-foundations

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.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit eeecf68).
Updated: 2026-08-02T04:05:02.878Z

Files in the reviewed diff
lib-python/3/inspect.py
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/function.rs
pyre/pyre-interpreter/src/typedef.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

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

None.

4. Structural adaptations

  • pyre/pyre-interpreter/src/function.rs:689 ↔ pypy/interpreter/function.py:658 — Pyre adds a CPython-visible method-wrapper class while retaining PyPy’s shared Method payload. This is a Rust/CPython surface adaptation, not a semantic divergence.

  • pyre/pyre-interpreter/src/typedef.rs:2001 ↔ pypy/interpreter/typedef.py:910 — Pyre splits fixed-code carriers into CPython method_descriptor, wrapper_descriptor, and classmethod_descriptor public types; PyPy exposes its corresponding objects through FunctionWithFixedCode, BuiltinFunction, and ClassMethod.

  • lib-python/3/inspect.py:1911 ↔ pypy/interpreter/function.py:783 — Restoring CPython’s _NonUserDefinedCallables type gate depends on the new concrete descriptor types. PyPy’s broader shared function carriers required the removed workaround; the new public types make the standard-library logic applicable again.

  • pyre/pyre-interpreter/src/typedef.rs:14715 ↔ pypy/interpreter/function.py:743 — Pyre directly binds a classmethod’s stored callable, matching the selected CPython 3.14 behavior; PyPy delegates through space.get, preserving nested-descriptor behavior. This is an explicitly documented Python-version adaptation.

  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:1693 ↔ pypy/interpreter/pyopcode.py:975 — Pyre exempts the ignored namespace_ptr NULL sentinel in its residual-call walker. PyPy performs LOAD_GLOBAL directly from the live frame and has no corresponding residual ABI; this is required by Pyre’s generated-JIT/frame-aliasing implementation.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eeecf68f34

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +1710 to +1711
if is_load_global && i == 0 {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the callee frame for LOAD_GLOBAL

When an inlined callee's namespace operand is NULL—the exact case this branch exempts—the residual executes even though its frame operand aliases an outer frame. bh_load_global_fn ignores the namespace operand, falls back to the code object's first-stamped globals, and still obtains builtins from the aliased outer frame; consequently, a shared code object used with distinct function globals or a cross-module callee with custom __builtins__ can resolve LOAD_GLOBAL from the wrong namespace after JIT compilation. Thread the callee's red frame/namespace instead of whitelisting the collapsed operand.

AGENTS.md reference: AGENTS.md:L32-L42

Useful? React with 👍 / 👎.

Comment on lines +13373 to +13374
match positional.get(1) {
Some(&cls) if pyre_object::is_type(cls) => {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require the descriptor owner to be a subtype

Checking only that cls is a type accepts unrelated classes, although a classmethod_descriptor requires a subtype of its defining owner. For example, dict.__dict__['fromkeys'](list, ['x']) is forwarded into dict_fromkeys_impl with list as cls rather than raising the required TypeError, potentially producing an unrelated container; explicit __get__ has the same missing validation. Check issubtype_w(cls, __objclass__) before binding or calling the wrapped function.

AGENTS.md reference: AGENTS.md:L195-L195

Useful? React with 👍 / 👎.

Comment on lines +13352 to +13358
make_builtin_function_with_arity(
"__get__",
|args| {
classmethod_descriptor_function(args[0], "__get__")?;
classmethod_descr_get(args)
},
3,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept an omitted owner in descriptor get

classmethod_descriptor.__get__ has the CPython signature __get__(instance, owner=None), so calls such as dict.__dict__['fromkeys'].__get__({}) must infer dict from the instance and bind successfully. Registering this implementation with fixed arity 3 rejects the two-argument raw call before classmethod_descr_get can exercise its existing missing-owner fallback; use an optional/variadic signature instead.

AGENTS.md reference: AGENTS.md:L195-L195

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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-interpreter/src/typedef.rs`:
- Around line 13288-13300: Update classmethod_descriptor_function to report
receiver mismatches using CPython’s standard descriptor text, including the
descriptor name, expected classmethod_descriptor type, and received object type.
Match the formatting used by method_wrapper_receiver and
method_descriptor_receiver, while preserving the existing successful return
path.
- Around line 13361-13390: Update the __call__ implementation registered for
classmethod_descriptor to validate that argument 2 is not only a type but also a
subtype of the descriptor’s d_type, using crate::baseobjspace::issubtype_w.
Preserve the existing missing-argument and non-type errors, and reject unrelated
types before invoking function_descr_call_impl.
- Around line 12759-12770: Update descr_reduce to root the owner obtained from
fget_func_objclass before allocating the name string, then root the allocated
name before constructing the nested argument tuple. Reload both rooted values
after each allocation-triggering call before passing them to w_tuple_new,
preserving the existing getattr result structure.
🪄 Autofix (Beta)

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: 7e2b3d90-ec8a-4943-a044-409c0c91d712

📥 Commits

Reviewing files that changed from the base of the PR and between c632d4f and eeecf68.

📒 Files selected for processing (39)
  • lib-python/3/inspect.py
  • pyre/bench/synth/float_div_zero_caught_loop.cranelift.jitstats
  • pyre/bench/synth/float_div_zero_caught_loop.dynasm.jitstats
  • pyre/bench/synth/float_div_zero_caught_loop.wasm.jitstats
  • pyre/bench/synth/hot_loop_exit_then_class_stmt.cranelift.jitstats
  • pyre/bench/synth/hot_loop_exit_then_class_stmt.dynasm.jitstats
  • pyre/bench/synth/hot_loop_exit_then_class_stmt.wasm.jitstats
  • pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats
  • pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats
  • pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats
  • pyre/bench/synth/inline_subwalk_mutating_residual.cranelift.jitstats
  • pyre/bench/synth/inline_subwalk_mutating_residual.dynasm.jitstats
  • pyre/bench/synth/inline_subwalk_mutating_residual.wasm.jitstats
  • pyre/bench/synth/inline_subwalk_property_mutates.cranelift.jitstats
  • pyre/bench/synth/inline_subwalk_property_mutates.dynasm.jitstats
  • pyre/bench/synth/inline_subwalk_property_mutates.wasm.jitstats
  • pyre/bench/synth/math_log_trig_hot.cranelift.jitstats
  • pyre/bench/synth/math_log_trig_hot.dynasm.jitstats
  • pyre/bench/synth/math_log_trig_hot.wasm.jitstats
  • pyre/bench/synth/mutate_then_raise_caught.cranelift.jitstats
  • pyre/bench/synth/mutate_then_raise_caught.dynasm.jitstats
  • pyre/bench/synth/mutate_then_raise_caught.wasm.jitstats
  • pyre/bench/synth/pickle_terminal_raise_resume.cranelift.jitstats
  • pyre/bench/synth/pickle_terminal_raise_resume.dynasm.jitstats
  • pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats
  • pyre/bench/synth/pypy_dict_primitives_nonbinding.cranelift.jitstats
  • pyre/bench/synth/pypy_dict_primitives_nonbinding.dynasm.jitstats
  • pyre/bench/synth/pypy_dict_primitives_nonbinding.wasm.jitstats
  • pyre/bench/synth/raise_reg_unbound_jitstress.cranelift.jitstats
  • pyre/bench/synth/raise_reg_unbound_jitstress.dynasm.jitstats
  • pyre/bench/synth/raise_reg_unbound_jitstress.wasm.jitstats
  • pyre/bench/synth/tuple_unpack_array_backed_hot.cranelift.jitstats
  • pyre/bench/synth/tuple_unpack_array_backed_hot.dynasm.jitstats
  • pyre/bench/synth/tuple_unpack_array_backed_hot.wasm.jitstats
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs

Comment on lines +12759 to +12770
/// `descrobject.c descr_reduce` — a descriptor reconstructs through
/// `getattr(objclass, name)`. Shared by `wrapper_descriptor` and
/// `method_descriptor`; without it `copyreg._reduce_ex` refuses the object at
/// protocols 0 and 1.
fn descr_reduce(descr: PyObjectRef) -> crate::PyResult {
let owner = unsafe { crate::function::fget_func_objclass(descr)? };
let name = pyre_object::w_str_new(unsafe { crate::function::function_get_name(descr) });
Ok(pyre_object::w_tuple_new(vec![
crate::baseobjspace::builtin_callable("getattr"),
pyre_object::w_tuple_new(vec![owner, name]),
]))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the `builtin_callable` symbol used by `descr_reduce`.
rg -nP -C4 '\bfn\s+builtin_callable\s*\(' pyre/pyre-interpreter/src

# Confirm the shadow-stack rooting helpers and the allocation behaviour of the callees.
rg -nP -C3 '\bpub fn (w_str_new|w_tuple_new)\s*\(' pyre/pyre-object/src
rg -nP -C3 '\bpub fn (push_roots|pin_root|shadow_stack_get|shadow_stack_len)\s*\(' pyre/pyre-object/src/gc_roots.rs

Repository: youknowone/pyre

Length of output: 4656


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== baseobjspace builtin_callable =="
sed -n '2175,2220p' pyre/pyre-interpreter/src/baseobjspace.rs

echo "== typedef descr_reduce and nearby stamp_method_rooting docs/usages =="
sed -n '1945,1975p' pyre/pyre-interpreter/src/typedef.rs
sed -n '12745,12775p' pyre/pyre-interpreter/src/typedef.rs

echo "== gc_roots relevant API =="
sed -n '220,320p' pyre/pyre-object/src/gc_roots.rs
sed -n '360,410p' pyre/pyre-object/src/gc_roots.rs

echo "== tuple allocator =="
sed -n '165,205p' pyre/pyre-object/src/tupleobject.rs

echo "== unicode allocator quick scan =="
sed -n '1,100p' pyre/pyre-object/src/unicodeobject.rs

Repository: youknowone/pyre

Length of output: 18794


Root descriptor fields before allocations in descr_reduce.

fget_func_objclass(descr) produces an object field, then w_str_new and w_tuple_new may run GC. Store these values in roots before the allocation calls and read them back before constructing the result tuple. Also root the allocated string before passing it into the nested tuple so the collector can relocate both fields across the GC malloc on the collection path.

🤖 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-interpreter/src/typedef.rs` around lines 12759 - 12770, Update
descr_reduce to root the owner obtained from fget_func_objclass before
allocating the name string, then root the allocated name before constructing the
nested argument tuple. Reload both rooted values after each
allocation-triggering call before passing them to w_tuple_new, preserving the
existing getattr result structure.

Comment on lines +13288 to +13300
/// The wrapped builtin callable of a `classmethod_descriptor`, which carries
/// the owner, name, doc and signature the descriptor publishes.
fn classmethod_descriptor_function(
obj: PyObjectRef,
name: &str,
) -> Result<PyObjectRef, crate::PyError> {
if !crate::function::is_classmethod_descriptor(obj) {
return Err(crate::PyError::type_error(format!(
"descriptor '{name}' requires a 'classmethod_descriptor' object"
)));
}
Ok(unsafe { pyre_object::function::w_classmethod_get_func(obj) })
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the receiver-mismatch message with CPython.

CPython raises the standard descriptor mismatch text for these getsets, for example descriptor '__objclass__' for 'classmethod_descriptor' objects doesn't apply to a 'int' object. The current message drops the received type and uses a different wording. The sibling helpers method_wrapper_receiver (Line 12971) and method_descriptor_receiver already produce the CPython form, so this helper is the only divergence.

🐛 Proposed fix
 fn classmethod_descriptor_function(
     obj: PyObjectRef,
     name: &str,
 ) -> Result<PyObjectRef, crate::PyError> {
     if !crate::function::is_classmethod_descriptor(obj) {
+        let received = type_name_of(obj);
         return Err(crate::PyError::type_error(format!(
-            "descriptor '{name}' requires a 'classmethod_descriptor' object"
+            "descriptor '{name}' for 'classmethod_descriptor' objects \
+             doesn't apply to a '{received}' object"
         )));
     }
     Ok(unsafe { pyre_object::function::w_classmethod_get_func(obj) })
 }
📝 Committable suggestion

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

Suggested change
/// The wrapped builtin callable of a `classmethod_descriptor`, which carries
/// the owner, name, doc and signature the descriptor publishes.
fn classmethod_descriptor_function(
obj: PyObjectRef,
name: &str,
) -> Result<PyObjectRef, crate::PyError> {
if !crate::function::is_classmethod_descriptor(obj) {
return Err(crate::PyError::type_error(format!(
"descriptor '{name}' requires a 'classmethod_descriptor' object"
)));
}
Ok(unsafe { pyre_object::function::w_classmethod_get_func(obj) })
}
/// The wrapped builtin callable of a `classmethod_descriptor`, which carries
/// the owner, name, doc and signature the descriptor publishes.
fn classmethod_descriptor_function(
obj: PyObjectRef,
name: &str,
) -> Result<PyObjectRef, crate::PyError> {
if !crate::function::is_classmethod_descriptor(obj) {
let received = type_name_of(obj);
return Err(crate::PyError::type_error(format!(
"descriptor '{name}' for 'classmethod_descriptor' objects \
doesn't apply to a '{received}' object"
)));
}
Ok(unsafe { pyre_object::function::w_classmethod_get_func(obj) })
}
🤖 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-interpreter/src/typedef.rs` around lines 13288 - 13300, Update
classmethod_descriptor_function to report receiver mismatches using CPython’s
standard descriptor text, including the descriptor name, expected
classmethod_descriptor type, and received object type. Match the formatting used
by method_wrapper_receiver and method_descriptor_receiver, while preserving the
existing successful return path.

Comment on lines +13361 to +13390
pyre_object::w_dict_setitem_str_no_proxy(
ns,
"__call__",
make_builtin_function("__call__", |args| {
let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args);
let descr = positional.first().copied().unwrap_or(pyre_object::PY_NULL);
let function = classmethod_descriptor_function(descr, "__call__")?;
// `descrobject.c classmethoddescr_call` — the receiver is the
// class itself, so it must be a type rather than an instance.
let name = crate::function::function_get_name(function);
let owner_name =
pyre_object::w_type_get_name(crate::function::fget_func_objclass(function)?);
match positional.get(1) {
Some(&cls) if pyre_object::is_type(cls) => {}
Some(&cls) => {
let got = type_name_of(cls);
return Err(crate::PyError::type_error(format!(
"descriptor '{name}' for type '{owner_name}' needs a type, \
not a '{got}' as arg 2"
)));
}
None => {
return Err(crate::PyError::type_error(format!(
"descriptor '{name}' of '{owner_name}' object needs an argument"
)));
}
}
function_descr_call_impl(positional, kwargs, function)
}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the subtype predicate used elsewhere for descriptor receiver checks.
rg -nP -C3 '\bfn\s+(issubtype_w|abstract_issubclass_w|issubclass_w)\s*\(' pyre/pyre-interpreter/src

# Compare with the instance-side descriptor checks already implemented.
rg -nP -C8 '\bfn\s+(method_descriptor_check_instance|slot_wrapper_check_instance)\s*\(' pyre/pyre-interpreter/src/typedef.rs

Repository: youknowone/pyre

Length of output: 2192


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the classmethod descriptor call implementation and nearby helpers.
sed -n '13320,13415p' pyre/pyre-interpreter/src/typedef.rs
printf '\n--- related checks ---\n'
sed -n '12700,12910p' pyre/pyre-interpreter/src/typedef.rs
printf '\n--- issubtype definition ---\n'
sed -n '355,390p' pyre/pyre-interpreter/src/baseobjspace.rs

Repository: youknowone/pyre

Length of output: 14143


Add the missing subtype check in classmethod_descriptor.__call__.

classmethod_descriptor.__call__ currently checks only that argument 2 exists and is a type. classmethoddescr_call also requires that the argument be a subtype of the descriptor’s d_type; without it, dict.__dict__['fromkeys'](int, [1]) can enter the dict implementation with an unrelated class. Use crate::baseobjspace::issubtype_w for this check.

🤖 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-interpreter/src/typedef.rs` around lines 13361 - 13390, Update the
__call__ implementation registered for classmethod_descriptor to validate that
argument 2 is not only a type but also a subtype of the descriptor’s d_type,
using crate::baseobjspace::issubtype_w. Preserve the existing missing-argument
and non-type errors, and reject unrelated types before invoking
function_descr_call_impl.

Source: Coding guidelines

@youknowone
youknowone merged commit 6285cb8 into main Aug 2, 2026
16 of 19 checks passed
@youknowone
youknowone deleted the agent/stdlib-foundations branch August 2, 2026 07:51
youknowone added a commit that referenced this pull request Aug 2, 2026
…line

Its committed baseline is `loops_aborted=3 loops_compiled=6
guard_failures=23380 bridges_compiled=0`. It measures `0 / 3 / 606 / 3`,
identically on dynasm and cranelift, and `loops_aborted=0` on wasm.

check.py failed it on `loops_compiled 6 -> 3`, a `JITSTATS_FALL_FIELDS`
gate. That fall is not a capability loss: `loops_compiled +
bridges_compiled` is 6 on both sides, so the three units it stopped
compiling as separate loops are the three it now compiles as bridges,
while the aborts go to 0 and the guard failures fall 38x.

Every gated field therefore moves the tightening way — `loops_aborted` and
`guard_failures` down, and the fall gate now pinned at 3 rather than 6.
Only `bridges_compiled` rises, and it is in no gated group.

Six further fixtures had no baseline at all and were recorded here before
the rebase; that half is dropped because #970 has since recorded all six on
main. Where the two recordings disagreed — `raise_reg_unbound_jitstress`,
`loops_aborted` 2 vs 10 and `loops_compiled` 5 vs 3 — main's is both the
authoritative side and the tighter gate, and a build at this base
reproduces main's numbers exactly.

Four fixtures are still left red on purpose: `exception_args_virtual`
(`loops_aborted` 0 -> 3), `exception_multi_handler_warmup` (0 -> 23),
`exception_reraise_tb_depth_jitstress` (0 -> 1198, `loops_compiled`
805 -> 305) and `list_length_hint_validate` (14 -> 34). These raise a
`JITSTATS_BADNESS_FIELDS` counter whose healthy value is 0, so recording
them would switch the floor off for those fixtures permanently. They
reproduce byte-identically on dynasm and cranelift, so the drift is
walker-level, and they are unchanged across #967's IS_OP fold and #971's
front-lowering folds.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 2, 2026
…ed base/top interpreter root stack (#966)

* macros: restore #[inline(never)] on the elidable and may-force surfaces

Reverts eb59bf3. That commit removed #[inline(never)] from #[elidable],
#[elidable_promote], the #[look_inside_iff] trampoline, #[jit_may_force] and
#[loop_invariant] on the grounds that the upstream decorators set only their
own flag (rlib/jit.py:72 sets _elidable_function_ and nothing else) and leave
the C backend's inliner free, while _dont_inline_ (objectmodel.py:214, "not the
JIT!") is a separate flag none of them carry.

The flag reading is correct, but the inliner it leaves free is not LLVM's.
auto_inlining stops a graph at weight >= threshold (backendopt/inline.py:654)
with weight = 0.9999 * measure_median_execution_cost + static_instruction_count
(:539 inlining_heuristic, hard reject at count >= 200) and roughly one weight
per lltype op (:479 OP_WEIGHTS), so a straight-line graph is charged about
twice its op count. The default DEFL_INLINE_THRESHOLD is 32.4, described at
config/translationoption.py:11-12 as "just enough to inline add__Int_Int() and
just small enough to prevent inlining of some rlist functions" -- on the order
of sixteen operations. 54 of the 124 elidable sites are in descroperation.rs
and 25 in longobject.rs; those graphs are far past that budget, so removing the
attribute hands them to a cost model upstream's free inliner would not have
applied.

Nothing measured argues the other way. Throughput was flat (1.0070 / 1.0000 on
the shadow-stack probes, 1.0000 on a pure integer control) and the bodies
inlining into the eval loop cost frame size: reachable Python recursion depth
fell 167700 to 159700 through the SP-based stack_check.rs current_sp().

eb59bf3's CI run also lost five macOS perf-ratio gates, which this commit
originally claimed as evidence. That attribution was wrong: f947413 raised
check.py's STARTUP_SAMPLES from 3 to 5 for exactly that failure mode -- a high
startup sample collapsing the exec-time denominator of a short bench -- and
macOS check.py is green on this branch with eb59bf3 still in it. The
argument here rests on the inlining budget, not on those gates.

#[dont_look_inside] keeps the removal from c29a31b: those bodies are inside
the budget -- gc_roots::shadow_stack_len is one call,
pyopcode::label_arg_to_usize one field read -- which is also why they fold
under /OPT:ICF. Of the three folded pairs 10b45ac names,
label_arg_to_usize / load_fast_var_num_to_index dissolves here because the
latter is #[elidable_cannot_raise]; convert_value_arg / special_method_arg and
hash_str_hooked_bytes / hash_str_hooked remain, so the last-write-wins insert
stays load-bearing.

Assisted-by: Claude

* gc_roots: give the interpreter root stack a fixed base/top sized off the recursion limit

`push_roots`/`pop_roots`'s thread-local shadow stack was a `Vec<PyObjectRef>`.
RPython's is a raw buffer of `root_stack_depth` slots addressed by
`root_stack_base` / `root_stack_top` (shadowstack.py:75-88), raw_malloc'd once
by `_prepare_unused_stack` (:344-349) and replaced only by
`increase_root_stack_depth` (:351-364). majit-gc's jitframe root stack already
carries that shape, from the same 163840 default.

Port the interpreter's stack to it: `RootStack{base, top, limit}` in `Cell`s,
`incr_stack` returning the slot it claimed, `truncate` rewinding `top`, and
`grow` copying the used portion into a larger raw allocation. Callers hold slot
indices rather than slot addresses across a push, so a grow is transparent to
them; `capture_shadow_stack_area` hands out the address of the `RootStack` cell
and not of its buffer, and `walk_shadow_stack_cell` re-reads `base` every
iteration, so a re-entrant push that grows cannot strand a walk in the freed
buffer.

The container is the point. A `Vec` puts drop glue on the thread-local, and a
thread-local carrying a destructor pays a registration-state check on every
access -- `pin_root` and `shadow_stack_get` sit on the interpreter's allocation
and call paths. User CPU against this branch minus this commit, min of 15
interleaved runs:

  gc22_any.py      .8888
  gc22_all.py      .8811
  gc22_control.py  .9722   list sort; roots too, so not a control
  gc36_pure_arith  1.0000  pure integer control

The buffer is released at thread exit by a `RootStackOwner` in its own
thread-local key, touched only from `grow`, so the key on the push path keeps
no destructor of its own.

sys.setrecursionlimit now sizes this stack the way pypy/module/sys/vm.py:97
sizes upstream's: `increase_root_stack_depth(int(new_limit * 0.001 * 163840))`.
stack_check.rs already did that for the jitframe stack; both root stacks now
follow the limit.

`RootScope` keeps caching the resolved cell for the bracket's lifetime, as
`*mut Vec<PyObjectRef>`. It names the cell and not its buffer, so a `grow`
under an open bracket leaves it valid.

Unlike upstream's unchecked `incr_stack`, the push tests `limit` and grows on
reaching it. pyre pins one slot per call argument as well as per livevar, so a
single variadic call can outrun the headroom `root_stack_depth` reserves per
recursion level, and the native stack check upstream relies on would not fire
first.

Also corrects two comments this branch left stale: the `RootStack` rename kept
naming `SHADOW_STACK` in five doc comments (one of them an intra-doc link that
no longer resolves), and the elidable site total quoted in `majit-macros` was
124 where the expansion covers 126 non-test sites.

Assisted-by: Claude

* gc_roots: let shadow_stack_copy_range take an empty range at the top of the stack

The `RootStack` conversion routed the copy's source through `slot(base)`,
whose bounds check is `index < len`. The range check above it admits
`base == len` when `dst` is empty, so an empty copy at the top of the stack
passed the range check and then panicked inside `slot`. The `Vec` version
this replaced took `&stack[base..base + dst.len()]`, and `&stack[len..len]`
is an empty slice, not a panic.

Return before the copy when there is nothing to copy. That also keeps
`copy_nonoverlapping` off a null source on a thread whose root stack has
never been allocated. A range starting past the live length still panics
whether or not it is empty, as indexing did.

No caller reaches this today: both `object_array.rs` sites guard on
`len > 0` / `cap > 0`, and `call_builtin_code_many_from_roots` sizes its
buffer `1 + nargs`. `pop_roots` is emitted even for zero livevars
(`shadowstack.py:37-40`), so the empty shape is one a caller may take.

Assisted-by: Claude

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

Its committed baseline is `loops_aborted=3 loops_compiled=6
guard_failures=23380 bridges_compiled=0`. It measures `0 / 3 / 606 / 3`,
identically on dynasm and cranelift, and `loops_aborted=0` on wasm.

check.py failed it on `loops_compiled 6 -> 3`, a `JITSTATS_FALL_FIELDS`
gate. That fall is not a capability loss: `loops_compiled +
bridges_compiled` is 6 on both sides, so the three units it stopped
compiling as separate loops are the three it now compiles as bridges,
while the aborts go to 0 and the guard failures fall 38x.

Every gated field therefore moves the tightening way — `loops_aborted` and
`guard_failures` down, and the fall gate now pinned at 3 rather than 6.
Only `bridges_compiled` rises, and it is in no gated group.

Six further fixtures had no baseline at all and were recorded here before
the rebase; that half is dropped because #970 has since recorded all six on
main. Where the two recordings disagreed — `raise_reg_unbound_jitstress`,
`loops_aborted` 2 vs 10 and `loops_compiled` 5 vs 3 — main's is both the
authoritative side and the tighter gate, and a build at this base
reproduces main's numbers exactly.

Four fixtures are still left red on purpose: `exception_args_virtual`
(`loops_aborted` 0 -> 3), `exception_multi_handler_warmup` (0 -> 23),
`exception_reraise_tb_depth_jitstress` (0 -> 1198, `loops_compiled`
805 -> 305) and `list_length_hint_validate` (14 -> 34). These raise a
`JITSTATS_BADNESS_FIELDS` counter whose healthy value is 0, so recording
them would switch the floor off for those fixtures permanently. They
reproduce byte-identically on dynasm and cranelift, so the drift is
walker-level, and they are unchanged across #967's IS_OP fold and #971's
front-lowering folds.

Assisted-by: Claude
@youknowone

Copy link
Copy Markdown
Collaborator Author

Follow-up in #981 for the review findings here.

Fixed, each verified against a CPython 3.14.2 oracle:

  • the missing PyType_IsSubtype check in classmethod_get / classmethoddescr_call (both the P2 on __get__ and the Major on __call__ were this one defect) — without it dict.__dict__['fromkeys'](list, ['x']) reached dict.fromkeys with list, and type.__dict__['__prepare__'](list, 'X', ()) returned {};
  • the fixed arity-3 __get__ registration, which wrap_descr_get unpacks as 1..2 — the same defect was in the wrapper_descriptor and method_descriptor entries, so all three now share one helper;
  • the classmethod_descriptor getset receiver-mismatch wording.

Not applicable, with the measurements:

  • bh_load_global_fn namespace resolution — the helper already discarded namespace_ptr before this PR (let _ = namespace_ptr; is untouched by the branch), so the arg-0 exemption decides only whether the walk aborts. One code object driven through 200k iterations under two different __globals__ returns ('module', 'alt') on dynasm with loops_compiled=4, matching CPython and PYRE_JIT=0. The custom-__builtins__ variant diverges identically with the JIT off, so it is a pre-existing interpreter gap.
  • rooting in descr_reduce — the owner is a W_TypeObject and w_str_new allocates through off-GC lltype::malloc_typed, both non-moving; and w_tuple_new roots its own items either way (w_tuple_new_array_backed pins each and refills from the shadow stack, as does w_specialised_tuple_oo_new).

New parity script classmethod_descriptor_kind.py pins all of it.

— commented by Claude

youknowone added a commit that referenced this pull request Aug 2, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 3, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 3, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 3, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 3, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 3, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 3, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 4, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 4, 2026
`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 4, 2026
…950)

* functools: port reduce and cmp_to_key accelerators

* builtins: preserve ExceptionGroup sequence repr

* builtins: release exhausted list reverse iterators

* builtins: make UnicodeError formatting mutation-safe

* builtins: validate SyntaxError details

* builtins: implement sys excepthook aliases

* builtins: preserve compiler SyntaxError locations

* builtins: clamp SyntaxError display offsets

* builtins: render SyntaxError file source ranges

* builtins: format file encoding errors

* builtins: expose missing frame line as None

* builtins: support multidimensional memoryview subviews

* builtins: qualify wrapped builtin methods

* builtins: port __pypy__ newmemoryview

* builtins: port __pypy__ bufferable

* builtins: port unicodedata UCD type

* builtins: complete identity_dict mapping API

* builtins: align eval and AST compile contracts

* builtins: preserve float NaN list ordering semantics

* builtins: align type and exception descriptors

* builtins: preserve exhausted list reverse iterators

* sys: bind coroutine origin depth keyword

* imp: align frozen and builtin module APIs

* posix: bind stat follow_symlinks keyword

* pickle: align PickleBuffer public type surface

* builtins: align bytes and unpack iterator surfaces

* memoryview: reject multidimensional count

* builtins: add function attribute deleters and method dir forwarding

function.__name__, __qualname__, __code__ and __dict__ gain deleters that
raise the setter's TypeError instead of falling through to a generic
delete. method.__class__ is rejected in object_delattr, and method.__dir__
forwards the underlying function's instance dict since the bound-method
wrapper has no writable dict field of its own.

func_set_code warns DeprecationWarning when the new code object changes
the callable's kind (plain / generator / coroutine / async generator).

Assisted-by: Claude

* builtins, struct, __pypy__: apply cargo fmt

Assisted-by: Claude

* Revert "builtins: preserve float NaN list ordering semantics"

This reverts commit c8ef282.

The float arm bypassed eq_w's identity shortcut and asked compare(Eq)
directly, so `[x] == [x]` for a single NaN object became False. pyre
stores list floats unboxed, so w_list_getitem hands back a fresh box and
the ptr::eq guard ahead of that arm never fires.

That made `x is y` true while `[x] == [y]` was false, and disagreed with
both oracles on the same-object case, which CPython 3.14 and PyPy both
answer True. synth/list_nan_identity failed as "wrong output" on every
backend and platform.

The commit aimed to keep two distinct NaN objects unequal; with unboxed
floats pyre has no such distinction to preserve.

Assisted-by: Claude

* builtins: raise NotImplementedError for memoryview sub-views again

An integer index into a multidimensional view and a tuple key shorter
than `ndim` returned a live sub-view; `memoryobject.py:326-346` raises
`NotImplementedError` for both, as does CPython.  Removes the now
unreachable `BufferView::ViewNDOffset` variant and its GC tracing arm.

Assisted-by: Claude

* builtins: root __setstate__ entries and name the receiver's type

`w_dict_items` returns raw pointers in an untraced Vec and `setattr` can
run a Python-level `__setattr__` that reaches a collection safepoint, so
the remaining pairs were read back after a move.  Publishes the receiver
and every entry through `pin_roots` first.

`SyntaxError.__init__` and `SystemExit.__init__` are inherited, so their
"takes no keyword arguments" message now reports the receiver's own type
instead of the class the initializer is installed on.

Assisted-by: Claude

* _functools: publish operands with pin_roots and fix reduce's arity messages

`pin_root` runs a forwarding query that can park behind another thread's
collection, so a second operand still held in a Rust local (or read out
of a payload through `&W_KeyWrapper`) could name a pre-move address.
`reduce` also read its initial value after `iter()` had allocated.  All
three sites now publish every operand in one `pin_roots` call and read
them back from their slots.

More than three positional arguments reported "expected at least 2
arguments"; both arity errors now carry their own wording.

Assisted-by: Claude

* sys: bind one object to displayhook/excepthook and their originals

Each name held a separately constructed function, so
`sys.excepthook is sys.__excepthook__` was False.  Stores one object
under both names, as `breakpointhook` and `unraisablehook` already do.

Assisted-by: Claude

* _struct: install the unpack_iterator type entries once

`make_unpack_iter` rewrote `__module__` and `__getattribute__` on the
shared type dict on every `iter_unpack()` call, allocating a fresh string
and function object each time and mutating a dict other threads read
through attribute lookup.  Moves the writes and the TypeDef flags behind
a `Once`.

Also compares the `identity_dict` key with `is`, since the snippet's two
equal-but-distinct lists made the equality assertion pass either way.

Assisted-by: Claude

* descroperation: drop the float arm from tuple element comparison

`compare_slot`'s tuple loop kept the float special case that the list
side already reverted, so `(a,) == (b,)` was False for two `float("nan")`
values while `a is b` and `[a] == [b]` were both True.  Restores
`tupleobject.py:137`'s plain `eq_w` element check.

Assisted-by: Claude

* baseobjspace, _functools: decline the slot fold under a custom
__getattribute__ and batch reduce's sequence operand

`exception_attr_slot_fold` only checked which class defines the name, so
a receiver whose type overrides `__getattribute__` would still fold to a
slot read.  Applies the same `getattribute_if_not_from_object` decline
`load_attr_fast_path` uses (mapdict.py:1497-1499).

`reduce` pinned its function and initial value together but then read
`positional[1]` out of the argument slice afterwards, past a `pin_roots`
that can park behind another thread's collection.  All three operands are
now published in one batch and the sequence is read back from its slot.

Assisted-by: Claude

* builtins: root the exception reduce and group constructor operands

Three sites held managed pointers in untraced Rust locals across calls
that allocate or run Python:

- `attribute_error_reduce` kept the fresh `w_exception_get_args` tuple in
  a local across `w_dict_new`.
- `import_error_reduce` kept the same tuple across the instance dict's
  Python-level `copy()`.
- `exception_group_new` kept the `w_str_new` of a custom sequence's
  `__repr__` across `fixedview`, which runs that sequence's `__len__` and
  `__getitem__`, and kept the `fixedview` result across the per-item
  `isinstance` walk.

Each now publishes its operands with `pin_roots` and reads them back from
their slots.

Assisted-by: Claude

* display: refetch the receiver between unicode error slot reads

The three `UnicodeError` `__str__` formatters read `encoding`, `start`,
`end` and `reason` off one `obj` local.  `unicode_err_int_slot` walks
`__index__` through `int_w` and `unicode_err_str_slot` calls `__str__`,
so any of those reads can run Python and move the exception; only the
final `object` read was refetched from the shadow stack.  Each read now
takes the receiver from its slot.

Assisted-by: Claude

* unicodedata: route ucd_3_2_0.name through the 3.2.0 database view

`W_UCD::name` called the module-level `name`, which reads the latest
bundled database, so `ucd_3_2_0.name` returned names for code points
Unicode 3.2.0 leaves unassigned.  Add `name_impl(db, args)`, gate the
name lookup on the view's own general category being other than `Cn`,
and select `LEGACY`/`MODERN` in `W_UCD::name` as the sibling methods do.

The latest view is unaffected: no code point in it has category `Cn`
and a name.

Assisted-by: Claude

* builtins: root SyntaxError.__init__ arguments across the details fixedview

`fixedview` runs the details object's iteration protocol and can reach a
collection safepoint.  `call_builtin_code_positional` hands builtins a
native slice copied off the shadow stack, and a collection does not
rewrite it, so the receiver and the positional arguments read after the
call named pre-move addresses.  Publish them with `pin_roots` first and
read each back from its slot, building the args list before reloading
the receiver so the list allocation cannot invalidate it.

Report the shorter-than-4 details tuple as
`function takes at least 4 arguments (n given)`, matching the
`at most 6` message in the same match.

Assisted-by: Claude

* __pypy__: report newmemoryview geometry errors with the reference wording

The six `ValueError` diagnostics named neither the offending geometry nor
the source view.  Derive `lgt`/`old_size` from the source and spell each
message as `interp_buffer.newmemoryview` does, so the shape, strides,
itemsize and source length appear in the text.

`lgt` comes from the byte extent rather than `len(w_obj)`: for an N-D
source `shape[0]` covers only the outermost dimension, and the reference
aborts on such a source before reaching these checks.

The length-mismatch message names the parsed shape; the reference raises
before filling its shape accumulator and always reports an empty list
there.

Assisted-by: Claude

* extra_tests: exercise os.stat follow_symlinks against a real symlink

The existing assertion compared `os.stat(".", follow_symlinks=False)`
with `os.stat(".")`; `.` is not a symlink, so both describe the same file
whether or not the flag is honored.  Create a symlink in a temporary
directory and check that the two calls disagree on `st_ino` and
`st_size`, and that the no-follow result matches `os.lstat`.

Assisted-by: Claude

* call: identify the key wrapper by its type, not its type name

`user_call_slot` admitted the `_functools` key wrapper past the
generic-instance guard by comparing the type's name against the literal
"functools.KeyWrapper".  Use `W_KeyWrapper::from_obj`, which checks the
registered type directly, so the branch no longer depends on the name
string and costs a type check rather than a string compare.

Assisted-by: Claude

* importing: decode source through the BOM and the PEP 263 cookie

`SourceProvider` handed back a `String`, so every provider decoded as
plain UTF-8 and a file declaring another encoding was lost.  A traceback
into a `# -*- coding: latin-1 -*-` module printed no source line at all,
and `load_source_module` failed the import outright.

Make `read_to_bytes` the provider primitive and decode at the call sites:

- `error.rs` renders the traceback line through `decode_source_bytes`,
  the rule `linecache`'s `tokenize.open` applies.
- `load_source_module` decodes the same way; a bad declaration surfaces
  as the tokenizer's SyntaxError rather than an ImportError.
- `pyrex` reads the script through the provider in every build, so the
  sandbox path decodes too and the two arms collapse into one.

`read_source_to_string` stays for callers holding UTF-8 text rather than
Python source.

Assisted-by: Claude

* extra_tests: skip the os.stat symlink checks on Windows

`os.symlink` needs a privilege the runner cannot be assumed to grant, and
pyre does not create the link there, so the added assertions failed on
Windows while CPython passed.  Guard the block on `os.name`; the
`follow_symlinks` keyword checks above it still run everywhere.

Assisted-by: Claude

* jit, builtins: register unicodedata.UCD and __pypy__.Bufferable in build_gc

Both are `#[pyre_class]` types allocated through `allocate_stable` with no
inline object payload, so the header `w_class` is the only edge their marker
forwards.  Neither was wired into `build_gc`, which the GC-root registration
completeness assertion rejects at init.

Registered after `functools.KeyWrapper` at the append-only tail and given
subclass-range ids 157 and 158, so no established AUTO-ID moves.

Assisted-by: Claude

* extra_tests: cover the producer-specific iterator identities

`iter()` on bytes, bytearray, str (ASCII and non-ASCII) and memoryview
reports `bytes_iterator`, `bytearray_iterator`, `str_ascii_iterator`,
`str_iterator` and `memory_iterator`, each in `builtins`, and yields the
same elements as the source.

Assisted-by: Claude

* exceptions: port Python 3.14 pickle state semantics

* builtins: complete Python 3.14 compensated sum

* numeric: honor subclass NotImplemented dispatch

* unpack: report exact dict size on excess values

* format: port Python 3.14 spec validation

* weakref: complete proxy operation forwarding

* weakref: preserve constructor keyword binding

* weakref: preserve lifeline finalization ordering

* generator: register finalizers only for protected yields

* jit: preserve recursive closure operand stacks

* builtins: preserve Python 3.14 NaN identity

* builtins: bind object classmethod qualnames

* builtins: use short names in setattr errors

* builtins: attach set-name exception notes

* builtins: validate structseq constructor fields

* builtins: add structseq replace support

* builtins: construct structseq classes as heap types

* builtins: complete generic class dispatch

* _structseq, call: apply rustfmt

Assisted-by: Claude

* builtins: validate duplicate integer grouping

* stdlib: implement atexit callback shutdown

* collections: preserve deque copy identities

* tuple: preserve boxed float identities

* generator: bind methods through the typedef

* generator: chain caller exceptions on resume

* call: apply rustfmt to the structseq keyword predicate

Assisted-by: Claude

* _functools: build the comparison operand before reading the result slot

`w_int_new` can allocate for a custom numeric comparator result, so
evaluating it after the shadow-stack read left the first argument naming a
pre-move address.

Assisted-by: Claude

* baseobjspace: restore the bitwise float and complex is_w

Pointer identity left `a is b` false while `function::immutable_unique_id`
still derived the same `id()` from the bits, and made `[a][0] is a` false
after FloatListStrategy unboxed and reboxed the element.

Assisted-by: Claude

* _structseq: batch-publish the constructor roots

The forwarding query inside the first `pin_root` can park behind another
thread's collection, leaving the remaining items and extras held only in the
Rust vectors.

Assisted-by: Claude

* _functools: export the app-level cmp_to_key again

The generated `W_KeyWrapper.__call__` is positional-only, so `key(obj=3)`
raised and `inspect` reported `(obj, /)` instead of `(obj)`; both are
required by test.test_functools, whose baseline main raised to PASS.

Assisted-by: Claude

* importing: root every bound module's dict, not just the named ones

`walk_module_dicts_gc` and `walk_process_import_roots` both enumerated
`SYS_MODULES.values()`.  A builtin module is allocated immortal
(`w_module_new_aliasing_dict` -> `malloc_typed`), so the collector never
traces it and its `w_dict` was reachable only through those two walks.
Binding a second module to a name therefore dropped the first module's
dict out of the root set while Python still held the module: after
`del sys.modules['_functools']`, a fresh `import _functools` and a
`gc.collect()`, reading `_functools.__dict__` spun inside
`w_module_dict_getitem_str` on freed storage.

Add `MODULE_DICT_ROOTS`, an add-only set of every module `set_sys_module`
has bound, and drive both walks from it through a shared
`walk_bound_module_dicts`.

test.test_functools: FAIL -> PASS.

Assisted-by: Claude

* __pypy__: align newmemoryview argument handling with interp_buffer.py

`if w_shape` / `if w_strides` in `interp_buffer.py:newmemoryview` are
RPython reference tests, so an empty sequence counts as supplied and only
the default `None` selects the derive-it path.  Key both on the argument
object rather than on the parsed vector being empty, which makes
`newmemoryview(mv, 1, 'B', ())` report `shape/itemsize []/1 does not
match obj len/itemsize 12/1` instead of deriving a `(12,)` shape.

Three further mismatches in the same function:

- Strides without a shape now raise "strides must have a single value if
  shape not provided" rather than deriving a shape and reporting a
  shape/strides mismatch.
- The supplied-shape/supplied-strides length mismatch raises before the
  reference fills its shape accumulator, so the diagnostic names `shape []`.
- `lgt` is the source's own length and `nbytes` follows from it, so an N-D
  source reports its outermost dimension rather than its element count.

Assisted-by: Claude

* address review: excepthook None stream, unraisable tag read, dimensions rooting

`sys_excepthook` collapsed an explicitly disabled `sys.stderr = None` into
the same `wrote == false` branch as a missing `sys` or a failing lookup,
so the host-seam fallback wrote the render to fd 2 for a stream the
application had turned off.  Return without printing for the `None` case,
matching `_PyErr_Display`, and keep the seam for the lookup failures.

`__pypy__.write_unraisable` handed its second argument to the unchecked
`PyError::from_exc_object`, which reads the `kind` tag past the header of
a `W_BaseException`; `write_unraisable(where, obj, x)` on a plain instance
therefore read out of bounds and transmuted the byte into an `ExcKind`.
Classify with `w_exception_kind_checked` first and fall back to the kind a
bare `BaseException` maps to, keeping the object as `exc_object`.

`interp_buffer::dimensions` converted the unpacked elements in place while
holding them in a plain `Vec`; an element's `__index__` allocates and can
relocate the ones not yet converted.  Publish the batch and read each back
through its root slot.

Assisted-by: Claude

* typedef: a class receiver names the defining class, not the metatype

`builtin_function_qualname` derived a bound builtin method's qualname from
`type(__self__)` unconditionally.  A `classmethod_descriptor` binds the
class itself, so once #970 made `dict.fromkeys` such a descriptor the
receiver's type became `type` and the qualname read `type.fromkeys`;
`int.from_bytes` and `float.__getformat__` were affected the same way.

Take the descriptor's stamped qualname when the receiver is a type. That
is the defining class `stamp_method_owners` recorded, which is also what
`bool.from_bytes` reporting `int.from_bytes` needs.

typedef::tests::builtin_static_and_class_methods_have_qualified_names:
FAILED -> ok.

Assisted-by: Claude

* gc: make runtime code wrappers collectable

* gc: root code wrappers retained by jitcodes

* builtins: align class construction and comparison dispatch

* type: expose Python 3.14 annotation descriptors

* gc: re-mark ephemeron edges after finalizer resurrection

`deal_with_objects_with_finalizers` marks a dying finalizer object's whole
graph, which can mark owners the preceding `mark_ephemeron_values_to_fixed_point`
had classified as dead. An address-keyed side table is not reached by that
tracing, so `prune_ephemeron_tables` kept those entries while nothing marked
their values, and the next major seeded a swept address as a root. Run the
ephemeron fixed point again once the resurrection loop finishes.

`seed_major_root` now validates the header type_id and takes the name of the
walker that produced the root, so an invalid root fails at the walker rather
than later in `mark_object`.

`cpython_tests/run.py` appends the panic message line to the crash reason, as
`check.py` already does.

Assisted-by: Claude

* exceptions: expose exception group members

* exceptions: expose BaseException instance dict

* exceptions: expose SyntaxError private metadata

* exceptions: make ExceptionGroup weakrefable

* exceptions: store StopIteration return value

* exceptions: enforce suppress context member semantics

* descr: add the w_value and w_weakreflifeline exception fields

`W_BASE_EXCEPTION_GC_PTR_OFFSETS` gained the StopIteration `w_value` and the
`ExceptionGroup` weakref lifeline slots, but `build_w_exception_group` still
listed 20 fields, so `clear_gc_fields` left both uninitialized on a
JIT-inlined exception allocation.
`exception_size_descr_clears_every_runtime_traced_gc_field` covers this.

Assisted-by: Claude

* address review: ephemeron fixed point, callee shadow owner, __code__ rooting

`mark_ephemeron_values_to_fixed_point` compared `incr_state.objects_marked`
across a pass, but only `incremental_mark_step` ever increments it, so the
loop always ran exactly once. Count what the pass itself drained: a second
owner made live by the first owner's lifeline now gets its own value marked
before `prune_ephemeron_tables` keeps its entry.

`reconcile_vstack_at_boundary` chose the callee-local shadow on
`fbw_callee_vstack_enabled()`, while a recursive closure is admitted with that
env gate off. Such a mirror filled its holes from the outer portal
virtualizable, which can record a caller operand as the callee's kept-stack
value. Key the choice on `inline_subwalk`, the admission predicate.

`fset_func_code` called `warn_deprecation` and `function_get_doc` — both
allocate — and then stored through the raw `obj` / `w_code` locals. Publish
both and read them back from their root slots.

Assisted-by: Claude

* exceptions: expose AttributeError members

* exceptions: expose NameError name member

* exceptions: expose ImportError members

* exceptions: expose OSError members

* exceptions: expose SystemExit code member

* exceptions: port SyntaxError member slots

* exceptions: expose Unicode error members

* exceptions: document exception group members

* descriptors: expose class wrapper members

* descriptors: expose property members

* check.py: record the shutdown atexit trace abort in the jit-stats baselines

`run_atexit_callbacks` imports the `atexit` module at interpreter shutdown,
so `app_atexit.py`'s module body executes there. The seven synthetic
fixtures that call `pypyjit.set_param("function_threshold=1")` therefore
trace that body: it reads a module global, which installs the module dict's
`version?` quasi-immutable watcher, and then writes one, which
`opimpl_jit_force_quasi_immutable` answers by forcing the field and
abandoning the trace.

That is one additional `loops_aborted` per process, measured identically on
all six fixtures whose regression floor it crosses.

Assisted-by: Claude

* descriptors: expose range members

* descriptors: expose slice members

* descriptors: expose super members

* descriptors: expose type flags and base members

* type: expose CPython 3.14 ABI members

* type: complete CPython 3.14 flags

* gc: walk the function a staticmethod/classmethod wraps

A builtin type dict binds the `staticmethod` / `classmethod` wrapper, so
`walk_raw_function_roots` applied to the dict value stopped at the wrapper
and never reached `w_function`. That function is Box-immortal, so nothing
else traces it, while the `w_qualname` / `w_objclass` stamped onto it by
the TypeCache sweep and its lazily allocated `w_func_dict` are young.

Add `walk_raw_wrapped_function_roots` and call it from the three root
walks that already descend through immortal wrappers.

Assisted-by: Claude

* extra_tests: drop two assertions CPython 3.14 does not hold

`float("0.125") is float("0.125")` is False on CPython and True on pyre and
PyPy, where `is` between plain floats compares `floatval`; give the pair that
must stay distinguishable different values instead.

`generator.__next__` is a `method-wrapper` on CPython, which has no
`__func__`; assert `__self__` instead.

Assisted-by: Claude

* type: expose descriptor protocol surface

* type: add type.__sizeof__

`w_type_get_abi_sizeof` reports `PyTypeObject` for a static type and
`PyHeapTypeObject` plus the cached-keys block for a heap type with a dict;
`init_type_type` binds it as `type.__sizeof__` with a text signature.

Assisted-by: Claude

* exceptions: fold slot reads past the typedef member descriptors

`exception_attr_slot_fold`'s shadow gate accepted only the canonical getset,
so moving `errno` / `strerror` / `filename` / `filename2` (and the SystemExit,
AttributeError, NameError, ImportError and UnicodeError fields) to member
descriptors made every such read decline the fold and residualize.

`is_exception_typedef_member` matches the descriptor's direct-member kind
against the selected slot; `direct_member_get` reads the same slot accessor
and diverges only when the slot is NULL, which the fold already declines.

Assisted-by: Claude

* exceptions: add Python 3.14 builtin docs

* builtins, __pypy__: read __dict__ through the attribute protocol in vars; keep newmemoryview geometry omission distinct from None

vars() dropped its is_instance/is_type/is_function/is_module whitelist and
now reads obj.__dict__ through getattr, mapping only AttributeError to the
TypeError; exceptions answer instead of being rejected.

newmemoryview() tracks whether shape/strides were supplied from the parsed
argument rather than testing the resulting value for None, so an explicit
None reaches dimensions() and raises "'NoneType' object is not iterable".

Assisted-by: Claude

* exceptions: add BaseExceptionGroup initializer

* exceptions: move the exception group message and exceptions onto W_BaseException slots

BaseExceptionGroup stored its message, its flattened exceptions tuple and the
constructor-time repr of the supplied sequence under __pyre_* keys in the
instance dictionary, so eg.__dict__ listed all three and a write to those keys
changed what `message`, `exceptions` and `repr` answered.

They become three traced W_BaseException fields with the write barrier the
other exception slots use, registered in the per-kind descr group and in
W_BASE_EXCEPTION_GC_PTR_OFFSETS.

Assisted-by: Claude

* exceptions: correct the exception group repr fallback comment

The comment attributed the constructor-time spelling to CPython 3.14, which
re-renders args[1] live; the behaviour matches PyPy. Restate what the branch
does and against which reference.

Assisted-by: Claude

* jit: finish materialized frames on exit

* builtins: root the memoryview lookup operands and store StopIteration.value before delegating

memoryview.count kept the iterator, the view and the sought value in raw locals
across the element comparison, and memoryview.index kept the view and the value
across it; the comparison runs a user __eq__, so a collection triggered from
there left the next iteration dereferencing vacated memory. Both now pin the
operands and reload them from their shadow-stack slots, reusing one slot for
the element under test.

W_StopIteration.descr_init assigns w_value before running W_Exception.descr_init;
the initializer allocates the replacement args list, so the previous ordering
also read w_self and w_value after a collection point.

Assisted-by: Claude

* check.py: re-record the dynasm jit-stats floor for trace_too_long_inline_multiframe

Measured on this branch with freshly extracted LLBC: loops_compiled 0 -> 2 and
loops_aborted 23 -> 24. The bench sits at the trace-length boundary, and the
branch's added exception surface moves it: the tracer now compiles two loops it
previously never compiled and aborts one more attempt. cranelift and wasm are
unchanged and keep their existing floors.

Assisted-by: Claude

* gc: publish multi-operand root sets in one write phase

`pin_root` resolves forwarding, and that resolve is a safepoint, so a
sequence of `pin_root` calls reads every operand behind the first one
after a foreign collection could already have moved it.  Replace those
sequences with a single `pin_roots`, or with paired `publish_roots` +
`normalize_roots` where the operands arrive in two slices:

- `DictOperationGuard::new` pinned the dictionary, then the caller's
  `refs` slice, so every `w_dict_setitem*` / lookup operand crossed the
  dictionary's own pin.
- `newmemoryview` pinned its five arguments in a loop.
- `W_PickleBuffer::__new__` pinned the acquired buffer, then the
  exporter.
- `memoryview` slice-view, `__eq__`, `count` and `index` pinned the view,
  then the second operand.

`AttributeError.__getstate__` now pins and reloads the `name` slot before
handing it to `w_dict_setitem_str`, matching the `args` insertion below.

Assisted-by: Claude

* type: declare ABI layout members beyond the builtins namespace

`cpython314_builtin_abi` only covered the `builtins` namespace, so every
other runtime type fell back to a bare object header: `function`,
`module`, `code`, `frame`, `generator`, `coroutine`, `async_generator`,
`traceback`, `cell`, `mappingproxy`, `method`,
`builtin_function_or_method`, `types.SimpleNamespace`,
`types.GenericAlias`, `typing.Union`, the six descriptor kinds, the
iterator kinds and the dict views all reported `__basicsize__` 16 with
zero item/dict/weakref offsets.

`ImportWarning` and `SyntaxWarning` were missing from the exception
group and hit the same fallback.

Structseq classes stay on the fallback: their header follows the field
count, so it belongs to the structseq constructor rather than to a
per-name table.

Measured against CPython 3.14 on 64-bit: 31 of the 33 diverging types now
match, the two remaining being the structseq `flags` and `version_info`.
`type_members_python314.py` asserts the added layouts.

Assisted-by: Claude

* check.py: carry the trace_too_long_inline_multiframe abort floor to cranelift

The dynasm floor for this bench was re-recorded at 24 aborts; cranelift
kept 23 and now reports the same rise, so the gate stayed red on that
backend alone.  wasm still measures 23 and keeps its floor.

`loops_aborted` is the only field CI reported as diverging, so it is the
only one moved here; `loops_compiled` stays at its recorded 0 rather than
taking a value this change did not measure.

Assisted-by: Claude

* jit: clear caught residual exceptions on normal finish

* compile: raise SyntaxError for source a declared codec cannot decode

`decode_source_bytes` converted only LookupError, so a file declaring a
real codec whose bytes that codec rejects — `# coding: ascii` followed by
`0xff` — propagated the decoder's UnicodeDecodeError out of the import.
`pyparse.py:130-139 _handle_encoding` turns both failures into the
parser's SyntaxError and lets anything else through; do the same, with
the decoder's own text as the message.

Both now carry the file they came from, so `str()` renders the
`(name, line 0)` suffix instead of a bare message, and the unknown-codec
message no longer embeds the path.

`syntax_error_located` gained `error.py:4-7 wrap_pos`: a zero offset,
end_lineno or end_offset reads back as `None` rather than as `0`.
`lineno` is exempt, matching the reference.

Assisted-by: Claude

* exceptions: exclude stateful kinds from the traced args-only constructor

The traced inline constructor rebuilds an exception from `kind` /
`w_class` / `args_w`, and `has_trivial_args_constructor` named only the
OSError and Unicode-error kinds as unsafe for that.  Every kind whose
`descr_init` writes flattened fields is equally unsafe, and this branch
gave several of them dedicated slots: a hot `SyntaxError("m", ("f.py",
1, 2, "src"))` came back with `msg` / `filename` / `lineno` / `offset` /
`text` all `None`, and a hot `StopIteration(n)` lost its independent
`value` once `args` was reassigned.

Add `SyntaxError`, `StopIteration`, `AttributeError`, `NameError`,
`SystemExit`, `ImportError` and `ModuleNotFoundError`.  The subclasses
that inherit one of these initializers share the parent's kind, so
`UnboundLocalError`, `IndentationError` and `TabError` are covered.

`exception_stateful_init_jit.py` builds each one under a threshold-1
JIT and reads a field `args_w` cannot carry.  `check.py --backend dynasm
--synthetic-only` is 355 passed with no jit-stats movement.

Assisted-by: Claude

* type, unicodedata: root the abstractmethods operands, close the UCD constructor

`__abstractmethods__`'s setter truth-tested the value, then stored the
receiver and the value from the gateway's native slice.  `is_true` runs
the value's own `__bool__`, which is a collection point, and no collector
rewrites that slice, so the class dictionary could receive a pre-move
address.  Publish both operands before the test and reload them after.

`abstract_instantiation_error` skipped a `__abstractmethods__` that was
not a set, reporting an empty method list.  `objectobject.py:17-21
_abstract_method_error` reaches the attribute through `sorted(...)`, so a
non-container reports its own TypeError; iterate it the same way and let
that error through.

`unicodedata.UCD` was constructible: `interp_ucd.py:311 UCD.typedef`
declares no `__new__`, so generic allocation handed back a `UCD` with no
database.  Mark it non-instantiable, as every sibling interpreter-owned
type already is.

Assisted-by: Claude

* gc: report both generations in the invalid-child panic

`grey_child`'s panic named neither end's generation, so an old-gen holder
naming a nursery child (a store that never reached the remembered set) and
an old-gen child (a sweep that reclaimed a still-referenced object) read
identically. Add `child_gen` / `holder_gen`.

Assisted-by: Claude

* gc: log old-gen allocations under the lifetime knob

`MAJIT_GC_LIFETIME_LOG` recorded remembered-set adds and old-gen frees but
not births, so a dangling reference could not be split into "the referrer
outlived its target" and "the referrer was born holding a dead address".
Log both the born-old and the promotion allocation.

Assisted-by: Claude

* _structseq: root the structseq heap types for the process lifetime

`make_heap_structseq_type` builds the type with `w_type_new`, which is the
mortal GC-managed constructor, while every caller caches its raw address in
a `OnceLock<usize>` the collector neither traces nor rewrites. Once nothing
else referenced the type the sweep reclaimed it and the cache kept handing
the address out, so each later instance was built with a `w_class` naming
freed memory; a major collection then greyed that address out of
`tuple_object_custom_trace` and panicked. Register the type in a stable slot
with `try_gc_add_root`, as `intern_str` does for a managed dynamic string.

Assisted-by: Claude

* typedef: store subclass tags through the write barrier

The seven `w_class` retag sites wrote the slot directly. The instances come
from a stable-address allocator, so they are born old — and born black while
a major cycle is marking, which leaves nothing to rescan them; only the
barrier's remembered-set entry brings a modified black object back to gray
(`incminimark.py:1792-1799`). `pyre_class`'s `__new__` already carries the
barrier for the same store. Route all seven through one helper.

This is not the producer of the `tuple_object_custom_trace` panic — that one
was the unrooted structseq type cache.

Assisted-by: Claude

* exceptions: source the implicit context per resume kind, record it once

`handle_exception` derived every implicit `__context__` from
`sys_exc_info()`, and re-derived it in each frame the exception unwound
through.

An exception thrown into a resumed generator now reads the flat EC slot
instead of walking the generator chain: `push_gen_or_coroutine` has already
swapped the generator's own handled exception into that slot, and the
caller's belongs to the caller, which delivered the exception rather than
raising it under a live handler.

An exception a frame raised itself records its context only where it first
surfaces, witnessed by `w_exception_get_traceback` still being null there --
`record_application_traceback` stamps this frame's node further down the
same function.  A thrown-in exception is exempt from the witness: it starts
a new error over an object that already carries a traceback.

`test.test_asyncio.test_timeouts` goes 33/34 -> 34/34 with the JIT on and
off.

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