Skip to content

locals() drops the frame's own extra locals, plus five defect fixes and a wasm codegen change - #1083

Merged
youknowone merged 8 commits into
mainfrom
wasm-jit
Aug 6, 2026
Merged

locals() drops the frame's own extra locals, plus five defect fixes and a wasm codegen change#1083
youknowone merged 8 commits into
mainfrom
wasm-jit

Conversation

@youknowone

@youknowone youknowone commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Six defect fixes plus one wasm codegen change, rebased onto origin/main.

locals() / vars() / dir() drop the frame's extra locals (task #62)

The zero-argument fold added in 5518b96f211 always built its mapping from a
fresh dict and bound one key per bound fastlocal, so a key the frame's own
mapping already carried — one an f_locals write put there (PEP 667) —
disappeared for as long as the loop stayed compiled, and fast2locals'
delitem arm was never emitted.

import sys
def f(n):
    sys._getframe().f_locals["extra"] = 99
    hits = 0
    for i in range(n):
        hits += "extra" in locals()
    return hits, n
print(f(200000))   # was (1041, 200000); CPython 3.14 and PYRE_NO_JIT=1 give (200000, 200000)

The fold now models pyframe.py:555-574 line by line: getorcreatedebug()
comes off the debugdata virtualizable field (so the frame never becomes a
residual operand and nothing arms the vable protocol), a new FrameDebugData
descr group carries w_locals for the emitted getfield_gc_r, unbound slots
emit a delitem, and locals() / vars() publish frame_locals_snapshot's
independent PEP 667 copy while dir() keeps reading the mapping itself.

A frame that has no mapping yet keeps the fresh newdict the residual would
have materialised, under a guard_isnull so a mapping appearing mid-loop
side-exits rather than being written past. Two details worth calling out:

  • The shadow's FrameDebugData is not the frame's — a root portal seed
    expands the shadow from the snapshot_for_tracing copy, whose
    clone_debugdata_ptr hands out a fresh holder around the same mapping.
    Comparing holders declines every portal trace; the check compares the
    mapping.
  • Keeping the fresh-dict arm is what preserves recursive_forced_frame_kept_stack
    (def f(n): d = locals() — every recursive call is a fresh frame whose
    w_locals is still null). Declining there brings the vable escape back.

Other fixes

  • mapdict: a devolved terminator's dict probe swallowed a raising __eq__
    and reported a miss. The read chain now has a fallible sibling and three
    call sites (getdictvalue, getattr_str_impl, object_getattribute)
    route through it.
  • dict: w_dict_getitem_wtf8_checked drained the pending-error flag after
    the unchecked spelling, and the strategy leaf
    (w_dict_lookup_object_strategy) is itself ..._checked(..).unwrap_or(None)
    — so it had already taken the flag and the check always read false. It now
    routes through w_dict_lookup_checked.
  • listobject: w_list_init_items released and reacquired the lock around
    the storage replacement, leaving the Object items block unpinned.
  • signal: siginterrupt, strsignal and pthread_sigmask's set walk
    narrowed before range-checking.
  • wasm backend: fuse a comparison into the guard that consumes it (the
    operand stack playing the role x86 condition flags play in
    next_op_can_accept_cc), and drop identity JUMP self-moves. −1.77% emitted
    module bytes over 40 trace modules on four benches, same module counts.

Verification

pyre/check.py: dynasm 388 passed, cranelift 388 passed, wasm 384 passed.
cargo test --all --no-default-features --features dynasm: rc=0, 101 test
binaries ok, zero failures.

One row fails and it is not this branch's:
synth/pickle_terminal_raise_resume loops_compiled 35 -> 30 (wasm 72 -> 67).
Controlled by path-checkout on this host:

