Skip to content

majit: keep a boxing cluster's w_class as its own store - #1197

Merged
youknowone merged 1 commit into
mainfrom
str
Aug 13, 2026
Merged

majit: keep a boxing cluster's w_class as its own store#1197
youknowone merged 1 commit into
mainfrom
str

Conversation

@youknowone

@youknowone youknowone commented Aug 13, 2026

Copy link
Copy Markdown
Owner

This PR's contents changed. It previously carried module: store the module name as the wrapped object; that change is now on main (it landed through the
#1187 review follow-ups), so the branch rebased down to the single commit below.

fuse_boxing_alloc dropped both header stores and let the NewWithVtable's
vtable stamp ob_type and w_class, so it could only fire where the cluster's
w_class was get_instantiate(&T) for the very &T its ob_type named.
Anything else declined and left malloc_typed residual, which the rtyper
rejects fail-closed; the whole graph then census-Skips to the legacy walker.

Change

jtransform.py:1040-1045 rewrite_op_malloc emits new_with_vtable carrying
only the sizedescr and leaves every other field to its own setfield. Do the
same with w_class: lower the cluster and re-emit the store as a FieldWrite
on the new object whenever the vtable does not stand for it.

Predecessors that disagree — one folding into the vtable and another not, or two
naming different classes — still decline, as they already did for a disagreeing
ob_type (malloc.py:176-186).

prune_dead_boxing_remnants needs no change: its liveness gate already keeps a
producer the flow reaches, which the re-emitted store now makes the w_class
feed.

Measurement

Both arms run on identical LLBC — only majit-translate/src/model.rs differs
(PYRE_LLBC_STRICT=0 demotes the repo-wide #1164 digest, which a translator-only
swap necessarily trips).

main this PR
survived fuse_boxing_alloc unfused 36 lines / 18 graphs 4 / 4
PREPASS phaseA fail 1497 1487

w_long_from_raw pairs &LONG_TYPE with get_instantiate(&INT_TYPE) so that
type(1 << 100) is int, and is the root cited by 14 of those 18. The other 4
are w_generator_or_coroutine_new and its three callers, which decline for the
other reason — three ob_type constants merge into one malloc, so no single
vtable stands for the cluster — and are exactly the 4 that survive.

Clearing the reject is not the same as lifting the graph. Of the 18: 9 now
annotate, 4 keep that reject, and 5 fail on a different error it was standing
in front of (compute_at_fixpoint, on unary_negative_value /
unary_invert_value under both spellings and on PositionCookie::to_object).

The −10 is not uniform: 14 fixed, 4 newly failing

Diffing the failing-graph sets rather than the totals decomposes the −10 into
14 fixed and 4 regressed. (That 14 is a different set from the 14 above: the 9
that now annotate, plus 5 that had been failing only through a cascade from them
__pyre_wrap_random, w_dict_proxy_new, w_float_new, w_range_compute_item,
w_range_new.)

The 4 that regress are PyFrame::getorcreate_debug_data and its three callers:

UnionError in mergeinputargs: longobject::W_LongObject ∪ pyframe::FrameDebugData
In <FunctionGraph of write>

core::ptr::write is lifted as one graph shared by every instantiation
(Lowering::new takes the declaration's fd.generics; the instantiation lives
at the call site), and the raw write survives fusion by design —
sink_fused_boxing_aggregates_at_raw_writes' test asserts "the raw write must
survive the rewrite". Until now only FrameDebugData reached that graph;
w_long_from_raw lifting means W_LongObject reaches it too, and slot 0 unions
two instances with no common base.

This is a latent landmine rather than a defect in this rewrite: any change
that lets w_long_from_raw lift arms it, so there is no narrower variant of
this patch that avoids it. The upstream shape is per-instantiation
specialization — Specializer::Argtype (specialize:argtype(N)) is already
implemented — but nothing in production ever sets annspecialcase on an
LLBC-lifted function, so wiring it is a front-end change of its own and is left
as follow-up. The 4 graphs fall back to the legacy walker, the same fail-closed
path the reject already had them on the edge of.

Widening the union instead would be the wrong repair: the analogous
List ∪ Inst(classdef-less) arm lifted 5 graphs and was reverted because phaseB
silently accepted the resulting erasure.

