Skip to content

cpyext: unrealized bytes, the refcount header, and a type mirror that is not immortal - #1278

Merged
youknowone merged 7 commits into
mainfrom
cpyext-rebased
Aug 17, 2026
Merged

cpyext: unrealized bytes, the refcount header, and a type mirror that is not immortal#1278
youknowone merged 7 commits into
mainfrom
cpyext-rebased

Conversation

@youknowone

Copy link
Copy Markdown
Owner

Six commits on the extension slice, on top of main.

PyBytes_FromStringAndSize(NULL, size) and Py_NewRef

bytesobject.py:64 new_empty_str / :100 bytes_realize — the second of
the two forms that hand out a mirror before its interpreter object
exists. The caller fills the buffer and pyobject::realize builds the
bytes at the first read; PyBytes_AsString / AsStringAndSize /
Size and the type tests answer from the buffer, so asking them
mid-fill does not decide the contents early.

Py_NewRef / Py_XNewRef needed a refcount.h: they are inline
functions calling Py_INCREF, which expands to the declared
Py_IncRef, so the six refcount macros moved out of object.h into a
header Python.h includes after pyre_decl.h.

Review findings from #1264

Six of them were one family, caused by making from_ref realize: an
argument() can now allocate, so every entry point that mints an
interpreter object after converting one had to realize first. A census
of the entry points found the same seven sites the reviewers named and
no others. PyErr_Restore's three degenerate inputs were reshaped to
errors.c:62-67, 77-86.

patchlevel.h in parts

PY_VERSION_HEX was a hard-coded 0x030E06F0 and the release level and
serial were not defined at all. It is now computed from the five parts;
the computed value equals the constant it replaces. A fixture reads the
macros back and compares them against sys.version_info,
sys.hexversion and sys.pyre_version_info.

The ABI gate covers the header's inline functions

cpyext-abi.py check read only the exports. The header also ships
static inline functions — the variadic ones and PyArg_UnpackTuple
which an extension calls the same way and which are hand-written rather
than generated, so nothing compared them against CPython's declaration.
The gate now reports 332 exports and 12 header inlines; all 12 match
as they stand. Verified the gate can fail: giving PyArg_UnpackTuple a
long where CPython says const char * exits 1 and names the argument.

A synthesized type mirror is no longer immortal

ensure_mirror stamped REFCNT_IMMORTAL on every type mirror. A
type mirror is minted for the type of every object handed to C, so any
class an extension merely observed became permanently uncollectable.

Nothing upstream ever mints an immortal mirror — _Py_IMMORTAL_REFCNT
is only read there, and create_ref leaves every mirror at exactly the
link share. What Py_TPFLAGS_HEAPTYPE decides is (a) whose storage the
block is (typeobject.py:716-722) and (b) whether a block holds a
reference to its own type (pyobject.py:91-93, released in
object.py:72-73). Both are implemented; an extension's own
PyTypeObject static stays immortal, since that storage is the
library's. tp_name was a leaked CString::into_raw justified by the
mirror being immortal, and is now dropped with the mirror.

One adjacent defect fell out of this: PyObject_Init reads
ob_pyre_link to tell an initialized block from raw bytes, and
PyObject_Malloc was handing out uninitialized ones. allocate_raw now
defines the header either way.

Verification

  • A class the fixture passes to C is collected again. Measured
    1/2/3 gc.collect() calls → False / True / True; two are needed
    because the instance mirror's reference to its heap type is given back
    by the first drain, and the test says so.
  • Negative control: restoring the immortal stamp makes exactly that
    assertion fail.
  • The CI cpyext leg is green — 77 test binaries, 0 failures.
  • markupsafe still runs against the release binary, including 200
    dynamically created classes routed through the C accelerator via
    __html__, which is what exercises the new heap-type mirror lifecycle.

opened by Claude

@coderabbitai

coderabbitai Bot commented Aug 16, 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: 41 minutes

Limit details: You’ve used all 2 included reviews currently available under your plan.

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: de6e807f-9107-4279-9c5b-642924a2e923

📥 Commits

Reviewing files that changed from the base of the PR and between c45b04f and c88e34d.