tree content reads
committed baseline (recorded by #1075 on CI) 35
this branch without the #62 commit 30
this branch with the mapdict + dict commits reverted 30
origin/main content, 3 runs 30 / 30 / 30

origin/main misses a baseline it recorded itself. Deliberately not
re-recorded.

bdea82761d4 and e2625032895 net to zero — their content reached main
through #1075 — and are kept only to avoid rewriting the branch.

Summary by CodeRabbit

  • New Features

    • Improved locals(), vars(), and dir() behavior in optimized code, including preservation of existing local variables.
    • Added safer dictionary and frame-local handling for dynamic language operations.
  • Bug Fixes

    • Prevented invalid signal numbers from being truncated into valid signals.
    • Improved error propagation during attribute and dictionary lookups.
    • Fixed list storage updates during concurrent memory operations.
    • Optimized conditional execution while preserving correct behavior.

`vable_after_residual_call` raises
`SwitchToBlackhole(Counters.ABORT_ESCAPE, raising_exception=True)`
(pyjitpl.py:3389-3390).  The walker's `VableEscapedDuringResidualCall`
decline staged no reason at all, so the ladder in `jitdriver` fell through
to `AbortReason::Generic`, whose `as_int()` is `ABORT_BRIDGE`.  The escape
was therefore tallied in the slot `jitprof.rs ABORT_COUNTER_KINDS` labels
`bridge_or_generic` -- a counter named after bridges, for a decline with no
bridge in it.

`note_vable_escape_abort` stages `counters::ABORT_ESCAPE` at the single
`Err(DispatchError::VableEscapedDuringResidualCall)` site, next to the
existing `note_force_quasi_immut_abort`, which was the only
`stage_abort_reason` caller in the tree.

`raising_exception` needs no counterpart here: pyre derives it from the
residual's own `exec_result` rather than from the staged reason, and the
escape's `Err` arm already sets it.

Measured on recursive_forced_frame_kept_stack (with the pending `locals()`
frame-force change in the tree, which is what produces an escape there):
`abrt_bridge=1 abrt_escape=0` becomes `abrt_bridge=0 abrt_escape=1`.
`loops_compiled=1 bridges_compiled=3 loops_aborted=1 guard_failures=600`
are unchanged -- this reclassifies a tally, it does not recover a trace.
No `.jitstats` field records an `abrt_*` counter, so no baseline moves.

Assisted-by: Claude
…alled

`note_vable_escape_abort` reached the driver through
`driver::driver_pair`, which resolves it via `callbacks::get` and panics
with "CallJitCallbacks not initialized" when no table is installed.  A
skeleton walk drives `jitcode_dispatch` directly with no pyre-jit eval
behind it, so `jitcode_dispatch::tests::may_force_vable_escape_surfaces_
typed_abort` — which exercises exactly the escape arm the note was added
to — panicked instead of asserting its typed error.  All three
`cargo test` legs failed on it, each 341 passed / 1 failed.

Add `driver::try_driver_pair`, the `callbacks::try_get` spelling of the
same lookup, and route both abort-reason notes through one
`stage_walker_abort_reason` that returns early without a driver.  The
adjacent `drain_backend_jit_exc` call in the same arm
(residual_call.rs:3223-3227) already takes `try_get` for this reason.

Staging a reason is accounting only: the abort travels in the
`DispatchError` the caller returns and no consumer reads the slot back,
so skipping it changes nothing a skeleton walk observes.  Production is
unaffected — the walker reaches the panicking `driver_pair` at ten other
sites, so a callback-less thread cannot get this far.
`note_force_quasi_immut_abort` had the same latent panic and is covered
by the shared helper.

cargo test --all --features dynasm: 7489 passed, 0 failed (101
harnesses).  The cranelift subset CI runs after it: 3054 passed, 0
failed (46 harnesses).

Assisted-by: Claude
…t walk before narrowing

The earlier pass gave `signal`/`getsignal` and `sigwait`'s set walk a
`check_signum_in_range` before the `as i32`, but left three siblings
narrowing first, so `(1 << 32) | SIGINT` still truncated to `SIGINT` and
acted on the signal it aliases.

Upstream authority, per entry point:

  siginterrupt      interp_signal.py:388  `check_signum_in_range(space, signum)`
  pthread_sigmask   interp_signal.py:492  `SignalMask.__enter__` checks every
                                          element -- the same helper `sigwait`
                                          already goes through here
  strsignal         interp_signal.py:593  spells the bound inline

`raise_signal`, `pthread_kill` and `pidfd_send_signal` are left alone:
the first two have no upstream range check (`pthread_kill` passes signum
straight to `c_pthread_kill` and reports through errno; the
`check_signum_in_range` at :492 belongs to `SignalMask.__enter__`, not to
`pthread_kill`), and `pidfd_send_signal` has no upstream counterpart.

`strsignal` does not reuse upstream's own bound.  interp_signal.py:593
writes `signalnum > NSIG`, which admits `NSIG`; 3.14 rejects it, and its
`pthread_sigmask` reports the range as `[1; NSIG - 1]`.  Take the
half-open `check_signum_in_range` the other entry points use, so
`strsignal(NSIG)` raises where pypy returns `'Unknown signal: 32'`.

Measured, `BIG = (1 << 32) | 2`:

                              before          after       3.14
  siginterrupt(BIG, 1)        acted on SIGINT ValueError  OverflowError
  strsignal(BIG)              'Interrupt'     ValueError  OverflowError
  pthread_sigmask(_, [BIG])   blocked SIGINT  ValueError  ValueError
  strsignal(NSIG)             'Unknown 32'    ValueError  ValueError
  strsignal(0)                ValueError      ValueError  ValueError

The remaining delta is the exception class for the out-of-C-int case:
pyre raises `ValueError` where 3.14 raises `OverflowError`, matching pypy
and matching what `signal`/`getsignal` already do here.  Moving that to
an `OverflowError`-raising unwrap is module-wide and is tracked
separately.

test.test_signal: SiginterruptTest passes 3/3.  The suite's
`StressTest.test_stress_delivery_simultaneous` segfaults, on this commit
and on a control binary built with these edits reverted alike, so it is
untouched by this change and filed on its own.

Assisted-by: Claude
… acquire

Taking `w_list_lock` in `w_list_init_items` put a safepoint between
`build_list_storage` and the store that installs its result.  `obj` and
the two typed blocks were bracketed across it; the Object items block was
not.

`build_list_storage` returns that block young: `alloc_list_items_block_gc`
pins it only inside its own `push_roots` scope, which ends at its return,
and the block takes its heap edge from `list.items = storage.block`
below.  Between the two it is a bare Rust local, and the root walker is
shadow-stack-only.  A contended `w_list_lock` goes through
`before_external_block`, which releases the GIL and drops this thread
from the RUNNING census -- that is precisely the state in which another
mutator takes the GIL and collects without waiting for this one.  The
block is unreachable to that collection, so the address stored at the
install is dead, and `list_write_barrier` then hands it to the collector
to trace as a live varsize array.

`reload_typed_blocks` cannot cover it: `ListStorage` carries
`int_block_root` and `float_block_root` and no slot for `block`.

Pin it before the acquire and re-read it where the typed blocks are
re-read -- after `drop_object_items`, whose `try_gc_owns_object` query is
the other safepoint in the window.  This is the bracket
`w_list_new_with_strategy` already puts around its header allocation.

Reachability needs two registered mutators whose lists collide on the
256-way stripe `(obj >> 4) & 255`, with the loser blocking while the
winner allocates.

Folding `block_root` into `ListStorage` so `reload_typed_blocks` covers
all three, which would let `w_list_new_with_strategy` drop its hand-rolled
copy, is a refactor and is left out.

cargo test -p pyre-jit --test gc_stress: 32 passed, 0 failed.

Assisted-by: Claude
…dict probe

`instance_node_getdictvalue` returned `Option<PyObjectRef>`, so the devolved
terminator's `space.finditem_str` equivalent (mapdict.py:383-388) reported a
raising comparison as a miss.  A stored non-string key whose hash collides with
the probed name can reach a user `__eq__`; the attribute then read as absent
and the class attribute won as the value.

Add checked variants down the read path -- `w_dict_getitem_wtf8_checked`,
`terminator_read_checked`, `node_read_checked`,
`instance_node_getdictvalue_checked` -- and write each swallowing spelling as
`checked(..).unwrap_or(None)`, whose `unwrap_or` also consumes the pending
error slot.  `W_Root.getdictvalue` and the two getattr step-3 sites take the
checked spelling.  The `dont_look_inside` residual `instance_node_getdictvalue`
keeps its signature and its JIT helper caller.

    class K:
        def __hash__(self): return hash("zz")
        def __eq__(self, other): raise ValueError("boom")

    class R(_random.Random):
        zz = "CLASSVALUE"
    r = R()
    for i in range(90): setattr(r, "a%d" % i, i)
    r.__dict__[K()] = 1
    r.zz

printed `CLASSVALUE`; python3.14 and pypy3 both raise `ValueError: boom`, and
now so does pyre.

Assisted-by: Claude
…ntity JUMP self-moves

`llsupport/regalloc.py:873 next_op_can_accept_cc` lets a comparison hand its
condition straight to the next op instead of materialising a boolean; x86
leaves it in the flags (`x86/regalloc.py:265 force_allocate_reg_or_cc`) and the
dynasm sibling ports it at `regalloc.rs:3665`.  wasm's operand stack plays the
same role, so the comparison's i32 now stays on the stack and the guard's `if`
tests it, dropping the `i64.extend_i32_u`/`local.set` and the guard's own
`local.get` re-test.  The wasm port takes only `GuardTrue`/`GuardFalse`, and
inverts with `i32.eqz` rather than the mirrored comparison, which would differ
on an unordered float operand.

The condition producers -- the six signed and four unsigned integer compares,
the pointer compares, the six float compares, `IntIsTrue` and `IntIsZero` --
move behind one `CondKind`, with `push_cond` leaving the i32 on the stack and
`emit_cond` the widening spelling.  `next_op_can_accept_cc` refuses when the
result has another reader (`HomeLiveness::last_use`), when it is one of the
guard's own fail args, when a LABEL resume loader captures it, or when it has a
Ref home.

Separately, the loop-closing JUMP's parallel move emitted `local.get v` /
`local.set v` for a pair whose jump arg is its label arg; the ref-home refresh
nine lines below already skipped that case.  Filter those pairs out of the move
as well.

Emitted module bytes over pyre/bench/{fib_loop,int_loop,nbody,fannkuch}
(PYRE_WASM_DUMP_ALL_TRACES, 40 trace modules): 166985 -> 164034, -1.77%
(-2.47% / -3.53% / -2.27% / -1.51%), with the same module count per bench.
`i64.extend_i32_u` occurrences 375 -> 261 on nbody and 695 -> 522 on fannkuch.
check.py: dynasm 389/389, cranelift 389/389, wasm 385/385, no jitstats change.

Assisted-by: Claude
The checked wrapper called the unchecked `w_dict_getitem_wtf8` and then drained
the key-error flag.  The strategy leaf it reaches,
`w_dict_lookup_object_strategy` (dictmultiobject.rs:2663-2669), is itself
`w_dict_lookup_object_strategy_checked(..).unwrap_or(None)`, so the flag was
already taken by the time the unchecked spelling returned and the trailing
`take_dict_key_error()` always read `false`.

That made 06cec233f0a's mapdict path inert: a devolved terminator's read of
`r.zz` against an instance dict holding a hash-colliding key with a raising
`__eq__` still reported a miss, and the class attribute won.  Verified on
`target/release/pyre-dynasm` (not `target/release/pyre`, which is a stale
leftover no build in the loop refreshes):

    RESULT: CLASSVALUE      before
    RAISED: ValueError boom  after, matching python3.14 and pypy3

check.py: dynasm 389/389, cranelift 389/389, wasm 385/385.
`cargo test --all --no-default-features --features dynasm`: 0 failed.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 39 minutes

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: 80f57d46-833b-4d49-b2f6-668f6f14d596

📥 Commits

Reviewing files that changed from the base of the PR and between e4f299c and 536a619.

📒 Files selected for processing (10)
  • majit/majit-backend-wasm/src/codegen.rs
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/module/signal/interp_signal.rs
  • pyre/pyre-interpreter/src/objspace/std/mapdict.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/virtualizable_spec.rs
  • pyre/pyre-object/src/dictmultiobject.rs
  • pyre/pyre-object/src/listobject.rs

Walkthrough

The PR adds WASM comparison-condition fusion, checked mapdict lookups, frame-owned JIT locals materialization, signal-range validation, and GC-safe list storage initialization.

Changes

WASM condition lowering

Layer / File(s) Summary
Condition fusion and unified emission
majit/majit-backend-wasm/src/codegen.rs
Comparison and truth-test operations use shared condition emission. Matching guards consume conditions directly when liveness checks allow fusion.
Jump parallel moves
majit/majit-backend-wasm/src/codegen.rs
Loop jump moves skip identity assignments while preserving validation and reverse-order writes.

Checked mapdict reads

Layer / File(s) Summary
Checked lookup propagation
pyre/pyre-object/src/dictmultiobject.rs, pyre/pyre-interpreter/src/objspace/std/mapdict.rs, pyre/pyre-interpreter/src/baseobjspace.rs
WTF-8 dictionary, mapdict node, terminator, and instance lookups now preserve lookup errors through checked APIs and interpreter callers.

JIT locals materialization

Layer / File(s) Summary
Frame locals contracts
pyre/pyre-interpreter/src/pyframe.rs, pyre/pyre-jit-trace/src/descr.rs, pyre/pyre-jit-trace/src/virtualizable_spec.rs
Frame layout constants and descriptors expose FrameDebugData.w_locals and validate the debugdata virtualizable field.
Locals helper operations
pyre/pyre-interpreter/src/pyframe.rs
JIT helpers delete unbound locals and create independent locals snapshots with failure signaling.
Frame-owned locals specialization
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
Specialization reuses valid frame mappings, updates bound and unbound slots, and selects result handling for locals(), vars(), and dir().

Signal range validation

Layer / File(s) Summary
Signal argument validation
pyre/pyre-interpreter/src/module/signal/interp_signal.rs
Signal APIs validate full integer arguments before narrowing them to i32.

List storage rooting

Layer / File(s) Summary
Object-strategy items rooting
pyre/pyre-object/src/listobject.rs
List initialization roots object-strategy storage across lock acquisition and reloads its relocated pointer.

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

Possibly related PRs

Suggested reviewers: lifthrasiir

Sequence Diagram(s)

sequenceDiagram
  participant JITSpecializer
  participant FrameDebugData
  participant LocalsDictionary
  participant ResultHelper
  JITSpecializer->>FrameDebugData: resolve debugdata.w_locals
  FrameDebugData-->>JITSpecializer: existing mapping or absence
  JITSpecializer->>LocalsDictionary: update bound or delete unbound local
  LocalsDictionary-->>JITSpecializer: updated mapping or failure
  JITSpecializer->>ResultHelper: create snapshot or sorted names
  ResultHelper-->>JITSpecializer: selected result or failure
Loading

Poem

I hop through conditions, crisp and bright,

Keep guards on the stack when the paths align right.
I root list blocks and check signals with care,
Carry errors through maps in the open air.
Frame locals now follow a safer track—
Squeak, squeak, the rabbit approves the stack!

🚥 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 locals() fix and identifies the additional defect fixes and WebAssembly codegen change.
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 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wasm-jit

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 6, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 536a619).
Updated: 2026-08-06T15:23:54.680Z

