Commit Graph
76 Commits
Author SHA1 Message Date
Jeong, YunWon cdf006cfa1 Split InterpreterFrame from FrameObject for stack-allocated execution (#8354)
* Add LightFrame for unobserved Python-to-Python calls

Allocate a lightweight LightFrame header on the DataStack instead of a
full Frame PyObject for specialized exact-args call paths. The frame is
materialized lazily only when observed (sys._getframe, traceback, tracing).

Key changes:
- LightFrame struct with borrowed pointers and no refcount overhead
- FrameSource enum (Heavy/Light) replacing the object field on ExecutingFrame
- CURRENT_LIGHT_FRAME TLS for the light frame chain
- Interleaved heavy/light frame walking in frame_at_offset_vm and
  current_thread_frame_vm
- invoke_light_slots on PyFunction for the fast call path

Call overhead reduced from ~92ns to ~58ns (~37% reduction).
fib(28) improved from ~152ms to ~111ms (~27% reduction).

Assisted-by: Claude

* Streamline with_frame: inline recursion check, skip tracing dispatch

- Inline check_recursive_call instead of wrapping in with_recursion closure
- Amortize C stack overflow check to every 64th call depth
- Skip dispatch_traced_frame when use_tracing is false (hot path)
- Add recursion_depth_increment/decrement helpers

Assisted-by: Claude

* Address review findings for LightFrame correctness

- Fix materialize_light_frame previous pointer: use payload pointer
  (&**frame) instead of Py<Frame> pointer, matching the chain convention
- Fix predecessor ref management: use retained_back instead of
  mem::forget to keep materialized predecessors alive
- Fix materialized ref reclaim: use FrameRef::from_raw to drop the
  leaked keeper ref without double-incrementing
- Use PyAtomic<u32> for LightFrame.lasti instead of AtomicU32, removing
  the unsound cast on non-threading targets
- Add amortized C stack overflow check to light frame path
- Add scopeguard to light frame execution for panic safety
- Remove unnecessary Send/Sync impls from LightFrame
- Make check_c_stack_overflow pub(crate) for light frame path access

Assisted-by: Claude

* Fix CI failures: cspell, stale materialized state, f_back chain gaps

- Fix cspell: 'amortised' → 'amortized'
- Sync lasti, prev_line, and fastlocals when re-observing a materialized
  light frame, fixing stale f_locals/f_lineno in test_inspect and others
- In with_frame, materialize any active light frame as the heavy frame's
  predecessor and store it in retained_back, so f_back and inspect.stack
  see the correct interleaved order (H_new → L_mat → H_old)
- Guard invoke_light_slots on NEWLOCALS | OPTIMIZED code flags

Assisted-by: Claude

* Unify frame chain: replace dual-chain with single FrameChainPtr

Replace the dual-chain frame management (CURRENT_FRAME AtomicPtr<Frame>
+ CURRENT_LIGHT_FRAME Cell<*const LightFrame>) with a single unified
chain using FrameChainPtr, a tagged pointer where bit 0 distinguishes
heavy (*const Frame) from light (*const LightFrame) entries.

Changes:
- Add FrameChainPtr type with tagged pointer encoding
- Change LightFrame: replace previous_light + saved_current_frame with
  single previous: FrameChainPtr field
- Change InterpreterFrame.previous from AtomicPtr<Frame> to PyAtomic<usize>
- Change CURRENT_FRAME TLS from AtomicPtr<Frame> to AtomicUsize
- Remove CURRENT_LIGHT_FRAME TLS entirely
- Simplify all chain walkers (frame_at_offset, frame_at_offset_vm,
  find_owned_chain_frame, for_each_current_frame, current_thread_frame)
- Remove light-frame materialization block from with_frame
- Update f_back to dispatch on FrameChainPtr tags
- Update faulthandler and gc_state to walk unified chain
- Adaptive C stack check: every call in debug, every 16th in release

Assisted-by: Claude

* Fix light frame materialization: current_frame, f_back, and GC tracking

- current_frame() now uses current_thread_frame_vm() to materialize
  light frames, fixing super()/compile()/exec() when called from light
  frame context
- materialize_light_frame stores retained_back for heavy predecessors
  too, so f_back resolves after the caller returns
- Escaped materialized frames (traceback, sys._getframe) are GC-tracked
  at cleanup so reference cycles are collectible
- Restore pre-existing @expectedFailure markers for test_frame proxy tests

Assisted-by: Claude

* Limit light frame materialization to super() only

current_frame() returns heavy-only again to avoid unnecessary
materialization that extends object lifetimes. super() init uses
current_thread_frame_vm() directly since it must inspect the
light frame's code and freevars to find __class__.

Assisted-by: Claude

* Fix remaining CI failures: C stack check, GC tracking threshold, expectedFailure

- Check C stack overflow every call in light frame path (not every
  16th) to prevent stack overflow on CI workers with smaller stacks
- Fix GC tracking threshold: escaped materialized frame detection
  uses strong_count > 1 (was > 2, missed frames referenced only by
  traceback in the result)
- current_frame() materializes light frames so warnings, compile(),
  and other callers see the correct frame
- Mark test_futures2 PyFutureTests.test_task_exc_handler_correct_context
  as expectedFailure (pre-existing on main: PyTask __del__ timing)

Assisted-by: Claude

* Avoid materialization in hot paths: add light-frame-aware accessors

current_frame() stays heavy-only to avoid costly materialization on
every call. Instead, add current_globals(), current_code(), and
current_builtins() that read directly from light frame raw pointers
without creating frame PyObjects.

Callers migrated:
- PyFunction:🆕 current_builtins() for fallback builtins lookup
- compile()/eval(): current_code() for future feature flags
- type.__new__: current_globals() for __module__ detection
- typevar/typing: current_globals() for caller module name
- _io: current_code() for source path check
- super(): current_thread_frame_vm() for __class__ cell access
- warnings.warn: current_thread_frame_vm() for stack level walk
- import: current_globals() for module resolution

Assisted-by: Claude

* Fix current_locals() to include light frames

current_locals() must materialize light frames so locals() builtin
returns the correct scope. This fixes test_zipimport doctest and
other tests that rely on locals() in light frame context.

Assisted-by: Claude

* Fix clippy, profile tracing, resume_gen_frame, and faulthandler issues

- Fix clippy: collapsible_if in materialize_light_frame, map_unwrap_or in import
- Fix profile breakage: use current_thread_frame_vm in trace_event_inner to
  materialize light frames, preventing frame identity mismatch when profiling
  is enabled inside a light frame (e.g. profile.Profile.runctx)
- Disable light frames inside trace/profile callbacks (tracing_is_suppressed)
- Fix resume_gen_frame: skip light frames in previous chain to avoid dangling
  pointers after generator suspend
- Fix faulthandler: emit '<no Python frame>' when chain has only light frames

Assisted-by: Claude

* Fix materialized frame refcount: transfer localsplus ownership on exit

Remove @expectedFailure from test_futures2: the underlying issue was that
materialized light frames cloned localsplus values (refcount +1 each),
keeping objects alive until the next GC cycle collection.

Fix: after run_light_frame returns, transfer ownership of localsplus
values from the light frame to the materialized frame, replacing the
clones. This eliminates the extra refcount, allowing objects to be
GC'd immediately when their last reference is dropped.

Also stabilize materialized frame's localsplus on the heap in
sync_materialized_on_exit before GC tracking — the data-stack backing
will be popped imminently.

Assisted-by: Claude

* Fix rustfmt, increase stack margin, address review comments

- Fix rustfmt formatting issues (lint CI failure)
- Increase STACK_MARGIN_BYTES from 2048 to 3072 words to prevent C stack
  overflow in deep recursion tests on macOS CI (test_functools SIGSEGV)
- Skip dispatch_traced_frame when use_tracing is false (review comment)
- Fix post-fork top_frame initialization: walk past light frames to find
  nearest heavy frame (review comment)

Assisted-by: Claude

* Remove redundant test override in test_futures2

The override only called super() without any marker, making it a
no-op that prek flags as a redundant test patch.

Assisted-by: Claude

* Fix test_pdb and test_functools CI failures

- Revert with_frame dispatch_traced_frame skip: tracing can be enabled
  mid-execution (pdb.set_trace), so Return events must always be checked.
  This fixes 3 test_pdb doctest failures across all platforms.
- Check C stack overflow on every with_frame call instead of every 64th:
  the previous sampling approach missed overflows between checks when
  native stack frames are large (invoke_light_slots + lru_cache recursion).
- Add #[inline(never)] to invoke_light_slots to prevent the optimizer
  from merging its stack frame into callers.
- Add early C stack check before DataStack allocation in invoke_light_slots.

Assisted-by: Claude

* Rename Frame to FrameObject, FrameRef to FrameObjectRef

Prepare for _PyInterpreterFrame/PyFrameObject separation by
renaming the PyObject-backed frame type to FrameObject.
Python-visible name ("frame") is unchanged.

Assisted-by: Claude

* Remove Deref<Target = InterpreterFrame> from FrameObject

Replace implicit field access through Deref with explicit
iframe()/iframe_ref()/iframe_mut() calls. This decouples
FrameObject from InterpreterFrame field layout, enabling
independent modification of InterpreterFrame in future commits.

Assisted-by: Claude

* Move InterpreterFrame identity fields to raw pointers

InterpreterFrame's code/globals/builtins/func_obj fields are now
borrowed raw pointers instead of owned PyRef/PyObjectRef. Ownership
of these references is anchored in FrameObject's new owned_code,
owned_globals, owned_builtins, owned_func_obj fields.

This aligns InterpreterFrame's layout with LightFrame's existing
raw-pointer pattern, enabling future DataStack-based allocation
without PyObject heap allocation.

- Add code()/globals()/builtins()/func_obj() accessor methods
- Add FrameObject::new_ref() for combined create+allocate+patch
- Update ExecutingFrame to use &Py<PyCode> / &Py<PyDict> / &PyObject
- Update monitoring.rs functions to take &Py<PyCode> instead of &PyRef<PyCode>
- Add Send+Sync impls for InterpreterFrame (raw pointers are !Send by default)

Assisted-by: Claude

* Remove LightFrame, FrameChainPtr, and dual-frame-type infrastructure

Delete LightFrame struct, FrameChainPtr tagged pointer, FrameSource enum,
and all supporting functions: materialize_light_frame, sync_light_to_materialized,
transfer_localsplus_to_materialized, sync_materialized_on_exit,
run_light_frame, invoke_light_slots body, LocalsPlus::from_datastack_raw.

All call paths now use a single frame type (FrameObject wrapping
InterpreterFrame). The frame chain is a simple *const FrameObject linked
list through InterpreterFrame.previous.

invoke_light_slots delegates to invoke_exact_args_slots.
ExecutingFrame.frame_source replaced with ExecutingFrame.frame.
Frame chain walking simplified throughout (no more light/heavy dispatch).

Net: -724 lines of dual-frame-type complexity.

Assisted-by: Claude

* Clean up invoke_light_slots and _vm function variants

- Replace invoke_light_slots calls with invoke_exact_args_slots
- Delete the invoke_light_slots delegate method
- Replace current_thread_frame_vm/frame_at_offset_vm with their
  non-_vm equivalents (no more light frame materialization needed)
- Remove stale light-frame comment in callable.rs

Assisted-by: Claude

* Fix CI lint issues

- Move github context expressions from run blocks to env blocks
  (zizmor/template-injection)
- Add workflow-level permissions: {} to lib-deps-check.yaml
- Suppress zizmor excessive-permissions for lib-deps-check.yaml
  (pull_request_target is required for PR comments)
- Apply rustfmt formatting

Assisted-by: Claude

* Stack-allocate InterpreterFrame for non-generator function calls

- Extract InterpreterFrame::new() from FrameObject::new()
- Change frame chain from *const FrameObject to *const InterpreterFrame
- Add InterpreterFrame.materialized field for lazy FrameObject creation
- Add vm.with_iframe()/run_frame_fast() for stack-allocated frame execution
- Move ExecutingFrame.frame to iframe: *const InterpreterFrame
- Implement lazy materialize() for on-demand FrameObject creation
- Update invoke_with_locals/invoke_prepared_exact_args to skip heap allocation
- Update frame chain walking in faulthandler, gc_state, builtins/frame
- super() reads InterpreterFrame directly without materialization

Assisted-by: Claude

* Remove unnecessary refcount and atomic ops from fast call path

- Use raw pointer for func_obj instead of cloning PyObjectRef (saves 2 atomic RMW)
- Skip owner AcqRel swap for stack-allocated frames (always Thread-owned)

Assisted-by: Claude

* Change trace field to Option<PyObjectRef>, remove vm from InterpreterFrame::new

- trace: PyMutex<PyObjectRef> -> PyMutex<Option<PyObjectRef>>
- Eliminates vm.ctx.none() refcount inc/dec per frame init
- Remove vm parameter from InterpreterFrame::new() (no longer needed)
- f_trace getter returns None when trace is unset
- f_trace setter stores None instead of the None singleton

Assisted-by: Claude

* Store current frame pointer on VirtualMachine for fast lookup

- Add current_frame_ptr Cell<usize> field to VirtualMachine
- Hot path (with_iframe) reads/writes vm.current_frame_ptr instead of TLS
- TLS CURRENT_FRAME still maintained for signal-safe traceback walking
- Use set_current_frame_nosave() to skip cross-thread top_frame update
- super(), frame_at_offset, current_thread_frame_materialize use vm Cell

Assisted-by: Claude

* Apply rustfmt

Assisted-by: Claude

* Fix clippy use_self warnings, add inline hints for hot path

Assisted-by: Claude

* Remove code refcount clone from fast call path

Defer code.to_owned() to the generator/coroutine path only. On the fast
path, use &Py<PyCode> directly — the code object is alive via the
PyFunction on the caller's stack.

Assisted-by: Claude

* Remove VM current_frame_ptr Cell (fix SIGSEGV in threading builds)

The Cell<usize> on VirtualMachine could desynchronize from the TLS
CURRENT_FRAME in nested VM scenarios (enter_vm called with different
VMs on the same thread). Revert to TLS-only frame chain management.

Keep set_current_frame_nosave() for the stack-frame fast path: it
skips cross-thread top_frame publication but still does the TLS swap.

Fix clippy not_unsafe_ptr_arg_deref in set_current_frame.
Fix unnecessary Result wrapper in make_generator_or_coro.

Assisted-by: Claude

* Fix CI failures: GC frame assertion, faulthandler Radium import, dead code warning

- Fix SIGSEGV in threading builds: GC debug assertion walked frame
  chain via top_frame, which is not updated by set_current_frame_nosave
  used by stack-allocated iframes. Walk CURRENT_FRAME TLS chain instead.
- Add InterpreterFrame::get_lasti() accessor, remove direct Radium
  import from faulthandler.rs (fixes unused import warning).
- Add cfg_attr allow(dead_code) for from_payload_ptr without threading
  (fixes WASM build warning).

Assisted-by: Claude

* Fix materialized frame: copy localsplus, clear stale previous pointers

- materialize_slow: copy fastlocals snapshot to heap instead of empty
  localsplus (fixes index-out-of-bounds panic in locals()/f_locals)
- materialize_slow: set previous to null (stack iframes become dangling)
- with_frame/resume_gen_frame: clear iframe.previous on pop (prevents
  dangling pointers to freed stack iframes)
- f_back: walk TLS CURRENT_FRAME chain instead of top_frame for
  cross-thread lookup (top_frame is stale for stack-allocated iframes)
- check_locals_access: match materialized frames by comparing the
  Py<FrameObject> address stored in the stack iframe's materialized field

Assisted-by: Claude

* Fix Windows clippy: gate unix-only imports and variables

FrameObject, Py imports and top_iframe variable are only used in
unix+threading cfg blocks. Gate them properly for non-unix builds.

Assisted-by: Claude

* Fall back to FrameObject when tracing; fix cross-thread frame access

- invoke_with_locals: fall back to heap-allocated FrameObject path
  when use_tracing is active, so trace/profile callbacks fire correctly.
  This fixes test_trace, test_bdb, test_monitoring regressions.
- set_current_frame: publish top_iframe alongside top_frame in ThreadSlot
  so cross-thread readers (sys._current_frames) can materialize
  stack-allocated frames from other threads under stop-the-world.
- get_all_current_frames: use TLS CURRENT_FRAME for current thread,
  fall back to top_iframe materialization for other threads.
- Use set_current_frame (not nosave) in with_iframe for proper
  cross-thread visibility.

Assisted-by: Claude

* Apply rustfmt

Assisted-by: Claude

* Fix Windows: gate unix-only top_iframe_ptr variable

Assisted-by: Claude

* Fix frame chain: f_back, retained_back, owner, faulthandler

- Set materialized frame owner to FrameObject (not Thread) so
  frame.clear() works on traceback frames
- Fix f_back for materialized frames: walk TLS chain to find source
  iframe and materialize its previous frame
- Fix faulthandler dump_traceback to use dump_live_frames (reads
  live lasti from stack iframes instead of stale materialized copies)
- Add retained_back propagation in with_frame cleanup: when a frame
  escapes, materialize caller and mark it escaped for chain propagation
- Fix release_datastack_frame: don't overwrite retained_back if
  already set by with_frame cleanup

Assisted-by: Claude

* Apply rustfmt

Assisted-by: Claude

* Refine with_frame retained_back: use frame_obj instead of materialize

Avoid materializing caller frames in with_frame cleanup to prevent
creating localsplus snapshots that keep extra refcounts. Only use
existing FrameObjects (frame_obj) for retained_back. Also keep
previous pointer alive in release_datastack_frame since the caller
is still executing at that point.

Assisted-by: Claude

* Fix f_lineno: use Cell<u32> for prev_line, update on every instruction

prev_line was only updated when tracing was active, causing f_lineno
to return stale values when observed mid-CALL (e.g. from warnings.warn
or sys._getframe). Changed prev_line from u32 to Cell<u32> for
interior mutability so it can be read safely while ExecutingFrame
holds a shared reference, and update it on every instruction so
f_lineno always returns the correct line.

Assisted-by: Claude

* Fix f_lineno for materialized frames: read live prev_line from TLS chain

Materialized FrameObjects have a snapshot of prev_line from materialize
time. When f_lineno is called on a materialized frame that is still
executing (its source iframe is on the TLS chain), walk the TLS chain
to find the source iframe and read its live prev_line. This fixes
lineno tracking in warnings, gettext, and other modules that read
frame line numbers during function calls.

Also fixed the pointer comparison: use from_payload_ptr to convert
FrameObject payload address to Py<FrameObject> address for matching
against the materialized field.

Assisted-by: Claude

* Sync live frame state for materialized frames

- Add find_live_source_iframe() to walk TLS chain and find the live
  source InterpreterFrame for a materialized FrameObject
- f_lasti: read live lasti from source iframe when available
- f_lineno: use read_volatile for live prev_line from source iframe
- sync_visible_locals_to_mapping: read live localsplus from source iframe
- framelocalsproxy_getval: read live localsplus from source iframe
- with_iframe cleanup: sync localsplus, prev_line, lasti to materialized
  FrameObject when the frame returns
- sync_fastlocals_from: new LocalsPlus method to update fastlocals

Fixes test_inspect (stale f_locals, stale lineno in positions),
test_listcomps (stale iteration variable in locals()).

Assisted-by: Claude

* Fix retained_back: use materialize_chain and strong_count check

- Add materialize_chain() that creates a lightweight FrameObject with
  empty localsplus for f_back chain building only. This avoids cloning
  local variables which would create extra refcounts (fixes test_memoryview).
- with_frame cleanup: use strong_count > 1 instead of escaped flag to
  determine if retained_back is needed, ensuring chain propagation.
- with_iframe cleanup: use materialize_chain for retained_back to avoid
  extra refcounts from localsplus snapshots.

Fixes test_memoryview (refcount from retained_back chain) and
test_traceback.test_extract_stack (f_back chain depth).

Assisted-by: Claude

* Fix cross-thread f_back chain for sys._current_frames/exceptions

- get_all_current_frames: materialize entire frame chain and link
  retained_back during stop-the-world for other threads
- f_back: use stop-the-world to safely materialize cross-thread frame
  chains on unix when prev iframe is on another thread
- f_back: check frame_obj() before STW for fast path

Fixes test_sys.test_current_frames and test_sys.test_current_exceptions.

Assisted-by: Claude

* Fix monitoring LINE events and faulthandler thread dump

- Skip all instrumented opcodes in bytecode loop prev_line update to
  avoid defeating InstrumentedLine de-duplication
- Add prev_line update in execute_instrumented for non-RESUME/non-LINE
  instrumented opcodes
- update_events_mask: walk TLS iframe chain to re-instrument all frames
  including stack-allocated ones (fixes missing LINE events for inline
  frames already past RESUME)
- faulthandler: use top_iframe fallback for stack-allocated frames in
  dump_all_threads and watchdog_thread
- faulthandler: use dump_live_frames for current thread dump

Fixes test_monitoring LINE/CALL tests, test_faulthandler thread dumps.

Assisted-by: Claude

* Fix clippy: remove duplicate Radium import, use previous() helper

Assisted-by: Claude

* Fix materialized frame issues: GC tracking, live locals, f_trace propagation

- Track materialized FrameObjects in GC so cycle collection works
  (fixes __del__ not called at exit for exception traceback cycles)
- materialize_chain returns owned PyRef without temporary_refs to
  avoid defeating GC cycle detection from non-GC-tracked storage
- framelocalsproxy_setval writes to live source iframe when available
  (fixes f_locals proxy writes not affecting executing frame)
- f_lineno uses lasti-based line for non-executing frames instead of
  prev_line (fixes PEP 626 wrong line after exception unwind)
- f_trace/f_trace_lines/f_trace_opcodes setters propagate to live
  source iframe (fixes pdb set_trace not working through materialized
  frames)
- frame.clear() checks find_live_source_iframe to reject clearing a
  frame that is backed by a live stack-allocated iframe
- Fix clippy warnings and unused imports in faulthandler
- Remove unsafe top_iframe access from faulthandler watchdog thread

Assisted-by: Claude

* Fix faulthandler crashes, GC tracking timing, and test_generators

- Use top_iframe instead of top_frame for frame chain walking in
  faulthandler (dump_all_threads and watchdog), avoiding stale or
  GC-cleared FrameObject pointer dereference
- Defer GC tracking of materialized FrameObjects to with_iframe
  cleanup (after set_current_frame restores old chain), preventing
  premature collection while temporary_refs still holds the only ref
- Add FrameObject::try_iframe() for safe access to potentially
  GC-cleared frames
- Remove expectedFailure for test_exhausted_generator_frame_cycle
  (now passes with GC tracking fix)
- Add 'noalias' to cspell dictionary

Assisted-by: Claude

* Fix Windows frame dump and remove dead code

- Publish top_iframe on all platforms (not just unix) so Windows
  faulthandler and sys._current_frames can see stack-allocated frames
- Use top_iframe instead of frames Mutex in Windows faulthandler
  dump and watchdog paths
- Use top_iframe in non-unix get_all_current_frames for sys._current_frames
- Remove unused dump_traceback_thread_chain (replaced by top_iframe walk)
- Mark test_pdb_await_support as expected failure

Assisted-by: Claude

* Fix ThreadSlot init on all platforms, skip test_pdb_await_support

- Add top_iframe to ThreadSlot initializer in init_thread_slot_if_needed
  (fixes wasm32, Windows compile errors)
- Skip test_pdb_await_support on RustPython (async pdb exception
  callback receives None exc argument)

Assisted-by: Claude

* Remove dead faulthandler code (dump_frame_from_ref, dump_traceback_thread_frames, try_iframe)

These functions are no longer called after switching all frame chain
walking to use top_iframe directly.

Assisted-by: Claude

* Fix Windows sys._current_frames: materialize full frame chain

The non-unix get_all_current_frames path was only materializing the
top iframe without linking retained_back, so f_back chain walking
could not find deeper frames like f123() in test_sys tests.
Replicate the full chain materialization from the unix path.

Assisted-by: Claude

* Use STW-based cross-thread f_back on all platforms

Replace the non-unix frames-mutex fallback with the same
stop-the-world iframe chain materialization used on unix.
Fixes test_current_exceptions on Windows where stack-allocated
iframes were not found in the FrameObject-only frames list.

Assisted-by: Claude

* Fix set_f_lineno to write to live iframe, use STW for cross-thread access

- set_f_lineno: write lasti/pending_stack_pops to the live source
  iframe instead of the materialized copy so debugger jumps take
  effect on stack-allocated frames
- f_back cross-thread: enter STW before dereferencing the prev
  pointer to prevent use-after-free if the owning thread returns
- get_all_current_frames (non-unix): add STW protection when
  walking cross-thread iframe chains, matching the unix path

Assisted-by: Claude

* Guard current_location() against lasti==0, always capture retained_back

- current_location(): return first_line_number instead of
  panicking when lasti is 0 (before first instruction executes)
- with_frame retained_back: materialize the caller iframe when
  needed so f_back always resolves for escaped FrameObjects

Assisted-by: Claude

* Fix format, revert eager retained_back materialization

Remove double blank line in frame.rs (lint failure).
Revert with_frame retained_back to only use already-materialized
caller FrameObjects — eagerly materializing the caller added
refcounts on local variables, preventing timely deallocation and
causing test_io, test_memoryview, and test_futures failures.

Assisted-by: Claude

* Remove rustpython-unicode-isolation-issue.md from gitignore

Assisted-by: Claude

* Remove test_pdb_await_support skip: async pdb now works correctly

Assisted-by: Claude

* Fix STOP_ITERATION monitoring: wrap value in StopIteration instance

fire_stop_iteration was passing the raw iterator return value to
callbacks, but the STOP_ITERATION event callback signature expects
a StopIteration exception instance. Wrap non-StopIteration values
in a new StopIteration(value), matching PyMonitoring_FireStopIterationEvent.

This fixes test_pdb_await_support where bdb's exception_callback
received None instead of a StopIteration instance.

Assisted-by: Claude
2026-08-01 00:59:41 +09:00
Bas Schoenmaeckers 57b630a6c7 Add str compare functions to c-api (#8268) 2026-07-15 23:24:26 +09:00
Jeong, YunWon 210ccc889c Thread-safe type operations: type lock, QSBR type-cache reclamation, GC stop-the-world, and interpreter optimizations (#7416)
* type lock

* Drop old PyObjectRef outside type lock to prevent deadlock

Dropping values inside with_type_lock can trigger weakref callbacks,
which may access attributes (LOAD_ATTR specialization) and re-acquire
the non-reentrant type mutex, causing deadlock.

Return old values from lock closures so they drop after lock release.

* Align type lock behavior with CPython

* Add PUBLISHED flag bit to RefCount state word

Assisted-by: Claude

* Add QSBR module for deferred memory reclamation

Assisted-by: Claude

* Defer memory free of cache-published objects via QSBR

Assisted-by: Claude

* Wire QSBR checkpoints into thread lifecycle and eval breaker

Assisted-by: Claude

* Publish type-cache values to QSBR and bypass cache without a VM

Mark cached type-method values as QSBR-published before storing their
pointer in TYPE_CACHE, so racing readers never try-incref freed memory.
Debug-assert that the current thread is ATTACHED whenever a lock-free
cache read happens.

Delete find_name_in_mro_without_vm and its unsound direct SeqLock read
outside any VM/thread registration; find_name_in_mro now falls back to
the locked, uncached MRO walk when no VM is current. This also removes
assign_version_tag()'s last caller, so its unlocked fallback is deleted
along with it (version_for_specialization keeps the locked path).

VirtualMachine::initialize() runs Python bytecode (e.g. importing
codecs/encodings) before any enter_vm scope exists, which left the
bootstrap thread not ATTACHED for cache reads during startup. Add
thread::VmBootstrapGuard, an RAII counterpart to enter_vm usable across
statements that need &mut VirtualMachine, and wrap initialize() with it.

Assisted-by: Claude

* Use try-incref reads and QSBR-backed swaps in specialization cache

Assisted-by: Claude

* Reset QSBR thread registry after fork in the child

Add Qsbr::reset_after_fork, which clears the registered thread slots
before draining the retire queue. Call it in py_os_after_fork_child,
right after type_cache_after_fork() and before
_thread::after_fork_child() re-registers the surviving thread.

Dead parent threads' Arc<QsbrSlot> handles live on in the child's
copied memory with no destructor ever running, so without this the
registry keeps them 'online' forever and future grace periods never
complete.

Assisted-by: Claude

* Add threaded stress test for type cache mutation races

Assisted-by: Claude

* Restrict qsbr internals to pub(crate) and fix clippy lints

Downgrade pub items in the private qsbr::threading module (QsbrSlot,
Qsbr, QSBR, and its methods) to pub(crate), since they were never
reachable outside the crate. Downgrade ThreadSlot::qsbr accordingly to
avoid a private-interfaces warning. Import Arc/Weak from alloc instead
of std to match clippy::std_instead_of_alloc. Rewrite the process()
comment: goals are not strictly ordered by push order since advance()
and the queue push aren't atomic together, so concurrent free_delayed
calls can interleave; the drained prefix stays sound because each item
individually passed poll, with reordering only affecting reclamation
latency.

Assisted-by: Claude

* Gate per-instruction QSBR check behind a global pending flag

eval_breaker_tripped() and check_signals() called
thread::qsbr_break_requested() on every bytecode instruction, which
does a thread-local lookup, RefCell borrow, and atomic load even when
no QSBR retirement is pending. This caused a measured 10-18% slowdown
on tight interpreter loops.

Add Qsbr::pending, a global AtomicBool set under the queue lock while
free_delayed() holds a retired allocation, and cleared under the same
lock by process()/drain_all() once the queue empties. The hot path now
checks Qsbr::break_pending() (a single relaxed static load) before
touching thread-local state, and only pays the TLS cost while a
retirement is actually in flight.

Assisted-by: Claude

* Merge signal and QSBR eval-breaker flags into one atomic word

Replace the separate ANY_TRIGGERED AtomicBool and QSBR pending
AtomicBool checks in the per-instruction eval-breaker path with a
single AtomicU8 (EVAL_BREAKER) holding a bit per source. The global
QSBR instance mirrors its pending state into the QSBR bit under the
same queue-lock critical sections that already toggle pending, while
local QSBR instances used by unit tests are left untouched.

eval_breaker_tripped() and check_signals() now read/act on the merged
word (eval_breaker_pending(), qsbr_bit_set()) instead of issuing two
separate atomic loads per instruction. set_triggered() switches from
store to fetch_or so a signal handler never clobbers the QSBR bit.

Removed the now-unreferenced is_triggered() helper; break_pending()
is kept for unit tests only and marked allow(dead_code) outside test
builds.

Assisted-by: Claude

* Skip freelist reuse for published objects in default_dealloc

Freelist-eligible payloads (tuple, int, list, dict, float, ...) were
routed to T::freelist_push before ever reaching PyInner::dealloc, so a
published object (e.g. a tuple cached as a type attribute) bypassed the
is_published -> free_delayed QSBR hook entirely. That let PyRef::new_ref
reuse the slot and rewrite the refcount word with a non-atomic
core::ptr::write racing a reader's atomic try-incref, and let
FreeList::drop call alloc::dealloc directly while a reader could still
be mid-read.

Gate the freelist branch in default_dealloc on
RefCount::is_published(), checked before any teardown. A published
object now always falls through to PyInner::dealloc, whose existing
hook defers the memory free via QSBR.

Document the non-unix (e.g. Windows) reclamation-latency limitation in
the QSBR design doc's Lifecycle integration section (previously only
noted in a code comment in vm/thread.rs).

Extend the type-cache stress test so the mutator also churns a
freelist-eligible published tuple class attribute (C.shape), read by
readers each inner loop and deleted on the same cadence as C.m, to
exercise the fixed bypass.

Assisted-by: Claude

* Use itoa for PyInt::to_str_radix_10 i64 fast path

Format small integers with itoa::Buffer instead of i64::to_string()
in PyInt::to_str_radix_10(), which backs both str(int)/repr(int) for
exact int and PyObject::str()'s exact-int fast path. Adds the itoa
crate as a workspace dependency.

Assisted-by: Claude

* Make object attribute dunders wrapper descriptors

Remove the __getattribute__, __setattr__ and __delattr__ #[pymethod]s
from PyBaseObject so add_operators installs PyWrapper slot wrappers for
them from the #[pyslot] functions instead of method descriptors.

lookup_slot_in_mro now classifies these entries as NativeSlot, so heap
types without Python-level overrides keep the native getattro/setattro
slot functions, which lets LOAD_ATTR/STORE_ATTR specialization apply to
instances of Python-defined classes.

Since __setattr__ and __delattr__ share the setattro slot, resolve both
names together in update_one_slot: overriding only one of them no longer
lets the other name's native resolution overwrite the dispatching
wrapper.

Assisted-by: Claude

* Resolve TpSetattro pair on attribute deletion, not just addition

update_one_slot's TpSetattro branch only ran the combined
__setattr__/__delattr__ resolution (added in the previous commit) when
ADD was true. On deletion (e.g. del C.__setattr__), it fell back to
inherit_from_mro, which copies the base class's slot and discards the
class's own remaining Python-level override of the other name.

lookup_slot_in_mro reads the current attribute dicts, so the same
resolution is correct for both addition and deletion; the ADD-only
guard is removed and the logic now always runs.

Assisted-by: Claude

* Generate Opcode::cache_entries/deopt as table lookups

generate_rs_opcode_metadata.py now emits a 256-entry const array for
each function instead of a chained match over opcode names.
Opcode::deopt() indexes an [Option<Opcode>; 256] table built from the
existing specialization-to-base mapping, and Opcode::cache_entries()
indexes a [u8; 256] table that bakes in the same deopt/to_base
composition deoptimize() previously performed before its own match.
Regenerated opcode_metadata.rs from the updated script; PseudoOpcode's
bodies are unchanged since it has no cache/deopt entries to table.

Added an exhaustive test in instruction.rs that checks every valid u8
opcode against a frozen copy of the old chained-match bodies.

Assisted-by: Claude

* Make Opcode/Instruction numeric conversions O(1)

Opcode::as_numeric, Opcode::as_instruction and Instruction::as_opcode
were each a per-variant match over ~230 arms in the define_opcodes!
macro. Opcode now carries the same explicit $op_id discriminants and
#[repr($typ)] as Instruction, which makes the conversions sound as:

- Opcode::as_numeric: a plain `self as $typ` identity cast.
- Opcode::as_instruction / Instruction::as_opcode: a mem::transmute,
  relying on both enums sharing one-$typ-wide layout (Instruction's
  payload fields are all the zero-sized Arg<T> marker, checked by a
  new per-instantiation size_of assertion).

try_from_numeric is left as a match: $op_id values have gaps
(specialized/instrumented opcode ranges), so a range check can't
replace it.

Adds #[inline] to the rewritten conversions plus deoptimize, and to
the small generated wrapper/table-lookup functions from the previous
opcode-metadata change (as_u8/as_u16, cache_entries, deopt, to_base)
via generate_rs_opcode_metadata.py, then regenerates
opcode_metadata.rs.

Extends the instruction.rs equivalence-test pattern with two
exhaustive tests (u8 and u16 instantiations) checking the new
identity-cast/transmute conversions against the untouched
try_from_numeric/TryFrom paths.

Assisted-by: Claude

* Pop CallIsinstance arguments directly instead of collecting into Vecs

The specialized handler built two heap-allocated Vecs per call
(pop_multiple().collect() plus a with_capacity(2) buffer) before
invoking is_instance. Pop the class and instance straight off the
stack for the effective two-argument case, removing the per-call
allocations.

Assisted-by: Claude

* Inherit tp_new instead of always installing new_wrapper

update_one_slot's TpNew branch stored new_wrapper for every ADD, so all
heap types had slots.new == new_wrapper and the CallAllocAndEnterInit
specialization never fired. Now __new__ is resolved through the MRO
dicts: an own or Python-level definition installs new_wrapper, while a
builtin __new__ entry (or none) inherits slots.new from the solid base.

The now-reachable CallAllocAndEnterInit path gains the missing guards:
the specializer requires co_argcount == oparg + 1 (via
can_specialize_call) and rejects generator-like __init__, and the
handler re-checks the argcount against the class-level init cache.
Without the argcount check, __init__ defaults broke re._parser
(UnboundLocalError in SubPattern.__init__). Remove the now-unused
is_simple_for_call_specialization.

Assisted-by: Claude

* Skip locals dict allocation for the init-cleanup shim frame

Mark the shim code object NEWLOCALS and create the shim frame with no
locals mapping so FrameLocals::lazy() is used. The shim only executes
ExitInitCheck/ReturnValue and never touches locals, so this removes one
dict allocation per specialized instantiation.

Assisted-by: Claude

* Guard CALL_ALLOC_AND_ENTER_INIT with cached function version

The init specialization cache stored only the type version, so swapping
__init__.__code__ (which zeroes func_version) did not deopt the warmed
call site and stale code assumptions (argcount shape, no varargs/kwonly,
not a generator) were applied to the new code object.

Store get_version_for_current_state() in the specialization cache when
caching __init__ and re-check func_version() in the handler before using
the cached function, deopting to the generic call path on mismatch, the
same way the getitem specialization cache does.

Assisted-by: Claude

* Apply rustfmt and ruff-format fixes to files from earlier commits

Assisted-by: Claude

* Add frame object freelist

Frame.iframe becomes FrameUnsafeCell<Option<InterpreterFrame>> so a dead
frame can be left as an empty husk. Traverse::clear extracts all owned
child references (including localsplus values via LocalsPlus::clear_into)
and empties the cell. Dead frame allocations are cached in a thread-local
FreeList<Frame> (up to 200 entries) and reused by PyRef::new_ref.

clear_generator returns early when the frame was already cleared by cycle
collection; generator drops can run after tp_clear of their frame.

gc: skip tp_clear for objects saved to gc.garbage by DEBUG_SAVEALL, since
they remain reachable from Python.

Assisted-by: Claude

* Drop frame children directly in tp_clear instead of extracting

Pushing the frame's ~10 child references into the clear buffer grew a
Vec on every frame deallocation. Take the interpreter frame out of the
cell and drop it in place; LocalsPlus::clear_into is removed.

Assisted-by: Claude

* Untrack objects before tp_clear and guard cross-thread f_locals

The cycle collector called tp_clear on unreachable objects while they
were still linked into the GC generation lists, so another thread could
obtain a strong reference to an already-cleared object (e.g. a frame
husk with iframe == None) via gc.get_objects() and access the cleared
payload. Untrack the dead set before the clear phase, mirroring the
untrack-then-clear ordering of the refcount dealloc path.

Objects that gained an external reference before untracking are found
by comparing each object's strong count against the references coming
from within the dead set; such late-resurrected objects and everything
reachable from them are re-tracked and skipped, letting a later
collection retry once the external references are released.

Also reject f_locals access for a frame currently executing on another
thread. Reading fastlocals of such a frame races with the executing
thread overwriting the slots (torn reads of dropped values). Access
from the executing thread itself (locals(), trace callbacks) is still
allowed via the current-frame chain check.

Assisted-by: Claude

* Skip localsplus heap copy for uniquely referenced dying frames

release_datastack_frame now checks the frame's strong count after
untracking it from the GC generation lists. When the caller holds the
only reference, localsplus values are dropped in place and the data
stack storage is released without the heap copy; escaped frames
(traceback, sys._getframe, trace callbacks) keep the copy path.
The three inline materialize+pop call sites in function.rs are routed
through release_datastack_frame.

Assisted-by: Claude

* Stage exact-args call arguments in stack slot buffers

CallPyExactArgs, CallBoundMethodExactArgs and CallAllocAndEnterInit no
longer collect popped arguments into a per-call Vec (plus a second Vec
for self-prepending). Arguments are popped into a fixed-size
Option<PyObjectRef> slot buffer (CallArgBuffer, 8 inline slots, heap
fallback for larger arities) and moved into fastlocals via the new
invoke_exact_args_slots / a take() iterator.

- prepare_exact_args_frame now accepts an ExactSizeIterator of args
- specialization_run_init_cleanup_shim takes the slot buffer and fills
  slot 0 with new_obj, dropping one redundant clone
- LoadAttrGetattributeOverridden, LoadAttrProperty and
  BinaryOpSubscrGetitem pass inline slot arrays instead of vec![]

Assisted-by: Claude

* Reduce allocations in generic call paths

- FuncArgs::prepend_arg: use reserve instead of reserve_exact so
  exact-capacity vectors do not realloc on every prepend.
- IntoFuncArgs::into_method_args: build the final args vec once with
  capacity len + 1 instead of prepending into a full vector.
- PyType::call: skip cloning FuncArgs when no init slot can run after
  slot_new (no init slot, not `type`, and slot_new is not new_wrapper).

Assisted-by: Claude

* Skip redundant exc_info restore on frame exit

with_frame_impl saves the current exc_info slot value and restores it
when the frame exits. When the slot still holds the same value, the
restore rewrites the slot and recomputes the thread-exception mirror
for no effect. Add restore_exception, which compares the slot against
the saved value by pointer identity and skips the store when they
match. The saved value is a strong reference held for the whole frame
scope, so the pointed-to object cannot be freed and its address reused
while the frame runs, making the pointer comparison free of ABA.

Assisted-by: Claude

* Apply rustfmt to gc_state.rs

Assisted-by: Claude

* Avoid empty FuncArgs clone in PyType::call

Cloning args for the init call clones the kwargs map even when args is
empty, which shows up in no-argument instantiation profiles. Prepare
the init-call args before slot_new: a default FuncArgs when args is
empty (indistinguishable from a clone of empty args), the existing
clone otherwise. The empty check also keeps the init-slot load of the
clone-elision path off the no-argument call path.

Assisted-by: Claude

* Push freelist husks only after tp_clear and child drops

default_dealloc pushed the object onto the thread-local freelist before
running clear_fn. Payloads that drop children inside clear (Frame) can
run __del__ there, and a reentrant allocation could pop the husk and
overwrite its payload while clear_fn still held a &mut borrow of it.
Tuples had the same window through the extracted-edges drop, which ran
after the push.

Reorder default_dealloc to clear, drop the extracted children, and only
then attempt the freelist push, so the husk becomes reusable only when
no borrows into the payload remain.

The tuple freelist bucketed husks by element count, which required
reading the payload during push; after this reordering the elements are
already cleared. PyInner<PyTuple> is a fixed-size allocation (the
elements box is dropped and replaced on reuse), so replace the per-size
buckets with the shared single-list FreeList. Remove the now-unused
pyinner_layout helper.

Assisted-by: Claude

* Resolve __set__ and __delete__ together for the descr_set slot

The descr_set slot serves both __set__ and __delete__, but the
TpDescrSet accessor resolved only the single modified name and fell
back to MRO inheritance on delete. Deleting one of the pair could
disable the surviving operation: after `del D.__set__`, an instance
delete stopped calling the remaining __delete__.

Resolve both names together like the TpSetattro accessor does: any
Python-level definition selects the dispatching wrapper, matching
native functions are stored directly, a single native function with
the other name absent is stored directly, and the slot is inherited
from the MRO only when neither name resolves.

Assisted-by: Claude

* Update type base on __bases__ assignment

set_bases replaced bases and mro but never updated the stored base, so
__base__ kept reporting the old value after reassignment and slot
resolution (tp_new inheritance, set_new/set_alloc during init_slots)
read the stale solid base.

Change the base field to PyAtomicRef<Option<PyType>> so it can be
swapped under the type lock while remaining lock-free for readers, and
recompute it in set_bases with the same best_base validation type
creation uses (BASETYPE flag, instance layout conflict among bases).
The swapped-out base is parked in the frame's temporary refs; other
references released inside the critical section are dropped after the
lock is released.

Also in set_bases:
- remove this class from the old bases' subclass lists (pruning dead
  entries), so repeated assignment no longer accumulates duplicates
- roll back bases, base, and all updated mros when the recursive mro
  update fails, instead of leaving the type half-reparented
- fix the misformatted empty-tuple error message

Remove the expectedFailure marker from test_unsubclassable_types,
which now passes.

Assisted-by: Claude

* Fix rollback order, dead weakrefs, and lock-held drops in set_bases

- Restore recorded mros in reverse on rollback so a class visited
  multiple times through diamond inheritance ends with its original
  mro instead of an intermediate one.
- Skip dead weakrefs in the subclass list during the recursive mro
  update instead of panicking on upgrade.
- Retire the replaced mros on the success path instead of dropping
  them while the type lock is held.

Assisted-by: Claude

* Reify number sub-slot wrapper once in update_one_slot

The update_sub_slot! macro expanded the Python-method wrapper closure at
both the own and inherited store sites. Each expansion is a distinct fn
item, so a base and a non-overriding subclass stored wrappers with
distinct addresses in unmerged debug builds. binary_op1 compares slot fn
addresses to detect whether a subclass overrides the operator, so the
inherited slot was misread as an override and C() // E() dispatched to
__rfloordiv__ instead of __floordiv__.

Bind the wrapper store in a single closure and call it from both branches
so the fn item is reified once and the slot value stays identical across
a base and its subclasses. Document the conservative-on-mismatch nature
of the new_wrapper address guard in call_wrapped.

Assisted-by: Claude

* Restrict type_cache_after_fork visibility and guard type-cache reads

type_cache_after_fork is only called from within the crate, so mark it
pub(crate) to resolve the unreachable-pub warning.

Add debug_assert_current_thread_attached at the has_name_in_mro lock-free
type-cache read site, matching lookup_ref_and_version_interned, and gate
the function on debug_assertions so it is not compiled unused in release
builds where the call sites are elided.

Assisted-by: Claude

* Guard BinaryOpSubscrGetitem with the cached type version

The specialized BINARY_OP_SUBSCR_GETITEM handler used the cached
__getitem__ after checking only the function version, unlike the sibling
specialized handlers which revalidate the type version tag first.

Store the type version in the inline cache at specialization time and
revalidate owner.class().tp_version_tag against it before using the cached
function, deopting to the generic subscript path on mismatch.

Assisted-by: Claude

* Call __abstractmethods__ __len__ once in object.__new__

The abstract-method check invoked the user-visible __len__ twice: once via
length_opt for the count and again while materializing the method names.
Derive the count from the materialized list instead, so __len__ runs once.

Assisted-by: Claude

* Use i64 fast paths for specialized int add/sub/mul

Rewrite execute_binary_op_int to box results through new_int via i64
checked arithmetic instead of raw BigInt ops with new_bigint. Add an
int_mul helper and a shared int_fast_op that computes the i64 result and
falls back to the BigInt operation on to_i64 or checked-op failure.

Wire int_mul into both the specialized BinaryOpMultiplyInt handler and
the generic execute_bin_op Multiply/InplaceMultiply path, gated on exact
int operands so subclasses keep dispatching through _mul/_imul.

Assisted-by: Claude

* Use i64 fast paths for exact int floordiv and remainder

Add floordiv_i64/mod_i64 computing i64 floor-division and divisor-signed
remainder, guarding zero divisor and i64::MIN overflow by returning None.
Wire them through int_floordiv/int_mod (shared int_div_fast_op boxes via
new_int) into the generic execute_bin_op FloorDivide/Remainder and their
Inplace variants, gated on exact int operands so subclasses and zero
divisors fall through to the existing _floordiv/_mod/_ifloordiv/_imod
slow path.

Assisted-by: Claude

* Skip trashcan and untrack for non-GC-tracked objects in dealloc

default_dealloc read is_gc_tracked() only to decide untracking, and
entered the trashcan recursion guard unconditionally. Non-GC objects
(int, float, str, ...) own no child references that recurse during
deallocation, so they need neither the trashcan nor untracking. Read
is_gc_tracked() once and gate both trashcan begin/end and untrack on it,
removing three thread-local accesses per non-GC object deallocation.

Assisted-by: Claude

* Merge trashcan depth and defer queue into one thread-local struct

trashcan::end accessed two separate thread-locals (DEALLOC_DEPTH and
DEALLOC_QUEUE) at the outermost deallocation. Combine both into a single
`Trashcan` thread-local holding Cell-based depth and queue fields, so
begin and end each reach their state through one thread-local access.
The queue is set back before each deferred dealloc call so reentrant
begin/end during draining never holds an outstanding borrow.

Assisted-by: Claude

* Check object layout compatibility on __bases__ and __class__ assignment

Add a shared compatible_for_assignment helper that walks each type to the
base that fixes its instance layout and rejects the assignment when the old
and new layouts differ (basicsize, itemsize, member count, __dict__,
__weakref__, and __slots__ names). Wire it into set_bases, which previously
performed no layout check, and replace the inline check in __class__
assignment, which compared the two types directly and over-rejected a
subclass that adds no layout.

Enable test_descr.test_builtin_bases, which the new set_bases check passes.

Assisted-by: Claude

* Stop the world around GC pointer-reading phases

The cycle collector reads each tracked object's interpreter state during
reference subtraction, the reachability walk and the strong-reference
snapshot, including the localsplus of frames other threads are executing.
Those slots are written without synchronization, so the reads are a data
race under threading.

collect_inner now stops the world before taking the generation read locks
and restarts it after the strong-reference snapshot, before the
finalizer/weakref/tp_clear phases. A debug assertion checks that no frame
on any thread's call stack was classified unreachable.

Automatic collections from maybe_collect are deferred to the next bytecode
safepoint via a new eval-breaker GC bit instead of running synchronously:
a synchronous collection can hold an internal lock (e.g. the lazy frame
locals cell) that another thread is blocked on with no way to reach a
safepoint, deadlocking the stop.

Hardens the stop-the-world machinery for these frequent, concurrent GC
stops: attach_thread now honors a pending stop after re-attaching so a
thread doing rapid allow_threads calls cannot run past the requester;
stop_the_world completion is level-triggered on all-threads-suspended
rather than an edge-triggered countdown; and the thread-start started/ready
handshakes detach while waiting so the waiter is parkable.

Updates the Frame and PyRwLock/PyMutex traversal SAFETY comments to state
the actual invariant.

Assisted-by: Claude

* Add threaded GC vs executing-frame stress snippet

Workers churn frame state (recursion, generators, frame cycles) while a
collector loops gc.collect() and an introspector walks live frame objects.
Exercises the stop-the-world barrier around GC traversal.

Assisted-by: Claude

* Serialize fork and GC stop-the-world requesters

fork() (posix before/after-fork) and the cycle collector both drive the
single StopTheWorldState. With no mutual exclusion their requester word and
suspension countdown could interleave and be clobbered, so the completion
check never converged and a requester waited on itself forever (reproducible
parent-side hang when forking with GC enabled).

Add one exclusion held for the whole stop->start span of either requester:
stop_the_world acquires it before any stop bookkeeping and
start_the_world/reset_after_fork release it. The acquire is park-friendly
(poll try-lock and honor a pending suspend between tries) so a requester
blocked behind an active stop can still be force-parked instead of deadlocking
the active requester. The acquirer holds no other lock while spinning.

Also correct the PyRwLock traverse SAFETY note: a failed try_read may mean a
force-parked thread holds the write lock; skipping is safe because
under-traversal only over-approximates liveness. Document the residual
gc.collect()-under-a-non-generation-lock exposure at the collect_inner barrier.

Assisted-by: Claude

* Add fork under concurrent GC stop-the-world snippet

Worker threads allocate cyclic garbage with GC enabled while the main thread
forks repeatedly and each child collects. Exercises the fork/GC
stop-the-world exclusion; the allocation rate is throttled so a collection
stays cheap in unoptimized builds.

Assisted-by: Claude

* Detach while acquiring the import lock

The global import lock is held across bytecode by the importlib bootstrap,
so its holder can be parked at a safepoint mid-hold. Acquiring it while
attached let another thread block attached on the lock, so a stop-the-world
requester could wait forever for that attached thread to suspend while the
holder stayed parked.

Wrap IMP_LOCK acquisition in allow_threads in both _imp.acquire_lock and the
pre-fork acquire_imp_lock_for_fork so the wait honors stop-the-world requests.
Correct the acquire_exclusion comment: a spinning requester may hold IMP_LOCK
(fork) or the collecting mutex (GC); safety relies on those never being
acquired attached-blocking by another thread.

Assisted-by: Claude

* Add concurrent-import vs GC deadlock snippet

Two threads re-import modules (contending the import lock) while a third
storms the cycle collector and a fourth allocates cyclic garbage. Regressed
as a hang before the import lock acquisition was made park-friendly.

Assisted-by: Claude

* Create call frames untracked and track generators explicitly

Add PyPayload::NEW_REF_UNTRACKED (default false, true for Frame) so
PyRef::new_ref skips auto-tracking ordinary call frames in the GC.
Generator/coroutine/async-generator frames are tracked explicitly in
invoke_with_locals before their generator back-reference is installed.
run_frame debug-asserts that a datastack frame is untracked on entry.

Assisted-by: Claude

* Track escaped call frames lazily at datastack release

Rewrite release_datastack_frame around the invariant that a datastack
frame is never GC-tracked while it runs: strong_count() == 1 means the
frame never escaped, so drop localsplus in place without touching the
GC; strong_count() > 1 means it escaped, so materialize localsplus onto
the heap and then track the frame. This removes the untrack and the
untrack-recheck dance from the common non-escaping path.

Assisted-by: Claude

* Guard the tracked-frame localsplus invariant

Document at Frame::traverse that references to a frame are always
recorded as graph edges, so the collector reads a frame's localsplus
only when the frame is itself a tracked candidate. Add debug assertions
at both frame track sites (escaped datastack frames and generator
frames) that a frame is heap-backed before it is tracked, so a collector
never reads data-stack-resident, still-mutating storage.

Assisted-by: Claude

* Reword frame/dealloc invariant comments and drop needless borrow

- frame.rs: replace the task codename in the tracked-frame localsplus
  invariant comment with a description of the collector-vs-executing-frame
  behavior.
- object.rs: remove needless borrow on current_cls in the __class__
  assignment compatibility check.
- core.rs: rewrite the default_dealloc trashcan/untrack-skip comment to
  state the actual invariant, which now covers untracked non-escaped
  frames releasing at interpreter depth with bounded recursion.

Assisted-by: Claude

* Move type SetAttr slot rewrite inside the type lock and tighten cache reads

Run update_slot inside the same with_type_lock transaction as
modified_inner and the attributes dict mutation, so the version
invalidation, dict change, and slot-table rewrite are published together.

Load init_version and getitem_version with Acquire in the specialization
cache readers to pair with the Release stores in the writers.

Assisted-by: Claude

* Capture the type version before reading mutable slots in specializers

specialize_load_attr, specialize_store_attr, specialize_to_bool, and the
CallAllocAndEnterInit path of specialize_call read the version tag with
version_for_specialization before inspecting getattro, setattro, the
bool/len slots, and tp_new/tp_alloc, so a concurrent install of the
corresponding dunder invalidates the version the specialization is cached
against.

In the BinaryOpSubscr __getitem__ path, check the HEAPTYPE and
eval-frame gates before lookup_ref_and_version_interned, which takes the
global type lock and may allocate a version tag.

Assisted-by: Claude

* Use _get_method_dict for the test_type check in the test runner

Replace the direct __func__.__dict__ access with the _get_method_dict
helper so plain functions without __func__ do not raise AttributeError.

Assisted-by: Claude

* Fix lint hooks: cspell dictionary entries, spelling fix, formatting

Add qsbr to the rustpython dictionary and reborrows/reparenting to
the top-level word list. Rename oldto/newto locals to old_to/new_to.
Fix Stabilise -> Stabilize typo in a frame.rs comment.
Apply cargo fmt to object.rs, type.rs, and frame.rs, and ruff
format/check fixes to two extra_tests snippet files.

Assisted-by: Claude

* Rebuild all slots for type and descendants on __bases__ reassignment

Add PyType::update_all_slots, which invalidates version tags and iterates
the full SLOT_DEFS name table calling update_slot::<true> for each distinct
name. Unlike init_slots, which is additive and driven only by dunder names
present in the current MRO, this resets a slot whose method left the MRO
instead of leaving a stale dispatcher pointer.

Call it from set_bases in place of the previous modified_inner + init_slots
pair, so reassigning __bases__ rebuilds slots for the type and every
descendant.

Add extra_tests/snippets/type_bases_slot_rebuild.py covering removed-method
resets on zelf and deep descendants, the wrong-target switch, __getattr__,
the added-method mirror, and a swap-away-and-back round trip.

Assisted-by: Claude

* Derive the thread-local frame stack from the frame chain

Remove the per-VM `frames` Vec. current_frame, sys._getframe,
sys._getframemodulename, frame.f_back, sys.monitoring re-instrumentation,
gc.get_referrers, faulthandler stack dumps and the post-fork slot rebuild
now walk the signal-safe CURRENT_FRAME/`previous` chain instead. Add
`Py::from_payload_ptr` to recover a frame object from a chain pointer.

The cross-thread `ThreadSlot::frames` registry (sys._current_frames,
cross-thread f_back, GC stop-the-world assertion) is unchanged.

Assisted-by: Claude

* Inherit sub-slot fallback into the field being resolved

update_one_slot's number/sequence/mapping fallback inherited via the
accessor's default field. Left and right binary ops (add/right_add)
share one accessor but occupy distinct fields, so resolving an absent
right op or deleting it reset the left op's field, dropping a
still-defined __add__ dispatcher. Inherit the exact field under
resolution instead.

Assisted-by: Claude

* Gate type_cache_after_fork to its fork caller and use Self in downcast

type_cache_after_fork is only called from the unix fork path, so gate it
with all(feature = "host_env", unix) to match the caller and avoid a
dead-code error on non-unix targets. Replace the explicit PyType with Self
in the modified_inner downcast.

Assisted-by: Claude

* Unmark test_attr and test_method_call_error in test_monitoring

Both TestLoadSuperAttr tests now pass; remove their expectedFailure markers.

Assisted-by: Claude

* Run tp_new specialization __init__ without a trampoline frame

CallAllocAndEnterInit ran __init__ inside a synthetic init-cleanup shim
frame whose code carried the __init__ name. Since the thread frame stack
is derived from the frame chain, that shim frame was visible to
sys._getframe, f_back walks, traceback construction and inspect.stack /
inspect.trace. Its code object has empty co_positions, so a stack walk
that reached it raised StopIteration inside inspect._get_code_position,
cascading into unrelated failures (test_inspect trace/stack/frame,
asyncio source traceback).

Call __init__ directly via run_frame, enforce the __init__() should
return None contract inline, and drop the shim: the init-cleanup code
object, its builder, with_frame_untraced, monitoring_disabled_for_code,
and the extra-frame datastack/recursion budget. Removing the second
frame per construction also cuts the specialization's per-call cost.

Assisted-by: Claude

* Replace per-call thread-frame mutex with an atomic top-of-stack (unix)

On unix threading builds, publish each thread's top Python frame in a
single relaxed AtomicPtr store from set_current_frame instead of pushing
onto a parking_lot::Mutex<Vec<FramePtr>> per call. Cross-thread readers
(sys._current_frames, cross-thread f_back, faulthandler.dump_traceback,
the GC unreachable debug-assert) run under stop-the-world and walk the
published top frame down the Frame::previous chain; the owning thread is
then parked at a safepoint, so the pointer and the frames it reaches are
quiescent and alive. The faulthandler watchdog is a plain OS thread that
cannot stop-the-world, so it walks the chain lock-free and best-effort.

Non-unix threading builds have no stop-the-world and keep the existing
mutex-guarded frame stack unchanged.

Assisted-by: Claude

* Skip exc_info save/restore for callees that never touch the slot

with_frame saves and restores the shared exc_info slot around every
Python call to contain frames that leave it unbalanced. Cache a
has_exc_handling bit on PyCode at creation, set when the bytecode
contains any opcode that calls vm.set_exception (PushExcInfo, PopExcept,
CheckEgMatch, EndAsyncFor, InstrumentedEndAsyncFor). A callee whose code
has none of these cannot mutate the slot, so the save and restore are
skipped for it. Generators go through resume_gen_frame and are
unaffected.

Assisted-by: Claude

* Return a write-through FrameLocalsProxy from frame.f_locals

Optimized (function) frames now expose `f_locals` as a `FrameLocalsProxy`
implementing PEP 667 semantics instead of a cached snapshot dict:

- reads go live through the fast-local slots; each access mints a fresh
  proxy; keys that do not name a fast local are stored in a per-frame
  `f_extra_locals` side dict and folded into `locals()`.
- writes to a fast-local key store into the slot (or its cell) in place;
  deleting a fast local raises ValueError; extra keys delete normally.
- full mapping protocol: keys/values/items (lists), get/pop/setdefault,
  update (dict or FrameLocalsProxy only), __or__/__ior__/__ror__ (dict
  result), copy (plain dict), __reduce__ blocks pickling/copy, repr with
  recursion guard, mapping-pattern and Mapping ABC support.

Class/module/exec frames keep returning their namespace mapping directly.
Cross-thread access to a frame running on another thread still raises
RuntimeError.

A closed generator now keeps its frame locals when a durable frame
reference escaped (f_locals proxy, sys._getframe, f_back), matching
take_ownership; the escape is tracked with a per-frame flag. The
snapshot-then-fold locals_to_fast/locals_dirty write-back is retired
since proxy writes reach the slots directly.

Assisted-by: Claude

* Retain the caller frame so f_back resolves after it returns

When a frame escapes its execution (referenced through a traceback,
`sys._getframe`, `f_locals`, ...), capture a strong reference to its
caller at release time. `f_back` consults it once the caller has left
the live frame chain, so the Python-visible frame chain survives return.
The retained reference is a GC-traversed edge and is cleared by
`frame.clear()`, so ancestor chains stay collectable.

Assisted-by: Claude

* Fix debug-build native stack overflow on deep recursion

Make check_c_stack_overflow one-sided (trip whenever the stack pointer
is below the soft limit) so a single native frame larger than the margin
cannot step past the danger band undetected.

Raise the debug STACK_MARGIN_BYTES from 4096 to 16384 words so the margin
exceeds a single debug interpreter frame, leaving headroom to raise
RecursionError. Release margin unchanged.

Clamp the soft-limit margin to half the stack so small explicit thread
stacks do not get a soft limit above their stack top.

Assisted-by: Claude

* Allocate exception instance __dict__ lazily

Exception construction went through into_ref_with_type, which eagerly
allocated an empty instance dict for every HAS_DICT type. Add
into_ref_with_type_lazy_dict, which builds the instance with an
unallocated dict slot, and route the four exception construction sites
(PyBaseException, PyOSError, OSErrorBuilder, PyBaseExceptionGroup)
through it. The dict now materializes on first attribute write or
__dict__ access via the existing get_or_insert path.

add_note and PyImportError::slot_init now obtain the dict through
object_get_dict so they materialize it instead of assuming it exists.

A freshly constructed exception no longer reports an empty dict in
gc.get_referents, matching the reference interpreter.

Assisted-by: Claude

* Allocate exception __dict__ lazily in vm.new_exception

Route vm.new_exception() through into_ref_with_type_lazy_dict so
internally raised exceptions (new_type_error, new_value_error, etc.)
start without an instance dict, matching the slot_new path. The dict
is materialized on the first attribute write or __dict__ access.

Assisted-by: Claude

* Validate FrameLocalsProxy.update() arguments

Reject keyword arguments and require exactly one positional argument,
raising TypeError with the "takes no keyword arguments" and "takes
exactly one argument (N given)" messages.

Assisted-by: Claude

* Address review nits in frame and faulthandler

- Drop stale vm.frames reference from the release_datastack_frame
  uniqueness argument.
- Assert has_exc_handling when unwinding an Except-typed stack slot,
  documenting the invariant that guards the shared exc_info write.
- Truncate the specialized __init__ return-type name to 200 chars,
  matching the unspecialized wrapper.
- Reword two comments to describe behavior without prose references
  to CPython.

Assisted-by: Claude

* Use scopeguard for start_the_world in faulthandler dump_all_threads

Wrap the unix stop-the-world registry walk in a scope with
scopeguard::defer! so start_the_world runs on panic, matching the
f_back and get_all_current_frames sites. Add scopeguard to the stdlib
dependencies. Also correct the restore_exception doc comment to name
with_frame after the rename.

Assisted-by: Claude

* Gitignore docs/superpowers

Assisted-by: Claude

* Rename type_bases_slot_rebuild.py to builtin_type_bases.py

Match the builtin_* naming convention of extra_tests/snippets.

Assisted-by: Claude

* Apply rustfmt, ruff, and cspell lint fixes

Reformat with rustfmt, fix import spacing in builtin_type_bases.py with
ruff, and add "pointee" to the cspell word list.

Assisted-by: Claude

* Gate unix-only QSBR methods to their call sites

`online`, `drain_all`, and `reset_after_fork` are called only from unix
code (thread attach/detach, post-fork reset), and `offline` from unix
code plus a unit test. Gate them with matching cfg so `-D dead_code`
does not fire on non-unix targets.

Assisted-by: Claude

* Visit the function closure tuple as a GC edge

PyFunction::traverse visited the cells inside the closure tuple instead
of the tuple object itself, so the tuple's reference from the function
was never subtracted during cycle collection. A closure tuple that
reached back to its function (or, through a frame retained by f_back,
to a Thread) was stranded as a false GC root and never collected,
leaking the whole cycle. Visit the tuple itself, matching clear().

Assisted-by: Claude

* Apply formatting hook fix to builtin_type_bases.py

Assisted-by: Claude

* Unmark asyncgen finalization-by-gc tests in test_base_events

test_asyncgen_finalization_by_gc and
test_asyncgen_finalization_by_gc_in_other_thread now pass; GC finalizes
the async generators.

Assisted-by: Claude

* Unmark test_sni_callback_refcycle in test_ssl

The servername-callback reference cycle is now collected by GC.

Assisted-by: Claude

* Widen GC stop-the-world gates from unix-only to all threading builds

Change `cfg(all(unix, feature = "threading"))` to `cfg(feature = "threading")`
on the stop-the-world machinery so it also compiles and runs on non-unix
threading builds:

- StopTheWorldState, its stats, stw_trace, and the stop_the_world field
- ThreadSlot state/stop_requested/thread fields and their initializers
- wait_while_suspended/attach_thread/detach_thread/suspend_if_needed/do_suspend,
  allow_threads, stop_requested_for_current_thread, and the enter_vm /
  VmBootstrapGuard / attach_current_thread / release_current_thread / cleanup
  attach-state wiring
- eval_breaker_tripped, check_signals, run_scheduled_gc, signal GC_BIT /
  schedule_gc / take_gc_scheduled, and the frame.rs safepoint call
- CollectStopTheWorld and its use in collect_inner
- QSBR::online/offline, now called from attach/detach on all threading builds
- debug_assert_current_thread_attached and its type-cache call sites

maybe_collect defers auto-collection to the bytecode safepoint on every
threading build instead of only unix; non-threading builds keep the inline
collect. stw_trace writes to std stderr on non-unix.

top_frame publishing (CURRENT_TOP_FRAME_SLOT, set_current_frame), the
frame-walk debug assert in collect_inner, and the fork reinit helpers remain
unix-only; non-unix keeps ThreadSlot::frames for introspection.

Assisted-by: Claude

* Detach current thread around blocking _winapi/_overlapped waits

Wrap the blocking Windows wait calls in `vm.allow_threads` so the calling
thread transitions ATTACHED -> DETACHED for the duration of the wait:

- _winapi: WaitForSingleObject, WaitForMultipleObjects,
  BatchedWaitForMultipleObjects, ConnectNamedPipe, ReadFile,
  Overlapped.GetOverlappedResult
- _overlapped: Overlapped.getresult

These previously blocked while ATTACHED, so a stop-the-world requester
could never suspend the thread and spun in its wait loop indefinitely.

* Format WaitForMultipleObjects allow_threads closure

* Treat concurrent sni_callback removal as no-op in invoke_sni_callback

* Assert capi refcount on a fresh mortal list instead of the int type object

The refcount test asserted exact incref/decref deltas on PyInt's shared
type object. That object's reference count is perturbed by the other capi
tests running in parallel, and is immortal under some interpreter
configurations, so the deltas were not reliably +1/-1. Assert them on a
freshly created, uniquely owned list whose reference count is private to
the test and mortal.

Assisted-by: Claude

* Traverse and GC-track PyOSError instances

The `#[pyexception]` struct macro now forwards a `traverse` option to the
generated `#[pyclass]`, and `ExceptionItemMeta` accepts the `traverse`
key. `PyOSError` is marked `traverse = "manual"`, so `HAS_TRAVERSE` is
true and OSError-family instances are tracked at creation and traversed
by the collector.

`PyOSError::traverse` now visits the underlying `PyBaseException`
(traceback, cause, context, args) instead of `PyException::try_traverse`,
which was a no-op because `PyException` has `HAS_TRAVERSE = false`.

* Unmark test_blockingioerror in test_io

The BlockingIOError reference cycle is now collected by GC.

Assisted-by: Claude
2026-07-08 15:21:41 +09:00
Bas Schoenmaeckers 1e0f95ab3c Add pyframe functions to c-api (#8215) 2026-07-07 00:03:43 +09:00
Bas Schoenmaeckers 4caf959e58 Use PyO3 main & enable c-api tests (#8000)
Enable `abi3t` support in PyO3
Add left over c-api functions

Fix PyLong_AsUnsignedLongLongMask

Update pyo3 to `0.29`
Add `PyInt::as_u64_mask`

Fix `PyInt::as_u32_mask`
2026-07-05 01:13:45 +09:00
Bas Schoenmaeckers d1792fd2ce Add more int functions to the c-api (#8159)
* Add more int functions to the c-api

* Simplify as_mask logic

* Add `PyLong_FromNativeBytes` & `PyLong_FromUnsignedNativeBytes`
2026-06-30 23:31:15 +09:00
Bas Schoenmaeckers ab3d050485 Add allocator functions to c-api (#8168) 2026-06-25 22:34:14 +09:00
Jeong, YunWon 5add3d6a69 Align codegen passes and opcode metadata with CPython (#7987)
* Share marshal ref table between code object and its internals

read_marshal_bytes, _str, _str_vec, _name_tuple, and _const_tuple now
take a shared ref table and resolve TYPE_REF / register FLAG_REF
entries. deserialize_code is split into a public wrapper and an inner
function that receives the ref table; deserialize_value_depth opens a
fresh inner ref space when it hits Type::Code, mirroring CPython's
behaviour of putting the code object itself at ref slot 0. Nested code
objects inside const tuples reuse the surrounding code's ref space via
the new read_const_value helper.

* Align PYC magic number, FORMAT_VERSION, and header check with CPython 3.14

PYC_MAGIC_NUMBER changes from 2994 to 3627, matching CPython 3.14's
pyc_magic_number_token (0x0a0d0e2b). marshal FORMAT_VERSION drops from
5 to 4 (the encoder/marshal.version value; the decoder already accepts
both). check_pyc_magic_number_bytes now compares all four magic bytes
instead of the first two.

* Accept CPython-tagged .pyc as read-only bytecode source

SourceFileLoader.get_code now also looks for .pyc files using
_RP_FALLBACK_CACHE_TAGS (currently ('cpython-314',)) in addition to
sys.implementation.cache_tag. The matched .pyc is only used for
reading; recompilation still writes to the RustPython-tagged path, so
CPython's .pyc is never overwritten. Source-stat / hash / timestamp
validation logic is unchanged.

* Apply rustfmt to marshal helpers

* Marshal PySlice from format version 4 instead of 5

CPython's marshal supports TYPE_SLICE from format version 4 onwards
and that is the default version. Rejecting slice dumps below version
5 made marshal.dumps(slice(...)) fail with the default version and
broke test.test_marshal.SliceTestCase.test_slice.

* Revert "Accept CPython-tagged .pyc as read-only bytecode source"

Lib/importlib/_bootstrap_external.py is CPython's own code copied
verbatim; local patches here defeat compatibility tracking. The
cpython-XX cache_tag fallback needs to live on the RustPython side
(Rust code or sys.implementation.cache_tag policy), not as edits to
the imported standard library.

This reverts commit 1fc426d0fb5fcdb50d35cad13bbb43e8f6ce1c7f.

* Set marshal FORMAT_VERSION to 5 to match CPython 3.14.5

Py_MARSHAL_VERSION is 5 in CPython 3.14.5 (Include/marshal.h:16) and
TYPE_SLICE serialization rejects version < 5 (Python/marshal.c:720).
Restore the same threshold and constant so marshal.version and the
slice-marshal gate match CPython.

* Thread marshal recursion depth through nested code objects

Code objects embedded in const-tuples reset the depth budget on each
recursion, so a hostile or pathological marshal stream of code-in-tuple-
in-code can blow the stack despite MAX_MARSHAL_STACK_DEPTH. Pass the
current depth through deserialize_code_inner and read_marshal_const_tuple
and decrement at each code-object/tuple boundary.

Also route dict keys through deserialize_value_after_header so TYPE_CODE
keys decode instead of failing with BadType.

* align compiler to CPython

* Align codegen with CPython compile.c

Rename CFG helpers and accessors to the names used in CPython's
compile.c (basicblock_next_instr, basicblock_last_instr,
basicblock_append_instructions, bb_has_fallthrough, is_jump,
make_cfg_traversal_stack, mark_warm/mark_cold, etc.). Drop the unused
boolop-folding gate, mark_cpython_cfg_label_block helper, and
ComprehensionLoopControl::iter_range field.

Track an is_coroutine flag on SymbolTable, set in async def, await, and
async comprehensions, and propagate it through non-generator
comprehensions per symtable_handle_comprehension().

Mark SetupCleanup/SetupFinally/SetupWith as has_arg pseudo-ops, mark
ForIter as a terminator, and add has_arg/has_const on AnyInstruction.
Fix Instruction::stack_effect_jump to delegate to the opcode's
stack_effect_jump rather than stack_effect.

* Align codegen IR with CPython CFG structures

* Match CPython CFG annotation offset arithmetic

* Propagate CPython CFG label translation errors

* Align CPython exception target labeling flow

* Propagate CPython CFG traversal stack allocation errors

* Match CPython optimize_load_fast allocation flow

* Propagate CPython basicblock allocation errors

* Propagate CPython redundant NOP cleanup errors

* Propagate CPython unused const cleanup errors

* Propagate CPython const folding errors

* Propagate CPython swaptimize allocation errors

* Match CPython list-to-tuple fold allocation

* Skip const folding on CPython allocation failures

* Skip subscript folding on CPython allocation failures

* Propagate CPython assembler allocation errors

* Propagate CPython localsplus allocation errors

* Propagate CPython localsplus setup allocation errors

* Propagate CPython jump label map allocation errors

* Propagate CPython instruction sequence allocation errors

* Propagate CPython instruction label allocation errors

* Guard CPython codegen block allocation

* Guard CPython label shadow allocation

* Propagate CPython label shadow allocation errors

* Align CPython c-array allocation updates

* Align CPython ref stack growth

* Match CPython CFG builder debug check

* Avoid Rust-only CFG append clone

* Reuse CPython cleared block slots

* Use CPython block append in copy_basicblock

* Match CPython cfg builder creation order

* Clear label map after CPython apply pass

* Match CPython cfg builder allocation check

* Propagate CPython c-array size errors

* Drop Rust-only label uniqueness check

* Drop Rust-only label shadow debug checks

* Propagate CFG block index overflow

* Propagate CFG label oparg overflow

* Model CPython basicblock instruction storage

* Drop Rust-only recorded CFG precheck

* Model CPython instruction sequence storage

* Propagate instruction sequence offset overflow

* Model CPython instruction sequence labels

* Match CPython jump offset arithmetic

* Match CPython exception table arithmetic

* Match CPython label index arithmetic

* Match CPython instruction offset casts

* Match CPython jump offset indexing

* Match CPython oparg locals casts

* Match CPython localsplus offset arithmetic

* Match CPython cell prefix indexing

* Match CPython C array growth arithmetic

* Match CPython label map allocation arithmetic

* Match CPython label map size tracking

* Use CPython label map size in sequence passes

* Assert CPython label map clearing invariants

* Match CPython label oparg assignment

* Match CPython compiler direct arithmetic

* Match CPython load fast local casts

* Match CPython load fast depth assert

* Match CPython resume depth flagging

* Match CPython stack depth arithmetic

* Drop Rust-only stack overflow error

* Return CPython stack depth directly

* Match CPython C array growth errors

* Match CPython instruction insert asserts

* Match CPython unreachable pseudo jump

* Match CPython CFG size guard

* Match CPython superinstruction assert

* Match CPython redundant jump assert

* Match CPython stackdepth errors

* Match CPython jump offset flow

* Match CPython assembler buffer defaults

* Match CPython bytecode emit growth

* Match CPython assembler entry growth

* Match CPython assembler growth overflow check

* Match CPython remove_unreachable structure

* Match CPython static swap flow

* Inline CPython code unit preprocessing

* Match CPython C array growth checks

* Match CPython label map size guard

* Match CPython load-fast flow

* Simplify CPython CFG condition flow

* Align exception fallthrough propagation

* Match CPython pseudo target table

* Match CPython annotations CFG assert

* Match CPython inverted op assert

* Match CPython many-locals guard

* Reject deopt opcodes in CFG stack effects

* Match CPython invalid stack effect error

* Test CPython deopt stack effect guard

* Match CPython load-fast extended-arg assert

* Match CPython instruction allocation asserts

* Match CPython basicblock last-instr asserts

* Match CPython opcode range asserts

* Assert CPython fallthrough line propagation invariant

* Assert CPython CFG target offset sign

* Assert CPython exception fallthrough invariant

* Assert CPython exception stack bounds

* Assert CPython traversal stack allocation

* Match CPython label-map allocation in shadow

* Mirror CPython label-map sentinel fill

* Match CPython CFG builder allocation asserts

* Match CPython exception stack structure

* Match CPython ref stack structure

* Match CPython CFG traversal stack structure

* Mirror CPython CFG traversal stack pointer

* Use CPython fixed exception handler stack

* Mirror CPython ref stack capacity field

* Match CPython swap optimizer scratch stack

* Align static swap helpers with CPython blocks

* Align swaptimize signature with CPython

* Match CPython redundant pair pass result

* Match CPython inline pass result

* Match CPython redundant NOP pass results

* Fix bytecode metadata after upstream rebase

* Match CPython opcode stack metadata

* Add CPython identifiers to cspell dictionary

Add CNOTAB, LNOTAB, ialloc, ioffset, iused, nblocks, ncellsused,
ncellvars, nextop, noffsets, nvars, swaptimize, untargeted to
.cspell.dict/cpython.txt for the new CFG/assembler code in
crates/codegen/src/ir.rs.

* Fix CI failures from bytecode-parity work

- clippy: drop redundant `test_` prefix on three test functions and
  remove an unnecessary `u32` cast in basicblock_clear_reuses_cpython_spare_slots_in_offset_order
- insta: regenerate nested_double_async_with snapshot to match the new
  CFG output that drops unreferenced labels after the redundant-NOP pass
- regrtest: drop `@expectedFailure` markers from test_func_args,
  test_meth_args (test_compile), test_disassemble_with,
  test_disassemble_try_finally (test_dis), and test_except_star
  (test_monitoring) which now pass

* Resync generated opcode metadata

Empty conf.toml since WithExceptStart and Setup{Cleanup,Finally,With}
stack effects already match CPython, so the TODO override entries are
stale and only cause CI hook diffs.

Regenerate opcode_metadata.rs and drop the matching SetupCleanup/
SetupFinally/SetupWith assertions on PseudoOpcode::has_arg(); their
`HAS_ARG` flag comes from pseudo definitions in bytecodes.c that the
upstream analyzer does not propagate through PseudoInstruction.properties,
so the generated has_arg() excludes them. has_target() still covers
these block-push pseudos via is_block_push().

* Drop is_block_push has_arg invariant

The CPython invariant `assert(OPCODE_HAS_ARG(op) || !IS_BLOCK_PUSH(op))`
relies on SETUP_{FINALLY,CLEANUP,WITH} carrying `HAS_ARG_FLAG` in
CPython's metadata. The autogen tool reads pseudo-opcode properties from
target instructions and does not propagate the pseudo's own
HAS_ARG flag, so PseudoOpcode::has_arg() omits these three opcodes.
Drop the debug_assert that fired inside py_freeze proc-macro expansion.

* Auto-generate has_eval_break and route AnyInstruction has_arg/has_const via macro

Add fn_has_eval_break to generate_rs_opcode_metadata.py using CPython's
Properties.eval_breaker, removing the hand-written matches! body for
Opcode::has_eval_break and PseudoOpcode::has_eval_break.

Forward has_arg/has_const from Instruction and PseudoInstruction to
their opcode, so AnyInstruction can use either_real_pseudo! like the
other has_* accessors instead of an open-coded match.
2026-05-28 09:19:11 +09:00
Bas Schoenmaeckers 1128bb8853 Add capsule support to c-api (#7940) 2026-05-26 23:12:54 +09:00
Jeong, YunWon 6440e64355 Align codegen metadata with CPython (#7952) 2026-05-23 20:16:03 +09:00
Bas Schoenmaeckers 6d0fde896c Add call functions to c-api (#7937)
* Add call functions to c-api

* Review

* Move conversion impl to c-api
2026-05-21 00:22:55 +09:00
Jeong, YunWon d438b2ef16 Bytecode parity - CFG reorders and LOAD_FAST_BORROW chain (#7870)
* Align LOAD_FAST_BORROW analysis with CPython chain shape

Three changes that bring optimize_load_fast_borrow closer to CPython's
optimize_load_fast in flowgraph.c:

* ir.rs: split mark_cold into the CPython-style two passes. Phase 1
  propagates "warm" from the entry block, phase 2 propagates "cold" from
  except_handler blocks. Blocks reached by neither phase keep cold=false
  and stay in their original b_next position, matching CPython's handling
  of empty placeholders left by remove_unreachable (e.g. the inner_end of
  a nested try/except whose incoming jumps were re-routed by optimize_cfg).

* ir.rs: in optimize_load_fast_borrow, push the fall-through successor
  only when the current block has a last instruction (is_some_and).
  Empty blocks now terminate fall-through propagation, matching the
  `term != NULL` check in optimize_load_fast.

* compile.rs: add switch_to_new_or_reuse_empty() helper and use it in
  compile_while. The helper reuses the current block when it is empty
  and unlinked, mirroring USE_LABEL absorption in
  cfg_builder_maybe_start_new_block. This stops a stray empty block
  from appearing between e.g. a try/except end_block and the following
  while loop header.

Four codegen tests that depended on the previous fall-through-through-
empty behavior are marked #[ignore] with TODO comments.

Also includes a handful of dictionary entries in .cspell.dict picked up
during the work.

* Interleave const fold passes per-block to match CPython

Mirror CPython's optimize_basic_block() (flowgraph.c) by walking each
block once in instruction order and trying tuple, list, set, unary, and
binop folding at each position before advancing. This replaces the
previous global-pass sequence where every fold_unary_constants pattern
in the whole CFG was registered before any tuple constant, leaving
negated literals like `-1` at co_consts positions earlier than CPython
produces (e.g. snippets.py: -1 at idx 280 vs CPython idx 726).

Changes:
- Extract `fold_unary_constant_at` and `fold_binop_constant_at` per-
  position helpers from the existing global passes; the global passes
  now call the helpers in a loop.
- Add `fold_constants_per_block` that walks each block to a fixed point,
  trying all five folds at each instruction position.
- Call the new walker before the legacy global passes in
  optimize_finalize so co_consts insertion order matches CPython's.

Measured on the full Lib tree: differing files 270 → 269; the only
newly matching file is `test/test_ast/snippets.py`, the case raised in
youknowone/RustPython#28.

* Inline small fast-return blocks only through unconditional jumps

`inline_small_fast_return_blocks` previously appended the target
`LOAD_FAST(_BORROW)/RETURN_VALUE` block's instructions onto any
predecessor whose fall-through eventually reached it, in addition to the
unconditional-jump case CPython handles in
`inline_small_or_no_lineno_blocks` (flowgraph.c:1210). CPython only
inlines through unconditional jumps, leaving fall-through predecessors
to reach the shared return block via the natural CFG layout. The extra
fall-through branch duplicated the return tail (e.g. `if/elif/return`
emitted two adjacent `LOAD_FAST_BORROW x; RETURN_VALUE` sequences).

Remove the fall-through inlining branch and keep only the
unconditional-jump path.

Measured on the full Lib tree: differing files 270 → 239 (-31), no new
regressions. Files newly matching include copy.py, argparse.py,
dataclasses.py, logging/__init__.py, pathlib/__init__.py, etc.

* Allow scope-exit/jump-back reorder within a shared except handler

`reorder_conditional_scope_exit_and_jump_back_blocks` previously skipped
any reorder where the conditional, scope-exit, or jump-back block had
an `except_handler` attached, even when all three shared the same
handler. CPython reorders these regardless of try/except context, as
long as the blocks stay within the same protected region. The over-
conservative guard left patterns like `try: for: if cond: return` with
the loop body's scope-exit ahead of the backedge, while CPython places
the backedge first and inverts the conditional.

Replace the `block_is_protected` triple-check with a single
`mismatched_protection` test: skip only when the three blocks do not
share the same `except_handler`. Same-handler reorders preserve the
protected range because every instruction's `except_handler` field
stays attached as `.next` pointers shift.

Measured on the full Lib tree: differing files 239 → 237; no new
regressions.

* Skip jump-over-cleanup reorder when target restarts an exception scope

reorder_jump_over_exception_cleanup_blocks was swapping a small
scope-exit target with a preceding cold cleanup chain even when the
target block began a fresh try (SETUP_FINALLY/SETUP_CLEANUP/SETUP_WITH).
The swap moved the next try's setup ahead of the prior handler's
cleanup_end/next_handler/cleanup_block, making the cleanup_body's
JUMP_FORWARD fall through directly to the cleanup_end and get elided as
redundant. The bytecode then lacked the JUMP_FORWARD that skips the
cleanup blocks and matched the prior handler's borrow tail incorrectly.

Skip the reorder when the target block contains any block-push pseudo
op so a new try's setup stays in source order. Re-enables the four
named/typed except-cleanup borrow tests that were marked #[ignore] in
commit 7481459ea.

* fix if block_idx == BlockIdx::NULL
2026-05-16 17:17:36 +09:00
Bas Schoenmaeckers 4ec52c2e94 Add basic capi error support (#7787)
* Add basic capi error support

* Add missing symbols to make tests compile again

* Add `pyerrors` to dictionary

* Return exception in `Py_GetConstantBorrowed`

* Remove `allow(dead_code)`

* Fix windows

* Load stdlib when calling `Py_InitializeEx`

* Debug tests

* Revert "Load stdlib when calling `Py_InitializeEx`"

This reverts commit bccd38e981.

* Disable tests on windows for now

* Truncate `PyType_GetFlags` to be always 32 bits

* Add test for exception type checking

* Remove subclass type flags

* Use latest pyo3 to make test work on windows

* Revert "Use latest pyo3 to make test work on windows"

This reverts commit b2c2f6913f.

* `set_main_interpreter` -> `init_main_interpreter`
2026-05-13 23:19:58 +09:00
Jeong, YunWon edd7b4d45d Align bytecode CFG cleanup with CPython (#7781) 2026-05-12 22:14:24 +09:00
Bas SchoenmaeckersandJeong, YunWon f122e3e12a Add minimal capi lifecycle support (#7648)
* Add minimal capi lifecycle support

* Force enable `threading` on `stdlib`

---------

Co-authored-by: Jeong, YunWon <jeong@youknowone.org>
2026-05-06 02:02:26 +09:00
Jeong, YunWon fb9cafa8c9 Enable PEP 709 inlined comprehensions (#7412)
* Enable PEP 709 inlined comprehensions for function-like scopes

Activate the existing compile_inlined_comprehension() implementation
by fixing 6 bugs that prevented it from working:

- LoadFastAndClear: push NULL (not None) when slot is empty so
  StoreFast can restore empty state after comprehension
- StoreFast: accept NULL from stack for the restore path
- sub_tables.remove(0) replaced with next_sub_table cursor to
  match the pattern used elsewhere in the compiler
- in_inlined_comp flag moved from non-inlined to inlined path
- is_inlined_comprehension_context() now checks comp_inlined flag
  and restricts inlining to function-like scopes
- comp_inlined set only when parent scope uses fastlocals

Symbol table analysis handles conflict detection:
- Nested scopes in comprehension → skip inlining
- Bound name conflicts with parent symbol → skip inlining
- Cross-comprehension reference conflicts → skip inlining
- Splice comprehension sub_tables into parent for nested scope tracking

* Add localspluskinds, unify DEREF to localsplus index

- Add CO_FAST_LOCAL/CELL/FREE/HIDDEN constants and
  localspluskinds field to CodeObject for per-slot metadata
- Change DEREF instruction opargs from cell-relative indices
  (NameIdx) to localsplus absolute indices (oparg::VarNum)
- Add fixup_deref_opargs pass in ir.rs to convert cell-relative
  indices to localsplus indices after finalization
- Replace get_cell_name with get_localsplus_name in
  InstrDisplayContext trait
- Update VM cell_ref/get_cell_contents/set_cell_contents to use
  localsplus indices directly (no nlocals offset)
- Update function.rs cell2arg, super.rs __class__ lookup with
  explicit nlocals offsets

* Fix clippy warnings, formatting, restore _opcode_metadata.py

Fix cast_possible_truncation, nonminimal_bool, collapsible_if,
manual_contains clippy lints. Restore _opcode_metadata.py to
upstream/main version (3.14 aligned).

Pre-copy closure cells in Frame::new for coroutine locals().
Handle raw values in merged cell slots during inlined comps.
Exclude async comprehensions from inlining path.

* Exclude async/await comprehensions from PEP 709 inlining in symboltable

Async comprehensions and comprehensions with await in the element
expression need their own coroutine scope and cannot be inlined.
The symboltable builder was not checking these conditions, causing
incorrect symbol scope resolution when an async comprehension was
nested inside an inlined comprehension (e.g. [[x async for x in g]
for j in items]).
2026-03-21 22:48:35 +09:00
Jeong, YunWon 60c1f77288 update cspell config (#7452)
* add allowCompoundWords

* cspell dict
2026-03-17 20:18:31 +09:00
Jeong, YunWon 13a0623eb1 Adapt bitflagset to use enum (#7419) 2026-03-14 09:44:02 +09:00
Jeong, YunWon 8bc9a47e68 Add per-size tuple freelist (20 buckets × 2000 each) (#7361)
* Add per-size tuple freelist (20 buckets × 2000 each)

Implement PyTuple freelist matching tuples[PyTuple_MAXSAVESIZE]:
- TupleFreeList with 20 per-size buckets (sizes 1..=20, 2000 capacity each)
- freelist_push uses pre-clear size hint for correct bucket selection
- freelist_pop takes &Self payload to select bucket by size
- Type guard in new_ref handles structseq types sharing PyTuple vtable
- Add pyinner_layout<T>() helper for custom freelist Drop impls
- Update freelist_pop/push signatures across all freelist types

* freelist: exact type check at pop call-site

Move exact-type filtering from freelist_pop implementations to the
single call-site in new_ref. This prevents structseq and other subtypes
from popping tuple freelist entries entirely, rather than popping and
then deallocating on type mismatch.

Add Context::try_genesis() that returns None during bootstrap to avoid
deadlock when genesis() is called during Context initialization.

* Move exact type check from freelist_push to call-site in default_dealloc

Remove typ parameter from freelist_push trait signature. The exact type
check is now done once at the call-site alongside the heaptype check,
simplifying all freelist_push implementations.

* Remove freelist_hint; call freelist_push before tp_clear

By calling freelist_push before tp_clear, the payload is still intact
and can be read directly (e.g. tuple element count for bucket selection).
This eliminates freelist_hint and the hint parameter entirely.
2026-03-09 22:34:37 +09:00
Jeong, YunWon 7333192406 Suspend Python threads before fork() (#7364)
* Suspend Python threads before fork()

Add stop-the-world thread suspension around fork() to prevent
deadlocks from locks held by dead parent threads in the child.

- Thread states: DETACHED / ATTACHED / SUSPENDED with atomic CAS
  transitions matching _PyThreadState_{Attach,Detach,Suspend}
- stop_the_world / start_the_world: park all non-requester threads
  before fork, resume after (parent) or reset (child)
- allow_threads (Py_BEGIN/END_ALLOW_THREADS): detach around blocking
  syscalls (os.read/write, waitpid, Lock.acquire, time.sleep) so
  stop_the_world can force-park via CAS
- Acquire/release import lock around fork lifecycle
- zero_reinit_after_fork: generic lock reset for parking_lot types
- gc_clear_raw: detach dict instead of clearing entries
- Lock-free double-check for descriptor cache reads (no read-side
  seqlock); write-side seqlock retained for writer serialization
- fork() returns PyResult, checks PythonFinalizationError, calls
  sys.audit
2026-03-07 20:20:16 +09:00
Jeong, YunWon 99060c1dcd Object header slimming: prefix allocation for ObjExt (#7334)
* Object header slimming: prefix allocation for ObjExt

Extract dict, weak_list, and slots fields from PyInner<T> into a
separate ObjExt struct allocated as a prefix before PyInner using
Layout::extend(). Objects that don't need these fields (int, str,
float, list, tuple, dict, etc.) skip the prefix entirely.

- Add HAS_WEAKREF flag to PyTypeFlags for per-type weakref control
- Add HAS_EXT bit to GcBits indicating prefix presence
- Define ObjExt struct with dict, weak_list, slots
- Shrink PyInner header from ~80-88 bytes to ~32 bytes for lightweight objects
- Update all accessor methods to go through ext_ref()
- Update bootstrap type hierarchy to use prefix allocation
- Add __weakref__ getset descriptor for heap types
- Set HAS_WEAKREF on builtin types that support weak references
- Remove test_weak_keyed_bad_delitem expectedFailure (now passes)

* Add HAS_WEAKREF to _asyncio Future/Task, rename weakref helpers

- Add HAS_WEAKREF flag to PyFuture and PyTask (matches CPython)
- Rename subtype_getweakref/setweakref to subtype_get_weakref/set_weakref
  to fix cspell unknown word lint

* Add HAS_WEAKREF to array, deque, _grouper; remove expectedFailure markers

- Add HAS_WEAKREF to PyArray and PyDeque (matches CPython)
- Add HAS_WEAKREF to PyItertoolsGrouper (internal use by groupby)
- Remove 6 expectedFailure markers from test_dataclasses for weakref/slots
  tests that now pass

* Add HAS_WEAKREF to code, union, partial, lock, IO, mmap, sre, sqlite3, typevar types

Add HAS_WEAKREF flag to built-in types that support weakref:
- PyCode, PyUnion, PyPartial, Lock, RLock
- All IO base/concrete classes (_IOBase, _RawIOBase, _BufferedIOBase,
  _TextIOBase, BufferedReader, BufferedWriter, BufferedRandom,
  BufferedRWPair, TextIOWrapper, StringIO, BytesIO, FileIO,
  WindowsConsoleIO)
- PyMmap, sre Pattern, sqlite3 Connection/Cursor
- TypeVar, ParamSpec, ParamSpecArgs, ParamSpecKwargs, TypeVarTuple

Remove 3 expectedFailure markers from test_descr for now-passing tests.

* Add HAS_DICT to type flags and handle non-METHOD/CLASS in descr_get

- Add HAS_DICT flag to PyType (type metaclass) alongside existing
  HAS_WEAKREF. All type objects are instances of type and need dict
  support, matching CPython's PyType_Type.
- Replace unimplemented!() in PyMethodDescriptor::descr_get with
  fallback to bind obj directly, matching CPython's method_get which
  uses PyCFunction_NewEx for non-METH_METHOD methods.

* Fix ext detection, HeapMethodDef ownership, WASM error

- Remove HAS_EXT gc_bits flag; detect ext from type flags
  using raw pointer reads to avoid Stacked Borrows violations
- Store HeapMethodDef owner in payload instead of dict hack
- Clear dict entries in gc_clear_raw to break cycles
- Add WASM error fallback when exception serialization fails
2026-03-06 01:19:22 +09:00
Jeong, YunWon cf26f85230 Remove Frame mutex and use DataStack bump allocator for LocalsPlus (#7333)
* Remove PyMutex<FrameState> from Frame, use UnsafeCell fields directly

Move stack, cells_frees, prev_line out of the mutex-protected FrameState
into Frame as FrameUnsafeCell fields. This eliminates mutex lock/unlock
overhead on every frame execution (with_exec).

Safety relies on the same single-threaded execution guarantee that
FastLocals already uses.

* Add thread-local DataStack for bump-allocating frame data

Introduce DataStack with linked chunks (16KB initial, doubling) and
push/pop bump allocation. Add datastack field to VirtualMachine.
Not yet wired to frame creation.

* Unify FastLocals and BoxVec stack into LocalsPlus

Replace separate FastLocals (Box<[Option<PyObjectRef>]>) and
BoxVec<Option<PyStackRef>> with a single LocalsPlus struct that
stores both in a contiguous Box<[usize]> array. The first
nlocalsplus slots are fastlocals and the rest is the evaluation
stack. Typed access is provided through transmute-based methods.

Remove BoxVec import from frame.rs.

* Use DataStack for LocalsPlus in non-generator function calls

Normal function calls now bump-allocate LocalsPlus data from the
per-thread DataStack instead of a separate heap allocation.
Generator/coroutine frames continue using heap allocation since
they outlive the call.

On frame exit, data is copied to the heap (materialize_to_heap)
to preserve locals for tracebacks, then the DataStack is popped.

VirtualMachine.datastack is wrapped in UnsafeCell for interior
mutability (safe because frame allocation is single-threaded LIFO).

* Fix clippy: import Layout from core::alloc instead of alloc::alloc

* Fix vectorcall compatibility with LocalsPlus API

Update vectorcall dispatch functions to use localsplus stack
accessors instead of direct stack field access. Add
stack_truncate method to LocalsPlus. Update vectorcall fast
path in function.rs to use datastack and fastlocals_mut().

* Add datastack, nlocalsplus, ncells, tstate to cspell dictionary

* Fix DataStack pop() for non-monotonic allocation addresses

Check both bounds of the current chunk when determining if a
pop base is in the current chunk. The previous check (base >=
chunk_start) fails on Windows where newer chunks may be
allocated at lower addresses than older ones.

* Fix stale comments: release_datastack -> materialize_localsplus

* Fix non-threading mode for parallel test execution

Two fixes for Cell-based types used in static items under non-threading
mode, which cause data races when Rust test runner uses parallel threads:

1. LazyLock: use std::sync::LazyLock when std is available instead of
   wrapping core::cell::LazyCell with a false `unsafe impl Sync`.
   The LazyCell wrapper is kept only for no-std (truly single-threaded).

2. gc_state: use static_cell! (thread-local in non-threading mode)
   instead of OnceLock, so each thread gets its own GcState with
   Cell-based PyRwLock/PyMutex that are not accessed concurrently.

* Fix CallAllocAndEnterInit to use LocalsPlus stack API

* Use checked arithmetic in LocalsPlus and DataStack allocators

* Address code review: checked arithmetic, threading feature deps, Send gate

- Use checked arithmetic for nlocalsplus in Frame::new
- Add "std" to threading feature dependencies in rustpython-common
- Gate GcState Send impl with #[cfg(feature = "threading")]

* Clean up comments: remove redundant/stale remarks, fix CPython references
2026-03-04 23:27:36 +09:00
Jeong, YunWon af8f41ded1 freelist optimization (#7337)
* Add object freelist infrastructure and PyFloat freelist

Add freelist_push/freelist_pop hooks to PyPayload trait with default
no-op implementations. Modify default_dealloc to offer dead objects
to the freelist before freeing memory. Modify PyRef::new_ref to try
popping from the freelist before allocating.

Implement thread-local freelist for PyFloat (max 100 entries).
Float objects are ideal candidates: fixed-size payload (f64),
no GC tracking, no instance dict, trivial Drop.

Benchmark improvement on float-heavy workloads:
  float_arith: ~12% faster
  float_list:  ~17% faster

* Add PyList freelist and GC-safe freelist infrastructure

Add HAS_FREELIST const to PyPayload trait. For freelist types, GC
untracking is done immediately (not deferred) during dealloc to
prevent race conditions when the object is reused.

Restructure new_ref so freelist-popped objects also get GC tracked,
enabling freelist support for GC-tracked types like PyList.

Add drop_in_place for old payload before writing new one, required
for types with non-trivial Drop (e.g. PyList's RwLock<Vec>).

Implement thread-local freelist for PyList (max 80 entries).

* Add PyDict freelist and exact-type guard for freelist push

Add thread-local freelist for PyDict (max 80 entries).

Add heaptype check in default_dealloc to prevent subclass instances
from entering the freelist. Subclass objects have a different Python
type than the base type, so reusing them would return objects with
the wrong __class__.

Skip PyTuple freelist: structseq types (stat_result, struct_time)
are static subtypes sharing the same Rust payload, making type-safe
reuse impractical with heaptype check alone.

* Add PySlice freelist (max 1, matching PySlice_MAXFREELIST)

* Add FreeList<T> wrapper to drain cached objects on thread teardown

Replace Cell<Vec<*mut PyObject>> with Cell<FreeList<T>> which implements
Drop to properly deallocate PyInner<T> when threads exit.
Also fix freelist_pop safety doc to match actual contract.

* Add manual Traverse impl for PySlice with clear() and fix comment

* Deduplicate allocation fallback in PyRef::new_ref
2026-03-04 20:00:18 +09:00
Shahar Naveh 80e0403729 Bytecode enum named oparg (#7294)
* Align real opcodes

* Align pseudoes

* Add `consti` to known words
2026-03-03 21:33:59 +00:00
Jeong, YunWon f95be1c82b Implement LOAD_ATTR inline caching with adaptive specialization (#7292)
* Implement LOAD_ATTR inline caching with adaptive specialization

Add type version counter (tp_version_tag) to PyType with subclass
invalidation cascade. Add cache read/write methods (u16/u32/u64)
to CodeUnits. Implement adaptive specialization in load_attr that
replaces the opcode with specialized variants on first execution:

- LoadAttrMethodNoDict: cached method lookup for slotted types
- LoadAttrMethodWithValues: cached method with dict shadow check
- LoadAttrInstanceValue: direct dict lookup skipping descriptors

Specialized opcodes guard on type_version_tag and deoptimize back
to generic LOAD_ATTR with backoff counter on cache miss.

* Add BINARY_OP and CALL adaptive specialization

BINARY_OP: Specialize int add/subtract/multiply and float
add/subtract/multiply with type guards and deoptimization.

CALL: Add func_version to PyFunction, specialize simple
function calls (CallPyExactArgs, CallBoundMethodExactArgs)
with invoke_exact_args fast path that skips FuncArgs
allocation and fill_locals_from_args.

* Lazy quickening for adaptive specialization counters

Move counter initialization from compile-time to RESUME execution,
matching CPython's _PyCode_Quicken pattern. Store counter in CACHE
entry's arg byte to preserve op=Instruction::Cache for dis/JIT.
Add PyCode.quickened flag for one-time initialization.

* Add Instruction::deoptimize() and CodeUnits::original_bytes()

- deoptimize() maps specialized opcodes back to their base adaptive variant
- original_bytes() produces deoptimized bytecode with zeroed CACHE entries
- co_code now returns deoptimized bytes, _co_code_adaptive returns current bytes
- Marshal serialization uses original_bytes() instead of raw transmute

* Fix monitoring and specialization interaction

- cache_entries() returns correct count for instrumented opcodes
- deoptimize() maps instrumented opcodes back to base
- quicken() skips adaptive counter for instrumented opcodes
- instrument_code Phase 3 deoptimizes specialized opcodes and
  clears CACHE entries to prevent stale pointer dereferences

* Address review: bounds checks, UB fix, version overflow, error handling

- Add bounds checks to read_cache_u16/u32/u64
- Fix quicken() aliasing UB by using &mut directly
- Add JumpBackwardJit/JumpBackwardNoJit to deoptimize()
- Guard can_specialize_call with NEWLOCALS flag check
- Use compare_exchange_weak for version tag to prevent wraparound
- Propagate dict lookup errors in LoadAttrMethodWithValues
- Apply adaptive backoff on version tag assignment failure
- Remove duplicate imports in frame.rs
2026-03-02 16:36:57 +09:00
Jeong, YunWon 0868e256d8 Implement PyStackRef borrowed stack references for LOAD_FAST_BORROW (#7290)
* Implement PyStackRef borrowed stack references for LOAD_FAST_BORROW

* Fix PyStackRef: use NonZeroUsize for niche optimization, revert borrowed refs

- Change PyStackRef.bits from usize to NonZeroUsize so Option<PyStackRef>
  has the same size as Option<PyObjectRef> via niche optimization
- Revert LoadFastBorrow to use clone instead of actual borrowed refs
  to avoid borrowed refs remaining on stack at yield points
- Add static size assertions for Option<PyStackRef>
- Add stackref, fastlocal to cspell dictionary
- Remove debug eprintln statements
- Fix clippy warning for unused push_borrowed

* Add borrowed ref debug_assert to InstrumentedYieldValue, clean up comments
2026-03-01 23:32:56 +09:00
Jeong, YunWon 6015e15c16 Implement trace/profile events and opcode tracing for generators (#7268)
- Add TraceEvent::Exception and Opcode variants with profile filtering
- Extract dispatch_traced_frame helper for Call/Return trace events
- Fire exception trace on new raises, SEND StopIteration, FOR_ITER StopIteration
- Fire opcode trace events gated by f_trace_opcodes
- Move prev_line to FrameState for persistence across generator suspend/resume
- Reset prev_line in gen_throw for correct LINE monitoring after yield
- Add per-code event mask (events_for_code) to prevent unrelated code instrumentation
- Remove expectedFailure markers from test_bdb (5) and test_sys_setprofile (14)
2026-03-01 13:18:37 +09:00
Jeong, YunWon e123b9c073 set_f_lineno, set_f_lasti, PyAtomic refactor
- Implement set_f_lineno with stack analysis and deferred unwinding
- Add Frame::set_lasti() for trace callback line jumps
- Implement co_branches() on code objects
- Clear _cache_format in opcode.py (no inline caches)
- Fix getattro slot inheritance: preserve native slot from inherit_slots
- Fix BRANCH_RIGHT src_offset in InstrumentedPopJumpIf*
- Move lasti increment before line event for correct f_lineno
- Skip RESUME instruction from generating line events
- Defer stack pops via pending_stack_pops/pending_unwind_from_stack
  to avoid deadlock with state mutex
- Fix ForIter exhaust target in mark_stacks to skip END_FOR
- Prevent exception handler paths from overwriting normal-flow stacks
- Replace #[cfg(feature = "threading")] duplication with PyAtomic<T>
  from rustpython_common::atomic (Radium-based unified API)
- Remove expectedFailure from 31 now-passing jump tests
2026-02-27 09:07:23 +09:00
Jeong, YunWon 8a74956590 more algorithm-independent GC infra (#7194)
* mark poluting tests

* GC-infra independent to EBR

* trashcan

* add overflow guard to inc(), #[must_use] on dec()/safe_inc(), trashcan debug_assert, weakref generic re-check
2026-02-22 21:31:42 +09:00
Jeong, YunWon 88242301ff Use _print_exception_bltin in excepthook, register source in linecache (#7177)
- excepthook: call traceback._print_exception_bltin instead of
  traceback.print_exception to match PyErr_Display behavior
- run_string: register compiled code in linecache._interactive_cache
  so traceback can display source lines and caret indicators
- Remove test_sys_tracebacklimit expectedFailure
2026-02-18 01:50:27 +09:00
CPython Developers 0b0450e7da Update test_winconsoleio from v3.14.3 2026-02-16 00:31:57 +09:00
Jeong, YunWon 42740f7ce6 Rename constevaluator_alloc to const_evaluator_alloc, add to cspell dict 2026-02-11 17:00:30 +09:00
Jeong, YunWon 654a3eab8c Implement more warnings 2026-02-05 23:50:08 +09:00
Jeong, YunWon 48e4e7c661 Implement more ast features (#6986)
Assisted-by: Codex:gpt-6
2026-02-05 16:20:22 +09:00
Jeong, YunWon bededc2c01 Add GC infrastructure: object tracking, tp_clear, and helper methods (#6994) 2026-02-04 18:29:09 +09:00
Jeong, YunWonandClaude Sonnet 4.5 3814348943 dealloc and finalize_modules (#6934)
* rewrite finalize_modules with phased algorithm

Replace the absence of module finalization during interpreter shutdown
with a 5-phase algorithm matching pylifecycle.c finalize_modules():

1. Set special sys attributes to None, restore stdio
2. Set all sys.modules values to None, collect module dicts
3. Clear sys.modules dict
4. Clear module dicts in reverse import order (2-pass _PyModule_ClearDict)
5. Clear sys and builtins dicts last

This ensures __del__ methods are called during shutdown and modules are
cleaned up in reverse import order without hardcoded module names.

* dealloc the rigth way

* fix finalize_modules: only clear __main__ dict, mark daemon thread tests as expected failure

Without GC, clearing all module dicts during finalization causes __del__
handlers to fail (globals are None). Restrict Phase 4 to only clear
__main__ dict — other modules' globals stay intact for their __del__
handlers.

Mark test_daemon_threads_shutdown_{stdout,stderr}_deadlock as expected
failures — without GC+GIL, finalize_modules clears __main__ globals
while daemon threads are still running.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-02 10:10:58 +09:00
Jeong, YunWonandgithub-actions[bot] 60bec8a561 rework weakref (#6916)
* Replace WeakListInner with inline atomic weakref list and stripe locks

Remove heap-allocated WeakListInner (OncePtr<PyMutex<WeakListInner>>).
WeakRefList now holds two inline atomic pointers (head, generic).
PyWeak.parent replaced with wr_object pointing directly to referent.
Add weakref_lock module with AtomicU8 spinlock array for thread safety.
Rewrite upgrade/clear/drop_inner/count/get_weak_references with stripe lock.
Make Pointers methods public in linked_list.rs.
Remove expectedFailure from test_subclass_refs_dont_replace_standard_refs.

* Auto-format: cargo fmt --all

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-01 12:12:33 +09:00
Jeong, YunWon 714d1ce58b gc module internal structure and API (#6910)
* gc module internal structure and API

Add gc_state module with GcState, GcGeneration, GcDebugFlags, GcStats.
Replace gc module stubs with working API backed by gc_state.
Add gc_callbacks and gc_garbage to Context.
Add is_gc_tracked, gc_finalized, gc_get_referents to PyObject.
Collection is stubbed (returns 0) — actual algorithm to follow.

* fix dict/weakref/generators

* unmark test_asyncio

* apply review
2026-02-01 08:51:39 +09:00
Jeong, YunWon 95abec4c21 More Load instructions including LoadLocals, LoadFastBorrow (#6886)
* fix stdlib_io

* more scpell dict

* LoadLocals

* LoadFastBorrow

* more ops
2026-01-29 19:33:46 +09:00
Jeong, YunWon c3a5f9d5b2 cspell dict 2026-01-28 16:57:34 +09:00
Jeong, YunWon a7f2351244 Remove BEFORE_WITH,BINARY_SUBSCR (#6822) 2026-01-21 02:58:24 +09:00
Jeong, YunWon ddc5ba0b98 cspell phcount 2026-01-20 17:32:58 +09:00
CPython Developers 2823053614 Update test_types from v3.14.2 2026-01-20 10:59:47 +09:00
Jeong, YunWonandgithub-actions[bot] 82a8f67c71 Replace custom opcodes with CPython standard sequences (#6794)
* Replace custom opcodes with standard sequences

Remove RustPython-specific opcodes (BuildListFromTuples,
BuildMapForCall, BuildSetFromTuples, BuildTupleFromTuples)
and replace their usage with CPython 3.14 standard opcode
sequences:

- BuildListFromTuples → BUILD_LIST + LIST_EXTEND loop
- BuildSetFromTuples → BUILD_SET + SET_UPDATE loop
- BuildTupleFromTuples → BUILD_LIST + LIST_EXTEND + CALL_INTRINSIC_1(ListToTuple)
- BuildMapForCall → DICT_MERGE loop

Implement missing opcodes:
- ListExtend: Extend list with iterable elements
- SetUpdate: Add iterable elements to set
- DictMerge: Merge dict with duplicate key checking

* Auto-generate: generate_opcode_metadata.py

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-20 00:34:46 +09:00
Jeong, YunWon 566b6f438b PEP 649 annotation phase 4 2026-01-17 19:21:11 +09:00
Jeong, YunWon eee360822c Rework compiler about exception handling (#6638)
* asyncgen

* fix coroutine

* rewrite compiler

* tests

* set pyc magic number
2026-01-04 09:15:38 +09:00
Jeong, YunWon 546d35b8c1 impl multiprocessing SemLock (#6542) 2026-01-03 00:14:43 +09:00
Jeong, YunWon e10dc94992 Fix stdio_encoding, stdio_errors (#6617)
* stdio_errors, stdio_encodings

* add ascii

* unmark tests
2026-01-01 14:48:27 +09:00
Jeong, YunWon e79a1a1a66 Fix traceback (#6569)
* Fix traceback

* Update traceback from CPython 3.13.11

* unmark test_traceback

* fix code

* fix debug range

* fix tests
2025-12-30 16:58:53 +09:00
Jeong, YunWon feb5066be2 stdlib_module_names (#6568) 2025-12-29 21:43:40 +09:00