📒 Files selected for processing (26)
  • include/pyre3.14t/Python.h
  • include/pyre3.14t/object.h
  • include/pyre3.14t/patchlevel.h
  • include/pyre3.14t/refcount.h
  • pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats
  • pyre/docs/cpyext.md
  • pyre/extra_tests/snippets/builtin_super.py
  • pyre/extra_tests/snippets/stdlib_ast.py
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/cpyext/bytesobject.rs
  • pyre/pyre-interpreter/src/cpyext/import_.rs
  • pyre/pyre-interpreter/src/cpyext/listobject.rs
  • pyre/pyre-interpreter/src/cpyext/mapping.rs
  • pyre/pyre-interpreter/src/cpyext/mod.rs
  • pyre/pyre-interpreter/src/cpyext/pyerrors.rs
  • pyre/pyre-interpreter/src/cpyext/pyobject.rs
  • pyre/pyre-interpreter/src/cpyext/sequence.rs
  • pyre/pyre-interpreter/src/cpyext/typeobject.rs
  • pyre/pyre-interpreter/src/module/_ast/convert.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
  • pyre/pyre-jit/src/jit/cpu.rs
  • pyre/pyre-jit/src/jit/flatten.rs
  • pyre/pyrex/tests/cpyext_methods.rs
  • pyre/pyrex/tests/fixtures/cpyext_methods.c
  • pyre/scripts/cpyext-abi.py

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

❤️ Share

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c31b72b45c

ℹ️ 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".

super::modsupport::forget_module_fields(address);
super::unicodeobject::forget_block(address);
super::bytesobject::forget_pending(address);
super::typeobject::forget_type_name(address);

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 Keep tp_name alive through tp_dealloc

When an interpreter-defined heap class uses a C-defined metaclass with tp_dealloc, deallocating its synthesized type mirror reaches this line. Removing TYPE_NAMES drops the CString backing tp_name before the callback is invoked below, so a metaclass deallocator that inspects ((PyTypeObject *)obj)->tp_name dereferences freed memory. Keep the name in the upstream heap-type ownership shape, or at minimum defer its removal until after tp_dealloc returns.

AGENTS.md reference: AGENTS.md:L115-L118

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit c88e34d).
Updated: 2026-08-16T23:08:36.192Z

Files in the reviewed diff
include/pyre3.14t/Python.h
include/pyre3.14t/object.h
include/pyre3.14t/patchlevel.h
include/pyre3.14t/refcount.h
pyre/docs/cpyext.md
pyre/extra_tests/snippets/builtin_super.py
pyre/extra_tests/snippets/stdlib_ast.py
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/cpyext/bytesobject.rs
pyre/pyre-interpreter/src/cpyext/import_.rs
pyre/pyre-interpreter/src/cpyext/listobject.rs
pyre/pyre-interpreter/src/cpyext/mapping.rs
pyre/pyre-interpreter/src/cpyext/mod.rs
pyre/pyre-interpreter/src/cpyext/pyerrors.rs
pyre/pyre-interpreter/src/cpyext/pyobject.rs
pyre/pyre-interpreter/src/cpyext/sequence.rs
pyre/pyre-interpreter/src/cpyext/typeobject.rs
pyre/pyre-interpreter/src/module/_ast/convert.rs
pyre/pyre-jit/src/call_jit.rs
pyre/pyre-jit/src/jit/codewriter.rs
pyre/pyre-jit/src/jit/cpu.rs
pyre/pyre-jit/src/jit/flatten.rs
pyre/pyrex/tests/cpyext_methods.rs
pyre/pyrex/tests/fixtures/cpyext_methods.c
pyre/scripts/cpyext-abi.py

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)

  • pyre/pyre-jit/src/jit/codewriter.rs:12256,12802 ↔ pypy/module/__builtin__/descriptor.py:25-26,106-124 — zero-argument super() in a non-portal inlined callee still receives frame_var, which is documented as the outermost portal frame. PyPy obtains the active callee frame and reads that frame’s self and __class__ cell. The new specialization therefore does not repair the existing frame-identity collapse: it can resolve super() against caller locals/cells. This behavior predates the patch; main’s generic call path had the same outer-frame dependency.