Files in the reviewed diff
lib-python/3/subprocess.py
majit/majit-backend-wasm/src/codegen.rs
majit/majit-gc/src/collector.rs
pyre/bench/synth/arith_int_bool.py
pyre/bench/synth/attr_cache_invalidation.py
pyre/bench/synth/attr_instance_shadows_class.py
pyre/bench/synth/attr_store_add_transition.py
pyre/bench/synth/bases_reassign_cache.py
pyre/bench/synth/binary_int_overflow_local_resume.py
pyre/bench/synth/binary_slice_index.py
pyre/bench/synth/bound_method_builtin_fold.py
pyre/bench/synth/break_except_live_local.py
pyre/bench/synth/bridge_branchy_callee.py
pyre/bench/synth/bridge_global_fold_invalidate_hot.py
pyre/bench/synth/bridge_recursion_overflow.py
pyre/bench/synth/build_class_surrogate_namespace.py
pyre/bench/synth/build_container_return_resume.py
pyre/bench/synth/build_set_hashability.py
pyre/bench/synth/bytes_split_whitespace_maxsplit.py
pyre/bench/synth/ca_bridge_multiframe_resume_double_call.py
pyre/bench/synth/call_loop_local_function.py
pyre/bench/synth/call_star_forms_inlined_callee.py
pyre/bench/synth/callable_iterator_type.py
pyre/bench/synth/calls_closures.py
pyre/bench/synth/class_attrs_methods.py
pyre/bench/synth/class_reassign_hot.py
pyre/bench/synth/closure_freevar_branch_resume.py
pyre/bench/synth/closure_per_call.py
pyre/bench/synth/complex_real_imag.py
pyre/bench/synth/comprehension_object_append_hot.py
pyre/bench/synth/const_arg_call_resume.py
pyre/bench/synth/context_manager.py
pyre/bench/synth/delete_negative_open_slice_hot.py
pyre/bench/synth/dict_ctor_consume.py
pyre/bench/synth/dict_hash_protocol.py
pyre/bench/synth/dict_set.py
pyre/bench/synth/dict_set_key_eq_operand_order.py
pyre/bench/synth/dict_update_hot.py
pyre/bench/synth/dict_view_set_ops.py
pyre/bench/synth/dir_dict_class_attrs.py
pyre/bench/synth/divmod_long_int_pair.py
pyre/bench/synth/dunder_repr_str_errors.py
pyre/bench/synth/exc_bridge_entry_guard_not_removed.py
pyre/bench/synth/exc_caught_in_callee_return_loop.py
pyre/bench/synth/exc_in_loop_divzero_continue.py
pyre/bench/synth/except_star.py
pyre/bench/synth/exception_args_virtual.py
pyre/bench/synth/exception_as_cell_cleanup.py
pyre/bench/synth/exception_bare_reraise_nested_outer.py
pyre/bench/synth/exception_catching_frame_tb_node.py
pyre/bench/synth/exception_const_operand_resume.py
pyre/bench/synth/exception_escape_hot_callee_tb_node_once.py
pyre/bench/synth/exception_group_type.py
pyre/bench/synth/exception_inline_callee_tb_frames.py
pyre/bench/synth/exception_inlined_callee_caught.py
pyre/bench/synth/exception_metadata_hot.py
pyre/bench/synth/exception_metadata_jitstress.py
pyre/bench/synth/exception_multi_handler_warmup.py
pyre/bench/synth/exception_oserror_fields.py
pyre/bench/synth/exception_raise_caught_same_frame_tb.py
pyre/bench/synth/exception_reduce.py
pyre/bench/synth/exception_reraise_tb_depth_hot.py
pyre/bench/synth/exception_reraise_tb_depth_jitstress.py
pyre/bench/synth/exception_residual_raise_caught_in_frame.py
pyre/bench/synth/exception_subclass_attrs.py
pyre/bench/synth/exception_traceback_lineno_chain.py
pyre/bench/synth/exception_traceback_loop_forms.py
pyre/bench/synth/exception_try_call_inlined_callee_raise.py
pyre/bench/synth/exception_value_op_caught.py
pyre/bench/synth/fast_local_swap.py
pyre/bench/synth/finally_bare_raise.py
pyre/bench/synth/float_div_zero_caught_loop.py
pyre/bench/synth/float_subclass_binop_dispatch.py
pyre/bench/synth/for_iter_select_receiver_swap.py
pyre/bench/synth/foriter_call_body.py
pyre/bench/synth/foriter_call_resume_drops_iteration.py
pyre/bench/synth/foriter_exempt_nested_foriter.py
pyre/bench/synth/foriter_in_while.py
pyre/bench/synth/foriter_inplace_immutable.py
pyre/bench/synth/foriter_user_iter_kept_stack.py
pyre/bench/synth/format_z_negative_zero.py
pyre/bench/synth/gc_deque_backing_list.py
pyre/bench/synth/gc_iterator_source_drop.py
pyre/bench/synth/generator_pep479.py
pyre/bench/synth/generator_tree_recursion.py
pyre/bench/synth/getattr_surrogate_hook.py
pyre/bench/synth/getattribute_override_no_bind.py
pyre/bench/synth/getframe_bridge_force_after_store.py
pyre/bench/synth/getframe_bridge_force_plain.py
pyre/bench/synth/getframe_force_cancel_journal.py
pyre/bench/synth/getframe_inlined_callee_own_frame.py
pyre/bench/synth/getframe_residual_callee_own_frame.py
pyre/bench/synth/getframe_stored_fback_walk.py
pyre/bench/synth/getframe_while_caller_locals_across_subwalk.py
pyre/bench/synth/getframe_while_captured_frame_outlives_call.py
pyre/bench/synth/getframe_while_escaping_read_frame_identity.py
pyre/bench/synth/getframe_while_inlined_callee_subwalk.py
pyre/bench/synth/getframe_while_subwalk_decline_shapes.py
pyre/bench/synth/global_cell_shortpreamble_hot.py
pyre/bench/synth/global_quasiimmut_invalidation.py
pyre/bench/synth/global_reassign.py
pyre/bench/synth/global_reassign_invalidation.py
pyre/bench/synth/goto_if_not_same_box.py
pyre/bench/synth/handler_reraise_second_exc.py
pyre/bench/synth/hash_subclass_disabled.py
pyre/bench/synth/if_else_jump_forward.py
pyre/bench/synth/import_from_name_path.py
pyre/bench/synth/import_none_sentinel.py
pyre/bench/synth/inheritance_dispatch.py
pyre/bench/synth/inline_chain_depth_typeflip.py
pyre/bench/synth/inline_freevar_after_mayforce.py
pyre/bench/synth/inline_gate_operand_provenance.py
pyre/bench/synth/inline_multiframe_branchy_carrier.py
pyre/bench/synth/inline_subwalk_mutating_residual.py
pyre/bench/synth/inline_subwalk_property_mutates.py
pyre/bench/synth/inline_subwalk_radd_consumed.py
pyre/bench/synth/inline_subwalk_user_iterator.py
pyre/bench/synth/inlined_callee_extended_arg_handler.py
pyre/bench/synth/inlined_helper_arith_hot.py
pyre/bench/synth/inlined_helper_mutation.py
pyre/bench/synth/instance_dict_reassign.py
pyre/bench/synth/instance_surrogate_attrs.py
pyre/bench/synth/int_base0_error_literal.py
pyre/bench/synth/int_from_bad_dunder.py
pyre/bench/synth/int_lshift_memoryerror.py
pyre/bench/synth/int_max_str_digits.py
pyre/bench/synth/int_mul_ovf_bignum_promote.py
pyre/bench/synth/itertools_cycle.py
pyre/bench/synth/jit_callee_raised_exc_value.py
pyre/bench/synth/jit_reg_const_pool_256_slot_decline.py
pyre/bench/synth/kept_stack_boxed_in_handler.py
pyre/bench/synth/kept_stack_branch_depths.py
pyre/bench/synth/kept_stack_deep_var_condexpr.py
pyre/bench/synth/kept_stack_deep_var_shortcircuit.py
pyre/bench/synth/kept_stack_depth_gt1.py
pyre/bench/synth/kept_stack_depth_gt1_heap.py
pyre/bench/synth/key_eq_resize_restart.py
pyre/bench/synth/key_eq_restart_forgets.py
pyre/bench/synth/kwargs_positional_only.py
pyre/bench/synth/len_dunder_validation.py
pyre/bench/synth/list_append_funcentry_helper.py
pyre/bench/synth/list_append_write_barrier_gc.py
pyre/bench/synth/list_bound_method_mutation.py
pyre/bench/synth/list_error_parity.py
pyre/bench/synth/list_inplace_mul_parity.py
pyre/bench/synth/list_insert.py
pyre/bench/synth/list_insert_pop_index.py
pyre/bench/synth/list_length_hint_validate.py
pyre/bench/synth/list_ops.py
pyre/bench/synth/list_reverse.py
pyre/bench/synth/list_setslice.py
pyre/bench/synth/list_subscript_index.py
pyre/bench/synth/list_to_tuple_star.py
pyre/bench/synth/listcomp_hot.py
pyre/bench/synth/load_fast_check.py
pyre/bench/synth/loop_callee_return.py
pyre/bench/synth/loop_exit_empty_dict_local_clobber.py
pyre/bench/synth/loop_in_try_raise_into_handler.py
pyre/bench/synth/loop_in_try_tail_raise_and_second_loop.py
pyre/bench/synth/loop_in_try_tail_unbound_check.py
pyre/bench/synth/loops_comprehension.py
pyre/bench/synth/mapdict_frozen_unboxing_fold.py
pyre/bench/synth/mapdict_unboxed_type_change_attr.py
pyre/bench/synth/match_sequence_of_class_patterns.py
pyre/bench/synth/math_isqrt_compare_bridge_resume.py
pyre/bench/synth/math_log_trig_hot.py
pyre/bench/synth/math_sqrt_hot.py
pyre/bench/synth/metaclass_getattribute_delattr.py
pyre/bench/synth/method_reassign_after_warmup.py
pyre/bench/synth/mutate_then_raise_caught.py
pyre/bench/synth/mutate_uncaught_raise_delivery.py
pyre/bench/synth/named_reraise_sibling_hot.py
pyre/bench/synth/nested_break_not_hot.py
pyre/bench/synth/nested_callee_chain_mutation_abort.py
pyre/bench/synth/nested_for_outer_local_postread.py
pyre/bench/synth/nested_list_comprehension_hot.py
pyre/bench/synth/nested_loop_correctness.py
pyre/bench/synth/nested_loop_gate_switch.py
pyre/bench/synth/newslice_step_hot.py
pyre/bench/synth/p2_local_result_bridge.py
pyre/bench/synth/pickle_ctor_args.py
pyre/bench/synth/polymorphic_binary_receiver.py
pyre/bench/synth/polymorphic_slot_retype.py
pyre/bench/synth/pow3_arg_types.py
pyre/bench/synth/range_ctor_in_loop.py
pyre/bench/synth/recursion_memo_branch.py
pyre/bench/synth/recursive_call_frame_relocation.py
pyre/bench/synth/residual_raise_except_resume.py
pyre/bench/synth/reversed_disabled.py
pyre/bench/synth/selfrec_tail_exception_unwind.py
pyre/bench/synth/seqiter_tuple_error_parity.py
pyre/bench/synth/set_hash_protocol.py
pyre/bench/synth/set_intersection_operand.py
pyre/bench/synth/set_key_protocol.py
pyre/bench/synth/set_method_arity.py
pyre/bench/synth/set_name_filtered_dict.py
pyre/bench/synth/set_remove_ord_errors.py
pyre/bench/synth/set_update_hash_other.py
pyre/bench/synth/set_update_hot.py
pyre/bench/synth/set_update_materialize_rhs.py
pyre/bench/synth/short_circuit_side_effects.py
pyre/bench/synth/short_circuit_value_kept_stack.py
pyre/bench/synth/short_circuit_value_local_kept.py
pyre/bench/synth/simple_namespace_type.py
pyre/bench/synth/slots_class_var_conflict.py
pyre/bench/synth/sre_pattern_methods.py
pyre/bench/synth/store_global_hot.py
pyre/bench/synth/store_slice_hot.py
pyre/bench/synth/str_encode_text_codec.py
pyre/bench/synth/str_fstring.py
pyre/bench/synth/str_getitem_len_hot.py
pyre/bench/synth/str_index_bytes_iter_surface.py
pyre/bench/synth/str_startswith_bounds.py
pyre/bench/synth/struct_pack_unpack.py
pyre/bench/synth/subscr_negative_index_deopt.py
pyre/bench/synth/subscr_user_getitem_inline.py
pyre/bench/synth/surrogate_class_kwargs.py
pyre/bench/synth/surrogate_dir.py
pyre/bench/synth/surrogate_kwargs.py
pyre/bench/synth/surrogate_metaclass_kwargs.py
pyre/bench/synth/swap_except_return_resume.py
pyre/bench/synth/syntaxerror_location.py
pyre/bench/synth/syntaxerror_str.py
pyre/bench/synth/trace_too_long_effect_replay.py
pyre/bench/synth/tuple_contains_eq_raises.py
pyre/bench/synth/tuple_str_bytes_subscript_index.py
pyre/bench/synth/tuple_unpack_array_backed_hot.py
pyre/bench/synth/type_call_inline_init_branch_deopt.py
pyre/bench/synth/type_descr_get_metaclass_getattr.py
pyre/bench/synth/type_dict_surrogate.py
pyre/bench/synth/type_dotted_name.py
pyre/bench/synth/type_error_message_parity.py
pyre/bench/synth/type_immutable_reject.py
pyre/bench/synth/type_metatype_data_descr.py
pyre/bench/synth/type_name_setter.py
pyre/bench/synth/type_name_surrogate_reject.py
pyre/bench/synth/unary_int_loop_carried.py
pyre/bench/synth/unary_negative.py
pyre/bench/synth/unary_positive_resume.py
pyre/bench/synth/unpack_drain_star_raise.py
pyre/bench/synth/unpack_ex_hot.py
pyre/bench/synth/unpack_wrong_arity_caught.py
pyre/bench/synth/wasm_ca_trampoline_decline.py
pyre/bench/synth/while_is_none.py
pyre/bench/synth/with_except_start_function_resume.py
pyre/check.py
pyre/extra_tests/parity_tests/compile_filename_boundary.py
pyre/extra_tests/parity_tests/dict_subscript_fold.py
pyre/extra_tests/parity_tests/dir_entry_uninstantiable_python314.py
pyre/extra_tests/parity_tests/exception_instance_dict_attr.py
pyre/extra_tests/parity_tests/import_unencodable_path_entry.py
pyre/extra_tests/parity_tests/memoryio_seek_whence_range.py
pyre/extra_tests/parity_tests/object_init_text_signature.py
pyre/extra_tests/parity_tests/os_call_effects.py
pyre/extra_tests/parity_tests/os_non_ascii_path.py
pyre/extra_tests/parity_tests/os_stat_file_descriptor.py
pyre/extra_tests/parity_tests/oserror_winerror.py
pyre/extra_tests/parity_tests/oserror_winerror_syscall.py
pyre/extra_tests/parity_tests/repr_surrogate_wtf8.py
pyre/extra_tests/parity_tests/run.py
pyre/extra_tests/parity_tests/subprocess_launch.py
pyre/extra_tests/parity_tests/symtable_filename_surrogateescape.py
pyre/extra_tests/parity_tests/thread_start_walk_abort_no_replay.py
pyre/extra_tests/parity_tests/type_members_python314.py
pyre/pyre-interpreter/Cargo.toml
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/call.rs
pyre/pyre-interpreter/src/display.rs
pyre/pyre-interpreter/src/error.rs
pyre/pyre-interpreter/src/gateway.rs
pyre/pyre-interpreter/src/module/_io/bytesio.rs
pyre/pyre-interpreter/src/module/_io/stringio.rs
pyre/pyre-interpreter/src/module/_symtable/mod.rs
pyre/pyre-interpreter/src/module/_winapi/mod.rs
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-interpreter/src/module/signal/interp_signal.rs
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-interpreter/src/module/thread/mod.rs
pyre/pyre-interpreter/src/objspace/std/mapdict.rs
pyre/pyre-interpreter/src/pyframe.rs
pyre/pyre-interpreter/src/typedef.rs
pyre/pyre-jit-trace/src/descr.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
pyre/pyre-jit-trace/src/liveness.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit-trace/src/virtualizable_spec.rs
pyre/pyre-jit/src/lib.rs
pyre/pyre-macros/src/lib.rs
pyre/pyre-object/src/dictmultiobject.rs
pyre/pyre-object/src/interp_exceptions.rs
pyre/pyre-object/src/listobject.rs
pyre/pyre-object/src/typedef.rs
pyre/pyre-object/src/typeobject.rs
pyre/pyre-wasm-runner/src/main.rs
pyre/pyre-wasm/src/lib.rs
pyre/pyrex/src/lib.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-interpreter/src/builtins.rs:5851 ↔ pypy/module/exceptions/interp_exceptions.py:565: Windows OSError construction now explicitly omits winerror, its errno remapping, and its attribute. PyPy retains the fourth argument on Windows and derives errno from it.

  • pyre/pyre-interpreter/src/module/_winapi/mod.rs:23 ↔ lib_pypy/_winapi.py:59: the patch removes _winapi process-launch primitives (CreatePipe, DuplicateHandle, CreateProcess, etc.), so Windows subprocess.Popen cannot launch a process. The PyPy implementation supplies these APIs.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:470 ↔ pypy/interpreter/error.py:792: Windows filesystem failures are converted to POSIX-style errno errors, losing the Win32 code/message and .winerror; PyPy preserves a WindowsError as w_winerror.

  • majit/majit-gc/src/collector.rs:94 ↔ rpython/memory/gc/env.py:17: the collector now reads only the process environment. This regresses the prior wasm host-supplied environment path, making PYPY_GC_* settings unavailable where std::env has no environment.