Verification

  • cargo test -p majit-translate --release --lib — 3190 passed, 0 failed.
  • python3 pyre/check.py --backend dynasm — 425/425; --backend cranelift — 425/425.
  • Semantic probes on the lifted graphs: type(1 << 100) is int, hot-loop -x /
    ~x arithmetic, and class B(int) subclass identity all correct.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 484db7bf-5938-48c1-bc0f-a4d173d8ae68

📥 Commits

Reviewing files that changed from the base of the PR and between 2dbcba3 and f012ab2.

📒 Files selected for processing (5)
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/module.rs
  • pyre/pyrex/src/lib.rs

Walkthrough

Module names now use traced Python objects instead of externally allocated WTF-8 buffers. Constructors, accessors, GC traversal, module initialization, and reachability checks preserve name objects, including lone surrogates.

Changes

Module name representation and collection

Layer / File(s) Summary
Traced module name storage
pyre/pyre-object/src/module.rs
Module::w_name now stores a traced PyObjectRef. GC offsets include the name object.
Name construction and access
pyre/pyre-object/src/module.rs
Module constructors and accessors use Python name objects. The same object seeds __name__, setters use the GC write barrier, and tests cover identity and lone-surrogate preservation.
GC and reachability integration
pyre/pyre-interpreter/src/typedef.rs, pyre/pyre-interpreter/src/importing.rs, pyre/pyrex/src/lib.rs, pyre/pyre-jit/src/eval.rs
Module initialization passes the original name object. Root traversal traces w_name, reachability checks validate the object safely, and GC layout documentation identifies both traced fields.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: 🔵 Low · up to f012a

The PR changes runtime module-name handling and garbage-collection layout. It is mergeable with owner awareness, but the required JIT validation and benchmark results should be provided as follow-up.

Sequence Diagram(s)

sequenceDiagram
  participant Module
  participant GCTraversal
  participant Reachability
  participant SysModules
  Module->>Module: store name in w_name
  Module->>Module: seed __name__ with the same object
  GCTraversal->>Module: trace w_name
  Reachability->>Module: read w_name
  Reachability->>SysModules: compare module identity
Loading

Possibly related PRs

Suggested reviewers: lifthrasiir

Poem

I’m a rabbit guarding names in the heap,
With traced strings tucked safely asleep.
Surrogates hop through without UTF-8 pain,
GC follows each link in the chain.
__name__ and w_name__ share one bright thread—
And old buffers now rest in their bed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes boxing cluster storage, but the changes update module name storage and GC handling. Use a title that identifies the module name storage change and preservation of lone-surrogate filenames.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 str

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

❤️ Share

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/33a155e97c9c3efc0322a1e4fedab7aefdd7a52e/pyre-object/src/module.rs#L230-L231
P2 Badge Trace names stored on immortal modules

When ModuleType.__init__ is applied to an existing bootstrap module such as sys, this stores a potentially GC-managed string in an off-heap malloc_typed module. The write barrier does not make that field a root: walk_module_dicts_gc only visits module.w_dict, while W_MODULE_GC_PTR_OFFSETS applies only to managed module objects. A program can therefore reinitialize sys with a dynamically created name, delete sys.__name__, and collect the string; release_frees_nothing later dereferences the dangling w_name during shutdown. The immortal-module root walker must visit w_name as well.

ℹ️ About Codex in GitHub

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

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

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

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

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 9df3324).
Updated: 2026-08-13T19:52:08.253Z

Files in the reviewed diff
majit/majit-translate/src/model.rs

1. Regressions to PyPy parity introduced by this patch

  • majit/majit-translate/src/model.rs:3348 ↔ rpython/jit/codewriter/jtransform.py:1024: resolve_header_plan() can return Some(HeaderPlan { vtable: 0, ... }), but the new code no longer rejects it before emitting NewWithVtable. Upstream branches on if vtable and uses plain new when absent. Main previously retained the residual call for zero; this patch now emits an invalid new_with_vtable that the assembler rejects for a null type pointer.

2. Other mismatches introduced by this patch

None.

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

None.

4. Structural adaptations

  • majit/majit-translate/src/model.rs:3183 ↔ rpython/jit/codewriter/jtransform.py:1038: HeaderPlan.w_class and the subsequent explicit FieldWrite are a necessary Rust/pyre object-layout adaptation. PyPy’s new_with_vtable only represents its type-pointer layout; pyre separately carries w_class, so preserving a subclass or shared-int class through a post-allocation store is structurally appropriate.