4. Structural adaptations

  • pyre/pyre-interpreter/src/cpyext/bytesobject.rs:20-139 ↔ pypy/module/cpyext/bytesobject.py:47-115 — Pyre represents an unrealized PyBytes_FromStringAndSize(NULL, n) buffer with a Rust cache plus pending-mirror set rather than PyPy’s inline PyBytesObject.ob_sval allocation. This is a Rust/C-layout adaptation; the patched accessors preserve PyPy’s delayed-realization boundary.

  • pyre/pyre-interpreter/src/cpyext/pyobject.rs:144-225 ↔ pypy/module/cpyext/pyobject.py:209-277 — synthesized Rust mirror blocks and explicit heap-type ownership replace PyPy’s raw PyObject/PyHeapTypeObject allocations. The ownership transition follows PyPy’s heap-type reference rule and is required by the different storage model.

  • pyre/pyre-interpreter/src/module/_ast/convert.rs:2735-2745 ↔ pypy/interpreter/astcompiler/astbuilder.py:4-76 — conversion uses the retained RustPython/Ruff numeric-token spelling and Pyre’s base-autodetect parser instead of PyPy’s parser-token radix walk. This is a compiler-representation adaptation, not an observable PyPy-parity divergence for valid literals.

`PyBytes_FromStringAndSize(NULL, size)` returns a mirror with no `bytes`
behind it and a `size + 1` buffer, as `bytesobject.py:64 new_empty_str` does.
The buffer is the mirror's `cached_bytes` entry -- the side table that stands
in for the `ob_sval` field upstream's mirror carries -- so `PyBytes_AS_STRING`
hands out one address before and after the object exists.

`pyobject::realize` builds the `bytes` from what the buffer holds and links the
two (`bytesobject.py:100 bytes_realize`), which is the same point the `str`
`PyUnicode_New` allocates is realized at. `PyBytes_Check`, `CheckExact`,
`Size`, `AsString` and `AsStringAndSize` answer from the buffer while the
mirror is pending, so reading one mid-fill does not build the object early;
the deallocator drops the pending record, so a mirror released without ever
being read is freed with no `bytes` behind it.

The refcount macros move from `object.h` to a new `refcount.h`, which
`Python.h` includes after `pyre_decl.h`, and `Py_NewRef` / `Py_XNewRef` join
them. They are `static inline` functions calling `Py_IncRef`, which is
declared in the generated header, so they cannot sit beside `Py_INCREF`.

`cpyext_methods` covers both: `bytes_fill`, `bytes_pairs`, `bytes_empty` and
`new_ref`.

Assisted-by: Claude
…L class

Reading a mirror allocates now: `from_ref` realizes the `str` `PyUnicode_New`
and the `bytes` `PyBytes_FromStringAndSize(NULL, size)` hand out unlinked, so
`argument` / `arguments` are collection points. Seven entry points still had
the older order -- mint the interpreter object, then read the receiver -- which
leaves the freshly minted object unpinned across that realization:
`PyMapping_GetItemString`, `PyMapping_DelItemString`, `PyMapping_HasKeyString`,
`PyList_GetSlice`, `PySequence_GetSlice`, `PySequence_SetSlice` and
`PySequence_DelSlice`. Each gets the `realize_all` prologue the two list
entry points already had, and `key_of`'s comment states the order that now
holds. A census of every mint-before-convert site in `cpyext/` finds no
others; `read_member` mints and converts in exclusive `match` arms.

`import_module` pinned `__import__` after building the name, so the slot was
seeded with a pre-move address; the two pins swap.

`PySequence_Index` and `PySequence_Count` held the unpacked element `Vec` and
the searched-for value in Rust locals across `eq_w`, which runs the element's
own `__eq__`. Both take the shadow-stack discipline `PyDict_Merge` uses.

`PyErr_Restore` left the previous exception standing when `ptype` was NULL and
`pvalue` was not an exception. `errors.c:62-67` clears on a NULL class whatever
the value is. On the same path `errors.c:77-86` builds an instance by calling
the class when the value is NULL, which `normalized` already did, so that case
no longer clears instead. `cpyext_methods.restore()` covers both.