2. Other mismatches introduced by this patch

  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs:7060 ↔ pypy/interpreter/pyframe.py:552: the new fast2locals JIT helper is emitted as CannotRaise, but jit_locals_dict_setitem_local calls unchecked w_dict_setitem_str (pyre/pyre-interpreter/src/pyframe.rs:4470). A colliding user key can invoke a raising __eq__; PyPy’s space.setitem_str propagates that exception.

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

  • pyre/pyre-interpreter/src/eval.rs:3235 ↔ pypy/objspace/std/callmethod.py:63: the untouched LOAD_METHOD shadow check still calls the error-swallowing instance_node_getdictvalue. PyPy’s getdictvalue → map.read → finditem_str propagates a raising equality comparison from a devolved dict.

  • majit/majit-gc/src/collector.rs:120 ↔ rpython/memory/gc/env.py:42: negative GC environment values are treated as unset, while PyPy converts with r_uint, yielding the unsigned wrapped value. This behavior predates the patch.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/signal/interp_signal.rs:627 ↔ pypy/module/signal/interp_signal.py:592: strsignal(NSIG) is intentionally rejected to match Python 3.14, whereas PyPy’s inclusive upper-bound check admits NSIG. This is a Python-version adaptation.

  • pyre/pyre-interpreter/src/pyframe.rs:4430 ↔ pypy/interpreter/pyframe.py:545: the JIT fold intentionally avoids storing a newly materialized locals dict back into the Rust frame, instead returning a PEP 667-style snapshot. PyPy stores it in debugdata.w_locals; this is a Python 3.14 frame-locals adaptation.