@youknowone
youknowone force-pushed the str branch 2 times, most recently from e28b31a to 47941f9 Compare August 13, 2026 12:26

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/e28b31a8f96b0175af53e60e286ca976ba68ea1f/pyre-interpreter/src/typedef.rs#L21507
P1 Badge Keep mmap buffer support enabled on Windows

When building a non-sandbox Windows target, this narrower cfg removes the mmap_buffer_view branch even though that helper and the mmap implementation are explicitly available under cfg(any(unix, windows)). Consequently buffer_as_bytes_like returns None for Windows mmap objects, causing consumers such as bytes concatenation, numeric parsing, codecs, and marshal to reject a valid buffer; retain the previous any(unix, windows) platform condition.

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/47941f970e9712980b3ec0d95a99c0ee6611446e/pyre-interpreter/src/typedef.rs#L21581
P2 Badge Keep mmap buffer conversion enabled on Windows

On Windows, this narrower cfg compiles out the mmap buffer path even though mmap_buffer_view remains implemented under cfg(any(unix, windows)). Consequently, buffer-consuming operations routed through buffer_as_bytes_like—notably bytes(m), bytearray(m), concatenation, and joins—no longer recognize a Windows mmap; constructor fallback iteration yields one-byte bytes objects rather than integers and raises TypeError instead of copying the mapping. Preserve the previous any(unix, windows) gate.

AGENTS.md reference: AGENTS.md:L231-L233

ℹ️ About Codex in GitHub

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

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

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

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

@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: f012ab2469