Assisted-by: Claude
`PY_VERSION_HEX` was a hard-coded 0x030E06F0 and the release level and
serial were not defined at all.  Define `PY_RELEASE_LEVEL_ALPHA` /
`BETA` / `GAMMA` / `FINAL`, `PY_RELEASE_LEVEL`, `PY_RELEASE_SERIAL` and
`PY_VERSION`, and compute `PY_VERSION_HEX` from the five parts.  Add
`PYRE_VERSION` and `PYRE_VERSION_NUM` beside them.

The computed value equals the constant it replaces.  `cpyext_methods`
gains a fixture reading the macros back and comparing them against
`sys.version_info`, `sys.hexversion` and `sys.pyre_version_info`.

Also drop a stale count from the same doc section: the number of
exports CPython does not declare is 52, not the 35 it named.

Assisted-by: Claude
`cpyext-abi.py check` read only the `#[unsafe(no_mangle)] extern "C"`
exports.  The header also ships `static inline` functions -- the
variadic ones and `PyArg_UnpackTuple` -- which an extension calls the
same way, and which are hand-written rather than generated, so nothing
compared them against CPython's declaration.

Parse them out of `include/pyre3.14t/*.h` and run them through the same
comparison.  The gate now reports 332 exports and 12 header inlines;
all 12 match as they stand.

Assisted-by: Claude
`ensure_mirror` stamped `REFCNT_IMMORTAL` on every type mirror it
allocated, and `describe_interpreter_type` re-stamped it.  A type mirror
is minted for the type of every object handed to C, so any class an
extension merely observed became permanently uncollectable.  Nothing
upstream ever mints an immortal mirror: `_Py_IMMORTAL_REFCNT` is only
read there, and `create_ref` leaves every mirror at exactly the link
share.

Allocate a synthesized type mirror at `REFCNT_FROM_PYRE`, and set
`Py_TPFLAGS_HEAPTYPE` on it from the interpreter type's
`flag_heaptype`.  A block now holds a reference to its own type's mirror
when that type is a heap type (`pyobject.py:91-93`), released in
`dealloc` (`object.py:72-73`); `set_ob_type` moves that reference when a
block is re-pointed.  An extension's own `PyTypeObject` static stays
immortal, since that storage is the library's.

`tp_name` was a leaked `CString::into_raw`, justified by the mirror
being immortal.  It is now held in a table keyed by the mirror's address
and dropped with the mirror.

`allocate_raw` clears the `CPyObject` header when the caller did not ask
for zeroed memory: `PyObject_Init` reads `ob_pyre_link` to tell an
initialized block from raw bytes, and `PyObject_Malloc` handed out
uninitialized ones.

`cpyext_methods` reads `Py_TYPE(x)->tp_name` and the heap-type flag back
for a built-in and for a Python class, and asserts a class the fixture
passed to C is collected after two collections -- two because the
instance mirror's reference to its type is given back by the first
drain.

Assisted-by: Claude
Threading the red `frame` operand through `load_super_attr` moved this
pattern's `guard_failures` from 1010 to 1008.  `loops_compiled` and
`bridges_compiled` are unchanged at 6 and 5.

The value is the one all three CI hosts observed -- ubuntu-24.04,
macos-latest and windows-latest each reported 1008 -- so the shared
baseline is re-recorded rather than given a per-platform overlay.

Assisted-by: Claude

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c88e34d636

ℹ️ 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".

global_super,
self_value,
cls_value,
frame_var.into(),

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 Pass the inlined callee's own frame to super

When LOAD_SUPER_ATTR runs in an inlined non-portal callee, this passes frame_var, but the same codewriter explicitly documents that this variable aliases the outermost caller frame for such callees. bh_load_super_attr_fn then calls builtin_super_from_frame on that caller, so zero-argument super() reads the caller's slot 0 and __class__ cell; for the added register -> parse_hookimpl_opts shape, the caller has no __class__ cell and the hot inlined path raises instead of constructing the callee's super proxy. The operand must be the callee's own per-frame red frame rather than the shared portal anchor.

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

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit cc7cc63 into main Aug 17, 2026
16 of 18 checks passed
@youknowone
youknowone deleted the cpyext-rebased branch August 17, 2026 02:13
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