@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/318269abb2478aa5ce0c14bb2a56f252714025b8/pyre-jit-trace/src/jitcode_dispatch/specialize.rs#L7041-L7042
P2 Badge Propagate bound-local dictionary errors

When a frame-owned locals dict contains a non-string key whose hash collides with a bound local name, this branch calls jit_locals_dict_setitem_local, which uses unchecked w_dict_setitem_str and always returns the dict even if the stored key's __eq__ raises. The interpreter's fast2locals instead uses fallible setitem_str_object and propagates that exception, so a compiled locals()/vars()/dir() can silently continue with a different mapping. Route bound stores through a checked helper and guard its failure just as the new delete branch does.

AGENTS.md reference: AGENTS.md:L14-L19

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

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

🤖 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/module/signal/interp_signal.rs`:
- Around line 626-638: Update check_signum_in_range and its call sites,
including strsignal and the other signal-number handlers, to use the
libc/host-derived maximum signal value rather than the fixed signalstate::NSIG
value of 64. Preserve the existing lower bound and error behavior while allowing
all signals reported as valid by the host platform.

In `@pyre/pyre-object/src/dictmultiobject.rs`:
- Around line 3465-3470: Update w_dict_getitem_wtf8_checked to root obj before
constructing w_key, then reload the rooted object before passing it to
w_dict_lookup_checked. Ensure the key allocation and lookup occur while obj
remains rooted, following the existing lock_dict_refs!/rooting pattern.
🪄 Autofix

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: c4d02102-5c51-4e30-94c9-3f81ec7b869a

📥 Commits

Reviewing files that changed from the base of the PR and between 1995d92 and 318269a.

📒 Files selected for processing (10)
  • majit/majit-backend-wasm/src/codegen.rs
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/module/signal/interp_signal.rs
  • pyre/pyre-interpreter/src/objspace/std/mapdict.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/virtualizable_spec.rs
  • pyre/pyre-object/src/dictmultiobject.rs
  • pyre/pyre-object/src/listobject.rs

Comment thread pyre/pyre-interpreter/src/module/signal/interp_signal.rs
Comment on lines +3465 to +3470
pub unsafe fn w_dict_getitem_wtf8_checked(
obj: PyObjectRef,
key: &rustpython_wtf8::Wtf8,
) -> Result<Option<PyObjectRef>, DictKeyError> {
let w_key = crate::w_str_from_wtf8(key.to_wtf8_buf());
w_dict_lookup_checked(obj, w_key)

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
set -euo pipefail

rg -n -C 12 --glob '*.rs' '\bfn\s+w_str_from_wtf8\b' pyre
rg -n -C 10 --glob '*.rs' 'macro_rules!\s+lock_dict_refs|lock_dict_refs!' pyre
rg -n -C 8 --glob '*.rs' 'ShadowStack|Root(?:ed)?|root.*PyObjectRef|PyObjectRef.*root' pyre

Repository: youknowone/pyre

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focused inspection around the reported helper, definition sites, and GC root APIs.
wc -l pyre/pyre-object/src/dictmultiobject.rs
sed -n '3430,3485p' pyre/pyre-object/src/dictmultiobject.rs

# Locate targeted definitions without flooding output.
python3 - <<'PY'
import pathlib,re
root=pathlib.Path("pyre")
for label in ["fn w_str_from_wtf8","macro_rules! lock_dict_refs","lock_dict_refs!","pub fn push_roots","pin_root","shadow_stack"]:
    print(f"\n=== {label} ===")
    count=0
    for p in root.rglob("*.rs"):
        text=p.read_text(errors="ignore")
        idx=text.find(label)
        while idx!=-1 and count<8:
            line=text[:idx].count('\n')+1
            lo=max(1,line-12); hi=min(text.count('\n')+1,line+18)
            lines=text.splitlines()
            print(f"{p}:{line}")
            for i in range(lo,hi+1):
                if i<=len(lines):
                    print(f"{i}: {lines[i-1]}")
            print()
            idx=text.find(label,idx+1)
            count+=1
PY

# Locate w_str_from_wtf8 and lock macro only.
python3 - <<'PY'
import pathlib,re
for glob,pattern in [
    ("**/*.rs", r"\bfn\s+w_str_from_wtf8\b"),
    ("**/*.rs", r"macro_rules!\s+lock_dict_refs|lock_dict_refs!"),
]:
    p=rule=pathlib.Path("./pyre")
    import fnmatch
    print(f"\n--- searches for {pattern} under pyre ---")
    for f in p.rglob("*.rs"):
        try:
            text=f.read_text(errors="ignore")
        except UnicodeDecodeError:
            continue
        if re.search(pattern, text):
            print(f)
PY

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read exact definitions once locations are known.
python3 - <<'PY'
import pathlib,re
root=pathlib.Path("pyre")
targets={}
for pat in [r"\bfn\s+w_str_from_wtf8\b",r"macro_rules!\s+lock_dict_refs|lock_dict_refs!",r"pub(?: fn| struct)\s+\b(push_roots|RootScope|pin_root|shadow_stack_get)\b",r"pub\s+(?:mod|fn)\s+gc_roots"]:
    targets.setdefault(pat, []).append(0)
    print(f"\n=== {pat} ===")
    for f in root.rglob("*.rs"):
        try:
            text=f.read_text(errors="ignore")
        except UnicodeDecodeError:
            continue
        for m in re.finditer(pat, text):
            line=text.count("\n",0,m.start())+1
            print(f"{f}:{line}")
PY

# Read the helper and one nearby allocation example.
sed -n '3450,3475p' pyre/pyre-object/src/dictmultiobject.rs
git ls-files 'pyre/pyre-object/src/*.rs' | head -50

Repository: youknowone/pyre

Length of output: 6610


Root obj before constructing w_key.

w_str_from_wtf8 allocates before w_dict_lookup_checked creates DictOperationGuard and reloads obj via lock_dict_refs!. Pin obj before constructing the key and reload it before w_dict_lookup_checked(obj, w_key), or move the allocation inside the root guard with w_key also rooted.

🤖 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-object/src/dictmultiobject.rs` around lines 3465 - 3470, Update
w_dict_getitem_wtf8_checked to root obj before constructing w_key, then reload
the rooted object before passing it to w_dict_lookup_checked. Ensure the key
allocation and lookup occur while obj remains rooted, following the existing
lock_dict_refs!/rooting pattern.