ℹ️ 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 thread pyre/pyre-interpreter/src/importing.rs Outdated
Comment on lines +1778 to +1779
if !module.w_name.is_null() {
visitor(&mut module.w_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.

P1 Badge Trace names for unregistered immortal modules

When the immortal module created by ExecutionContext::get_builtin is reinitialized with a collectable string and its dictionary's __name__ entry is subsequently deleted or overwritten, this walk never reaches the new w_name: it scans only MODULE_DICT_ROOTS, while that module is held in builtin_dict_cache and is never passed to set_sys_module. The EC root walker merely forwards the unmanaged module pointer, so a major collection can sweep the name and release_frees_nothing can later dereference stale memory if the module is released from a user global during shutdown. Trace w_name for every immortal module owner, not only modules registered in sys.modules.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

@youknowone youknowone changed the title module: store the module name as the wrapped object majit: keep a boxing cluster's w_class as its own store Aug 13, 2026
@youknowone

Copy link
Copy Markdown
Owner Author

Rebased onto current main, which changes what this PR contains.

The module: store the module name as the wrapped object commit is no longer
here — an equivalent w_name: PyObjectRef port landed on main through the
#1187 review follow-ups, so the rebase dropped it. I diffed my version against
main's across all five touched files before letting it go: the structure
matches and main additionally carries a
module_init_retains_a_surrogate_name_object test, so nothing was lost.

What remains is the single majit commit, and the description above has been
rewritten for it. One thing worth reviewer attention: the census improvement is
14 graphs fixed against 4 newly failing, not a uniform gain. The 4 are
diagnosed in the description — core::ptr::write is lifted as one graph for all
instantiations, so the first change that lets a second type reach it hits a
UnionError. That is reachable by any patch that makes w_long_from_raw lift,
not something this one can dodge.

commented 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: df126ff8dd

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

// payloads. Each is a plain `FieldWrite` the assembler lowers to its
// own `setfield_gc`, which is the shape `jtransform.py:1044` leaves
// every field the allocation itself does not carry.
for (k, payload) in site.w_class.into_iter().chain(site.payloads).enumerate() {

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 Teach virtualization to store the re-emitted w_class

When w_class differs from the vtable-derived class (notably w_long_from_raw and bytes-subclass allocation), this chain now emits a FieldWrite carrying the original PyObject.w_class descriptor immediately after NewWithVtable. The runtime descriptor bridge resolves that access to the canonical w_class_descr, whose parent descriptor is deliberately None, but OptVirtualize::optimize_setfield_gc unconditionally calls get_parent_descr().expect(...) for every non-typeptr field on a virtual allocation. Because NewWithVtable is virtualized before this adjacent store is processed, compiling either newly fused path panics instead of producing a trace; handle w_class as a special virtual header field (and preserve its explicit value) before enabling this fusion.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

`fuse_boxing_alloc` dropped both header stores and let the `NewWithVtable`'s
vtable stamp `ob_type` and `w_class`, so it could only fire where the cluster's
`w_class` was `get_instantiate(&T)` for the very `&T` its `ob_type` named.
Anything else declined and left `malloc_typed` residual, which the rtyper
rejects fail-closed; the whole graph then census-Skips to the legacy walker.

`jtransform.py:1040-1045 rewrite_op_malloc` emits `new_with_vtable` carrying
only the sizedescr and leaves every other field to its own `setfield`. Do the
same with `w_class`: lower the cluster and re-emit the store as a `FieldWrite`
on the new object whenever the vtable does not stand for it. Predecessors that
disagree — one folding into the vtable and another not, or two naming different
classes — still decline, as they already did for a disagreeing `ob_type`
(`malloc.py:176-186`).

`prune_dead_boxing_remnants` needs no change — its liveness gate already keeps
a producer the flow reaches, which the re-emitted store now makes the `w_class`
feed.

The reject population over the pyre sources goes from 36 lines across 18 graphs
to 4 across 4. `w_long_from_raw` pairs `&LONG_TYPE` with
`get_instantiate(&INT_TYPE)` so that `type(1 << 100) is int`, and is the root
cited by 14 of those 18; the remaining 4 are `w_generator_or_coroutine_new` and
its three callers, which decline for the other reason — three `ob_type`
constants merge into one malloc, so no single vtable stands for the cluster —
and are exactly the 4 that survive.

Clearing the reject is not the same as lifting the graph. Of the 18: 9 now
annotate, 4 keep that reject, and 5 fail on a different error it was standing
in front of (`compute_at_fixpoint`, on `unary_negative_value` /
`unary_invert_value` under both spellings and on `PositionCookie::to_object`).

Across the whole census phaseA goes 1497 -> 1487. Diffing the failing-graph
sets rather than the totals decomposes that into 14 fixed and 4 newly failing —
a different 14 from the one above: the 9 that now annotate plus 5 that had been
failing only through a cascade from them (`__pyre_wrap_random`,
`w_dict_proxy_new`, `w_float_new`, `w_range_compute_item`, `w_range_new`).

The 4 that regress are `PyFrame::getorcreate_debug_data` and its three callers:

    UnionError in mergeinputargs: longobject::W_LongObject ∪ pyframe::FrameDebugData
    In <FunctionGraph of write>

`core::ptr::write` is lifted as one graph shared by every instantiation
(`Lowering::new` takes the declaration's `fd.generics`; the instantiation lives
at the call site), and the raw write survives fusion by design
(`sink_fused_boxing_aggregates_at_raw_writes`). Until now only `FrameDebugData`
reached it; `w_long_from_raw` lifting means `W_LongObject` reaches it too, and
slot 0 unions two instances with no common base. Any change that lets
`w_long_from_raw` lift arms this, so it is not something a narrower variant of
this patch avoids. The upstream shape is per-instantiation specialization —
`Specializer::Argtype` (`specialize:argtype(N)`) is already implemented — but
nothing in production sets `annspecialcase` on an LLBC-lifted function, so
wiring it is a front-end change of its own. Those 4 fall back to the legacy
walker, the same fail-closed path the reject already had them on the edge of.

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: 9df332441c

ℹ️ 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 +3348 to 3349
let Some(header) = resolve_header_plan(graph, agg) else {
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.

P2 Badge Reject a zero vtable before recording the fusion

When a registered host-static address is 0, resolve_header_plan now returns Some(HeaderPlan { vtable: 0, ... }) because resolved? checks only for absence, and this site no longer preserves the previous explicit vtable == 0 rejection. The resulting NewWithVtable reaches codewriter/assembler.rs, whose new_with_vtable arm deliberately panics on a zero vtable, turning a cluster that previously remained a fail-closed residual malloc_typed into a translation failure. Keep the cluster unfused when header.vtable == 0.

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit 18c7d29 into main Aug 13, 2026
14 of 17 checks passed
@youknowone
youknowone deleted the str branch August 13, 2026 22:57
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