Source: Learnings

The zero-argument locals()/vars()/dir() walker fold always built its mapping
from a fresh `jit_locals_dict_new()` and bound one key per bound fastlocal.  A
key the frame's own mapping already carried — one an `f_locals` write put
there (PEP 667) — was therefore absent for as long as the loop stayed
compiled, and `fast2locals`' `delitem` arm for an unbound slot was never
emitted.

Read `getorcreatedebug().w_locals` (pyframe.py:555-556) and rewrite that when
the frame already carries a mapping.  `debugdata` is a virtualizable field, so
the read answers from `virtualizable_boxes` and the frame never becomes a
residual operand.  A `FrameDebugData` descr group carries `w_locals` for the
emitted `getfield_gc_r`; the mapping is guarded non-null and exact-dict; an
unbound slot emits the new `jit_locals_dict_delitem_local`; and `locals()` /
`vars()` publish `jit_locals_dict_snapshot`, `frame_locals_snapshot`'s
independent PEP 667 copy, while `dir()` keeps reading the mapping itself
through `jit_dir_names_from_locals`.

A frame that carries no mapping yet keeps the fresh `newdict` the residual
would have materialised (pyframe.py:557), now under a `guard_isnull` on
whichever of `debugdata` / `w_locals` is absent, so a mapping that appears
mid-loop side-exits instead of being written past.  Nothing else references
that dict, so it is already the independent copy and its `delitem` arm is a
no-op.

The mapping check compares the shadow's `w_locals` against the frame's rather
than the two `FrameDebugData` holders: a root portal seed expands the shadow
from the `snapshot_for_tracing` copy, whose `clone_debugdata_ptr` hands out a
fresh holder around the same mapping, so a holder comparison declines every
portal trace.

Assisted-by: Claude
@youknowone
youknowone merged commit 678319f into main Aug 6, 2026
1 of 2 checks passed
@youknowone
youknowone deleted the wasm-jit branch August 6, 2026 14:55

@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: 536a6198e0

ℹ️ 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 +7038 to +7041
let (helper, args, arg_types): (_, Vec<OpRef>, Vec<majit_ir::Type>) = if bound {
(
pyre_interpreter::pyframe::jit_locals_dict_setitem_local as *const (),
vec![dict_op, code_const, index_const, slot_op],

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 Propagate bound-slot update failures

When an optimized frame already has an exact locals dict containing a non-string key whose hash collides with a fast-local name—for example, it was inserted through f_locals before that local was bound—this new frame-owned arm passes the dict to jit_locals_dict_setitem_local. That helper uses unchecked w_dict_setitem_str, so an ObjectDictStrategy comparison whose __eq__ raises has its DictKeyError dropped and the helper still returns a non-null dict; the emitted call is also marked CannotRaise, and only the deletion arm receives a null guard. Consequently compiled locals(), vars(), or dir() suppresses an exception that residual PyFrame::fast2locals propagates, so bound stores need a fallible propagation path too.

AGENTS.md reference: AGENTS.md:L14-L18

Useful? React with 👍 / 👎.

Comment on lines +7073 to +7075
if !bound {
// The delete reports a raising comparison as PY_NULL instead of
// publishing it; side-exit so the residual re-runs and raises.

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 Avoid replaying failed deletion callbacks

When deleting an unbound local encounters a colliding key whose __eq__ raises, jit_locals_dict_delitem_local has already executed that user callback, drained its pending error, and returned null. This guard then side-exits at the original call position so the eval loop re-runs residual locals()/vars()/dir(), invoking the same deletion and callback a second time before raising; callback mutations or counters therefore occur twice only under the JIT. Preserve and propagate the original failure instead of replaying the operation after the guard exit.

AGENTS.md reference: AGENTS.md:L14-L18

Useful? React with 👍 / 👎.

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