Skip to main content

rustpython_vm/
frame.rs

1// spell-checker: ignore compactlong compactlongs
2
3use crate::anystr::AnyStr;
4
5use crate::{
6    AsObject, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, PyStackRef,
7    TryFromObject, VirtualMachine,
8    builtins::{
9        PyBaseException, PyBaseExceptionRef, PyBaseObject, PyCode, PyCoroutine, PyDict, PyDictRef,
10        PyFloat, PyFrozenSet, PyGenerator, PyInt, PyInterpolation, PyList, PyModule, PyProperty,
11        PySet, PySlice, PyStr, PyStrInterned, PyTemplate, PyTraceback, PyType, PyUtf8Str,
12        builtin_func::PyNativeFunction,
13        descriptor::{PyMemberDescriptor, PyMethodDescriptor},
14        frame::stack_analysis,
15        function::{PyBoundMethod, PyCell, PyCellRef, PyFunction, vectorcall_function},
16        list::PyListIterator,
17        range::PyRangeIterator,
18        tuple::{PyTuple, PyTupleIterator, PyTupleRef},
19    },
20    bytecode::{
21        self, ADAPTIVE_COOLDOWN_VALUE, Instruction, LoadAttr, LoadSuperAttr, Opcode, SpecialMethod,
22    },
23    convert::{ToPyObject, ToPyResult},
24    coroutine::Coro,
25    exceptions::ExceptionCtor,
26    function::{ArgMapping, Callee, Either, FuncArgs, KwArgs, PyMethodFlags},
27    object::PyAtomicBorrow,
28    object::{Traverse, TraverseFn},
29    protocol::{PyIter, PyIterReturn},
30    scope::Scope,
31    sliceable::SliceableSequenceOp,
32    stdlib::{
33        _typing, builtins,
34        sys::monitoring::{self, MonitoringEvent},
35    },
36    types::{PyComparisonOp, PyTypeFlags},
37    vm::{Context, PyMethod},
38};
39use alloc::fmt;
40use bstr::ByteSlice;
41use core::cell::UnsafeCell;
42use core::ptr::NonNull;
43use core::sync::atomic;
44use core::sync::atomic::Ordering::{Acquire, Relaxed};
45use itertools::Itertools;
46use malachite_bigint::BigInt;
47use num_traits::{ToPrimitive, Zero};
48use rustpython_common::atomic::{PyAtomic, Radium};
49use rustpython_common::{
50    lock::{OnceCell, PyMutex},
51    wtf8::{Wtf8, Wtf8Buf, wtf8_concat},
52};
53use rustpython_compiler_core::{OneIndexed, SourceLocation};
54
55pub type FrameObjectRef = PyRef<FrameObject>;
56
57// -- Frame chain utilities --
58// The frame chain is a singly-linked list of `*const InterpreterFrame` stored
59// as `usize` values in `InterpreterFrame.previous` and the TLS
60// `CURRENT_FRAME`. Null (0) marks the end.
61// Each InterpreterFrame has a `materialized` pointer that is non-null when
62// a FrameObject wraps it (always the case today; stack-allocated frames will
63// leave it null until observed).
64
65/// Recover an owned reference to the FrameObject that wraps the given
66/// InterpreterFrame, or `None` if null or not materialized.
67///
68/// # Safety
69/// A non-null `iframe` must reference a live InterpreterFrame on the current
70/// thread's execution chain.
71unsafe fn owned_chain_frame(iframe: *const InterpreterFrame) -> Option<FrameObjectRef> {
72    if iframe.is_null() {
73        return None;
74    }
75    let iframe_ref = unsafe { &*iframe };
76    let fo = iframe_ref.frame_obj()?;
77    Some(fo.to_owned())
78}
79
80/// The current thread's topmost frame object, if any.
81#[must_use]
82pub fn current_thread_frame() -> Option<FrameObjectRef> {
83    let ptr = crate::vm::thread::get_current_frame();
84    unsafe { owned_chain_frame(ptr) }
85}
86
87/// Get the current thread's topmost InterpreterFrame pointer.
88#[must_use]
89pub fn current_thread_iframe() -> *const InterpreterFrame {
90    crate::vm::thread::get_current_frame()
91}
92
93/// The current thread's topmost frame object, materializing if necessary.
94/// Unlike `current_thread_frame()`, this always returns `Some` if there is
95/// an active frame, even if it's stack-allocated and hasn't been observed yet.
96/// Uses `vm.current_frame` Cell for fast lookup (no TLS).
97#[must_use]
98pub fn current_thread_frame_materialize(vm: &VirtualMachine) -> Option<FrameObjectRef> {
99    let ptr = crate::vm::thread::get_current_frame();
100    if ptr.is_null() {
101        return None;
102    }
103    let iframe = unsafe { &*ptr };
104    Some(iframe.materialize(vm).to_owned())
105}
106
107/// Read the globals dict from the topmost frame on this thread's chain.
108/// Returns `None` if the chain is empty.
109#[must_use]
110pub fn current_globals() -> Option<PyDictRef> {
111    let ptr = crate::vm::thread::get_current_frame();
112    if ptr.is_null() {
113        return None;
114    }
115    Some(unsafe { (*ptr).globals().to_owned() })
116}
117
118/// Read the code object from the topmost frame on this thread's chain.
119/// Returns `None` if the chain is empty.
120#[must_use]
121pub fn current_code() -> Option<PyRef<PyCode>> {
122    let ptr = crate::vm::thread::get_current_frame();
123    if ptr.is_null() {
124        return None;
125    }
126    Some(unsafe { (*ptr).code().to_owned() })
127}
128
129/// Read the builtins object from the topmost frame on this thread's chain.
130#[must_use]
131pub fn current_builtins() -> Option<PyObjectRef> {
132    let ptr = crate::vm::thread::get_current_frame();
133    if ptr.is_null() {
134        return None;
135    }
136    Some(unsafe { (*ptr).builtins().to_owned() })
137}
138
139/// The frame `offset` positions below the current thread's top frame (offset 0
140/// is the top), or `None` if the stack is not that deep.
141/// Materializes the FrameObject on demand for stack-allocated frames.
142#[must_use]
143pub fn frame_at_offset(offset: usize, vm: &VirtualMachine) -> Option<FrameObjectRef> {
144    let mut cur = crate::vm::thread::get_current_frame();
145    let mut remaining = offset;
146    while !cur.is_null() {
147        if remaining == 0 {
148            let iframe = unsafe { &*cur };
149            return Some(iframe.materialize(vm).to_owned());
150        }
151        remaining -= 1;
152        cur = unsafe { (*cur).previous.load(Relaxed) as *const InterpreterFrame };
153    }
154    None
155}
156
157/// If a FrameObject wrapping `target` InterpreterFrame is on the current
158/// thread's chain, return an owned reference to it; otherwise `None`.
159#[must_use]
160pub fn find_owned_chain_frame_by_iframe(target: *const InterpreterFrame) -> Option<FrameObjectRef> {
161    let mut cur = crate::vm::thread::get_current_frame();
162    while !cur.is_null() {
163        if core::ptr::eq(cur, target) {
164            return unsafe { owned_chain_frame(cur) };
165        }
166        cur = unsafe { (*cur).previous.load(Relaxed) as *const InterpreterFrame };
167    }
168    None
169}
170
171/// If `target` FrameObject is on the current thread's chain, return an
172/// owned reference to it; otherwise `None`. Presence on the chain proves liveness.
173#[must_use]
174pub fn find_owned_chain_frame(target: *const FrameObject) -> Option<FrameObjectRef> {
175    let mut cur = crate::vm::thread::get_current_frame();
176    while !cur.is_null() {
177        let iframe_ref = unsafe { &*cur };
178        if let Some(fo) = iframe_ref.frame_obj() {
179            let fo_payload: *const FrameObject = &**fo;
180            if core::ptr::eq(fo_payload, target) {
181                return Some(fo.to_owned());
182            }
183        }
184        cur = iframe_ref.previous.load(Relaxed) as *const InterpreterFrame;
185    }
186    None
187}
188
189/// Invoke `f` for each frame on the current thread's chain, from the
190/// topmost frame down to the bottom.
191pub fn for_each_current_frame(mut f: impl FnMut(&Py<FrameObject>)) {
192    let mut cur = crate::vm::thread::get_current_frame();
193    while !cur.is_null() {
194        let iframe_ref = unsafe { &*cur };
195        if let Some(fo) = iframe_ref.frame_obj() {
196            f(fo);
197        }
198        cur = iframe_ref.previous.load(Relaxed) as *const InterpreterFrame;
199    }
200}
201
202/// The reason why we might be unwinding a block.
203/// This could be return of function, exception being
204/// raised, a break or continue being hit, etc..
205#[derive(Clone, Debug)]
206enum UnwindReason {
207    /// We are returning a value from a return statement.
208    Returning { value: PyObjectRef },
209
210    /// We hit an exception, so unwind any try-except and finally blocks. The exception should be
211    /// on top of the vm exception stack.
212    Raising {
213        exception: PyBaseExceptionRef,
214        /// Instruction that raised; used for exception-table lookup.
215        offset: u32,
216    },
217}
218
219/// Tracks who owns a frame.
220// = `_PyFrameOwner`
221#[repr(i8)]
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub(crate) enum FrameOwner {
224    /// Being executed by a thread (FRAME_OWNED_BY_THREAD).
225    Thread = 0,
226    /// Owned by a generator/coroutine (FRAME_OWNED_BY_GENERATOR).
227    Generator = 1,
228    /// Not executing; held only by a frame object or traceback
229    /// (FRAME_OWNED_BY_FRAME_OBJECT).
230    FrameObject = 2,
231}
232
233impl FrameOwner {
234    pub(crate) fn from_i8(v: i8) -> Self {
235        match v {
236            0 => Self::Thread,
237            1 => Self::Generator,
238            _ => Self::FrameObject,
239        }
240    }
241}
242
243/// Lock-free mutable storage for frame-internal data.
244///
245/// # Safety
246/// FrameObject execution is single-threaded: only one thread at a time executes
247/// a given frame (enforced by the owner field and generator running flag).
248/// External readers (e.g. `f_locals`) are on the same thread as execution
249/// (trace callback) or the frame is not executing.
250pub(crate) struct FrameUnsafeCell<T>(UnsafeCell<T>);
251
252impl<T> FrameUnsafeCell<T> {
253    fn new(value: T) -> Self {
254        Self(UnsafeCell::new(value))
255    }
256
257    /// # Safety
258    /// Caller must ensure no concurrent mutable access.
259    #[inline(always)]
260    unsafe fn get(&self) -> *mut T {
261        self.0.get()
262    }
263
264    /// Safe exclusive access through `&mut self`.
265    #[inline(always)]
266    fn get_mut(&mut self) -> &mut T {
267        self.0.get_mut()
268    }
269}
270
271// SAFETY: FrameObject execution is single-threaded. See FrameUnsafeCell doc.
272#[cfg(feature = "threading")]
273unsafe impl<T: Send> Send for FrameUnsafeCell<T> {}
274#[cfg(feature = "threading")]
275unsafe impl<T: Send> Sync for FrameUnsafeCell<T> {}
276
277/// Compile-time switch for borrowed `LOAD_FAST_BORROW` pushes.
278///
279/// With this off, `LOAD_FAST_BORROW` behaves exactly like `LOAD_FAST`: it
280/// clones the fastlocals slot, so every load pays an atomic increment and the
281/// consuming instruction an atomic decrement. With it on, the entry the load
282/// pushes is a tagged borrow that owns no count, and both of those disappear
283/// for every consumer that only ever reads through the entry.
284///
285/// The safety argument lives on `PyStackRef` in `object/core.rs`; the codegen
286/// analysis that establishes it is `optimize_load_fast` in
287/// `crates/codegen/src/ir.rs`. Debug builds audit it in
288/// `LocalsPlus::debug_audit_local_release`.
289pub(crate) const BORROW_LOCAL_LOADS: bool = true;
290
291/// Unified storage for local variables and evaluation stack.
292///
293/// Memory layout (each slot is `usize`-sized):
294///   `[0..nlocalsplus)` — fastlocals (`Option<PyObjectRef>`)
295///   `[nlocalsplus..nlocalsplus+stack_top)` — active evaluation stack (`Option<PyStackRef>`)
296///   `[nlocalsplus+stack_top..capacity)` — unused stack capacity
297///
298/// Both `Option<PyObjectRef>` and `Option<PyStackRef>` are `usize`-sized
299/// (niche optimization on NonNull / NonZeroUsize). The raw storage is
300/// `usize` to unify them; typed access is provided through methods.
301pub struct LocalsPlus {
302    /// Backing storage.
303    data: LocalsPlusData,
304    /// Number of fastlocals slots (nlocals + ncells + nfrees).
305    nlocalsplus: u32,
306    /// Current evaluation stack depth.
307    stack_top: u32,
308}
309
310enum LocalsPlusData {
311    /// Heap-allocated storage (generators, coroutines, exec/eval frames).
312    Heap(Box<[usize]>),
313    /// Data stack allocated storage (normal function calls).
314    /// The pointer is valid while the enclosing data stack frame is alive.
315    DataStack { ptr: *mut usize, capacity: usize },
316}
317
318// SAFETY: DataStack variant points to thread-local DataStack memory.
319// FrameObject execution is single-threaded (enforced by owner field).
320#[cfg(feature = "threading")]
321unsafe impl Send for LocalsPlusData {}
322#[cfg(feature = "threading")]
323unsafe impl Sync for LocalsPlusData {}
324
325const _: () = {
326    assert!(core::mem::size_of::<Option<PyObjectRef>>() == core::mem::size_of::<usize>());
327    // PyStackRef size is checked in object/core.rs
328};
329
330impl LocalsPlus {
331    /// Create a new heap-backed LocalsPlus.  All slots start as None (0).
332    fn new(nlocalsplus: usize, stacksize: usize) -> Self {
333        let capacity = nlocalsplus
334            .checked_add(stacksize)
335            .expect("LocalsPlus capacity overflow");
336        let nlocalsplus_u32 = u32::try_from(nlocalsplus).expect("nlocalsplus exceeds u32");
337        Self {
338            data: LocalsPlusData::Heap(vec![0usize; capacity].into_boxed_slice()),
339            nlocalsplus: nlocalsplus_u32,
340            stack_top: 0,
341        }
342    }
343
344    /// Create a new LocalsPlus backed by the thread data stack.
345    /// All slots are zero-initialized.
346    ///
347    /// When the frame finishes, the caller must migrate data to the heap with
348    /// `materialize_localsplus()` (or drop it in place with
349    /// `release_localsplus()`), then `datastack_pop()` to free the memory.
350    pub(crate) fn new_on_datastack(
351        nlocalsplus: usize,
352        stacksize: usize,
353        vm: &VirtualMachine,
354    ) -> Self {
355        let capacity = nlocalsplus
356            .checked_add(stacksize)
357            .expect("LocalsPlus capacity overflow");
358        let byte_size = capacity
359            .checked_mul(core::mem::size_of::<usize>())
360            .expect("LocalsPlus byte size overflow");
361        let nlocalsplus_u32 = u32::try_from(nlocalsplus).expect("nlocalsplus exceeds u32");
362        let ptr = vm.datastack_push(byte_size) as *mut usize;
363        // Zero-initialize all slots (0 = None for both PyObjectRef and PyStackRef).
364        unsafe { core::ptr::write_bytes(ptr, 0, capacity) };
365        Self {
366            data: LocalsPlusData::DataStack { ptr, capacity },
367            nlocalsplus: nlocalsplus_u32,
368            stack_top: 0,
369        }
370    }
371
372    /// Migrate data-stack-backed storage to the heap, preserving all values.
373    /// Returns the data stack base pointer for `DataStack::pop()`.
374    /// Returns `None` if already heap-backed.
375    fn materialize_to_heap(&mut self) -> Option<*mut u8> {
376        if let LocalsPlusData::DataStack { ptr, capacity } = &self.data {
377            let base = *ptr as *mut u8;
378            let heap_data = unsafe { core::slice::from_raw_parts(*ptr, *capacity) }
379                .to_vec()
380                .into_boxed_slice();
381            self.data = LocalsPlusData::Heap(heap_data);
382            Some(base)
383        } else {
384            None
385        }
386    }
387
388    /// Drop all contained values and detach the data stack backing without
389    /// copying to the heap, leaving an empty heap-backed husk.
390    /// Returns the data stack base pointer for `DataStack::pop()`.
391    /// Returns `None` if already heap-backed.
392    ///
393    /// Only valid when the values can never be observed again (the enclosing
394    /// frame is uniquely referenced): the locals are gone afterwards.
395    pub(crate) fn release_datastack(&mut self) -> Option<*mut u8> {
396        let LocalsPlusData::DataStack { ptr, .. } = &self.data else {
397            return None;
398        };
399        let base = *ptr as *mut u8;
400        // Drop values while the backing store is still valid. Value drops may
401        // run `__del__`, which can push nested data stack frames above `base`;
402        // those are popped before the caller pops `base` (LIFO preserved).
403        self.drop_values();
404        self.data = LocalsPlusData::Heap(Box::default());
405        // Keep the accessors consistent with the empty backing store.
406        // stack_top is already 0 after drop_values().
407        self.nlocalsplus = 0;
408        Some(base)
409    }
410
411    /// Update fastlocals in `self` from `src`. For each slot, drops the old
412    /// value and clones the new one. `self` must be heap-backed.
413    ///
414    /// # Safety
415    /// Both `self` and `src` must have valid backing storage, and the caller
416    /// must ensure no concurrent mutable access.
417    pub(crate) unsafe fn sync_fastlocals_from(&mut self, src: &Self) {
418        let n = core::cmp::min(self.nlocalsplus as usize, src.nlocalsplus as usize);
419        let dst = self.fastlocals_mut();
420        let source = src.fastlocals();
421        for i in 0..n {
422            let old = dst[i].take();
423            dst[i].clone_from(&source[i]);
424            drop(old);
425        }
426    }
427
428    /// Drop all contained values without freeing the backing storage.
429    fn drop_values(&mut self) {
430        self.stack_clear();
431        let fastlocals = self.fastlocals_mut();
432        for slot in fastlocals.iter_mut() {
433            let _ = slot.take();
434        }
435    }
436
437    // -- Data access helpers --
438
439    #[inline(always)]
440    fn data_as_slice(&self) -> &[usize] {
441        match &self.data {
442            LocalsPlusData::Heap(b) => b,
443            LocalsPlusData::DataStack { ptr, capacity } => unsafe {
444                core::slice::from_raw_parts(*ptr, *capacity)
445            },
446        }
447    }
448
449    #[inline(always)]
450    fn data_as_mut_slice(&mut self) -> &mut [usize] {
451        match &mut self.data {
452            LocalsPlusData::Heap(b) => b,
453            LocalsPlusData::DataStack { ptr, capacity } => unsafe {
454                core::slice::from_raw_parts_mut(*ptr, *capacity)
455            },
456        }
457    }
458
459    /// Total capacity (fastlocals + stack).
460    #[inline(always)]
461    fn capacity(&self) -> usize {
462        match &self.data {
463            LocalsPlusData::Heap(b) => b.len(),
464            LocalsPlusData::DataStack { capacity, .. } => *capacity,
465        }
466    }
467
468    /// Whether the backing storage still lives on the thread data stack (a
469    /// running call frame that has not been materialized onto the heap).
470    fn is_datastack_backed(&self) -> bool {
471        matches!(self.data, LocalsPlusData::DataStack { .. })
472    }
473
474    /// Stack capacity (max stack depth).
475    #[inline(always)]
476    fn stack_capacity(&self) -> usize {
477        self.capacity() - self.nlocalsplus as usize
478    }
479
480    // -- Fastlocals access --
481
482    /// Immutable access to fastlocals as `Option<PyObjectRef>` slice.
483    #[inline(always)]
484    pub(crate) fn fastlocals(&self) -> &[Option<PyObjectRef>] {
485        let data = self.data_as_slice();
486        let ptr = data.as_ptr() as *const Option<PyObjectRef>;
487        unsafe { core::slice::from_raw_parts(ptr, self.nlocalsplus as usize) }
488    }
489
490    /// Mutable access to fastlocals as `Option<PyObjectRef>` slice.
491    #[inline(always)]
492    pub(crate) fn fastlocals_mut(&mut self) -> &mut [Option<PyObjectRef>] {
493        let nlocalsplus = self.nlocalsplus as usize;
494        let data = self.data_as_mut_slice();
495        let ptr = data.as_mut_ptr() as *mut Option<PyObjectRef>;
496        unsafe { core::slice::from_raw_parts_mut(ptr, nlocalsplus) }
497    }
498
499    // -- Stack access --
500
501    /// Current stack depth.
502    #[inline(always)]
503    fn stack_len(&self) -> usize {
504        self.stack_top as usize
505    }
506
507    /// Whether the stack is empty.
508    #[inline(always)]
509    fn stack_is_empty(&self) -> bool {
510        self.stack_top == 0
511    }
512
513    /// Push a value onto the evaluation stack.
514    #[inline(always)]
515    fn stack_push(&mut self, val: Option<PyStackRef>) {
516        let idx = self.nlocalsplus as usize + self.stack_top as usize;
517        debug_assert!(
518            idx < self.capacity(),
519            "stack overflow: stack_top={}, capacity={}",
520            self.stack_top,
521            self.stack_capacity()
522        );
523        let data = self.data_as_mut_slice();
524        data[idx] = unsafe { core::mem::transmute::<Option<PyStackRef>, usize>(val) };
525        self.stack_top += 1;
526    }
527
528    /// Try to push; returns Err if stack is full.
529    #[inline(always)]
530    fn stack_try_push(&mut self, val: Option<PyStackRef>) -> Result<(), Option<PyStackRef>> {
531        let idx = self.nlocalsplus as usize + self.stack_top as usize;
532        if idx >= self.capacity() {
533            return Err(val);
534        }
535        let data = self.data_as_mut_slice();
536        data[idx] = unsafe { core::mem::transmute::<Option<PyStackRef>, usize>(val) };
537        self.stack_top += 1;
538        Ok(())
539    }
540
541    /// Push a PyObjectRef onto the evaluation stack.
542    /// Panics on overflow.
543    pub(crate) fn push_stack(&mut self, value: PyObjectRef) {
544        self.stack_try_push(Some(PyStackRef::new_owned(value)))
545            .expect("stack overflow in push_stack");
546    }
547
548    /// Pop a value from the evaluation stack.
549    #[inline(always)]
550    fn stack_pop(&mut self) -> Option<PyStackRef> {
551        debug_assert!(self.stack_top > 0, "stack underflow");
552        self.stack_top -= 1;
553        let idx = self.nlocalsplus as usize + self.stack_top as usize;
554        let data = self.data_as_mut_slice();
555        let raw = core::mem::replace(&mut data[idx], 0);
556        unsafe { core::mem::transmute::<usize, Option<PyStackRef>>(raw) }
557    }
558
559    /// Debug-mode audit of the borrow invariant: nothing may drop the last
560    /// strong count of an object that a borrowed stack entry still points at.
561    ///
562    /// Called from the eval loop wherever a fastlocals slot is about to
563    /// release what it holds (`STORE_FAST`, `DELETE_FAST` and the fused
564    /// forms). Codegen's `optimize_load_fast` is what guarantees this cannot
565    /// happen; the check is here to catch a gap in that analysis, or an
566    /// instruction whose modelled stack effect does not match the one the
567    /// interpreter actually has, before it turns into a use-after-free.
568    #[inline(always)]
569    fn debug_audit_local_release(&self, idx: usize) {
570        #[cfg(debug_assertions)]
571        {
572            let Some(old) = self.fastlocals()[idx].as_ref() else {
573                return;
574            };
575            // Another owner keeps it alive; releasing this slot frees nothing.
576            if old.strong_count() != 1 {
577                return;
578            }
579            let old_ptr = old.as_object() as *const PyObject;
580            for i in 0..self.stack_top as usize {
581                if let Some(stack_ref) = self.stack_index(i)
582                    && stack_ref.is_borrowed()
583                    && core::ptr::eq(stack_ref.as_object() as *const PyObject, old_ptr)
584                {
585                    panic!(
586                        "borrow invariant violated: fastlocals[{idx}] is dropping the last \
587                         reference to an object that stack slot {i} still borrows"
588                    );
589                }
590            }
591        }
592        #[cfg(not(debug_assertions))]
593        let _ = idx;
594    }
595
596    /// Give every borrowed stack ref its own reference.
597    ///
598    /// A borrowed ref is only sound while whatever it points at is guaranteed
599    /// to outlive it, which stops holding where the frame itself outlives the
600    /// running block — at a yield, where the stack is saved with the frame.
601    fn promote_stack(&mut self) {
602        for idx in 0..self.stack_top as usize {
603            if let Some(stack_ref) = self.stack_index_mut(idx) {
604                stack_ref.promote();
605            }
606        }
607    }
608
609    /// Immutable view of the active stack as `Option<PyStackRef>` slice.
610    #[inline(always)]
611    fn stack_as_slice(&self) -> &[Option<PyStackRef>] {
612        let data = self.data_as_slice();
613        let base = self.nlocalsplus as usize;
614        let ptr = unsafe { (data.as_ptr().add(base)) as *const Option<PyStackRef> };
615        unsafe { core::slice::from_raw_parts(ptr, self.stack_top as usize) }
616    }
617
618    /// Get a reference to a stack slot by index from the bottom.
619    #[inline(always)]
620    fn stack_index(&self, idx: usize) -> Option<&PyStackRef> {
621        debug_assert!(idx < self.stack_top as usize);
622        let data = self.data_as_slice();
623        let raw_idx = self.nlocalsplus as usize + idx;
624        unsafe { (*(data.as_ptr().add(raw_idx) as *const Option<PyStackRef>)).as_ref() }
625    }
626
627    /// Get a mutable reference to a stack slot by index from the bottom.
628    #[inline(always)]
629    fn stack_index_mut(&mut self, idx: usize) -> &mut Option<PyStackRef> {
630        debug_assert!(idx < self.stack_top as usize);
631        let raw_idx = self.nlocalsplus as usize + idx;
632        let data = self.data_as_mut_slice();
633        unsafe { &mut *(data.as_mut_ptr().add(raw_idx) as *mut Option<PyStackRef>) }
634    }
635
636    /// Get the last stack element (top of stack).
637    #[inline(always)]
638    fn stack_last(&self) -> Option<Option<&PyStackRef>> {
639        if self.stack_top == 0 {
640            None
641        } else {
642            Some(self.stack_index(self.stack_top as usize - 1))
643        }
644    }
645
646    /// Get mutable reference to the last stack element.
647    #[inline(always)]
648    fn stack_last_mut(&mut self) -> Option<&mut Option<PyStackRef>> {
649        if self.stack_top == 0 {
650            None
651        } else {
652            let idx = self.stack_top as usize - 1;
653            Some(self.stack_index_mut(idx))
654        }
655    }
656
657    /// Swap two stack elements.
658    #[inline(always)]
659    fn stack_swap(&mut self, a: usize, b: usize) {
660        let base = self.nlocalsplus as usize;
661        let data = self.data_as_mut_slice();
662        data.swap(base + a, base + b);
663    }
664
665    /// Truncate the stack to `new_len` elements, dropping excess values.
666    fn stack_truncate(&mut self, new_len: usize) {
667        debug_assert!(new_len <= self.stack_top as usize);
668        while self.stack_top as usize > new_len {
669            let _ = self.stack_pop();
670        }
671    }
672
673    /// Clear the stack, dropping all values.
674    fn stack_clear(&mut self) {
675        while self.stack_top > 0 {
676            let _ = self.stack_pop();
677        }
678    }
679
680    /// Move active stack references out as owned references without running
681    /// finalizers while this locals-plus storage is mutably borrowed.
682    fn take_stack_object_refs(&mut self) -> Vec<PyObjectRef> {
683        let mut refs = Vec::with_capacity(self.stack_top as usize);
684        while self.stack_top > 0 {
685            if let Some(value) = self.stack_pop() {
686                refs.push(value.to_pyobj());
687            }
688        }
689        refs
690    }
691
692    /// Extract every owned Python reference for GC's deferred-drop phase.
693    fn clear_into(&mut self, out: &mut Vec<PyObjectRef>) {
694        while self.stack_top > 0 {
695            if let Some(value) = self.stack_pop() {
696                out.push(value.to_pyobj());
697            }
698        }
699        out.extend(self.fastlocals_mut().iter_mut().filter_map(Option::take));
700    }
701
702    /// Drain stack elements from `from` to the end, returning an iterator
703    /// that yields `Option<PyStackRef>` in forward order and shrinks the stack.
704    fn stack_drain(
705        &mut self,
706        from: usize,
707    ) -> impl ExactSizeIterator<Item = Option<PyStackRef>> + '_ {
708        let end = self.stack_top as usize;
709        debug_assert!(from <= end);
710        // Reduce stack_top now; the drain iterator owns the elements.
711        self.stack_top = from as u32;
712        LocalsPlusStackDrain {
713            localsplus: self,
714            current: from,
715            end,
716        }
717    }
718
719    /// Extend the stack with values from an iterator.
720    fn stack_extend(&mut self, iter: impl Iterator<Item = Option<PyStackRef>>) {
721        for val in iter {
722            self.stack_push(val);
723        }
724    }
725}
726
727/// Iterator for draining stack elements in forward order.
728struct LocalsPlusStackDrain<'a> {
729    localsplus: &'a mut LocalsPlus,
730    /// Current read position (stack-relative index).
731    current: usize,
732    /// End position (exclusive, stack-relative index).
733    end: usize,
734}
735
736impl Iterator for LocalsPlusStackDrain<'_> {
737    type Item = Option<PyStackRef>;
738
739    fn next(&mut self) -> Option<Self::Item> {
740        if self.current >= self.end {
741            return None;
742        }
743        let idx = self.localsplus.nlocalsplus as usize + self.current;
744        let data = self.localsplus.data_as_mut_slice();
745        let raw = core::mem::replace(&mut data[idx], 0);
746        self.current += 1;
747        Some(unsafe { core::mem::transmute::<usize, Option<PyStackRef>>(raw) })
748    }
749
750    fn size_hint(&self) -> (usize, Option<usize>) {
751        let remaining = self.end - self.current;
752        (remaining, Some(remaining))
753    }
754}
755
756impl ExactSizeIterator for LocalsPlusStackDrain<'_> {}
757
758impl Drop for LocalsPlusStackDrain<'_> {
759    fn drop(&mut self) {
760        while self.current < self.end {
761            let idx = self.localsplus.nlocalsplus as usize + self.current;
762            let data = self.localsplus.data_as_mut_slice();
763            let raw = core::mem::replace(&mut data[idx], 0);
764            let _ = unsafe { core::mem::transmute::<usize, Option<PyStackRef>>(raw) };
765            self.current += 1;
766        }
767    }
768}
769
770impl Drop for LocalsPlus {
771    fn drop(&mut self) {
772        // drop_values handles both stack and fastlocals.
773        // For DataStack-backed storage, the caller should have called
774        // materialize_localsplus() + datastack_pop() before drop.
775        // If not (e.g. panic), the DataStack memory is leaked but
776        // values are still dropped safely.
777        self.drop_values();
778    }
779}
780
781unsafe impl Traverse for LocalsPlus {
782    fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
783        self.fastlocals().traverse(tracer_fn);
784        self.stack_as_slice().traverse(tracer_fn);
785    }
786}
787
788/// Lazy locals dict for frames. For NEWLOCALS frames, the dict is
789/// only allocated on first access (most function frames never need it).
790pub struct FrameLocals {
791    inner: OnceCell<ArgMapping>,
792}
793
794impl FrameLocals {
795    /// Create with an already-initialized locals mapping (non-NEWLOCALS frames).
796    pub(crate) fn with_locals(locals: ArgMapping) -> Self {
797        let cell = OnceCell::new();
798        let _ = cell.set(locals);
799        Self { inner: cell }
800    }
801
802    /// Create an empty lazy locals (for NEWLOCALS frames).
803    /// The dict will be created on first access.
804    pub(crate) fn lazy() -> Self {
805        Self {
806            inner: OnceCell::new(),
807        }
808    }
809
810    /// Get the locals mapping, creating it lazily if needed.
811    #[inline]
812    pub fn get_or_create(&self, vm: &VirtualMachine) -> &ArgMapping {
813        self.inner
814            .get_or_init(|| ArgMapping::from_dict_exact(vm.ctx.new_dict()))
815    }
816
817    /// Get the locals mapping if already created.
818    #[inline]
819    pub fn get(&self) -> Option<&ArgMapping> {
820        self.inner.get()
821    }
822
823    #[inline]
824    pub fn mapping(&self, vm: &VirtualMachine) -> crate::protocol::PyMapping<'_> {
825        self.get_or_create(vm).mapping()
826    }
827
828    #[inline]
829    pub fn clone_mapping(&self, vm: &VirtualMachine) -> ArgMapping {
830        self.get_or_create(vm).clone()
831    }
832
833    pub fn into_object(&self, vm: &VirtualMachine) -> PyObjectRef {
834        self.clone_mapping(vm).into()
835    }
836
837    pub fn as_object(&self, vm: &VirtualMachine) -> &PyObject {
838        self.get_or_create(vm).obj()
839    }
840
841    fn take(&mut self) -> Option<ArgMapping> {
842        self.inner.take()
843    }
844}
845
846impl fmt::Debug for FrameLocals {
847    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
848        f.debug_struct("FrameLocals")
849            .field("initialized", &self.inner.get().is_some())
850            .finish()
851    }
852}
853
854impl Clone for FrameLocals {
855    fn clone(&self) -> Self {
856        let cell = OnceCell::new();
857        if let Some(locals) = self.inner.get() {
858            let _ = cell.set(locals.clone());
859        }
860        Self { inner: cell }
861    }
862}
863
864unsafe impl Traverse for FrameLocals {
865    fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
866        if let Some(locals) = self.inner.get() {
867            locals.traverse(tracer_fn);
868        }
869    }
870}
871
872/// Cold fields of InterpreterFrame that are only accessed during tracing,
873/// debugging, frame inspection, or GC. Lazily allocated on first access
874/// to keep the hot InterpreterFrame small.
875pub(crate) struct FrameColdData {
876    pub trace: PyMutex<Option<PyObjectRef>>,
877    pub trace_opcodes: PyMutex<bool>,
878    pub temporary_refs: PyMutex<Vec<PyObjectRef>>,
879    pub f_extra_locals: PyMutex<Option<PyDictRef>>,
880    /// Backing storage for the borrowed reference returned by the legacy
881    /// `PyEval_GetLocals()` C API. It is populated only by that adapter.
882    pub f_locals_cache: PyMutex<Option<PyDictRef>>,
883    pub f_overwritten_fast_locals: PyMutex<Vec<PyObjectRef>>,
884    pub retained_back: PyMutex<Option<FrameObjectRef>>,
885    pub pending_stack_pops: PyAtomic<u32>,
886    pub pending_unwind_from_stack: PyAtomic<i64>,
887    /// Thread that is still running the frame this one was materialized from,
888    /// or 0 once that frame has returned (and for every frame object that was
889    /// not materialized from a running frame). Only a thread id, never a
890    /// pointer: reading it can never chase freed memory, so it stays usable
891    /// as the gate for frames that belong to another thread.
892    pub attached_tid: atomic::AtomicU64,
893}
894
895impl Default for FrameColdData {
896    fn default() -> Self {
897        Self {
898            trace: PyMutex::new(None),
899            trace_opcodes: PyMutex::new(false),
900            temporary_refs: PyMutex::new(Vec::new()),
901            f_extra_locals: PyMutex::new(None),
902            f_locals_cache: PyMutex::new(None),
903            f_overwritten_fast_locals: PyMutex::new(Vec::new()),
904            retained_back: PyMutex::new(None),
905            pending_stack_pops: Default::default(),
906            pending_unwind_from_stack: Default::default(),
907            attached_tid: atomic::AtomicU64::new(0),
908        }
909    }
910}
911
912/// Lightweight execution frame. Not a PyObject.
913/// Analogous to CPython's `_PyInterpreterFrame`.
914///
915/// The four identity fields (`code`, `globals`, `builtins`, `func_obj`)
916/// are borrowed raw pointers. Construction is `unsafe` and is what
917/// establishes that they stay valid for the frame's life (or until
918/// `init_iframe_ptrs` overwrites them). A live `InterpreterFrame` may
919/// then be executed through safe APIs such as `run_iframe`.
920#[repr(C)]
921pub struct InterpreterFrame {
922    // Borrowed pointers — owned by FrameObject or by PyFunction on caller's stack.
923    pub(crate) code: *const Py<PyCode>,
924    pub(crate) func_obj: *const PyObject, // nullable
925    pub(crate) globals: *const Py<PyDict>,
926    pub(crate) builtins: *const PyObject,
927
928    /// Unified storage for local variables and evaluation stack.
929    pub(crate) localsplus: LocalsPlus,
930    pub locals: FrameLocals,
931
932    /// index of last instruction ran
933    pub lasti: PyAtomic<u32>,
934
935    /// Previous line number for LINE event suppression.
936    pub(crate) prev_line: core::cell::Cell<u32>,
937
938    /// Back-reference to owning generator/coroutine/async generator.
939    /// Borrowed reference (not ref-counted) to avoid Generator↔FrameObject cycle.
940    /// Cleared by the generator's Drop impl.
941    pub generator: PyAtomicBorrow,
942    /// Linked-list pointer to the previous frame in the call chain.
943    /// Stores a `*const FrameObject` as `usize`.
944    pub(crate) previous: PyAtomic<usize>,
945    /// Who owns this frame. Mirrors `_PyInterpreterFrame.owner`.
946    /// Used by `frame.clear()` to reject clearing an executing frame,
947    /// even when called from a different thread.
948    pub(crate) owner: atomic::AtomicI8,
949    /// Base pointer of the datastack allocation when this frame and its
950    /// localsplus are bump-allocated together. Null for heap-backed frames.
951    pub(crate) datastack_base: *mut u8,
952    /// Pointer to the owning `Py<FrameObject>`, or null for stack-allocated
953    /// frames that have not been materialized yet.
954    /// Stored as `usize` for `PyAtomic` compatibility.
955    pub(crate) materialized: PyAtomic<usize>,
956
957    /// Lazily-allocated cold data (tracing, debugging, frame inspection).
958    /// Not allocated until first access via `cold()`.
959    pub(crate) cold: OnceCell<Box<FrameColdData>>,
960}
961
962// Raw pointers make InterpreterFrame !Send+!Sync by default.
963// SAFETY: The pointers reference heap-resident PyObjects whose lifetimes
964// are managed by the owning FrameObject (or PyFunction). Frame execution
965// is single-threaded (enforced by the owner field).
966#[cfg(feature = "threading")]
967unsafe impl Send for InterpreterFrame {}
968#[cfg(feature = "threading")]
969unsafe impl Sync for InterpreterFrame {}
970
971impl InterpreterFrame {
972    /// Construct a new InterpreterFrame with raw pointers set from the given references.
973    ///
974    /// For FrameObject-owned frames, `init_iframe_ptrs` patches the pointers
975    /// after heap allocation; the pointers passed here are then overwritten.
976    ///
977    /// # Safety
978    /// `code`, `globals`, `builtins`, and `func_obj` (if any) must remain
979    /// valid for the lifetime of this frame, or until `init_iframe_ptrs`
980    /// overwrites the stored pointers.
981    #[allow(clippy::too_many_arguments)]
982    #[inline(always)]
983    pub(crate) unsafe fn new(
984        code: &Py<PyCode>,
985        globals: &Py<PyDict>,
986        builtins: &PyObject,
987        func_obj: Option<&PyObject>,
988        localsplus: LocalsPlus,
989        locals: FrameLocals,
990        closure: &[PyCellRef],
991        owner: FrameOwner,
992    ) -> Self {
993        let mut localsplus = localsplus;
994        let nlocalsplus = code.localspluskinds.len();
995
996        // Pre-copy closure cells into free var slots so that locals() works
997        // even before COPY_FREE_VARS runs (e.g. coroutine before first send).
998        // COPY_FREE_VARS will overwrite these on first execution.
999        {
1000            let nfrees = code.freevars.len();
1001            if nfrees > 0 {
1002                let freevar_start = nlocalsplus - nfrees;
1003                let fastlocals = localsplus.fastlocals_mut();
1004                for (i, cell) in closure.iter().enumerate() {
1005                    fastlocals[freevar_start + i] = Some(cell.clone().into());
1006                }
1007            }
1008        }
1009
1010        // For generators/coroutines, initialize prev_line to the def line
1011        // so that preamble instructions (RETURN_GENERATOR, POP_TOP) don't
1012        // fire spurious LINE events.
1013        let prev_line = if code.flags.intersects(
1014            bytecode::CodeFlags::GENERATOR
1015                | bytecode::CodeFlags::COROUTINE
1016                | bytecode::CodeFlags::ASYNC_GENERATOR,
1017        ) {
1018            code.first_line_number.map_or(0, |line| line.get() as u32)
1019        } else {
1020            0
1021        };
1022
1023        Self {
1024            code: code as *const Py<PyCode>,
1025            func_obj: match func_obj {
1026                Some(obj) => obj as *const PyObject,
1027                None => core::ptr::null(),
1028            },
1029            globals: globals as *const Py<PyDict>,
1030            builtins: builtins as *const PyObject,
1031            localsplus,
1032            locals,
1033            lasti: Radium::new(0),
1034            prev_line: core::cell::Cell::new(prev_line),
1035            generator: PyAtomicBorrow::new(),
1036            previous: Radium::new(0),
1037            owner: atomic::AtomicI8::new(owner as i8),
1038            datastack_base: core::ptr::null_mut(),
1039            materialized: Radium::new(0),
1040            cold: OnceCell::new(),
1041        }
1042    }
1043
1044    /// Allocate an InterpreterFrame and its LocalsPlus data together on the
1045    /// thread data stack in a single bump allocation.
1046    ///
1047    /// Layout: `[InterpreterFrame | localsplus usize×capacity]`
1048    ///
1049    /// Returns a mutable reference whose lifetime is bounded by the data
1050    /// stack's LIFO discipline. The caller must call
1051    /// `release_datastack_frame()` when done, then
1052    /// `vm.datastack_pop_frame(base, size)`. The reference must not be used after
1053    /// `release_datastack_frame` returns.
1054    ///
1055    /// # Safety
1056    /// `code`, `globals`, `builtins`, and `func_obj` (if any) must remain
1057    /// valid until `release_datastack_frame` returns.
1058    #[allow(clippy::too_many_arguments)]
1059    #[inline(always)]
1060    pub(crate) unsafe fn new_on_datastack<'a>(
1061        code: &Py<PyCode>,
1062        globals: &Py<PyDict>,
1063        builtins: &PyObject,
1064        func_obj: Option<&PyObject>,
1065        locals: FrameLocals,
1066        closure: &[PyCellRef],
1067        vm: &VirtualMachine,
1068    ) -> &'a mut Self {
1069        let nlocalsplus = code.localspluskinds.len();
1070        let stacksize = code.max_stackdepth as usize;
1071        let capacity = nlocalsplus
1072            .checked_add(stacksize)
1073            .expect("LocalsPlus capacity overflow");
1074
1075        let total_bytes = datastack_iframe_total_bytes(nlocalsplus, stacksize);
1076        let (base, reused_cleared_frame) = vm.datastack_push_frame(total_bytes);
1077
1078        // InterpreterFrame lives at the start of the allocation.
1079        let iframe_ptr = base as *mut Self;
1080        // LocalsPlus data follows the InterpreterFrame, aligned to usize.
1081        let localsplus_data_ptr =
1082            unsafe { base.add(datastack_iframe_localsplus_offset()) } as *mut usize;
1083
1084        if !reused_cleared_frame {
1085            // Fresh or differently shaped storage may contain old frame data.
1086            unsafe { core::ptr::write_bytes(localsplus_data_ptr, 0, capacity) };
1087        }
1088
1089        let nlocalsplus_u32 = u32::try_from(nlocalsplus).expect("nlocalsplus exceeds u32");
1090        let localsplus = LocalsPlus {
1091            data: LocalsPlusData::DataStack {
1092                ptr: localsplus_data_ptr,
1093                capacity,
1094            },
1095            nlocalsplus: nlocalsplus_u32,
1096            stack_top: 0,
1097        };
1098
1099        let mut iframe = unsafe {
1100            // SAFETY: the caller of `new_on_datastack` guarantees these
1101            // objects outlive the datastack frame.
1102            Self::new(
1103                code,
1104                globals,
1105                builtins,
1106                func_obj,
1107                localsplus,
1108                locals,
1109                closure,
1110                FrameOwner::Thread,
1111            )
1112        };
1113        iframe.datastack_base = base;
1114
1115        // Write the fully initialized InterpreterFrame into the datastack.
1116        unsafe {
1117            core::ptr::write(iframe_ptr, iframe);
1118            &mut *iframe_ptr
1119        }
1120    }
1121
1122    /// Release this datastack-allocated frame's resources and return the
1123    /// base pointer for `vm.datastack_pop()`.
1124    ///
1125    /// Drops all localsplus values, runs destructors for all frame fields
1126    /// (trace, temporary_refs, retained_back, etc.), and detaches the
1127    /// backing store.
1128    /// Returns `None` if this frame is not datastack-allocated.
1129    ///
1130    /// After this call, the InterpreterFrame at `self` is logically dead —
1131    /// the caller must not use `self` again except to pass the returned
1132    /// base to `vm.datastack_pop()`.
1133    pub(crate) unsafe fn release_datastack_frame(&mut self) -> Option<(*mut u8, usize)> {
1134        let base = self.datastack_base;
1135        if base.is_null() {
1136            return None;
1137        }
1138        let total_bytes = datastack_iframe_total_bytes(
1139            self.localsplus.nlocalsplus as usize,
1140            self.localsplus.stack_capacity(),
1141        );
1142        self.datastack_base = core::ptr::null_mut();
1143        // Drop all localsplus values while the backing store is still valid.
1144        self.localsplus.drop_values();
1145        // Detach from the data stack so further accesses see an empty frame.
1146        self.localsplus.data = LocalsPlusData::Heap(Box::default());
1147        self.localsplus.nlocalsplus = 0;
1148        // Drop remaining frame fields (trace, temporary_refs, retained_back,
1149        // etc.) by running destructors in place. The localsplus is already
1150        // empty/heap-backed, so this only drops non-localsplus fields.
1151        // SAFETY: `self` points to valid, initialized memory on the data
1152        // stack. After this call the memory is logically dead.
1153        unsafe { core::ptr::drop_in_place(self) };
1154        Some((base, total_bytes))
1155    }
1156
1157    /// Get the last instruction index.
1158    #[inline(always)]
1159    pub fn get_lasti(&self) -> u32 {
1160        self.lasti.load(Relaxed)
1161    }
1162
1163    /// Get the previous InterpreterFrame in the chain, or null.
1164    #[inline(always)]
1165    pub fn previous(&self) -> *const Self {
1166        self.previous.load(Relaxed) as *const Self
1167    }
1168
1169    /// Get the owning FrameObject, if this frame has been materialized.
1170    #[inline(always)]
1171    pub(crate) fn frame_obj(&self) -> Option<&Py<FrameObject>> {
1172        let ptr = self.materialized.load(Relaxed);
1173        if ptr == 0 {
1174            None
1175        } else {
1176            Some(unsafe { &*(ptr as *const Py<FrameObject>) })
1177        }
1178    }
1179
1180    /// Materialize a FrameObject for this InterpreterFrame on demand.
1181    /// If already materialized, returns the existing one.
1182    /// The created FrameObject shares the raw pointers with this frame.
1183    #[cold]
1184    #[inline(never)]
1185    pub(crate) fn materialize(&self, vm: &VirtualMachine) -> &Py<FrameObject> {
1186        if let Some(fo) = self.frame_obj() {
1187            return fo;
1188        }
1189        self.materialize_slow(vm)
1190    }
1191
1192    /// Take a standalone copy of this frame, values included, for a thread
1193    /// that does not own it.
1194    ///
1195    /// Nothing links the copy back to this frame: the owning thread will not
1196    /// find it at `exit_iframe` and so never writes into it once the world
1197    /// restarts. That is the whole point — a linked copy is a buffer the owner
1198    /// rewrites slot by slot while the reader clones out of it.
1199    ///
1200    /// # Safety
1201    /// Caller must hold the world stopped, so the owning thread is parked and
1202    /// its fast locals are not moving while they are read.
1203    #[cfg(feature = "threading")]
1204    #[cold]
1205    #[inline(never)]
1206    pub(crate) unsafe fn materialize_detached(&self, vm: &VirtualMachine) -> FrameObjectRef {
1207        // Deliberately not `materialize_chain`: that hands back an existing
1208        // linked copy when the owning thread has already made one.
1209        let fo = self.materialize_slow_chain(vm);
1210        unsafe {
1211            fo.iframe_mut()
1212                .localsplus
1213                .sync_fastlocals_from(&self.localsplus)
1214        };
1215        fo
1216    }
1217
1218    /// Copy this frame and everything it was called from for a thread that
1219    /// does not own them, linking `f_back` along the way, and return the copy
1220    /// of this frame. The links are `retained_back`, so the chain keeps
1221    /// resolving once the world restarts and the real frames return.
1222    ///
1223    /// # Safety
1224    /// Caller must hold the world stopped, so the owning thread is parked and
1225    /// the chain is not being popped while it is walked.
1226    #[cfg(feature = "threading")]
1227    #[cold]
1228    #[inline(never)]
1229    pub(crate) unsafe fn materialize_detached_chain(&self, vm: &VirtualMachine) -> FrameObjectRef {
1230        let top = unsafe { self.materialize_detached(vm) };
1231        let mut child = top.clone();
1232        let mut cur = self.previous();
1233        while !cur.is_null() {
1234            let caller = unsafe { &*cur };
1235            let caller_fo = unsafe { caller.materialize_detached(vm) };
1236            {
1237                let mut guard = child.iframe().cold().retained_back.lock();
1238                if guard.is_none() {
1239                    *guard = Some(caller_fo.clone());
1240                }
1241            }
1242            child = caller_fo;
1243            cur = caller.previous();
1244        }
1245        top
1246    }
1247
1248    /// Create a lightweight FrameObject with empty localsplus, suitable for
1249    /// f_back chain building (retained_back). Unlike `materialize`, this does
1250    /// NOT store into `temporary_refs` or set the `materialized` pointer, so
1251    /// the returned FrameObject is only kept alive by the caller's `PyRef`.
1252    /// This prevents non-GC-tracked `temporary_refs` on a stack-allocated
1253    /// iframe from defeating cycle collection.
1254    #[cold]
1255    #[inline(never)]
1256    pub(crate) fn materialize_chain(&self, vm: &VirtualMachine) -> FrameObjectRef {
1257        if let Some(fo) = self.frame_obj() {
1258            return fo.to_owned();
1259        }
1260        self.materialize_slow_chain(vm)
1261    }
1262
1263    #[cold]
1264    fn materialize_slow(&self, vm: &VirtualMachine) -> &Py<FrameObject> {
1265        // Create a full FrameObject with its own InterpreterFrame copy.
1266        // The FrameObject owns references to the same objects (code, globals, etc).
1267        let code: PyRef<PyCode> = self.code().to_owned();
1268        let globals: PyDictRef = self.globals().to_owned();
1269        let builtins: PyObjectRef = self.builtins().to_owned();
1270        let func_obj: Option<PyObjectRef> = self.func_obj().map(|o| o.to_owned());
1271
1272        // Empty localsplus, sized for the code object. While the source frame
1273        // runs, every reader resolves it through `find_live_source_iframe`, and
1274        // `exit_iframe` fills these slots from the live frame as it returns.
1275        // Copying the values here instead would give each of them a second
1276        // reference lasting as long as this FrameObject — a frame reached by
1277        // one traceback entry would keep all of its locals alive.
1278        let nlocalsplus = code.localspluskinds.len() as u32;
1279        let localsplus = LocalsPlus {
1280            data: LocalsPlusData::Heap(vec![0usize; nlocalsplus as usize].into_boxed_slice()),
1281            nlocalsplus,
1282            stack_top: 0,
1283        };
1284
1285        // Copy the locals mapping if it exists.
1286        let locals = match self.locals.get() {
1287            Some(mapping) => FrameLocals::with_locals(mapping.clone()),
1288            None => FrameLocals::lazy(),
1289        };
1290
1291        // Build a fresh InterpreterFrame inside the FrameObject.
1292        // Its raw pointers will be patched by init_iframe_ptrs.
1293        let inner_iframe = Self {
1294            code: core::ptr::null(),
1295            func_obj: core::ptr::null(),
1296            globals: core::ptr::null(),
1297            builtins: core::ptr::null(),
1298            localsplus,
1299            locals,
1300            lasti: Radium::new(self.lasti.load(Relaxed)),
1301            prev_line: core::cell::Cell::new(self.prev_line.get()),
1302            generator: PyAtomicBorrow::new(),
1303            // Do NOT copy previous — it may point to stack-allocated frames
1304            // that become dangling after their call returns. The f_back chain
1305            // is resolved through the TLS CURRENT_FRAME chain instead.
1306            previous: Radium::new(0),
1307            // Always FrameObject-owned. If we copied Thread from the source
1308            // iframe, frame.clear() would reject the frame with "cannot clear
1309            // an executing frame"; `attached_tid` carries the "still running"
1310            // half of that state instead, so the owner field does not have to.
1311            owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8),
1312            datastack_base: core::ptr::null_mut(),
1313            materialized: Radium::new(0),
1314            cold: OnceCell::from(Box::new(FrameColdData {
1315                attached_tid: atomic::AtomicU64::new(current_thread_ident()),
1316                ..FrameColdData::default()
1317            })),
1318        };
1319
1320        let frame_obj = FrameObject {
1321            f_trace_lines: core::sync::atomic::AtomicBool::new(self.trace_lines_flag()),
1322            owned_code: Some(code),
1323            owned_globals: Some(globals),
1324            owned_builtins: Some(builtins),
1325            owned_func_obj: func_obj,
1326            iframe: FrameUnsafeCell::new(Some(inner_iframe)),
1327        };
1328        let frame_ref = frame_obj.into_ref(&vm.ctx);
1329        FrameObject::init_iframe_ptrs(&frame_ref);
1330        // Set the inner iframe's materialized pointer to self
1331        unsafe {
1332            frame_ref
1333                .iframe_mut()
1334                .materialized
1335                .store(&*frame_ref as *const Py<FrameObject> as usize, Relaxed);
1336        }
1337
1338        // Store the materialized pointer on this stack frame.
1339        let fo_ptr = &*frame_ref as *const Py<FrameObject> as usize;
1340        self.materialized.store(fo_ptr, Relaxed);
1341
1342        // Keep the FrameObject alive by storing it in temporary_refs.
1343        // GC tracking is deferred to `exit_iframe`, where the frame is no
1344        // longer executing and temporary_refs is cleared — at that
1345        // point the FrameObject is self-sustaining and GC can safely
1346        // traverse and collect it.
1347        self.cold()
1348            .temporary_refs
1349            .lock()
1350            .push(frame_ref.clone().into());
1351
1352        // SAFETY: the pointer we stored above remains valid because
1353        // temporary_refs holds a strong reference.
1354        unsafe { &*(fo_ptr as *const Py<FrameObject>) }
1355    }
1356
1357    /// Like `materialize_slow` but with empty localsplus to avoid extra
1358    /// refcounts on local variables. Only suitable for f_back chain building.
1359    /// Returns an owned `PyRef` without storing into `temporary_refs` or
1360    /// setting the `materialized` pointer, so GC can still detect cycles.
1361    #[cold]
1362    fn materialize_slow_chain(&self, vm: &VirtualMachine) -> FrameObjectRef {
1363        let code: PyRef<PyCode> = self.code().to_owned();
1364        let globals: PyDictRef = self.globals().to_owned();
1365        let builtins: PyObjectRef = self.builtins().to_owned();
1366        let func_obj: Option<PyObjectRef> = self.func_obj().map(|o| o.to_owned());
1367
1368        // Empty localsplus — reads go through find_live_source_iframe.
1369        let nlocalsplus = code.localspluskinds.len() as u32;
1370        let localsplus = LocalsPlus {
1371            data: LocalsPlusData::Heap(vec![0usize; nlocalsplus as usize].into_boxed_slice()),
1372            nlocalsplus,
1373            stack_top: 0,
1374        };
1375
1376        let locals = match self.locals.get() {
1377            Some(mapping) => FrameLocals::with_locals(mapping.clone()),
1378            None => FrameLocals::lazy(),
1379        };
1380
1381        let inner_iframe = Self {
1382            code: core::ptr::null(),
1383            func_obj: core::ptr::null(),
1384            globals: core::ptr::null(),
1385            builtins: core::ptr::null(),
1386            localsplus,
1387            locals,
1388            lasti: Radium::new(self.lasti.load(Relaxed)),
1389            prev_line: core::cell::Cell::new(self.prev_line.get()),
1390            generator: PyAtomicBorrow::new(),
1391            previous: Radium::new(0),
1392            owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8),
1393            datastack_base: core::ptr::null_mut(),
1394            materialized: Radium::new(0),
1395            cold: OnceCell::new(),
1396        };
1397
1398        let frame_obj = FrameObject {
1399            f_trace_lines: core::sync::atomic::AtomicBool::new(self.trace_lines_flag()),
1400            owned_code: Some(code),
1401            owned_globals: Some(globals),
1402            owned_builtins: Some(builtins),
1403            owned_func_obj: func_obj,
1404            iframe: FrameUnsafeCell::new(Some(inner_iframe)),
1405        };
1406        let frame_ref = frame_obj.into_ref(&vm.ctx);
1407        FrameObject::init_iframe_ptrs(&frame_ref);
1408
1409        frame_ref
1410    }
1411
1412    /// Borrowed code object.
1413    #[inline(always)]
1414    pub fn code(&self) -> &Py<PyCode> {
1415        // SAFETY: established by `new` / `init_iframe_ptrs`.
1416        unsafe { &*self.code }
1417    }
1418
1419    /// Borrowed globals dict.
1420    #[inline(always)]
1421    pub fn globals(&self) -> &Py<PyDict> {
1422        // SAFETY: established by `new` / `init_iframe_ptrs`.
1423        unsafe { &*self.globals }
1424    }
1425
1426    /// Borrowed builtins object.
1427    #[inline(always)]
1428    pub fn builtins(&self) -> &PyObject {
1429        // SAFETY: established by `new` / `init_iframe_ptrs`.
1430        unsafe { &*self.builtins }
1431    }
1432
1433    /// Borrowed function object, or None if not set.
1434    #[inline(always)]
1435    pub fn func_obj(&self) -> Option<&PyObject> {
1436        if self.func_obj.is_null() {
1437            None
1438        } else {
1439            // SAFETY: established by `new` / `init_iframe_ptrs`.
1440            Some(unsafe { &*self.func_obj })
1441        }
1442    }
1443
1444    /// Synchronize `prev_line` to the line of the instruction currently
1445    /// in flight (derived from `lasti`, same as `FrameObject::lineno`).
1446    ///
1447    /// `prev_line` is normally only updated on the cold 'line'-trace-event
1448    /// path (see the dispatch loop in `ExecutingFrame::run`), so it can go
1449    /// stale while a frame runs untraced. Call this whenever a trace
1450    /// function is newly installed on an already-executing frame — e.g.
1451    /// `frame.f_trace = ...` from inside a running frame, or a per-frame
1452    /// trace being installed at a 'call' event — so the very next
1453    /// instruction doesn't fire a spurious 'line' event for a line that
1454    /// was already current before tracing started.
1455    pub(crate) fn sync_prev_line_from_lasti(&self) {
1456        let lasti = self.lasti.load(Relaxed);
1457        if lasti == 0 {
1458            // Execution hasn't started yet (or is at the def line for a
1459            // fresh generator/coroutine, already reflected in prev_line).
1460            return;
1461        }
1462        let idx = lasti as usize - 1;
1463        // `lasti` points past the in-flight instruction even while RESUME
1464        // (the very first instruction of a fresh frame) is executing -- a
1465        // 'call'/PY_START trace-install can observe `lasti == 1` before the
1466        // frame has produced any user-visible line at all. Don't treat that
1467        // as "already executing a real line": doing so would set
1468        // `prev_line` to RESUME's own line (typically the same as the
1469        // frame's first statement), suppressing the legitimate first 'line'
1470        // event once the dispatch loop reaches it.
1471        if matches!(
1472            self.code().instructions.read_op(idx),
1473            Instruction::Resume { .. } | Instruction::InstrumentedResume
1474        ) {
1475            return;
1476        }
1477        if let Some((loc, _)) = self.code().locations.get(idx) {
1478            self.prev_line.set(loc.line.get() as u32);
1479        }
1480    }
1481
1482    /// Access the lazily-allocated cold data, allocating on first use.
1483    #[inline]
1484    pub(crate) fn cold(&self) -> &FrameColdData {
1485        self.cold.get_or_init(|| Box::new(FrameColdData::default()))
1486    }
1487
1488    /// Access cold data without allocating. Returns `None` if cold data
1489    /// has not been allocated yet.
1490    #[inline]
1491    pub(crate) fn cold_opt(&self) -> Option<&FrameColdData> {
1492        self.cold.get().map(|b| &**b)
1493    }
1494
1495    /// `f_trace_lines` of the materialized frame object. True when the frame
1496    /// has not been materialized.
1497    pub(crate) fn trace_lines_flag(&self) -> bool {
1498        let mat = self.materialized.load(atomic::Ordering::Relaxed);
1499        if mat != 0 {
1500            // SAFETY: `materialized` holds a `Py<FrameObject>` that stays
1501            // allocated while the pointer is published.
1502            return unsafe { &*(mat as *const Py<FrameObject>) }
1503                .f_trace_lines
1504                .load(core::sync::atomic::Ordering::Relaxed);
1505        }
1506        true
1507    }
1508
1509    /// Thread still running the frame this one was materialized from, or 0.
1510    #[inline]
1511    pub(crate) fn attached_tid(&self) -> u64 {
1512        self.cold_opt()
1513            .map_or(0, |c| c.attached_tid.load(atomic::Ordering::Acquire))
1514    }
1515
1516    /// Mark the frame this one was materialized from as returned, so its
1517    /// values may be read from here.
1518    #[inline]
1519    pub(crate) fn detach(&self) {
1520        if let Some(cold) = self.cold_opt() {
1521            cold.attached_tid.store(0, atomic::Ordering::Release);
1522        }
1523    }
1524}
1525
1526// Python-visible frame object (`PyFrameObject`). Currently always wraps an `InterpreterFrame`.
1527#[pyclass(module = false, name = "frame", traverse = "manual")]
1528pub struct FrameObject {
1529    // The executing iframe reads this when it points at the frame object.
1530    #[pymember(name = "f_trace_lines", writable)]
1531    pub(crate) f_trace_lines: core::sync::atomic::AtomicBool,
1532    // Owned references — keep the pointed-to objects alive for InterpreterFrame's
1533    // raw pointers. Wrapped in Option so Traverse::clear can release them,
1534    // allowing GC cycle collection to reclaim referenced objects.
1535    pub(crate) owned_code: Option<PyRef<PyCode>>,
1536    pub(crate) owned_globals: Option<PyDictRef>,
1537    pub(crate) owned_builtins: Option<PyObjectRef>,
1538    pub(crate) owned_func_obj: Option<PyObjectRef>,
1539
1540    /// Always `Some` while the frame is reachable from Python. Emptied only
1541    /// by `Traverse::clear` during deallocation, leaving a trivially-droppable
1542    /// husk that the freelist can cache.
1543    pub(crate) iframe: FrameUnsafeCell<Option<InterpreterFrame>>,
1544}
1545
1546impl FrameObject {
1547    /// Shared access to the embedded interpreter frame.
1548    ///
1549    /// # Safety
1550    /// Caller must ensure no concurrent mutable access (see `FrameUnsafeCell`)
1551    /// and that the frame has not been cleared (i.e. it is still reachable
1552    /// from Python; `Traverse::clear` only runs during deallocation).
1553    #[inline(always)]
1554    pub(crate) unsafe fn iframe_ref(&self) -> &InterpreterFrame {
1555        let opt = unsafe { &*self.iframe.get() };
1556        #[cfg(debug_assertions)]
1557        if opt.is_none() {
1558            cleared_frame_access();
1559        }
1560        // SAFETY: iframe is always Some while the frame is reachable (see above).
1561        unsafe { opt.as_ref().unwrap_unchecked() }
1562    }
1563
1564    /// Exclusive access to the embedded interpreter frame.
1565    ///
1566    /// # Safety
1567    /// Caller must ensure exclusive access (see `FrameUnsafeCell`) and that
1568    /// the frame has not been cleared.
1569    #[inline(always)]
1570    #[allow(clippy::mut_from_ref)]
1571    pub(crate) unsafe fn iframe_mut(&self) -> &mut InterpreterFrame {
1572        let opt = unsafe { &mut *self.iframe.get() };
1573        #[cfg(debug_assertions)]
1574        if opt.is_none() {
1575            cleared_frame_access();
1576        }
1577        // SAFETY: iframe is always Some while the frame is reachable (see above).
1578        unsafe { opt.as_mut().unwrap_unchecked() }
1579    }
1580
1581    /// Shared access to the embedded interpreter frame. Safe to call on any
1582    /// reachable FrameObject: immutable fields and atomic/mutex fields are
1583    /// always safe to access.
1584    #[inline(always)]
1585    pub fn iframe(&self) -> &InterpreterFrame {
1586        // SAFETY: FrameObject is always reachable from Python when this is
1587        // called. Immutable fields and atomic/mutex fields provide their own
1588        // synchronization. Mutable fields (localsplus, prev_line) are only
1589        // mutated during single-threaded execution via with_exec.
1590        unsafe { self.iframe_ref() }
1591    }
1592}
1593
1594/// Out-of-line panic for the debug-only cleared-frame check, keeping the
1595/// inlined accessors' stack frames minimal.
1596#[cfg(debug_assertions)]
1597#[cold]
1598#[inline(never)]
1599fn cleared_frame_access() -> ! {
1600    panic!("frame accessed after clear");
1601}
1602
1603// NOTE: Deref<Target = InterpreterFrame> removed to decouple FrameObject
1604// from InterpreterFrame field layout. Access through iframe_ref()/iframe_mut().
1605
1606thread_local! {
1607    /// Free list of dead frame objects for reuse. Entries are cleared husks
1608    /// (`iframe == None`) whose child references were already released.
1609    /// Py<FrameObject> is fixed-size (localsplus storage is out-of-line),
1610    /// so a single bucket suffices.
1611    static FRAME_FREELIST: core::cell::Cell<crate::object::FreeList<FrameObject>> =
1612        const { core::cell::Cell::new(crate::object::FreeList::new()) };
1613}
1614
1615impl PyPayload for FrameObject {
1616    const MAX_FREELIST: usize = 200;
1617    const HAS_FREELIST: bool = true;
1618    // Ordinary call frames are created untracked and only enter the GC when
1619    // they escape (see `release_datastack_frame`); generator/coroutine frames
1620    // are tracked explicitly at creation in `invoke_with_locals`.
1621    const NEW_REF_UNTRACKED: bool = true;
1622
1623    #[inline]
1624    fn class(ctx: &Context) -> &'static Py<PyType> {
1625        ctx.types.frame_type
1626    }
1627
1628    #[inline]
1629    unsafe fn freelist_push(obj: *mut PyObject) -> bool {
1630        FRAME_FREELIST
1631            .try_with(|fl| {
1632                let mut list = fl.take();
1633                let stored = if list.len() < Self::MAX_FREELIST {
1634                    list.push(obj);
1635                    true
1636                } else {
1637                    false
1638                };
1639                fl.set(list);
1640                stored
1641            })
1642            .unwrap_or(false)
1643    }
1644
1645    #[inline]
1646    unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
1647        FRAME_FREELIST
1648            .try_with(|fl| {
1649                let mut list = fl.take();
1650                let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) });
1651                fl.set(list);
1652                result
1653            })
1654            .ok()
1655            .flatten()
1656    }
1657}
1658
1659unsafe impl Traverse for FrameObject {
1660    fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
1661        // Visit the owned reference anchors on FrameObject.
1662        // After clear(), these are None.
1663        self.owned_code.traverse(tracer_fn);
1664        self.owned_func_obj.traverse(tracer_fn);
1665        self.owned_globals.traverse(tracer_fn);
1666        self.owned_builtins.traverse(tracer_fn);
1667
1668        // Visit interior references in the InterpreterFrame.
1669        let Some(iframe) = (unsafe { &*self.iframe.get() }) else {
1670            return;
1671        };
1672        iframe.localsplus.traverse(tracer_fn);
1673        iframe.locals.traverse(tracer_fn);
1674        if let Some(cold) = iframe.cold_opt() {
1675            cold.trace.traverse(tracer_fn);
1676            cold.temporary_refs.traverse(tracer_fn);
1677            cold.f_extra_locals.traverse(tracer_fn);
1678            cold.f_locals_cache.traverse(tracer_fn);
1679            cold.f_overwritten_fast_locals.traverse(tracer_fn);
1680            cold.retained_back.traverse(tracer_fn);
1681        }
1682    }
1683
1684    fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
1685        // Extract every child before dropping the frame husk. GC drops `out`
1686        // after this exclusive payload borrow ends, so re-entrant finalizers
1687        // cannot alias frame storage or run under one of its locks.
1688        // Drain the frame where it lies and empty the slot afterwards.
1689        // `Option::take` would move the whole `InterpreterFrame` -- a couple
1690        // of hundred bytes -- onto the stack only to drop it there.
1691        let slot = self.iframe.get_mut();
1692        if let Some(iframe) = slot.as_mut() {
1693            iframe.localsplus.clear_into(out);
1694            if let Some(locals) = iframe.locals.take() {
1695                out.push(locals.into());
1696            }
1697            if let Some(cold) = iframe.cold.take() {
1698                let cold = *cold;
1699                if let Some(trace) = cold.trace.into_inner() {
1700                    out.push(trace);
1701                }
1702                out.extend(cold.temporary_refs.into_inner());
1703                if let Some(extra) = cold.f_extra_locals.into_inner() {
1704                    out.push(extra.into());
1705                }
1706                if let Some(cache) = cold.f_locals_cache.into_inner() {
1707                    out.push(cache.into());
1708                }
1709                out.extend(cold.f_overwritten_fast_locals.into_inner());
1710                if let Some(back) = cold.retained_back.into_inner() {
1711                    out.push(back.into());
1712                }
1713            }
1714        }
1715        *slot = None;
1716        if let Some(code) = self.owned_code.take() {
1717            out.push(code.into());
1718        }
1719        if let Some(globals) = self.owned_globals.take() {
1720            out.push(globals.into());
1721        }
1722        if let Some(builtins) = self.owned_builtins.take() {
1723            out.push(builtins);
1724        }
1725        if let Some(func) = self.owned_func_obj.take() {
1726            out.push(func);
1727        }
1728    }
1729}
1730
1731// Running a frame can result in one of the below:
1732pub enum ExecutionResult {
1733    Return(PyObjectRef),
1734    Yield(PyObjectRef),
1735    /// The bytecode loop wants to tail-call into a new frame that has
1736    /// already been prepared on the datastack. The trampoline reads the
1737    /// pending frame pointer from `vm.pending_tailcall_frame`.
1738    TailCall,
1739    /// The bytecode loop wants a generator or coroutine resumed in this same
1740    /// eval loop rather than through a nested `Coro::send`. The trampoline
1741    /// reads which one, the value to send it, and the continuation below
1742    /// from `vm.pending_gen_resume`.
1743    GenResume,
1744}
1745
1746/// What the frame that issued a [`ExecutionResult::GenResume`] does with the
1747/// resumed generator's outcome.
1748///
1749/// The trampoline only ever parks a frame at a `SEND` in the canonical
1750/// `yield from` / `await` shape, where the instruction right after the `SEND`
1751/// is the `YIELD_VALUE` that re-yields whatever the sub-generator produced:
1752///
1753/// ```text
1754///   send_idx: SEND exit
1755///             CACHE              <- the parked frame's lasti is here + 1
1756///             YIELD_VALUE 1
1757/// resumed_at: RESUME
1758///             JUMP_BACKWARD_NO_INTERRUPT -> send_idx
1759///       exit: END_SEND
1760/// ```
1761///
1762/// A value the sub-generator yields is re-yielded by the trampoline itself —
1763/// park `lasti` at `resumed_at`, hand the value to the next frame out — so a
1764/// level of delegation runs none of its own instructions. Once the
1765/// sub-generator is done instead, its `StopIteration` value is pushed and the
1766/// frame carries on at `exit`, which is what `SEND` itself would have done.
1767///
1768/// Any other `SEND`, and every `FOR_ITER`, keeps the recursive path: the
1769/// frame would have to be re-entered on every value, and re-entering the eval
1770/// loop costs more than the nested `Coro::send` it would save.
1771#[derive(Clone, Copy)]
1772pub(crate) struct GenCont {
1773    pub(crate) exit: u32,
1774    /// Where the skipped `YIELD_VALUE` leaves `lasti`. Zero where a
1775    /// suspended frame has no continuation at all, i.e. it is waiting on an
1776    /// ordinary call rather than on a generator — no frame parked at a `SEND`
1777    /// can have `lasti` 0, so the two never collide.
1778    pub(crate) resumed_at: u32,
1779}
1780
1781impl GenCont {
1782    /// The placeholder a frame waiting on an ordinary call carries.
1783    pub(crate) const NONE: Self = Self {
1784        exit: 0,
1785        resumed_at: 0,
1786    };
1787
1788    /// Whether this is a real `yield from` continuation.
1789    #[inline]
1790    pub(crate) const fn is_some(self) -> bool {
1791        self.resumed_at != 0
1792    }
1793}
1794
1795/// A valid execution result, or an exception
1796type FrameResult = PyResult<Option<ExecutionResult>>;
1797
1798impl FrameObject {
1799    pub(crate) fn new(
1800        code: PyRef<PyCode>,
1801        scope: Scope,
1802        builtins: PyObjectRef,
1803        closure: &[PyCellRef],
1804        func_obj: Option<PyObjectRef>,
1805        use_datastack: bool,
1806        vm: &VirtualMachine,
1807    ) -> Self {
1808        let nlocalsplus = code.localspluskinds.len();
1809        let max_stackdepth = code.max_stackdepth as usize;
1810        let localsplus = if use_datastack {
1811            LocalsPlus::new_on_datastack(nlocalsplus, max_stackdepth, vm)
1812        } else {
1813            LocalsPlus::new(nlocalsplus, max_stackdepth)
1814        };
1815
1816        let locals = match scope.locals {
1817            Some(locals) => FrameLocals::with_locals(locals),
1818            None if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) => FrameLocals::lazy(),
1819            None => FrameLocals::with_locals(ArgMapping::from_dict_exact(scope.globals.clone())),
1820        };
1821
1822        // Pointers are initially set from owned fields' references but will be
1823        // dangling after the FrameObject moves into heap allocation — they get
1824        // patched by `init_iframe_ptrs` after `into_ref`.
1825        let iframe = unsafe {
1826            // SAFETY: `init_iframe_ptrs` overwrites these pointers after
1827            // `into_ref`, before the frame is executed.
1828            InterpreterFrame::new(
1829                &code,
1830                &scope.globals,
1831                &builtins,
1832                func_obj.as_deref(),
1833                localsplus,
1834                locals,
1835                closure,
1836                FrameOwner::FrameObject,
1837            )
1838        };
1839        Self {
1840            f_trace_lines: core::sync::atomic::AtomicBool::new(true),
1841            owned_code: Some(code),
1842            owned_globals: Some(scope.globals),
1843            owned_builtins: Some(builtins),
1844            owned_func_obj: func_obj,
1845            iframe: FrameUnsafeCell::new(Some(iframe)),
1846        }
1847    }
1848
1849    /// Patch the InterpreterFrame's raw pointers to point at this
1850    /// FrameObject's owned fields. Must be called once after the
1851    /// FrameObject is allocated on the heap (i.e. after `into_ref`).
1852    fn init_iframe_ptrs(self_: &Py<Self>) {
1853        let iframe = unsafe { self_.iframe_mut() };
1854        iframe.code = &**self_.owned_code.as_ref().unwrap() as *const Py<PyCode>;
1855        iframe.globals = &**self_.owned_globals.as_ref().unwrap() as *const Py<PyDict>;
1856        iframe.builtins = &**self_.owned_builtins.as_ref().unwrap() as *const PyObject;
1857        iframe.func_obj = match &self_.owned_func_obj {
1858            Some(obj) => &**obj as *const PyObject,
1859            None => core::ptr::null(),
1860        };
1861        // Link the InterpreterFrame back to its owning FrameObject.
1862        iframe
1863            .materialized
1864            .store(self_ as *const Py<Self> as usize, Relaxed);
1865    }
1866
1867    /// Create a new FrameObject, allocate it on the heap, and patch
1868    /// the InterpreterFrame's raw pointers. Returns an owned reference.
1869    pub(crate) fn new_ref(
1870        code: PyRef<PyCode>,
1871        scope: Scope,
1872        builtins: PyObjectRef,
1873        closure: &[PyCellRef],
1874        func_obj: Option<PyObjectRef>,
1875        use_datastack: bool,
1876        vm: &VirtualMachine,
1877    ) -> FrameObjectRef {
1878        let frame = Self::new(code, scope, builtins, closure, func_obj, use_datastack, vm)
1879            .into_ref(&vm.ctx);
1880        Self::init_iframe_ptrs(&frame);
1881        frame
1882    }
1883
1884    /// Access fastlocals immutably.
1885    ///
1886    /// # Safety
1887    /// Caller must ensure no concurrent mutable access (frame not executing,
1888    /// or called from the same thread during trace callback).
1889    #[inline(always)]
1890    pub unsafe fn fastlocals(&self) -> &[Option<PyObjectRef>] {
1891        unsafe { self.iframe_ref().localsplus.fastlocals() }
1892    }
1893
1894    /// Access fastlocals mutably.
1895    ///
1896    /// # Safety
1897    /// Caller must ensure exclusive access (frame not executing).
1898    #[inline(always)]
1899    #[allow(clippy::mut_from_ref)]
1900    pub unsafe fn fastlocals_mut(&self) -> &mut [Option<PyObjectRef>] {
1901        unsafe { self.iframe_mut().localsplus.fastlocals_mut() }
1902    }
1903
1904    /// Migrate data-stack-backed storage to the heap, preserving all values,
1905    /// and return the data stack base pointer for `DataStack::pop()`.
1906    /// Returns `None` if already heap-backed.
1907    ///
1908    /// # Safety
1909    /// Caller must ensure the frame is not executing and the returned
1910    /// pointer is passed to `VirtualMachine::datastack_pop()`.
1911    pub(crate) unsafe fn materialize_localsplus(&self) -> Option<*mut u8> {
1912        unsafe { self.iframe_mut().localsplus.materialize_to_heap() }
1913    }
1914
1915    /// Drop all localsplus values in place and detach the data stack backing
1916    /// without the heap copy. Returns the data stack base pointer for
1917    /// `VirtualMachine::datastack_pop()`, or `None` if heap-backed.
1918    ///
1919    /// # Safety
1920    /// Caller must ensure the frame is not executing, that no other reference
1921    /// to the frame exists or can be created (localsplus is unobservable
1922    /// afterwards), and that the returned pointer is passed to
1923    /// `VirtualMachine::datastack_pop()`.
1924    pub(crate) unsafe fn release_localsplus(&self) -> Option<*mut u8> {
1925        unsafe { self.iframe_mut().localsplus.release_datastack() }
1926    }
1927
1928    /// Whether this frame's localsplus is still data-stack-backed. A frame
1929    /// must have heap-backed localsplus before it is GC-tracked so that a
1930    /// concurrent collector never reads data-stack-resident, still-mutating
1931    /// storage. Used only in debug assertions at the track sites.
1932    pub(crate) fn localsplus_is_datastack_backed(&self) -> bool {
1933        // SAFETY: called at a track site where the frame is not executing.
1934        unsafe { self.iframe_ref().localsplus.is_datastack_backed() }
1935    }
1936
1937    /// Clear evaluation stack and state-owned cell/free references.
1938    /// For full local/cell cleanup, call `clear_locals_and_stack()`.
1939    pub(crate) fn clear_stack_and_cells(&self) {
1940        // SAFETY: Called when frame is not executing (generator closed).
1941        // Cell refs in fastlocals[nlocals..] are cleared by clear_locals_and_stack().
1942        let localsplus = unsafe { &mut self.iframe_mut().localsplus };
1943        // A frame that ran to its `return` left nothing on the stack, which is
1944        // how every generator that simply finished arrives here.
1945        if localsplus.stack_is_empty() {
1946            return;
1947        }
1948        let refs = localsplus.take_stack_object_refs();
1949        drop(refs);
1950    }
1951
1952    /// Clear locals and stack after generator/coroutine close.
1953    /// Releases references held by the frame, matching _PyFrame_ClearLocals.
1954    pub(crate) fn clear_locals_and_stack(&self) {
1955        self.clear_stack_and_cells();
1956        // Move references out before dropping them. Their finalizers may
1957        // re-enter this frame, so no locals borrow or cold-data lock may be
1958        // held while they run.
1959        let fastlocals = {
1960            // SAFETY: FrameObject is not executing (generator closed).
1961            let slots = unsafe { self.iframe_mut().localsplus.fastlocals_mut() };
1962            slots
1963                .iter_mut()
1964                .filter_map(Option::take)
1965                .collect::<Vec<_>>()
1966        };
1967        // Cold data is allocated lazily, by tracing and frame introspection
1968        // only. A frame that never grew it holds none of the references read
1969        // below, so ask for it without allocating: forcing the allocation here
1970        // would hand every finishing generator a `FrameColdData` to malloc and
1971        // free just to find all three fields empty.
1972        let (extra_locals, locals_cache, overwritten) = match self.iframe().cold_opt() {
1973            Some(cold) => {
1974                let extra_locals = {
1975                    let mut guard = cold.f_extra_locals.lock();
1976                    guard.take()
1977                };
1978                let locals_cache = {
1979                    let mut guard = cold.f_locals_cache.lock();
1980                    guard.take()
1981                };
1982                let overwritten = {
1983                    let mut guard = cold.f_overwritten_fast_locals.lock();
1984                    core::mem::take(&mut *guard)
1985                };
1986                (extra_locals, locals_cache, overwritten)
1987            }
1988            None => (None, None, Vec::new()),
1989        };
1990        drop((fastlocals, extra_locals, locals_cache, overwritten));
1991    }
1992
1993    /// Store a borrowed back-reference to the owning generator/coroutine.
1994    /// The caller must ensure the generator outlives the frame.
1995    pub fn set_generator(&self, generator: &PyObject) {
1996        self.iframe().generator.store(generator);
1997        self.iframe()
1998            .owner
1999            .store(FrameOwner::Generator as i8, atomic::Ordering::Release);
2000    }
2001
2002    /// Clear the generator back-reference. Called when the generator is finalized.
2003    pub fn clear_generator(&self) {
2004        // The generator's drop may run after this frame was already cleared
2005        // by cycle collection (both were garbage and the frame was cleared
2006        // first); nothing to unlink then.
2007        // SAFETY: shared access; the finalizing generator owns the frame,
2008        // which is not executing.
2009        let Some(iframe) = (unsafe { &*self.iframe.get() }) else {
2010            return;
2011        };
2012        iframe.generator.clear();
2013        iframe
2014            .owner
2015            .store(FrameOwner::FrameObject as i8, atomic::Ordering::Release);
2016    }
2017
2018    pub fn current_location(&self) -> SourceLocation {
2019        let lasti = self.lasti() as usize;
2020        if lasti == 0 {
2021            return SourceLocation {
2022                line: self
2023                    .iframe()
2024                    .code()
2025                    .first_line_number
2026                    .unwrap_or(OneIndexed::MIN),
2027                character_offset: OneIndexed::from_zero_indexed(0),
2028            };
2029        }
2030        self.iframe().code().locations[lasti - 1].0
2031    }
2032
2033    /// Get the previous InterpreterFrame in the chain.
2034    /// Returns null if the frame has been cleared (GC deallocation).
2035    pub fn previous_iframe(&self) -> *const InterpreterFrame {
2036        // Use raw access instead of iframe() to avoid panicking on cleared frames.
2037        let iframe_opt = unsafe { &*self.iframe.get() };
2038        match iframe_opt.as_ref() {
2039            Some(iframe) => {
2040                iframe.previous.load(atomic::Ordering::Relaxed) as *const InterpreterFrame
2041            }
2042            None => core::ptr::null(),
2043        }
2044    }
2045
2046    /// Get the previous FrameObject in the chain, if any.
2047    /// Walks through the chain to find the next materialized frame.
2048    pub fn previous_frame(&self) -> *const Self {
2049        let mut cur = self.previous_iframe();
2050        while !cur.is_null() {
2051            let iframe = unsafe { &*cur };
2052            if let Some(fo) = iframe.frame_obj() {
2053                return &**fo as *const Self;
2054            }
2055            cur = iframe.previous.load(atomic::Ordering::Relaxed) as *const InterpreterFrame;
2056        }
2057        core::ptr::null()
2058    }
2059
2060    pub fn lasti(&self) -> u32 {
2061        self.iframe().lasti.load(Relaxed)
2062    }
2063
2064    pub fn set_lasti(&self, val: u32) {
2065        self.iframe().lasti.store(val, Relaxed);
2066    }
2067
2068    /// Fast-local slots of the live source frame when this frame object's
2069    /// frame is still running on this thread, and this frame object's own
2070    /// slots otherwise. A running frame's slots live on the data stack; the
2071    /// frame object's are empty until `exit_iframe` fills them.
2072    ///
2073    /// # Safety
2074    /// Caller must ensure no concurrent mutable access: either the frame is
2075    /// not executing (callers pass through `check_locals_access`), or this is
2076    /// a trace callback on the thread that is executing it.
2077    unsafe fn live_fastlocals(&self) -> &[Option<PyObjectRef>] {
2078        let live = self.find_live_source_iframe();
2079        if live.is_null() {
2080            unsafe { self.iframe_ref().localsplus.fastlocals() }
2081        } else {
2082            unsafe { (*live).localsplus.fastlocals() }
2083        }
2084    }
2085
2086    /// True when this frame currently has bound PEP 709 hidden comprehension
2087    /// locals. Matches CPython `_PyFrame_HasHiddenLocals`.
2088    pub(crate) fn has_active_hidden_locals(&self) -> bool {
2089        use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE, CO_FAST_HIDDEN};
2090        let code = self.iframe().code();
2091        // SAFETY: callers first pass through `check_locals_access`, or this is
2092        // `locals()` on the thread running this frame.
2093        let fastlocals = unsafe { self.live_fastlocals() };
2094        code.localspluskinds.iter().enumerate().any(|(i, &kind)| {
2095            if kind & CO_FAST_HIDDEN == 0 {
2096                return false;
2097            }
2098            match fastlocals[i].as_ref() {
2099                None => false,
2100                Some(obj) => {
2101                    if kind & (CO_FAST_CELL | CO_FAST_FREE) != 0 {
2102                        obj.downcast_ref::<PyCell>()
2103                            .is_none_or(|cell| cell.get().is_some())
2104                    } else {
2105                        true
2106                    }
2107                }
2108            }
2109        })
2110    }
2111
2112    #[inline]
2113    pub(crate) fn has_hidden_local_slots(&self) -> bool {
2114        use rustpython_compiler_core::bytecode::CO_FAST_HIDDEN;
2115        self.iframe()
2116            .code()
2117            .localspluskinds
2118            .iter()
2119            .any(|kind| kind & CO_FAST_HIDDEN != 0)
2120    }
2121
2122    /// Decide whether Python-visible locals use a write-through proxy rather
2123    /// than the frame's namespace mapping.
2124    pub(crate) fn uses_locals_proxy(&self, vm: &VirtualMachine) -> PyResult<bool> {
2125        let is_optimized = self
2126            .iframe()
2127            .code()
2128            .flags
2129            .contains(bytecode::CodeFlags::OPTIMIZED);
2130        if !is_optimized && !self.has_hidden_local_slots() {
2131            return Ok(false);
2132        }
2133        self.check_locals_access(vm)?;
2134        Ok(is_optimized || self.has_active_hidden_locals())
2135    }
2136
2137    /// Reject locals access for a frame that is executing on another thread.
2138    ///
2139    /// A thread-owned frame mutates `localsplus` without synchronization, so
2140    /// reading fastlocals from a different thread would be a data race (the
2141    /// executing thread overwrites slots and drops the old values while the
2142    /// reader clones them). Access from the executing thread itself (locals()
2143    /// builtin, trace callbacks) is fine: the frame sits on the current
2144    /// thread's frame chain and is at a bytecode boundary.
2145    pub(crate) fn check_locals_access(&self, vm: &VirtualMachine) -> PyResult<()> {
2146        // A frame object materialized from a running data stack frame is
2147        // FrameObject-owned, so the owner test below cannot speak for it: the
2148        // thread running that frame fills these slots when it returns.
2149        let attached = self.iframe().attached_tid();
2150        if attached != 0 && attached != current_thread_ident() {
2151            return Err(vm.new_runtime_error(
2152                "cannot access frame locals while the frame is executing in another thread",
2153            ));
2154        }
2155        let owner = FrameOwner::from_i8(self.iframe().owner.load(atomic::Ordering::Acquire));
2156        if owner != FrameOwner::Thread {
2157            return Ok(());
2158        }
2159        let self_iframe = self.iframe() as *const InterpreterFrame;
2160        // Get the Py<FrameObject> address from &FrameObject (payload).
2161        // materialized stores a *const Py<FrameObject>.
2162        let self_py_ptr = unsafe { Py::<Self>::from_payload_ptr(self) } as usize;
2163        let mut cur = crate::vm::thread::get_current_frame();
2164        while !cur.is_null() {
2165            if core::ptr::eq(cur, self_iframe) {
2166                return Ok(());
2167            }
2168            // Also match if this FrameObject is the materialized version
2169            // of a stack-allocated frame in the chain.
2170            let materialized = unsafe { (*cur).materialized.load(Relaxed) };
2171            if materialized == self_py_ptr {
2172                return Ok(());
2173            }
2174            cur = unsafe { (*cur).previous.load(Relaxed) as *const InterpreterFrame };
2175        }
2176        Err(vm.new_runtime_error(
2177            "cannot access frame locals while the frame is executing in another thread",
2178        ))
2179    }
2180
2181    pub fn locals(&self, vm: &VirtualMachine) -> PyResult<ArgMapping> {
2182        if self.uses_locals_proxy(vm)? {
2183            Ok(ArgMapping::from_dict_exact(
2184                self.framelocalsproxy_snapshot(vm)?,
2185            ))
2186        } else {
2187            Ok(self.iframe().locals.clone_mapping(vm))
2188        }
2189    }
2190
2191    /// Return the lazily allocated dict used by APIs that must keep a
2192    /// frame-owned locals snapshot. Ordinary VM execution never accesses it.
2193    pub fn locals_snapshot_cache(&self, vm: &VirtualMachine) -> PyDictRef {
2194        let mut cache = self.iframe().cold().f_locals_cache.lock();
2195        cache.get_or_insert_with(|| vm.ctx.new_dict()).clone()
2196    }
2197
2198    /// Read a fast-local slot's visible value, dereferencing cells. `None` if
2199    /// the slot is empty or its cell holds no value.
2200    fn framelocalsproxy_getval(&self, i: usize) -> Option<PyObjectRef> {
2201        use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE};
2202        // SAFETY: callers first pass through `check_locals_access`, so the
2203        // frame is not executing on another thread.
2204        let fastlocals = unsafe { self.live_fastlocals() };
2205        let obj = fastlocals.get(i)?.as_ref()?;
2206        let kind = self
2207            .iframe()
2208            .code()
2209            .localspluskinds
2210            .get(i)
2211            .copied()
2212            .unwrap_or(0);
2213        if kind & (CO_FAST_CELL | CO_FAST_FREE) != 0 {
2214            if let Some(cell) = obj.downcast_ref::<PyCell>() {
2215                cell.get()
2216            } else {
2217                Some(obj.clone())
2218            }
2219        } else {
2220            Some(obj.clone())
2221        }
2222    }
2223
2224    /// Write `value` into fast-local slot `i`, routing through the cell when
2225    /// the slot holds one so closures keep sharing the same cell.
2226    fn framelocalsproxy_setval(&self, i: usize, value: PyObjectRef) {
2227        use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE};
2228        let kind = self
2229            .iframe()
2230            .code()
2231            .localspluskinds
2232            .get(i)
2233            .copied()
2234            .unwrap_or(0);
2235        let live = self.find_live_source_iframe();
2236        let old_value = if !live.is_null() {
2237            // SAFETY: callers first pass through `check_locals_access`.
2238            unsafe { (*live).localsplus.fastlocals()[i].clone() }
2239        } else {
2240            // SAFETY: callers first pass through `check_locals_access`.
2241            unsafe { self.iframe_ref().localsplus.fastlocals()[i].clone() }
2242        };
2243
2244        if kind & (CO_FAST_CELL | CO_FAST_FREE) != 0
2245            && let Some(cell) = old_value
2246                .as_ref()
2247                .and_then(|obj| obj.clone().downcast::<PyCell>().ok())
2248        {
2249            cell.set(Some(value));
2250            return;
2251        }
2252
2253        if old_value.as_ref().is_some_and(|old| old.is(&value)) {
2254            return;
2255        }
2256
2257        // Keep the overwritten value alive with the frame. Its finalizer may
2258        // re-enter this frame, so it must not run while `fastlocals` is
2259        // mutably borrowed. CPython uses f_overwritten_fast_locals likewise.
2260        let old_value = if !live.is_null() {
2261            // SAFETY: callers first pass through `check_locals_access`.
2262            unsafe { &mut *live.cast_mut() }.localsplus.fastlocals_mut()[i].replace(value)
2263        } else {
2264            // SAFETY: callers first pass through `check_locals_access`.
2265            unsafe { self.iframe_mut().localsplus.fastlocals_mut()[i].replace(value) }
2266        };
2267        if let Some(old_value) = old_value {
2268            self.iframe()
2269                .cold()
2270                .f_overwritten_fast_locals
2271                .lock()
2272                .push(old_value);
2273        }
2274    }
2275
2276    /// Resolve `key` to a fast-local slot index, or `None` if it names no fast
2277    /// local. `read` selects read semantics (only bound slots match) versus
2278    /// write semantics (hidden slots are skipped, unbound slots still match).
2279    /// Raises `TypeError` for an unhashable key.
2280    fn framelocalsproxy_getkeyindex(
2281        &self,
2282        key: &PyObject,
2283        read: bool,
2284        vm: &VirtualMachine,
2285    ) -> PyResult<Option<usize>> {
2286        use rustpython_compiler_core::bytecode::CO_FAST_HIDDEN;
2287        // Hash first both for hashability and to match dict-key equivalence.
2288        let key_hash = key.hash(vm)?;
2289        for (i, &kind) in self.iframe().code().localspluskinds.iter().enumerate() {
2290            let name = localsplus_name(self.iframe().code(), i);
2291            if name.as_object().hash(vm)? != key_hash {
2292                continue;
2293            }
2294            if !name
2295                .as_object()
2296                .rich_compare_bool(key, PyComparisonOp::Eq, vm)?
2297            {
2298                continue;
2299            }
2300            if read {
2301                if self.framelocalsproxy_getval(i).is_some() {
2302                    return Ok(Some(i));
2303                }
2304            } else if kind & CO_FAST_HIDDEN == 0 {
2305                return Ok(Some(i));
2306            }
2307        }
2308        Ok(None)
2309    }
2310
2311    /// Return the proxy's items in CPython iteration order. Fast locals come
2312    /// first, followed by extra locals. The two groups may contain equal keys.
2313    pub(crate) fn framelocalsproxy_items(
2314        &self,
2315        vm: &VirtualMachine,
2316    ) -> PyResult<Vec<(PyObjectRef, PyObjectRef)>> {
2317        self.check_locals_access(vm)?;
2318        let code = self.iframe().code();
2319        let mut items = Vec::with_capacity(code.localspluskinds.len());
2320        for i in 0..code.localspluskinds.len() {
2321            if let Some(value) = self.framelocalsproxy_getval(i) {
2322                let name = localsplus_name(code, i).to_owned().into();
2323                items.push((name, value));
2324            }
2325        }
2326        let extra = self.iframe().cold().f_extra_locals.lock().clone();
2327        if let Some(extra) = extra {
2328            items.extend(&extra);
2329        }
2330        Ok(items)
2331    }
2332
2333    /// Build a dict as `dict(FrameLocalsProxy)` does in CPython. Insert fast
2334    /// locals first, then only extra keys that are still missing, so a
2335    /// colliding fast local keeps precedence.
2336    pub(crate) fn framelocalsproxy_snapshot(&self, vm: &VirtualMachine) -> PyResult<PyDictRef> {
2337        self.check_locals_access(vm)?;
2338        let dict = vm.ctx.new_dict();
2339        let code = self.iframe().code();
2340        for i in 0..code.localspluskinds.len() {
2341            if let Some(value) = self.framelocalsproxy_getval(i) {
2342                let key: PyObjectRef = localsplus_name(code, i).to_owned().into();
2343                dict.set_item(&*key, value, vm)?;
2344            }
2345        }
2346        let extra = self.iframe().cold().f_extra_locals.lock().clone();
2347        if let Some(extra) = extra {
2348            dict.merge_object_if_missing(extra.into(), vm)?;
2349        }
2350        Ok(dict)
2351    }
2352
2353    /// `proxy[key]`: read a fast local live, else fall back to extra locals.
2354    pub(crate) fn framelocalsproxy_getitem(
2355        &self,
2356        key: PyObjectRef,
2357        vm: &VirtualMachine,
2358    ) -> PyResult {
2359        self.check_locals_access(vm)?;
2360        if let Some(i) = self.framelocalsproxy_getkeyindex(&key, true, vm)?
2361            && let Some(value) = self.framelocalsproxy_getval(i)
2362        {
2363            return Ok(value);
2364        }
2365        let extra = self.iframe().cold().f_extra_locals.lock().clone();
2366        if let Some(extra) = extra
2367            && let Some(value) = extra.get_item_opt(&*key, vm)?
2368        {
2369            return Ok(value);
2370        }
2371        Err(vm.new_key_error(key))
2372    }
2373
2374    /// `key in proxy`.
2375    pub(crate) fn framelocalsproxy_contains(
2376        &self,
2377        key: &PyObject,
2378        vm: &VirtualMachine,
2379    ) -> PyResult<bool> {
2380        self.check_locals_access(vm)?;
2381        if self.framelocalsproxy_getkeyindex(key, true, vm)?.is_some() {
2382            return Ok(true);
2383        }
2384        let extra = self.iframe().cold().f_extra_locals.lock().clone();
2385        if let Some(extra) = extra {
2386            return Ok(extra.get_item_opt(key, vm)?.is_some());
2387        }
2388        Ok(false)
2389    }
2390
2391    /// `proxy[key] = value`: fast-key writes the slot in place, other keys go
2392    /// to the extra-locals side dict.
2393    pub(crate) fn framelocalsproxy_setitem(
2394        &self,
2395        key: &PyObject,
2396        value: PyObjectRef,
2397        vm: &VirtualMachine,
2398    ) -> PyResult<()> {
2399        self.check_locals_access(vm)?;
2400        if let Some(i) = self.framelocalsproxy_getkeyindex(key, false, vm)? {
2401            self.framelocalsproxy_setval(i, value);
2402            return Ok(());
2403        }
2404        let extra = self.extra_locals_get_or_create(vm);
2405        extra.set_item(key, value, vm)
2406    }
2407
2408    /// `del proxy[key]`: deleting a fast local raises ValueError; extra keys are
2409    /// removed (KeyError if absent).
2410    pub(crate) fn framelocalsproxy_delitem(
2411        &self,
2412        key: PyObjectRef,
2413        vm: &VirtualMachine,
2414    ) -> PyResult<()> {
2415        self.check_locals_access(vm)?;
2416        if self
2417            .framelocalsproxy_getkeyindex(&key, false, vm)?
2418            .is_some()
2419        {
2420            return Err(vm.new_value_error("cannot remove local variables from FrameLocalsProxy"));
2421        }
2422        let extra = self.iframe().cold().f_extra_locals.lock().clone();
2423        if let Some(extra) = extra
2424            && extra.get_item_opt(&*key, vm)?.is_some()
2425        {
2426            return extra.del_item(&*key, vm);
2427        }
2428        Err(vm.new_key_error(key))
2429    }
2430
2431    /// `proxy.pop(key[, default])`.
2432    pub(crate) fn framelocalsproxy_pop(
2433        &self,
2434        key: PyObjectRef,
2435        default: Option<PyObjectRef>,
2436        vm: &VirtualMachine,
2437    ) -> PyResult {
2438        self.check_locals_access(vm)?;
2439        if self
2440            .framelocalsproxy_getkeyindex(&key, false, vm)?
2441            .is_some()
2442        {
2443            return Err(vm.new_value_error("cannot remove local variables from FrameLocalsProxy"));
2444        }
2445        let extra = self.iframe().cold().f_extra_locals.lock().clone();
2446        if let Some(extra) = extra
2447            && let Some(value) = extra.pop_item(&*key, vm)?
2448        {
2449            return Ok(value);
2450        }
2451        default.ok_or_else(|| vm.new_key_error(key))
2452    }
2453
2454    /// `proxy.setdefault(key, default)`.
2455    pub(crate) fn framelocalsproxy_setdefault(
2456        &self,
2457        key: &PyObject,
2458        default: PyObjectRef,
2459        vm: &VirtualMachine,
2460    ) -> PyResult {
2461        match self.framelocalsproxy_getitem(key.to_owned(), vm) {
2462            Ok(value) => Ok(value),
2463            Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
2464                self.framelocalsproxy_setitem(key, default.clone(), vm)?;
2465                Ok(default)
2466            }
2467            Err(e) => Err(e),
2468        }
2469    }
2470
2471    fn extra_locals_get_or_create(&self, vm: &VirtualMachine) -> PyDictRef {
2472        let mut extra = self.iframe().cold().f_extra_locals.lock();
2473        extra.get_or_insert_with(|| vm.ctx.new_dict()).clone()
2474    }
2475}
2476
2477impl Py<FrameObject> {
2478    #[inline(always)]
2479    fn with_exec<R>(&self, vm: &VirtualMachine, f: impl FnOnce(ExecutingFrame<'_>) -> R) -> R {
2480        // SAFETY: FrameObject execution is single-threaded. Only one thread at a time
2481        // executes a given frame (enforced by the owner field and generator
2482        // running flag). Same safety argument as FastLocals (UnsafeCell).
2483        let iframe = unsafe { self.iframe_mut() };
2484        // Raw pointer deref, not `iframe.code()`, so the later `&mut`
2485        // localsplus/prev_line borrows are not aliasing a live `&self`.
2486        // SAFETY: established by `new` / `init_iframe_ptrs`.
2487        let (code, globals, builtins, func_obj) = unsafe {
2488            (
2489                &*iframe.code,
2490                &*iframe.globals,
2491                &*iframe.builtins,
2492                if iframe.func_obj.is_null() {
2493                    None
2494                } else {
2495                    Some(&*iframe.func_obj)
2496                },
2497            )
2498        };
2499        let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) {
2500            builtins
2501                .downcast_ref_if_exact::<PyDict>(vm)
2502                // SAFETY: downcast_ref_if_exact already verified exact type
2503                .map(|d| unsafe { PyExact::ref_unchecked(d) })
2504        } else {
2505            None
2506        };
2507        let iframe_ptr = iframe as *const InterpreterFrame;
2508        let exec = ExecutingFrame {
2509            code,
2510            localsplus: &mut iframe.localsplus,
2511            locals: &iframe.locals,
2512            globals,
2513            builtins,
2514            builtins_dict,
2515            lasti: &iframe.lasti,
2516            iframe: iframe_ptr,
2517            func_obj,
2518            prev_line: &iframe.prev_line,
2519            monitoring_mask: 0,
2520            flatten: Flatten::Nothing,
2521            call_traced: false,
2522        };
2523        f(exec)
2524    }
2525
2526    // #[cfg_attr(feature = "flame-it", flame("FrameObject"))]
2527    pub fn run(&self, vm: &VirtualMachine) -> PyResult<ExecutionResult> {
2528        self.with_exec(vm, |mut exec| exec.run(vm))
2529    }
2530
2531    /// Resume a suspended generator or coroutine body, pushing `value` as the
2532    /// result of the `yield` it stopped at.
2533    ///
2534    /// The body runs under the same trampoline that flattens ordinary
2535    /// Python-to-Python calls, so the plain calls it makes cost no Rust stack.
2536    /// The caller (`resume_gen_frame`) owns the frame's chain bookkeeping.
2537    pub(crate) fn resume(
2538        &self,
2539        value: Option<PyObjectRef>,
2540        vm: &VirtualMachine,
2541    ) -> PyResult<ExecutionResult> {
2542        // SAFETY: same as `with_exec` — only one thread at a time executes a
2543        // given frame, enforced by the owner field and the running claim.
2544        let iframe = unsafe { self.iframe_mut() };
2545        if let Some(value) = value {
2546            // Parked at a `yield from` of its own: hand the value to the
2547            // delegate and let the trampoline re-yield for this frame, so a
2548            // chain costs no instruction at any level, this one included.
2549            if let Some((delegate, cont)) = yield_from_delegate(iframe, vm)
2550                && crate::coroutine::as_builtin_coro(&delegate).is_some_and(is_delegating)
2551                && gen_collapse_allowed(vm)
2552            {
2553                park_at_send(iframe, cont);
2554                return vm.run_gen_frame_delegating(iframe, delegate, value, cont);
2555            }
2556            iframe.localsplus.push_stack(value);
2557        }
2558        vm.run_gen_frame(iframe)
2559    }
2560
2561    pub(crate) fn gen_throw(
2562        &self,
2563        vm: &VirtualMachine,
2564        exc_type: PyObjectRef,
2565        exc_val: PyObjectRef,
2566        exc_tb: PyObjectRef,
2567    ) -> PyResult<ExecutionResult> {
2568        self.with_exec(vm, |mut exec| exec.gen_throw(vm, exc_type, exc_val, exc_tb))
2569    }
2570
2571    pub fn yield_from_target(&self) -> Option<PyObjectRef> {
2572        // If the frame is currently executing (owned by thread), it has no
2573        // yield-from target to report.
2574        let owner = FrameOwner::from_i8(self.iframe().owner.load(atomic::Ordering::Acquire));
2575        if owner == FrameOwner::Thread {
2576            return None;
2577        }
2578        // SAFETY: FrameObject is not executing, so UnsafeCell access is safe.
2579        let iframe = unsafe { self.iframe_mut() };
2580        // SAFETY: established by `new` / `init_iframe_ptrs`.
2581        let (code, globals, builtins, func_obj) = unsafe {
2582            (
2583                &*iframe.code,
2584                &*iframe.globals,
2585                &*iframe.builtins,
2586                if iframe.func_obj.is_null() {
2587                    None
2588                } else {
2589                    Some(&*iframe.func_obj)
2590                },
2591            )
2592        };
2593        let iframe_ptr = iframe as *const InterpreterFrame;
2594        let exec = ExecutingFrame {
2595            code,
2596            localsplus: &mut iframe.localsplus,
2597            locals: &iframe.locals,
2598            globals,
2599            builtins,
2600            builtins_dict: None,
2601            lasti: &iframe.lasti,
2602            iframe: iframe_ptr,
2603            func_obj,
2604            prev_line: &iframe.prev_line,
2605            monitoring_mask: 0,
2606            flatten: Flatten::Nothing,
2607            call_traced: false,
2608        };
2609        exec.yield_from_target().map(PyObject::to_owned)
2610    }
2611
2612    pub fn is_internal_frame(&self) -> bool {
2613        let code = self.f_code();
2614        let filename = code.co_filename();
2615        let filename = filename.as_bytes();
2616        filename.find(b"importlib").is_some() && filename.find(b"_bootstrap").is_some()
2617    }
2618
2619    pub fn next_external_frame(&self, vm: &VirtualMachine) -> Option<FrameObjectRef> {
2620        let mut frame = self.f_back(vm);
2621        while let Some(ref f) = frame {
2622            if !f.is_internal_frame() {
2623                break;
2624            }
2625            frame = f.f_back(vm);
2626        }
2627        frame
2628    }
2629}
2630
2631/// Identity of the calling thread, or 0 where there is only one thread to be.
2632/// 0 doubles as "no thread", which is what `attached_tid` wants for a build
2633/// that cannot have a frame running anywhere else.
2634#[inline]
2635fn current_thread_ident() -> u64 {
2636    #[cfg(feature = "threading")]
2637    {
2638        crate::stdlib::_thread::get_ident()
2639    }
2640    #[cfg(not(feature = "threading"))]
2641    {
2642        0
2643    }
2644}
2645
2646/// Byte offset from the start of a datastack allocation to the LocalsPlus data,
2647/// accounting for alignment padding after the InterpreterFrame header.
2648#[inline]
2649fn datastack_iframe_localsplus_offset() -> usize {
2650    let iframe_size = core::mem::size_of::<InterpreterFrame>();
2651    (iframe_size + core::mem::align_of::<usize>() - 1) & !(core::mem::align_of::<usize>() - 1)
2652}
2653
2654/// Total bytes needed to co-allocate an InterpreterFrame and its LocalsPlus
2655/// data on the thread data stack.
2656pub(crate) fn datastack_iframe_total_bytes(nlocalsplus: usize, stacksize: usize) -> usize {
2657    let iframe_padded = datastack_iframe_localsplus_offset();
2658    let capacity = nlocalsplus
2659        .checked_add(stacksize)
2660        .expect("LocalsPlus capacity overflow");
2661    let data_bytes = capacity
2662        .checked_mul(core::mem::size_of::<usize>())
2663        .expect("LocalsPlus byte size overflow");
2664    iframe_padded
2665        .checked_add(data_bytes)
2666        .expect("datastack iframe total size overflow")
2667}
2668
2669/// Handle an exception propagating into a suspended caller frame in the
2670/// trampoline. Adds a traceback entry at the caller's call site, then
2671/// tries the caller's exception table via `unwind_blocks`.
2672///
2673/// Returns:
2674/// - `Ok(None)` — handler found, the caller's `run_iframe` can be re-entered
2675/// - `Ok(Some(result))` — handler returned a result (break from the run loop)
2676/// - `Err(exc)` — no handler, exception propagates to the next caller
2677pub(crate) fn trampoline_handle_exception(
2678    iframe: &mut InterpreterFrame,
2679    exception: &Py<PyBaseException>,
2680    vm: &VirtualMachine,
2681) -> FrameResult {
2682    let mut exec = exec_iframe(iframe, Flatten::Nothing, vm);
2683
2684    // lasti points past the call opcode and its inline caches. Do not
2685    // decode those cache units: their bytes are not valid opcodes.
2686    let idx = (exec.lasti() as usize).saturating_sub(1);
2687
2688    // Add traceback entry at the call site.
2689    if let Some((loc, _end_loc)) = exec.code.locations.get(idx) {
2690        let next = exception.traceback();
2691        let new_traceback = PyTraceback::new(next, exec.frame_object(vm), idx as i32 * 2, loc.line);
2692        exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
2693    }
2694
2695    exec.fire_exception_trace(exception, vm)?;
2696    let exception = {
2697        let mon_events = vm.state.monitoring_events.load();
2698        if mon_events & MonitoringEvent::Raise.mask() != 0 {
2699            let offset = idx as u32 * 2;
2700            let exc_obj: PyObjectRef = exception.to_owned().into();
2701            match monitoring::fire_raise(vm, exec.code, offset, &exc_obj) {
2702                Ok(()) => exception.to_owned(),
2703                Err(monitor_exc) => monitor_exc,
2704            }
2705        } else {
2706            exception.to_owned()
2707        }
2708    };
2709
2710    exec.unwind_blocks(
2711        vm,
2712        UnwindReason::Raising {
2713            exception,
2714            offset: idx as u32,
2715        },
2716    )
2717}
2718
2719/// Borrow an `InterpreterFrame` as an `ExecutingFrame`.
2720#[inline(always)]
2721fn exec_iframe<'a>(
2722    iframe: &'a mut InterpreterFrame,
2723    flatten: Flatten,
2724    vm: &VirtualMachine,
2725) -> ExecutingFrame<'a> {
2726    // Raw pointer deref, not `iframe.code()`, so the later `&mut`
2727    // localsplus/prev_line borrows are not aliasing a live `&self`.
2728    // SAFETY: established by `new` / `init_iframe_ptrs`.
2729    let (code, globals, builtins, func_obj) = unsafe {
2730        (
2731            &*iframe.code,
2732            &*iframe.globals,
2733            &*iframe.builtins,
2734            if iframe.func_obj.is_null() {
2735                None
2736            } else {
2737                Some(&*iframe.func_obj)
2738            },
2739        )
2740    };
2741    let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) {
2742        builtins
2743            .downcast_ref_if_exact::<PyDict>(vm)
2744            .map(|d| unsafe { PyExact::ref_unchecked(d) })
2745    } else {
2746        None
2747    };
2748    let iframe_ptr = iframe as *const InterpreterFrame;
2749    ExecutingFrame {
2750        code,
2751        localsplus: &mut iframe.localsplus,
2752        locals: &iframe.locals,
2753        globals,
2754        builtins,
2755        builtins_dict,
2756        lasti: &iframe.lasti,
2757        iframe: iframe_ptr,
2758        func_obj,
2759        prev_line: &iframe.prev_line,
2760        monitoring_mask: 0,
2761        flatten,
2762        call_traced: false,
2763    }
2764}
2765
2766/// Execute an InterpreterFrame's bytecode directly, without a FrameObject.
2767#[inline(always)]
2768pub(crate) fn run_iframe(
2769    iframe: &mut InterpreterFrame,
2770    flatten: Flatten,
2771    vm: &VirtualMachine,
2772) -> PyResult<ExecutionResult> {
2773    exec_iframe(iframe, flatten, vm).run(vm)
2774}
2775
2776/// Whether the trampoline may skip a delegating frame's own instructions.
2777///
2778/// It may not while anything is watching them run: `sys.settrace` fires line
2779/// and opcode events per instruction, and `sys.monitoring` both fires events
2780/// and rewrites the very opcodes the `yield from` shape is recognized by.
2781#[inline]
2782pub(crate) fn gen_collapse_allowed(vm: &VirtualMachine) -> bool {
2783    !vm.use_tracing.get() && vm.state.monitoring_events.load() == 0
2784}
2785
2786/// The sub-generator a frame suspended in a `yield from` / `await` is
2787/// delegating to, if the trampoline can resume it in this frame's place.
2788///
2789/// Recognizes the shape documented on [`GenCont`] — `lasti` at the `RESUME`
2790/// that follows the delegating `YIELD_VALUE`, with the delegate on top of the
2791/// stack — and requires the `SEND` to have already specialized to `SendGen`,
2792/// so that a `Send` still collecting specialization feedback keeps running
2793/// normally. The frame is left untouched; `park_at_send` commits to it.
2794pub(crate) fn yield_from_delegate(
2795    iframe: &InterpreterFrame,
2796    vm: &VirtualMachine,
2797) -> Option<(PyObjectRef, GenCont)> {
2798    let code = iframe.code();
2799    let send_caches = Instruction::from(Opcode::Send).cache_entries();
2800    let resumed_at = iframe.lasti.load(Relaxed) as usize;
2801    // The SEND, its cache and the YIELD_VALUE sit below `resumed_at`.
2802    let send_idx = resumed_at.checked_sub(2 + send_caches)?;
2803    if !matches!(
2804        code.instructions.get(resumed_at)?.op,
2805        Instruction::Resume { .. }
2806    ) {
2807        return None;
2808    }
2809    let yield_unit = code.instructions.get(resumed_at - 1)?;
2810    if !matches!(yield_unit.op, Instruction::YieldValue { .. }) || u8::from(yield_unit.arg) < 1 {
2811        return None;
2812    }
2813    let send_unit = code.instructions.get(send_idx)?;
2814    if !matches!(send_unit.op, Instruction::SendGen) {
2815        return None;
2816    }
2817    // An EXTENDED_ARG prefix would make the raw oparg below the wrong exit.
2818    if send_idx > 0
2819        && matches!(
2820            code.instructions.get(send_idx - 1)?.op,
2821            Instruction::ExtendedArg
2822        )
2823    {
2824        return None;
2825    }
2826    let delegate = match iframe.localsplus.stack_last() {
2827        Some(Some(top)) => top.as_object(),
2828        _ => return None,
2829    };
2830    // The same guards `SendGen` applies before resuming a generator itself.
2831    if delegate.downcast_ref_if_exact::<PyGenerator>(vm).is_none()
2832        && delegate.downcast_ref_if_exact::<PyCoroutine>(vm).is_none()
2833    {
2834        return None;
2835    }
2836    let coro = crate::coroutine::as_builtin_coro(delegate)?;
2837    if coro.running() || coro.closed() {
2838        return None;
2839    }
2840    // `SEND`'s jump is relative to the code unit after the instruction and
2841    // its caches, exactly as the handler computes it from its own `lasti`.
2842    let exit = (send_idx + 1 + send_caches) as u32 + u32::from(u8::from(send_unit.arg));
2843    Some((
2844        delegate.to_owned(),
2845        GenCont {
2846            exit,
2847            resumed_at: resumed_at as u32,
2848        },
2849    ))
2850}
2851
2852/// Park a frame the trampoline is about to run a delegate for, exactly where
2853/// the frame's own `SEND` would have left it — one code unit before the
2854/// `RESUME` the skipped `YIELD_VALUE` leads to, which is that `YIELD_VALUE`
2855/// itself, so simply running the frame from there stays correct.
2856#[inline]
2857pub(crate) fn park_at_send(iframe: &mut InterpreterFrame, cont: GenCont) {
2858    iframe.lasti.store(cont.resumed_at - 1, Relaxed);
2859}
2860
2861/// Whether a generator or coroutine is itself suspended at a `yield from`, so
2862/// that the chain below it goes at least one level deeper.
2863///
2864/// This is what makes handing a chain to the trampoline worth its setup: one
2865/// lone level is cheaper to send into from the eval loop the caller is
2866/// already in — an `await` of a future's `__await__`, which yields once and
2867/// then returns, is the shape that would otherwise pay and never collect.
2868pub(crate) fn is_delegating(coro: &Coro) -> bool {
2869    let iframe = coro.frame_ref().iframe();
2870    let code = iframe.code();
2871    let resumed_at = iframe.lasti.load(Relaxed) as usize;
2872    if resumed_at == 0 {
2873        return false;
2874    }
2875    matches!(
2876        code.instructions.get(resumed_at).map(|u| u.op),
2877        Some(Instruction::Resume { .. })
2878    ) && code
2879        .instructions
2880        .get(resumed_at - 1)
2881        .is_some_and(|u| matches!(u.op, Instruction::YieldValue { .. }) && u8::from(u.arg) >= 1)
2882}
2883
2884/// Finish a `yield from` level the trampoline ran in place of the frame:
2885/// leave `lasti` where the skipped `YIELD_VALUE` would have left it.
2886#[inline]
2887pub(crate) fn park_after_yield_from(iframe: &mut InterpreterFrame, resumed_at: u32) {
2888    // The `YIELD_VALUE` this stands in for never ran, so nothing has promoted
2889    // the parked stack yet. Do it here for the same reason that handler does.
2890    iframe.localsplus.promote_stack();
2891    debug_assert!(
2892        iframe
2893            .localsplus
2894            .stack_as_slice()
2895            .iter()
2896            .flatten()
2897            .all(|sr| !sr.is_borrowed()),
2898        "borrowed refs on stack at yield point"
2899    );
2900    iframe.lasti.store(resumed_at, Relaxed);
2901}
2902
2903/// Apply the continuation of a flattened `SEND` whose generator finished:
2904/// the tail of the `SendGen` handler, which the trampoline runs on the parked
2905/// frame's behalf once the generator's frame is unlinked.
2906pub(crate) fn trampoline_gen_stop(
2907    iframe: &mut InterpreterFrame,
2908    value: Option<PyObjectRef>,
2909    cont: GenCont,
2910    vm: &VirtualMachine,
2911) -> PyResult<()> {
2912    if vm.use_tracing.get() {
2913        // Tracing can be switched on while the sub-generator runs, so the
2914        // `StopIteration` event the recursive path fires is checked for here
2915        // rather than where the frame was parked.
2916        let exec = exec_iframe(iframe, Flatten::Nothing, vm);
2917        if exec.trace_is_set(vm) {
2918            let stop_exc = vm.new_stop_iteration(value.clone());
2919            exec.fire_exception_trace(&stop_exc, vm)?;
2920        }
2921    }
2922    iframe.localsplus.push_stack(vm.unwrap_or_none(value));
2923    iframe.lasti.store(cont.exit, Relaxed);
2924    Ok(())
2925}
2926
2927/// An executing frame; borrows mutable frame-internal data for the duration
2928/// of bytecode execution.
2929pub(crate) struct ExecutingFrame<'a> {
2930    code: &'a Py<PyCode>,
2931    localsplus: &'a mut LocalsPlus,
2932    locals: &'a FrameLocals,
2933    globals: &'a Py<PyDict>,
2934    builtins: &'a PyObject,
2935    /// Cached downcast of builtins to PyDict for fast LOAD_GLOBAL.
2936    /// Only set when both globals and builtins are exact dict types (not
2937    /// subclasses), so that `__missing__` / `__getitem__` overrides are
2938    /// not bypassed.
2939    builtins_dict: Option<&'a PyExact<PyDict>>,
2940    /// Raw pointer to the underlying InterpreterFrame. Used to access the
2941    /// materialized FrameObject (via `frame_obj()`) and frame-level fields
2942    /// like trace, pending_stack_pops, etc. Stored as a raw pointer because
2943    /// mutable borrows to `localsplus` and `prev_line` are also held.
2944    /// All accesses through this pointer use atomic/mutex operations.
2945    iframe: *const InterpreterFrame,
2946    /// Borrowed function object that created this frame (if any).
2947    func_obj: Option<&'a PyObject>,
2948    lasti: &'a PyAtomic<u32>,
2949    prev_line: &'a core::cell::Cell<u32>,
2950    /// Cached monitoring events mask. Reloaded at Resume instruction only,
2951    monitoring_mask: u32,
2952    /// What this frame may hand back to the trampoline, if it is running
2953    /// under one at all.
2954    flatten: Flatten,
2955    /// PY_START/PY_RESUME Call already fired for this activation.
2956    call_traced: bool,
2957}
2958
2959/// How much of what a frame does the trampoline can take over.
2960#[derive(Clone, Copy, PartialEq, Eq)]
2961pub(crate) enum Flatten {
2962    /// Nothing: the frame is not running under the trampoline
2963    /// (FrameObject-based execution), so it must not return `TailCall` or
2964    /// `GenResume`.
2965    Nothing,
2966    /// A generator or coroutine body: it may park a generator, but makes its
2967    /// plain calls itself. Tail-calling them would mean re-entering the
2968    /// trampoline on every resume, which for a body that yields often costs
2969    /// more than the nested call it saves.
2970    GenResume,
2971    /// An ordinary frame under the trampoline: both.
2972    CallAndGenResume,
2973}
2974
2975#[inline]
2976fn specialization_compact_int_value(i: &Py<PyInt>) -> Option<isize> {
2977    // _PyLong_IsCompact(): a one-digit PyLong (base 2^30),
2978    // i.e. abs(value) <= 2^30 - 1.
2979    const CPYTHON_COMPACT_LONG_ABS_MAX: i64 = (1i64 << 30) - 1;
2980    let v = i.try_to_i64_fast()?;
2981    if (-CPYTHON_COMPACT_LONG_ABS_MAX..=CPYTHON_COMPACT_LONG_ABS_MAX).contains(&v) {
2982        Some(v as isize)
2983    } else {
2984        None
2985    }
2986}
2987
2988#[inline]
2989fn compact_int_from_obj(obj: &PyObject, vm: &VirtualMachine) -> Option<isize> {
2990    obj.downcast_ref_if_exact::<PyInt>(vm)
2991        .and_then(specialization_compact_int_value)
2992}
2993
2994#[inline]
2995fn exact_float_from_obj(obj: &PyObject, vm: &VirtualMachine) -> Option<f64> {
2996    obj.downcast_ref_if_exact::<PyFloat>(vm).map(|f| f.to_f64())
2997}
2998
2999#[inline]
3000fn specialization_nonnegative_compact_index(i: &Py<PyInt>, vm: &VirtualMachine) -> Option<usize> {
3001    // _PyLong_IsNonNegativeCompact(): a single base-2^30 digit.
3002    const CPYTHON_COMPACT_LONG_MAX: u64 = (1u64 << 30) - 1;
3003    let v = i.try_to_primitive::<u64>(vm).ok()?;
3004    if v <= CPYTHON_COMPACT_LONG_MAX {
3005        Some(v as usize)
3006    } else {
3007        None
3008    }
3009}
3010
3011/// Get the variable name for a localsplus index of `code`.
3012fn localsplus_name(code: &Py<PyCode>, idx: usize) -> &'static PyStrInterned {
3013    code.localsplus_name(idx)
3014}
3015
3016/// Free a finished call frame's data stack storage.
3017///
3018/// When the caller holds the only reference to the frame, the locals and
3019/// stack values are dropped in place and the storage is released without a
3020/// heap copy. Otherwise (the frame escaped through a traceback,
3021/// `sys._getframe`, a trace callback, ...) the values are copied to the heap
3022/// first so they stay readable through the escaped reference.
3023pub(crate) fn release_datastack_frame(frame: &Py<FrameObject>, vm: &VirtualMachine) {
3024    let frame_obj = frame.as_object();
3025    // Uniqueness argument: at this point the frame is already out of
3026    // the thread-frames registry and the current-frame chain
3027    // (both unlinked inside `with_frame` before it returned), and the
3028    // frame type has no weakref support. A datastack frame is created
3029    // untracked and stays untracked while it runs, so it is in no GC
3030    // generation list and no collector can observe or incref it. Hence no
3031    // thread can mint a new reference without already holding one, and every
3032    // escape (traceback, `sys._getframe`, `f_back`, a stored trace-hook arg)
3033    // is a heap reference created on this thread while the frame ran.
3034    // Therefore `strong_count() == 1` here means nothing escaped, and
3035    // `strong_count() > 1` means the frame escaped.
3036    debug_assert!(
3037        !frame_obj.is_gc_tracked(),
3038        "datastack frame is GC-tracked at release"
3039    );
3040    if frame_obj.strong_count() == 1 {
3041        // A reference minted and already released by another thread (through a
3042        // heap escape carried across threads) ends in a release-decref; the
3043        // fence orders that thread's memory before our drops below.
3044        atomic::fence(Acquire);
3045        // SAFETY: unique owner and no way to mint a new reference, so
3046        // localsplus can never be observed again. The base pointer came
3047        // from this thread's data stack.
3048        unsafe {
3049            if let Some(base) = frame.release_localsplus() {
3050                vm.datastack_pop(base);
3051            }
3052        }
3053        return;
3054    }
3055    // Escaped. Stabilize localsplus on the heap FIRST, then join the GC. This
3056    // order guarantees a concurrent (stop-the-world) collector only ever sees a
3057    // tracked frame whose localsplus is heap-resident and no longer mutating:
3058    // the frame has stopped executing before it becomes a candidate, so its
3059    // outgoing edges are stable while a collector traverses them.
3060    // SAFETY: the frame finished executing; the base pointer came from this
3061    // thread's data stack.
3062    unsafe {
3063        if let Some(base) = frame.materialize_localsplus() {
3064            vm.datastack_pop(base);
3065        }
3066    }
3067    // Retain a strong reference to the caller so `f_back` keeps resolving once
3068    // the caller returns and leaves the live frame chain. The caller is still
3069    // executing here (this frame is unwinding back into it), so its payload
3070    // pointer is live.
3071    {
3072        let mut guard = frame.iframe().cold().retained_back.lock();
3073        if guard.is_none() {
3074            let prev = frame.previous_iframe();
3075            *guard = unsafe { owned_chain_frame(prev) };
3076        }
3077    }
3078    // Note: previous is NOT cleared here. retained_back captures the
3079    // caller reference, and previous may be read again by f_back or
3080    // frame chain walkers (the pointer is live as long as the caller
3081    // is still executing, which it is at this point).
3082    // Invariant: a tracked frame must always have heap-backed localsplus
3083    // (proven here for escaped datastack frames and by construction for
3084    // generator frames, which are born heap-backed). A stop-the-world
3085    // collector reads a frame's localsplus only when the frame is a tracked
3086    // candidate, so this keeps it from ever reading data-stack-resident,
3087    // still-mutating storage of an executing frame.
3088    debug_assert!(
3089        !frame.localsplus_is_datastack_backed(),
3090        "escaped frame tracked before its localsplus was materialized"
3091    );
3092    // SAFETY: the frame is alive (held by `frame` and the escaped reference)
3093    // and untracked.
3094    unsafe {
3095        crate::gc_state::gc_state()
3096            .track_object(NonNull::from(frame_obj), crate::gc_state::current_owner())
3097    };
3098}
3099
3100type BinaryOpExtendGuard = fn(&PyObject, &PyObject, &VirtualMachine) -> bool;
3101type BinaryOpExtendAction = fn(&PyObject, &PyObject, &VirtualMachine) -> Option<PyObjectRef>;
3102
3103struct BinaryOpExtendSpecializationDescr {
3104    oparg: bytecode::BinaryOperator,
3105    guard: BinaryOpExtendGuard,
3106    action: BinaryOpExtendAction,
3107}
3108
3109const BINARY_OP_EXTEND_EXTERNAL_CACHE_OFFSET: usize = 1;
3110
3111/// Max total args (including self) staged in a fixed-size stack buffer by the
3112/// exact-args call fast paths; larger arities fall back to a heap buffer.
3113const MAX_INLINE_CALL_ARGS: usize = 8;
3114
3115/// Staging buffer for exact-args call fast paths: fixed-size inline storage
3116/// for small arities, avoiding a per-call Vec allocation.
3117enum CallArgBuffer {
3118    Inline(usize, [Option<PyObjectRef>; MAX_INLINE_CALL_ARGS]),
3119    Heap(Vec<Option<PyObjectRef>>),
3120}
3121
3122impl CallArgBuffer {
3123    fn new(total_nargs: usize) -> Self {
3124        if total_nargs <= MAX_INLINE_CALL_ARGS {
3125            Self::Inline(total_nargs, [const { None }; MAX_INLINE_CALL_ARGS])
3126        } else {
3127            Self::Heap(vec![None; total_nargs])
3128        }
3129    }
3130
3131    fn slots(&mut self) -> &mut [Option<PyObjectRef>] {
3132        match self {
3133            Self::Inline(len, buf) => &mut buf[..*len],
3134            Self::Heap(buf) => buf,
3135        }
3136    }
3137}
3138
3139#[inline]
3140fn compactlongs_guard(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> bool {
3141    compact_int_from_obj(lhs, vm).is_some() && compact_int_from_obj(rhs, vm).is_some()
3142}
3143
3144/// A conditional jump the instruction ahead of it can perform itself.
3145///
3146/// Built by [`ExecutingFrame::fused_bool_jump`] and consumed by
3147/// [`ExecutingFrame::take_fused_bool_jump`].
3148#[derive(Clone, Copy)]
3149struct FusedBoolJump {
3150    /// Boolean value that takes the branch.
3151    jump_on: bool,
3152    /// Code-unit index the branch lands on when taken.
3153    taken: u32,
3154    /// Code-unit index just past the jump and its cache, where the compiler
3155    /// puts the `NOT_TAKEN` marker.
3156    fallthrough: u32,
3157}
3158
3159macro_rules! bitwise_longs_action {
3160    ($name:ident, $op:tt) => {
3161        #[inline]
3162        fn $name(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> Option<PyObjectRef> {
3163            let lhs_val = compact_int_from_obj(lhs, vm)?;
3164            let rhs_val = compact_int_from_obj(rhs, vm)?;
3165            Some(vm.ctx.new_int(lhs_val $op rhs_val).into())
3166        }
3167    };
3168}
3169bitwise_longs_action!(compactlongs_or, |);
3170bitwise_longs_action!(compactlongs_and, &);
3171bitwise_longs_action!(compactlongs_xor, ^);
3172
3173#[inline]
3174fn float_compactlong_guard(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> bool {
3175    exact_float_from_obj(lhs, vm).is_some_and(|f| !f.is_nan())
3176        && compact_int_from_obj(rhs, vm).is_some()
3177}
3178
3179#[inline]
3180fn nonzero_float_compactlong_guard(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> bool {
3181    float_compactlong_guard(lhs, rhs, vm) && compact_int_from_obj(rhs, vm).is_some_and(|v| v != 0)
3182}
3183
3184macro_rules! float_long_action {
3185    ($name:ident, $op:tt) => {
3186        #[inline]
3187        fn $name(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> Option<PyObjectRef> {
3188            let lhs_val = exact_float_from_obj(lhs, vm)?;
3189            let rhs_val = compact_int_from_obj(rhs, vm)?;
3190            Some(vm.ctx.new_float(lhs_val $op rhs_val as f64).into())
3191        }
3192    };
3193}
3194float_long_action!(float_compactlong_add, +);
3195float_long_action!(float_compactlong_subtract, -);
3196float_long_action!(float_compactlong_multiply, *);
3197float_long_action!(float_compactlong_true_div, /);
3198
3199#[inline]
3200fn compactlong_float_guard(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> bool {
3201    compact_int_from_obj(lhs, vm).is_some()
3202        && exact_float_from_obj(rhs, vm).is_some_and(|f| !f.is_nan())
3203}
3204
3205#[inline]
3206fn nonzero_compactlong_float_guard(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> bool {
3207    compactlong_float_guard(lhs, rhs, vm) && exact_float_from_obj(rhs, vm).is_some_and(|f| f != 0.0)
3208}
3209
3210macro_rules! long_float_action {
3211    ($name:ident, $op:tt) => {
3212        #[inline]
3213        fn $name(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> Option<PyObjectRef> {
3214            let lhs_val = compact_int_from_obj(lhs, vm)?;
3215            let rhs_val = exact_float_from_obj(rhs, vm)?;
3216            Some(vm.ctx.new_float(lhs_val as f64 $op rhs_val).into())
3217        }
3218    };
3219}
3220long_float_action!(compactlong_float_add, +);
3221long_float_action!(compactlong_float_subtract, -);
3222long_float_action!(compactlong_float_multiply, *);
3223long_float_action!(compactlong_float_true_div, /);
3224
3225static BINARY_OP_EXTEND_DESCRIPTORS: &[BinaryOpExtendSpecializationDescr] = &[
3226    // long-long arithmetic
3227    BinaryOpExtendSpecializationDescr {
3228        oparg: bytecode::BinaryOperator::Or,
3229        guard: compactlongs_guard,
3230        action: compactlongs_or,
3231    },
3232    BinaryOpExtendSpecializationDescr {
3233        oparg: bytecode::BinaryOperator::And,
3234        guard: compactlongs_guard,
3235        action: compactlongs_and,
3236    },
3237    BinaryOpExtendSpecializationDescr {
3238        oparg: bytecode::BinaryOperator::Xor,
3239        guard: compactlongs_guard,
3240        action: compactlongs_xor,
3241    },
3242    BinaryOpExtendSpecializationDescr {
3243        oparg: bytecode::BinaryOperator::InplaceOr,
3244        guard: compactlongs_guard,
3245        action: compactlongs_or,
3246    },
3247    BinaryOpExtendSpecializationDescr {
3248        oparg: bytecode::BinaryOperator::InplaceAnd,
3249        guard: compactlongs_guard,
3250        action: compactlongs_and,
3251    },
3252    BinaryOpExtendSpecializationDescr {
3253        oparg: bytecode::BinaryOperator::InplaceXor,
3254        guard: compactlongs_guard,
3255        action: compactlongs_xor,
3256    },
3257    // float-long arithmetic
3258    BinaryOpExtendSpecializationDescr {
3259        oparg: bytecode::BinaryOperator::Add,
3260        guard: float_compactlong_guard,
3261        action: float_compactlong_add,
3262    },
3263    BinaryOpExtendSpecializationDescr {
3264        oparg: bytecode::BinaryOperator::Subtract,
3265        guard: float_compactlong_guard,
3266        action: float_compactlong_subtract,
3267    },
3268    BinaryOpExtendSpecializationDescr {
3269        oparg: bytecode::BinaryOperator::TrueDivide,
3270        guard: nonzero_float_compactlong_guard,
3271        action: float_compactlong_true_div,
3272    },
3273    BinaryOpExtendSpecializationDescr {
3274        oparg: bytecode::BinaryOperator::Multiply,
3275        guard: float_compactlong_guard,
3276        action: float_compactlong_multiply,
3277    },
3278    // long-float arithmetic
3279    BinaryOpExtendSpecializationDescr {
3280        oparg: bytecode::BinaryOperator::Add,
3281        guard: compactlong_float_guard,
3282        action: compactlong_float_add,
3283    },
3284    BinaryOpExtendSpecializationDescr {
3285        oparg: bytecode::BinaryOperator::Subtract,
3286        guard: compactlong_float_guard,
3287        action: compactlong_float_subtract,
3288    },
3289    BinaryOpExtendSpecializationDescr {
3290        oparg: bytecode::BinaryOperator::TrueDivide,
3291        guard: nonzero_compactlong_float_guard,
3292        action: compactlong_float_true_div,
3293    },
3294    BinaryOpExtendSpecializationDescr {
3295        oparg: bytecode::BinaryOperator::Multiply,
3296        guard: compactlong_float_guard,
3297        action: compactlong_float_multiply,
3298    },
3299];
3300
3301impl fmt::Debug for ExecutingFrame<'_> {
3302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3303        f.debug_struct("ExecutingFrame")
3304            .field("code", self.code)
3305            .field("stack_len", &self.localsplus.stack_len())
3306            .finish()
3307    }
3308}
3309
3310/// Run bytecode in a light frame context.
3311#[allow(clippy::too_many_arguments)]
3312impl ExecutingFrame<'_> {
3313    /// Get the underlying InterpreterFrame.
3314    #[inline(always)]
3315    fn iframe(&self) -> &InterpreterFrame {
3316        // SAFETY: the iframe pointer is valid for the lifetime of the ExecutingFrame.
3317        unsafe { &*self.iframe }
3318    }
3319
3320    /// Get the frame object. Materializes a FrameObject on demand if this
3321    /// is a stack-allocated frame that hasn't been observed yet.
3322    #[cold]
3323    #[inline(never)]
3324    fn frame_object(&self, vm: &VirtualMachine) -> FrameObjectRef {
3325        self.iframe().materialize(vm).to_owned()
3326    }
3327
3328    /// Whether this frame has a per-frame trace function set.
3329    #[inline]
3330    fn trace_is_set(&self, _vm: &VirtualMachine) -> bool {
3331        self.iframe()
3332            .cold_opt()
3333            .is_some_and(|c| c.trace.lock().is_some())
3334    }
3335
3336    /// PY_START / PY_RESUME → PyTrace_CALL. Fired from the first RESUME of
3337    /// this activation so lasti is already the resume unit
3338    /// (COPY_FREE_VARS / RETURN_GENERATOR that precede it are not a 'call',
3339    /// and later RESUMEs in a SEND loop are not a new call either).
3340    fn trace_call_from_resume(&mut self, vm: &VirtualMachine, resume_type: u32) -> PyResult<()> {
3341        if self.call_traced {
3342            return Ok(());
3343        }
3344        self.call_traced = true;
3345        if !vm.use_tracing.get() {
3346            return Ok(());
3347        }
3348        // RESUME oparg 0 is PY_START; nonzero is PY_RESUME.
3349        let what = if resume_type == 0 {
3350            monitoring::MonitoringEvent::PyStart
3351        } else {
3352            monitoring::MonitoringEvent::PyResume
3353        };
3354        let trace_result = vm.trace_event_what(crate::protocol::TraceEvent::Call, what, None)?;
3355        if let Some(local_trace) = trace_result {
3356            let was_unset = self.iframe().cold().trace.lock().is_none();
3357            *self.iframe().cold().trace.lock() = Some(local_trace);
3358            if was_unset {
3359                self.iframe().sync_prev_line_from_lasti();
3360            }
3361        }
3362        Ok(())
3363    }
3364
3365    /// Access the frame's trace_opcodes lock.
3366    #[inline]
3367    fn trace_opcodes_is_set(&self) -> bool {
3368        self.iframe()
3369            .cold_opt()
3370            .is_some_and(|c| *c.trace_opcodes.lock())
3371    }
3372
3373    /// f_trace_lines, defaulting to true when no frame object exists.
3374    #[inline]
3375    fn trace_lines_is_set(&self) -> bool {
3376        self.iframe().trace_lines_flag()
3377    }
3378
3379    /// Get pending_stack_pops from the frame.
3380    #[inline]
3381    fn pending_stack_pops(&self) -> u32 {
3382        self.iframe()
3383            .cold_opt()
3384            .map_or(0, |c| c.pending_stack_pops.load(Relaxed))
3385    }
3386
3387    /// Get pending_unwind_from_stack from the frame.
3388    #[inline]
3389    fn pending_unwind_from_stack(&self) -> i64 {
3390        self.iframe()
3391            .cold_opt()
3392            .map_or(0, |c| c.pending_unwind_from_stack.load(Relaxed))
3393    }
3394
3395    /// Set pending_stack_pops on the frame.
3396    #[inline]
3397    fn set_pending_stack_pops(&self, val: u32) {
3398        self.iframe().cold().pending_stack_pops.store(val, Relaxed);
3399    }
3400
3401    /// Run `__init__` for the tp_new specialization. `args` holds the
3402    /// `__init__` args with slot 0 left empty; it is filled with `new_obj`
3403    /// here. Enforces the `__init__() should return None` contract and
3404    /// returns the constructed object.
3405    fn specialization_run_init(
3406        &self,
3407        new_obj: PyObjectRef,
3408        init_func: &Py<PyFunction>,
3409        args: &mut [Option<PyObjectRef>],
3410        vm: &VirtualMachine,
3411    ) -> PyResult<PyObjectRef> {
3412        args[0] = Some(new_obj.clone());
3413        let taken = args
3414            .iter_mut()
3415            .map(|slot| slot.take().expect("arg slot must be filled"));
3416
3417        let init_result = init_func.invoke_prepared_exact_args(taken, vm)?;
3418
3419        if !vm.is_none(&init_result) {
3420            return Err(vm.new_type_error(format!(
3421                "__init__() should return None, not '{:.200}'",
3422                init_result.class().name()
3423            )));
3424        }
3425        Ok(new_obj)
3426    }
3427
3428    #[inline(always)]
3429    fn update_lasti(&mut self, f: impl FnOnce(&mut u32)) {
3430        let mut val = self.lasti.load(Relaxed);
3431        f(&mut val);
3432        self.lasti.store(val, Relaxed);
3433    }
3434
3435    #[inline(always)]
3436    fn lasti(&self) -> u32 {
3437        self.lasti.load(Relaxed)
3438    }
3439
3440    /// Access the PyCellRef at the given localsplus index.
3441    #[inline(always)]
3442    fn cell_ref(&self, localsplus_idx: usize) -> &PyCell {
3443        let fastlocals = self.localsplus.fastlocals();
3444        let slot = &fastlocals[localsplus_idx];
3445        slot.as_ref()
3446            .expect("cell slot empty")
3447            .downcast_ref::<PyCell>()
3448            .expect("cell slot is not a PyCell")
3449    }
3450
3451    /// Apply a pending `f_lineno` jump's stack pops, if any.
3452    fn apply_pending_lineno_jump(&mut self, vm: &VirtualMachine) {
3453        let pops = self.pending_stack_pops();
3454        if pops > 0 {
3455            let from_stack = self.pending_unwind_from_stack();
3456            self.unwind_stack_for_lineno(pops as usize, from_stack, vm);
3457            self.set_pending_stack_pops(0);
3458        }
3459    }
3460
3461    /// `_YIELD_VALUE_EVENT`: fire PY_YIELD while the value is still on the
3462    /// stack. If the tracer assigned `f_lineno`, continue at the new lasti
3463    /// instead of suspending.
3464    fn yield_value_event(&mut self, vm: &VirtualMachine) -> PyResult<bool> {
3465        let lasti_before = self.lasti();
3466        if vm.use_tracing.get() {
3467            let value = self.top_value().to_owned();
3468            vm.trace_event_what(
3469                crate::protocol::TraceEvent::Return,
3470                monitoring::MonitoringEvent::PyYield,
3471                Some(value),
3472            )?;
3473            if self.lasti() != lasti_before {
3474                self.apply_pending_lineno_jump(vm);
3475                return Ok(true);
3476            }
3477        }
3478        if self.monitoring_mask & MonitoringEvent::PyYield.mask() != 0 {
3479            let value = self.top_value().to_owned();
3480            let offset = (self.lasti() - 1) * 2;
3481            monitoring::fire_py_yield(vm, self.code, offset, &value)?;
3482            if self.lasti() != lasti_before {
3483                self.apply_pending_lineno_jump(vm);
3484                return Ok(true);
3485            }
3486        }
3487        Ok(false)
3488    }
3489
3490    /// Perform deferred stack unwinding after set_f_lineno.
3491    ///
3492    /// set_f_lineno cannot pop the value stack directly because the execution
3493    /// loop holds the state mutex.  Instead it records the work in
3494    /// `pending_stack_pops` / `pending_unwind_from_stack` and we execute it
3495    /// here, inside the execution loop where we already own the state.
3496    fn unwind_stack_for_lineno(&mut self, pop_count: usize, from_stack: i64, vm: &VirtualMachine) {
3497        let mut cur_stack = from_stack;
3498        for _ in 0..pop_count {
3499            let val = self.pop_value_opt();
3500            if stack_analysis::top_of_stack(cur_stack) == stack_analysis::Kind::Except as i64
3501                && let Some(exc_obj) = val
3502            {
3503                // An Except-typed stack slot is only produced by bytecode that
3504                // also carries one of the opcodes scanned by
3505                // `PyCode::has_exc_handling`; otherwise the save/restore that
3506                // brackets this frame's exc_info is elided and this write would
3507                // corrupt the shared exc_info slot.
3508                debug_assert!(
3509                    self.code.has_exc_handling,
3510                    "unwinding an Except slot in a frame without exc-handling opcodes"
3511                );
3512                if vm.is_none(&exc_obj) {
3513                    vm.set_exception(None);
3514                } else {
3515                    let exc = exc_obj.downcast::<PyBaseException>().ok();
3516                    vm.set_exception(exc);
3517                }
3518            }
3519            cur_stack = stack_analysis::pop_value(cur_stack);
3520        }
3521    }
3522
3523    /// Fire 'exception' trace event (sys.settrace) with (type, value, traceback) tuple.
3524    /// Matches `_PyEval_MonitorRaise` → `PY_MONITORING_EVENT_RAISE` →
3525    /// `sys_trace_exception_func` in legacy_tracing.c.
3526    fn fire_exception_trace(&self, exc: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<()> {
3527        if vm.use_tracing.get() && self.trace_is_set(vm) {
3528            let exc_type: PyObjectRef = exc.class().to_owned().into();
3529            let exc_value: PyObjectRef = exc.to_owned().into();
3530            let exc_tb: PyObjectRef = exc
3531                .traceback()
3532                .map_or_else(|| vm.ctx.none(), |tb| -> PyObjectRef { tb.into() });
3533            let tuple = vm.ctx.new_tuple(vec![exc_type, exc_value, exc_tb]).into();
3534            vm.trace_event(crate::protocol::TraceEvent::Exception, Some(tuple))?;
3535        }
3536        Ok(())
3537    }
3538
3539    fn run(&mut self, vm: &VirtualMachine) -> PyResult<ExecutionResult> {
3540        flame_guard!(format!(
3541            "FrameObject::run({obj_name})",
3542            obj_name = self.code.obj_name
3543        ));
3544        // Execute until return or exception:
3545        //
3546        // `lasti_cell` and `instructions` are plain shared references with the
3547        // frame's own lifetime, so copying them into locals is free; read back
3548        // through `self` inside the loop they would be re-loaded after every
3549        // handler (which writes through `&mut self`), adding three dependent
3550        // loads in front of the code-unit fetch.
3551        //
3552        // `idx` is likewise carried in a local: the next instruction's index
3553        // is known from `lasti_before` and the cache count unless the handler
3554        // jumped, so re-reading the frame's `lasti` at the top of the loop
3555        // only added a store-to-load round trip to the fetch chain.
3556        let lasti_cell = self.lasti;
3557        // The instruction array never moves once the code object exists, so
3558        // its base pointer is hoisted here; reading it through `self.code`
3559        // inside the loop cost two more dependent loads per instruction.
3560        let units_ptr = self.code.instructions.units_ptr();
3561        let mut arg_state = bytecode::OpArgState::default();
3562        let mut idx = lasti_cell.load(Relaxed) as usize;
3563        // Previous opcode in this frame, for the `(prev, op)` pair histogram.
3564        // Zero (`CACHE`, never dispatched) marks "no predecessor yet".
3565        #[cfg(feature = "opcode-histogram")]
3566        let mut prev_op: u8 = 0;
3567        loop {
3568            // Advance lasti past the current instruction BEFORE firing the
3569            // line event.  This ensures that f_lineno (which reads
3570            // locations[lasti - 1]) returns the line of the instruction
3571            // being traced, not the previous one. Stored from `idx` rather
3572            // than read-modify-written, which would re-load what was just read.
3573            lasti_cell.store(idx as u32 + 1, Relaxed);
3574
3575            // Read once and reuse for both the line-trace check below and
3576            // the opcode-trace check after the instruction is decoded,
3577            // instead of re-reading the Cell twice per instruction. This is
3578            // safe even though the intervening trace_event call could in
3579            // principle toggle it, because we refresh `tracing` below right
3580            // after that call returns (that cold path is only taken when
3581            // tracing was already on, so it costs nothing on the hot path).
3582            let mut tracing = vm.use_tracing.get();
3583
3584            // Fire 'line' trace event when line number changes.
3585            // Only fire if this frame has a per-frame trace function set
3586            // (frames entered before sys.settrace() have trace=None).
3587            // Skip RESUME – it should not generate user-visible line events.
3588            // Skip NO_LOCATION units (addr2line == -1); the locations table
3589            // fills those with a dummy line, which would emit a 'line'
3590            // event whose f_lineno is None.
3591            if tracing
3592                && self.trace_is_set(vm)
3593                && self.trace_lines_is_set()
3594                && !matches!(
3595                    self.code.instructions.read_op(idx),
3596                    Instruction::Resume { .. } | Instruction::InstrumentedResume
3597                )
3598            {
3599                let line = self.code.addr2line(idx as i32 * 2);
3600                if line >= 0 && line as u32 != self.prev_line.get() {
3601                    self.prev_line.set(line as u32);
3602                    match vm.trace_event(crate::protocol::TraceEvent::Line, None) {
3603                        Ok(_) => {}
3604                        Err(exception) => {
3605                            if let Some((loc, _end_loc)) = self.code.locations.get(idx) {
3606                                let next = exception.traceback();
3607                                let new_traceback = PyTraceback::new(
3608                                    next,
3609                                    self.frame_object(vm),
3610                                    idx as i32 * 2,
3611                                    loc.line,
3612                                );
3613                                exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
3614                            }
3615                            match self.unwind_blocks(
3616                                vm,
3617                                UnwindReason::Raising {
3618                                    exception,
3619                                    offset: idx as u32,
3620                                },
3621                            ) {
3622                                Ok(None) => {
3623                                    arg_state.reset();
3624                                    idx = lasti_cell.load(Relaxed) as usize;
3625                                    continue;
3626                                }
3627                                Ok(Some(value)) => break Ok(value),
3628                                Err(e) => break Err(e),
3629                            }
3630                        }
3631                    }
3632                    // The trace callback may have toggled tracing (e.g. via
3633                    // sys.settrace(None)); refresh before the opcode-trace check
3634                    // below reuses this flag.
3635                    tracing = vm.use_tracing.get();
3636                    // Trace callback may have changed lasti via set_f_lineno.
3637                    // Re-read and restart the loop from the new position.
3638                    if lasti_cell.load(Relaxed) != (idx as u32 + 1) {
3639                        // set_f_lineno defers stack unwinding because we hold
3640                        // the state mutex.  Perform it now.
3641                        let pops = self.pending_stack_pops();
3642                        if pops > 0 {
3643                            let from_stack = self.pending_unwind_from_stack();
3644                            self.unwind_stack_for_lineno(pops as usize, from_stack, vm);
3645                            self.set_pending_stack_pops(0);
3646                        }
3647                        arg_state.reset();
3648                        idx = lasti_cell.load(Relaxed) as usize;
3649                        continue;
3650                    }
3651                }
3652            }
3653            // One aligned acquire load fetches opcode and arg together; two
3654            // separate atomic reads would force the instruction array pointer
3655            // to be re-loaded across the acquire barrier.
3656            // SAFETY: `idx` is a position this code object's own control flow
3657            // produced, so it is in bounds of the instruction array, and
3658            // `units_ptr` stays valid for as long as `self.code` is borrowed.
3659            let unit = unsafe { bytecode::CodeUnits::read_unit_from(units_ptr, idx) };
3660            let op = unit.op;
3661            let arg = arg_state.extend(unit.arg);
3662            let mut do_extend_arg = false;
3663
3664            // f_lineno for a live (currently executing) frame is derived
3665            // lazily from lasti/locations (see `FrameObject::lineno`) rather
3666            // than maintained here on every instruction. lasti already
3667            // points past the instruction currently executing (see the
3668            // `self.lasti.store` above), so `locations[lasti - 1]` gives
3669            // exactly the line of the in-flight instruction — the same
3670            // value this unconditional prev_line write used to compute.
3671            // prev_line itself is now only touched on the (cold) tracing
3672            // path, where it deduplicates consecutive 'line' events.
3673
3674            if tracing {
3675                // Fire 'opcode' trace event for sys.settrace when f_trace_opcodes
3676                // is set. Skip RESUME and ExtendedArg
3677                // (_Py_call_instrumentation_instruction).
3678                if self.trace_is_set(vm)
3679                    && self.trace_opcodes_is_set()
3680                    && !matches!(
3681                        op.into(),
3682                        Opcode::Resume | Opcode::InstrumentedResume | Opcode::ExtendedArg
3683                    )
3684                {
3685                    vm.trace_event(crate::protocol::TraceEvent::Opcode, None)?;
3686                }
3687            }
3688
3689            // The body is out of line so that the signal/QSBR/GC code it
3690            // pulls in does not sit inside the dispatch loop, where it
3691            // inflates register pressure (and hence per-instruction spills)
3692            // for every opcode.
3693            #[cold]
3694            #[inline(never)]
3695            fn eval_breaker_work(vm: &VirtualMachine) -> PyResult<()> {
3696                vm.check_signals()?;
3697                // Run a scheduled automatic collection here — a safepoint with
3698                // no interpreter locks held — instead of synchronously inside
3699                // the allocation that tripped the threshold.
3700                #[cfg(feature = "threading")]
3701                vm.run_scheduled_gc();
3702                Ok(())
3703            }
3704            if vm.eval_breaker_tripped()
3705                && let Err(exception) = eval_breaker_work(vm)
3706            {
3707                #[cold]
3708                fn handle_signal_exception(
3709                    frame: &mut ExecutingFrame<'_>,
3710                    exception: PyBaseExceptionRef,
3711                    idx: usize,
3712                    vm: &VirtualMachine,
3713                ) -> FrameResult {
3714                    if let Some((loc, _end_loc)) = frame.code.locations.get(idx) {
3715                        let next = exception.traceback();
3716                        let new_traceback = PyTraceback::new(
3717                            next,
3718                            frame.frame_object(vm),
3719                            idx as i32 * 2,
3720                            loc.line,
3721                        );
3722                        exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
3723                    }
3724                    vm.contextualize_exception(&exception);
3725                    frame.unwind_blocks(
3726                        vm,
3727                        UnwindReason::Raising {
3728                            exception,
3729                            offset: idx as u32,
3730                        },
3731                    )
3732                }
3733                match handle_signal_exception(self, exception, idx, vm) {
3734                    Ok(None) => {}
3735                    Ok(Some(value)) => {
3736                        break Ok(value);
3737                    }
3738                    Err(exception) => {
3739                        break Err(exception);
3740                    }
3741                }
3742                // The handler this unwound to starts a fresh instruction,
3743                // so drop any EXTENDED_ARG prefix collected for the one
3744                // the signal interrupted — the loop's own reset at the
3745                // bottom is skipped by this `continue`.
3746                arg_state.reset();
3747                idx = lasti_cell.load(Relaxed) as usize;
3748                continue;
3749            }
3750            // lasti was just stored as `idx + 1` above and nothing between
3751            // there and here writes it, so the pre-dispatch value is known
3752            // without an extra atomic load.
3753            let lasti_before = idx as u32 + 1;
3754            #[cfg(feature = "opcode-histogram")]
3755            {
3756                let op_byte = u8::from(op);
3757                crate::opcode_histogram::record(prev_op, op_byte);
3758                prev_op = op_byte;
3759            }
3760            let result = self.execute_instruction(op, arg, &mut do_extend_arg, vm);
3761            // Skip inline cache entries if instruction fell through (no jump).
3762            // `cache_entries()` is a table lookup, so it is done here rather
3763            // than before dispatch: computing it up front kept the count live
3764            // across the whole handler and cost a spill and a reload of it on
3765            // every instruction.
3766            let caches = op.cache_entries();
3767            let mut next_idx = lasti_cell.load(Relaxed);
3768            if caches > 0 && next_idx == lasti_before {
3769                next_idx = lasti_before + caches as u32;
3770                lasti_cell.store(next_idx, Relaxed);
3771            }
3772            match result {
3773                Ok(None) => {}
3774                Ok(Some(value)) => {
3775                    break Ok(value);
3776                }
3777                // Instruction raised an exception
3778                Err(exception) => {
3779                    #[cold]
3780                    fn handle_exception(
3781                        frame: &mut ExecutingFrame<'_>,
3782                        exception: PyBaseExceptionRef,
3783                        idx: usize,
3784                        is_reraise: bool,
3785                        is_new_raise: bool,
3786                        vm: &VirtualMachine,
3787                    ) -> FrameResult {
3788                        // 1. Extract traceback from exception's '__traceback__' attr.
3789                        // 2. Add new entry with current execution position (filename, lineno, code_object) to traceback.
3790                        // 3. First, try to find handler in exception table
3791
3792                        // RERAISE instructions should not add traceback entries - they're just
3793                        // re-raising an already-processed exception
3794                        if !is_reraise {
3795                            // Check if the exception already has traceback entries before
3796                            // we add ours. If it does, it was propagated from a callee
3797                            // function and we should not re-contextualize it.
3798                            let had_prior_traceback = exception.traceback().is_some();
3799
3800                            // PyTraceBack_Here always adds a new entry without
3801                            // checking for duplicates. Each time an exception passes through
3802                            // a frame (e.g., in a loop with repeated raise statements),
3803                            // a new traceback entry is added.
3804                            if let Some((loc, _end_loc)) = frame.code.locations.get(idx) {
3805                                let next = exception.traceback();
3806
3807                                let new_traceback = PyTraceback::new(
3808                                    next,
3809                                    frame.frame_object(vm),
3810                                    idx as i32 * 2,
3811                                    loc.line,
3812                                );
3813                                vm_trace!(
3814                                    "Adding to traceback: {:?} {:?}",
3815                                    new_traceback,
3816                                    loc.line
3817                                );
3818                                exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
3819                            }
3820
3821                            // _PyErr_SetObject sets __context__ only when the exception
3822                            // is first raised. When an exception propagates through frames,
3823                            // __context__ must not be overwritten. We contextualize when:
3824                            // - It's an explicit raise (raise/raise from)
3825                            // - The exception had no prior traceback (originated here)
3826                            if is_new_raise || !had_prior_traceback {
3827                                vm.contextualize_exception(&exception);
3828                            }
3829                        }
3830
3831                        // Use exception table for zero-cost exception handling
3832                        frame.unwind_blocks(
3833                            vm,
3834                            UnwindReason::Raising {
3835                                exception,
3836                                offset: idx as u32,
3837                            },
3838                        )
3839                    }
3840
3841                    // Check if this is a RERAISE instruction
3842                    // Both AnyInstruction::Raise { kind: Reraise/ReraiseFromStack } and
3843                    // AnyInstruction::Reraise are reraise operations that should not add
3844                    // new traceback entries.
3845                    // EndAsyncFor and CleanupThrow also re-raise non-matching exceptions.
3846                    let is_reraise = match op {
3847                        Instruction::RaiseVarargs { argc: kind } => matches!(
3848                            kind.get(arg),
3849                            bytecode::RaiseKind::BareRaise | bytecode::RaiseKind::ReraiseFromStack
3850                        ),
3851                        Instruction::Reraise { .. }
3852                        | Instruction::EndAsyncFor
3853                        | Instruction::CleanupThrow => true,
3854                        _ => false,
3855                    };
3856
3857                    // Explicit raise instructions (raise/raise from) - these always
3858                    // need contextualization even if the exception has prior traceback
3859                    let is_new_raise = matches!(
3860                        op,
3861                        Instruction::RaiseVarargs { argc: kind }
3862                            if matches!(
3863                                kind.get(arg),
3864                                bytecode::RaiseKind::Raise | bytecode::RaiseKind::RaiseCause
3865                            )
3866                    );
3867
3868                    // Fire RAISE or RERAISE monitoring event.
3869                    // If the callback raises, replace the original exception.
3870                    let exception = {
3871                        let mon_events = vm.state.monitoring_events.load();
3872                        if is_reraise {
3873                            if mon_events & MonitoringEvent::Reraise.mask() != 0 {
3874                                let offset = idx as u32 * 2;
3875                                let exc_obj: PyObjectRef = exception.clone().into();
3876                                match monitoring::fire_reraise(vm, self.code, offset, &exc_obj) {
3877                                    Ok(()) => exception,
3878                                    Err(monitor_exc) => monitor_exc,
3879                                }
3880                            } else {
3881                                exception
3882                            }
3883                        } else if mon_events & MonitoringEvent::Raise.mask() != 0 {
3884                            let offset = idx as u32 * 2;
3885                            let exc_obj: PyObjectRef = exception.clone().into();
3886                            match monitoring::fire_raise(vm, self.code, offset, &exc_obj) {
3887                                Ok(()) => exception,
3888                                Err(monitor_exc) => monitor_exc,
3889                            }
3890                        } else {
3891                            exception
3892                        }
3893                    };
3894
3895                    // Fire 'exception' trace event for sys.settrace.
3896                    // Only for new raises, not re-raises (matching the
3897                    // `error` label that calls _PyEval_MonitorRaise).
3898                    if !is_reraise {
3899                        self.fire_exception_trace(&exception, vm)?;
3900                    }
3901
3902                    match handle_exception(self, exception, idx, is_reraise, is_new_raise, vm) {
3903                        // The handler unwound to a new position, so the next
3904                        // index is whatever it left in the frame.
3905                        Ok(None) => next_idx = lasti_cell.load(Relaxed),
3906                        Ok(Some(result)) => break Ok(result),
3907                        Err(exception) => {
3908                            // Fire PY_UNWIND: exception escapes this frame
3909                            let exception = if vm.state.monitoring_events.load()
3910                                & MonitoringEvent::PyUnwind.mask()
3911                                != 0
3912                            {
3913                                let offset = idx as u32 * 2;
3914                                let exc_obj: PyObjectRef = exception.clone().into();
3915                                match monitoring::fire_py_unwind(vm, self.code, offset, &exc_obj) {
3916                                    Ok(()) => exception,
3917                                    Err(monitor_exc) => monitor_exc,
3918                                }
3919                            } else {
3920                                exception
3921                            };
3922
3923                            break Err(exception);
3924                        }
3925                    }
3926                }
3927            }
3928            if !do_extend_arg {
3929                arg_state.reset()
3930            }
3931            idx = next_idx as usize;
3932        }
3933    }
3934
3935    fn yield_from_target(&self) -> Option<&PyObject> {
3936        // checks gi_frame_state == FRAME_SUSPENDED_YIELD_FROM
3937        // which is set when YIELD_VALUE with oparg >= 1 is executed.
3938        // In RustPython, we check:
3939        // 1. lasti points to RESUME (after YIELD_VALUE)
3940        // 2. The previous instruction was YIELD_VALUE with arg >= 1
3941        // 3. Stack top is the delegate (receiver)
3942        //
3943        // First check if stack is empty - if so, we can't be in yield-from
3944        if self.localsplus.stack_is_empty() {
3945            return None;
3946        }
3947        let lasti = self.lasti() as usize;
3948        if let Some(unit) = self.code.instructions.get(lasti) {
3949            match &unit.op {
3950                Instruction::Send { .. } => return Some(self.top_value()),
3951                Instruction::Resume { .. } | Instruction::InstrumentedResume => {
3952                    // Check if previous instruction was YIELD_VALUE with arg >= 1
3953                    // This indicates yield-from/await context
3954                    if lasti > 0
3955                        && let Some(prev_unit) = self.code.instructions.get(lasti - 1)
3956                        && matches!(
3957                            &prev_unit.op.into(),
3958                            Opcode::YieldValue | Opcode::InstrumentedYieldValue
3959                        )
3960                    {
3961                        // YIELD_VALUE arg: 0 = direct yield, >= 1 = yield-from/await
3962                        // OpArgByte.0 is the raw byte value
3963                        if u8::from(prev_unit.arg) >= 1 {
3964                            // In yield-from/await context, delegate is on top of stack
3965                            return Some(self.top_value());
3966                        }
3967                    }
3968                }
3969                _ => {}
3970            }
3971        }
3972        None
3973    }
3974
3975    /// Handle throw() on a generator/coroutine.
3976    fn gen_throw(
3977        &mut self,
3978        vm: &VirtualMachine,
3979        exc_type: PyObjectRef,
3980        exc_val: PyObjectRef,
3981        exc_tb: PyObjectRef,
3982    ) -> PyResult<ExecutionResult> {
3983        self.monitoring_mask = vm.state.monitoring_events.load();
3984        if let Some(jen) = self.yield_from_target() {
3985            // Check if the exception is GeneratorExit (type or instance).
3986            // For GeneratorExit, close the sub-iterator instead of throwing.
3987            let is_gen_exit = if let Some(typ) = exc_type.downcast_ref::<PyType>() {
3988                typ.fast_issubclass(vm.ctx.exceptions.generator_exit)
3989            } else {
3990                exc_type.fast_isinstance(vm.ctx.exceptions.generator_exit)
3991            };
3992
3993            if is_gen_exit {
3994                // gen_close_iter: close the sub-iterator. A failed
3995                // lookup of close is unraisable; a failed close()
3996                // call is raised into this generator.
3997                let close_result = if let Some(coro) = self.builtin_coro(jen) {
3998                    coro.close(jen, vm).map(|_| ())
3999                } else {
4000                    match vm.get_attribute_opt(jen, "close") {
4001                        Ok(Some(close_meth)) => close_meth.call((), vm).map(|_| ()),
4002                        Ok(None) => Ok(()),
4003                        Err(e) => {
4004                            let msg = jen
4005                                .repr(vm)
4006                                .ok()
4007                                .map(|r| format!("Exception ignored while closing generator {r}"));
4008                            vm.run_unraisable(e, msg, vm.ctx.none());
4009                            Ok(())
4010                        }
4011                    }
4012                };
4013                if let Err(err) = close_result {
4014                    let idx = self.lasti().saturating_sub(1) as usize;
4015                    if idx < self.code.locations.len() {
4016                        let (loc, _end_loc) = self.code.locations[idx];
4017                        let next = err.traceback();
4018                        let new_traceback =
4019                            PyTraceback::new(next, self.frame_object(vm), idx as i32 * 2, loc.line);
4020                        err.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
4021                    }
4022
4023                    self.push_value(vm.ctx.none());
4024                    vm.chain_stack_item(&err);
4025                    return match self.unwind_blocks(
4026                        vm,
4027                        UnwindReason::Raising {
4028                            exception: err,
4029                            offset: idx as u32,
4030                        },
4031                    ) {
4032                        Ok(None) => {
4033                            self.prev_line.set(0);
4034                            self.run(vm)
4035                        }
4036                        Ok(Some(result)) => Ok(result),
4037                        Err(exception) => Err(exception),
4038                    };
4039                }
4040                // Fall through to throw_here to raise GeneratorExit in the generator
4041            } else {
4042                // For non-GeneratorExit, delegate throw to sub-iterator
4043                let thrower = if let Some(coro) = self.builtin_coro(jen) {
4044                    Some(Either::A(coro))
4045                } else {
4046                    vm.get_attribute_opt(jen, "throw")?.map(Either::B)
4047                };
4048                if let Some(thrower) = thrower {
4049                    let ret = match thrower {
4050                        Either::A(coro) => coro
4051                            .throw(jen, exc_type, exc_val, exc_tb, vm)
4052                            .to_pyresult(vm),
4053                        Either::B(meth) => {
4054                            // Omit trailing None so a 1-arg throw() stays 1-arg.
4055                            // Passing None fillers makes throw() look like the
4056                            // deprecated 3-arg form and warns under -W error.
4057                            let args = if !vm.is_none(&exc_tb) {
4058                                vec![exc_type, exc_val, exc_tb]
4059                            } else if !vm.is_none(&exc_val) {
4060                                vec![exc_type, exc_val]
4061                            } else {
4062                                vec![exc_type]
4063                            };
4064                            meth.call(args, vm)
4065                        }
4066                    };
4067                    return ret.map(ExecutionResult::Yield).or_else(|err| {
4068                        // Add traceback entry for the yield-from/await point.
4069                        // gen_send_ex2 resumes the frame with a pending exception,
4070                        // which goes through error: → PyTraceBack_Here. We add the
4071                        // entry here before calling unwind_blocks.
4072                        let idx = self.lasti().saturating_sub(1) as usize;
4073                        if idx < self.code.locations.len() {
4074                            let (loc, _end_loc) = self.code.locations[idx];
4075                            let next = err.traceback();
4076                            let new_traceback = PyTraceback::new(
4077                                next,
4078                                self.frame_object(vm),
4079                                idx as i32 * 2,
4080                                loc.line,
4081                            );
4082                            err.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
4083                        }
4084
4085                        self.push_value(vm.ctx.none());
4086                        vm.chain_stack_item(&err);
4087                        match self.unwind_blocks(
4088                            vm,
4089                            UnwindReason::Raising {
4090                                exception: err,
4091                                offset: idx as u32,
4092                            },
4093                        ) {
4094                            Ok(None) => {
4095                                self.prev_line.set(0);
4096                                self.run(vm)
4097                            }
4098                            Ok(Some(result)) => Ok(result),
4099                            Err(exception) => Err(exception),
4100                        }
4101                    });
4102                }
4103            }
4104        }
4105        // throw_here: no delegate has throw method, or not in yield-from
4106        // Validate the exception type first. Invalid types propagate directly to
4107        // the caller. Valid types with failed instantiation (e.g. __new__ returns
4108        // wrong type) get thrown into the generator via PyErr_SetObject path.
4109        let ctor = ExceptionCtor::try_from_object(vm, exc_type)?;
4110        let exception = match ctor.instantiate_value(exc_val, vm) {
4111            Ok(exc) => {
4112                if let Some(tb) = Option::<PyRef<PyTraceback>>::try_from_object(vm, exc_tb)? {
4113                    exc.set_traceback(Some(tb));
4114                }
4115                exc
4116            }
4117            Err(err) => err,
4118        };
4119
4120        // Add traceback entry for the generator frame at the yield site
4121        let idx = self.lasti().saturating_sub(1) as usize;
4122        if idx < self.code.locations.len() {
4123            let (loc, _end_loc) = self.code.locations[idx];
4124            let next = exception.traceback();
4125            let new_traceback =
4126                PyTraceback::new(next, self.frame_object(vm), idx as i32 * 2, loc.line);
4127            exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
4128        }
4129
4130        // Fire PY_THROW and RAISE events before raising the exception.
4131        // If a monitoring callback fails, its exception replaces the original.
4132        if vm.use_tracing.get() {
4133            let what = monitoring::MonitoringEvent::PyThrow;
4134            if let Some(local_trace) =
4135                vm.trace_event_what(crate::protocol::TraceEvent::Call, what, None)?
4136            {
4137                *self.iframe().cold().trace.lock() = Some(local_trace);
4138            }
4139            self.fire_exception_trace(&exception, vm)?;
4140        }
4141        let exception = {
4142            let mon_events = vm.state.monitoring_events.load();
4143            let exception = if mon_events & MonitoringEvent::PyThrow.mask() != 0 {
4144                let offset = idx as u32 * 2;
4145                let exc_obj: PyObjectRef = exception.clone().into();
4146                match monitoring::fire_py_throw(vm, self.code, offset, &exc_obj) {
4147                    Ok(()) => exception,
4148                    Err(monitor_exc) => monitor_exc,
4149                }
4150            } else {
4151                exception
4152            };
4153            if mon_events & MonitoringEvent::Raise.mask() != 0 {
4154                let offset = idx as u32 * 2;
4155                let exc_obj: PyObjectRef = exception.clone().into();
4156                match monitoring::fire_raise(vm, self.code, offset, &exc_obj) {
4157                    Ok(()) => exception,
4158                    Err(monitor_exc) => monitor_exc,
4159                }
4160            } else {
4161                exception
4162            }
4163        };
4164
4165        // PyErr_Restore: do not touch __context__. Chain only when this
4166        // generator's own exc_info slot is occupied (_PyErr_ChainStackItem).
4167        vm.chain_stack_item(&exception);
4168
4169        // always pushes Py_None before calling gen_send_ex with exc=1
4170        // This is needed for exception handler to have correct stack state
4171        self.push_value(vm.ctx.none());
4172
4173        match self.unwind_blocks(
4174            vm,
4175            UnwindReason::Raising {
4176                exception,
4177                offset: idx as u32,
4178            },
4179        ) {
4180            Ok(None) => {
4181                // Reset prev_line so that the first instruction in the handler
4182                // fires a LINE event. In CPython, gen_send_ex re-enters the
4183                // eval loop which reinitializes its local prev_instr tracker.
4184                self.prev_line.set(0);
4185                self.run(vm)
4186            }
4187            Ok(Some(result)) => Ok(result),
4188            Err(exception) => {
4189                // Fire PY_UNWIND: exception escapes the generator frame.
4190                let exception =
4191                    if vm.state.monitoring_events.load() & MonitoringEvent::PyUnwind.mask() != 0 {
4192                        let offset = idx as u32 * 2;
4193                        let exc_obj: PyObjectRef = exception.clone().into();
4194                        match monitoring::fire_py_unwind(vm, self.code, offset, &exc_obj) {
4195                            Ok(()) => exception,
4196                            Err(monitor_exc) => monitor_exc,
4197                        }
4198                    } else {
4199                        exception
4200                    };
4201                Err(exception)
4202            }
4203        }
4204    }
4205
4206    fn unbound_cell_exception(
4207        &self,
4208        localsplus_idx: usize,
4209        vm: &VirtualMachine,
4210    ) -> PyBaseExceptionRef {
4211        use rustpython_compiler_core::bytecode::CO_FAST_FREE;
4212        let kind = self
4213            .code
4214            .localspluskinds
4215            .get(localsplus_idx)
4216            .copied()
4217            .unwrap_or(0);
4218        if kind & CO_FAST_FREE != 0 {
4219            let name = self.localsplus_name(localsplus_idx);
4220            vm.new_name_error(
4221                format!("cannot access free variable '{name}' where it is not associated with a value in enclosing scope"),
4222                name.to_owned(),
4223            )
4224        } else {
4225            // Both merged cells (LOCAL|CELL) and non-merged cells get unbound local error
4226            let name = self.localsplus_name(localsplus_idx);
4227            vm.new_unbound_local_error(format!(
4228                "local variable '{name}' referenced before assignment"
4229            ))
4230        }
4231    }
4232
4233    /// Get the variable name for a localsplus index.
4234    fn localsplus_name(&self, idx: usize) -> &'static PyStrInterned {
4235        localsplus_name(self.code, idx)
4236    }
4237
4238    /// Execute a single instruction.
4239    #[inline(always)]
4240    fn execute_instruction(
4241        &mut self,
4242        instruction: Instruction,
4243        arg: bytecode::OpArg,
4244        extend_arg: &mut bool,
4245        vm: &VirtualMachine,
4246    ) -> FrameResult {
4247        flame_guard!(format!(
4248            "FrameObject::execute_instruction({instruction:?} {arg:?})"
4249        ));
4250
4251        #[cfg(feature = "vm-tracing-logging")]
4252        {
4253            trace!("=======");
4254            /* TODO:
4255            for frame in self.frames.iter() {
4256                trace!("  {:?}", frame);
4257            }
4258            */
4259            trace!("  {:#?}", self);
4260            trace!("  Executing opcode: {instruction:?} {arg:?}",);
4261            trace!("=======");
4262        }
4263
4264        #[cold]
4265        fn name_error(name: &'static PyStrInterned, vm: &VirtualMachine) -> PyBaseExceptionRef {
4266            vm.new_name_error(format!("name '{name}' is not defined"), name.to_owned())
4267        }
4268
4269        match instruction {
4270            Instruction::BinaryOp { op } => {
4271                let op_val = op.get(arg);
4272                self.adaptive(|s, ii, cb| s.specialize_binary_op(vm, op_val, ii, cb));
4273                self.execute_bin_op(vm, op_val)
4274            }
4275            // Super-instruction for BINARY_OP_ADD_UNICODE + STORE_FAST targeting
4276            // the left local, matching BINARY_OP_INPLACE_ADD_UNICODE shape.
4277            Instruction::BinaryOpInplaceAddUnicode => {
4278                let b = self.top_value();
4279                let a = self.nth_value(1);
4280                let instr_idx = self.lasti() as usize - 1;
4281                let cache_base = instr_idx + 1;
4282                let target_local = self.binary_op_inplace_unicode_target_local(cache_base, a);
4283                if let (Some(_a_str), Some(_b_str), Some(target_local)) = (
4284                    a.downcast_ref_if_exact::<PyStr>(vm),
4285                    b.downcast_ref_if_exact::<PyStr>(vm),
4286                    target_local,
4287                ) {
4288                    let right = self.pop_value();
4289                    let left = self.pop_value();
4290
4291                    let local_obj = self.localsplus.fastlocals_mut()[target_local]
4292                        .take()
4293                        .expect("BINARY_OP_INPLACE_ADD_UNICODE target local missing");
4294                    debug_assert!(local_obj.is(&left));
4295                    let mut local_str = local_obj
4296                        .downcast_exact::<PyStr>(vm)
4297                        .expect("BINARY_OP_INPLACE_ADD_UNICODE target local not exact str")
4298                        .into_pyref();
4299                    drop(left);
4300                    let right_str = right
4301                        .downcast_ref_if_exact::<PyStr>(vm)
4302                        .expect("BINARY_OP_INPLACE_ADD_UNICODE right operand not exact str");
4303                    local_str.concat_in_place(right_str.as_wtf8(), vm);
4304
4305                    self.localsplus.fastlocals_mut()[target_local] = Some(local_str.into());
4306                    self.jump_relative_forward(
4307                        1,
4308                        Instruction::BinaryOpInplaceAddUnicode.cache_entries() as u32,
4309                    );
4310                    Ok(None)
4311                } else {
4312                    self.execute_bin_op(vm, self.binary_op_from_arg(arg))
4313                }
4314            }
4315            Instruction::BinarySlice => {
4316                // Stack: [container, start, stop] -> [result]
4317                let stop = self.pop_value();
4318                let start = self.pop_value();
4319                let container = self.pop_value();
4320                let slice: PyObjectRef = PySlice {
4321                    start: Some(start),
4322                    stop,
4323                    step: None,
4324                }
4325                .into_ref(&vm.ctx)
4326                .into();
4327                let result = container.get_item(&*slice, vm)?;
4328                self.push_value(result);
4329                Ok(None)
4330            }
4331            Instruction::BuildList { count: size } => {
4332                let sz = size.get(arg) as usize;
4333                let elements = self.pop_multiple(sz).collect();
4334                let list_obj = vm.ctx.new_list(elements);
4335                self.push_value(list_obj.into());
4336                Ok(None)
4337            }
4338            Instruction::BuildMap { count: size } => self.execute_build_map(vm, size.get(arg)),
4339            Instruction::BuildSet { count: size } => {
4340                let set = PySet::default().into_ref(&vm.ctx);
4341                for element in self.pop_multiple(size.get(arg) as usize) {
4342                    set.add(element, vm)?;
4343                }
4344                self.push_value(set.into());
4345                Ok(None)
4346            }
4347            Instruction::BuildSlice { argc } => Ok(self.execute_build_slice(vm, argc.get(arg))),
4348            /*
4349             Instruction::ToBool => {
4350                 dbg!("Shouldn't be called outside of match statements for now")
4351                 let value = self.pop_value();
4352                 // call __bool__
4353                 let result = value.try_to_bool(vm)?;
4354                 self.push_value(vm.ctx.new_bool(result).into());
4355                 Ok(None)
4356            }
4357            */
4358            Instruction::BuildString { count: size } => {
4359                let s: Wtf8Buf = self
4360                    .pop_multiple(size.get(arg) as usize)
4361                    .map(|pyobj| pyobj.downcast::<PyStr>().unwrap())
4362                    .collect();
4363                self.push_value(vm.ctx.new_str(s).into());
4364                Ok(None)
4365            }
4366            Instruction::BuildTuple { count: size } => {
4367                let elements = self.pop_multiple(size.get(arg) as usize).collect();
4368                let list_obj = vm.ctx.new_tuple(elements);
4369                self.push_value(list_obj.into());
4370                Ok(None)
4371            }
4372            Instruction::BuildTemplate => {
4373                // Stack: [strings_tuple, interpolations_tuple] -> [template]
4374                let interpolations = self.pop_value();
4375                let strings = self.pop_value();
4376
4377                let strings = strings
4378                    .downcast::<PyTuple>()
4379                    .map_err(|_| vm.new_type_error("BUILD_TEMPLATE expected tuple for strings"))?;
4380                let interpolations = interpolations.downcast::<PyTuple>().map_err(|_| {
4381                    vm.new_type_error("BUILD_TEMPLATE expected tuple for interpolations")
4382                })?;
4383
4384                let template = PyTemplate::new(strings, interpolations);
4385                self.push_value(template.into_pyobject(vm));
4386                Ok(None)
4387            }
4388            Instruction::BuildInterpolation { format: oparg } => {
4389                // oparg encoding: (conversion << 2) | has_format_spec
4390                // Stack: [value, expression_str, (format_spec)?] -> [interpolation]
4391                let oparg_val = oparg.get(arg);
4392                let has_format_spec = (oparg_val & 1) != 0;
4393                let conversion_code = oparg_val >> 2;
4394
4395                let format_spec = if has_format_spec {
4396                    self.pop_value().downcast::<PyStr>().map_err(|_| {
4397                        vm.new_type_error("BUILD_INTERPOLATION expected str for format_spec")
4398                    })?
4399                } else {
4400                    vm.ctx.empty_str.to_owned()
4401                };
4402
4403                let expression = self.pop_value().downcast::<PyStr>().map_err(|_| {
4404                    vm.new_type_error("BUILD_INTERPOLATION expected str for expression")
4405                })?;
4406                let value = self.pop_value();
4407
4408                // conversion: 0=None, 1=Str, 2=Repr, 3=Ascii
4409                let conversion: PyObjectRef = match conversion_code {
4410                    0 => vm.ctx.none(),
4411                    1 => vm.ctx.new_str("s").into(),
4412                    2 => vm.ctx.new_str("r").into(),
4413                    3 => vm.ctx.new_str("a").into(),
4414                    _ => vm.ctx.none(), // should not happen
4415                };
4416
4417                let interpolation =
4418                    PyInterpolation::new(value, expression, conversion, format_spec, vm)?;
4419                self.push_value(interpolation.into_pyobject(vm));
4420                Ok(None)
4421            }
4422            Instruction::Call { argc: nargs } => {
4423                // Stack: [callable, self_or_null, arg1, ..., argN]
4424                let nargs_val = nargs.get(arg);
4425                self.adaptive(|s, ii, cb| s.specialize_call(vm, nargs_val, ii, cb));
4426                self.execute_call_vectorcall(nargs_val, vm)
4427            }
4428            Instruction::CallKw { argc: nargs } => {
4429                let nargs = nargs.get(arg);
4430                self.adaptive(|s, ii, cb| s.specialize_call_kw(vm, nargs, ii, cb));
4431                // Stack: [callable, self_or_null, arg1, ..., argN, kwarg_names]
4432                self.execute_call_kw_vectorcall(nargs, vm)
4433            }
4434            Instruction::CallFunctionEx => {
4435                // Stack: [callable, self_or_null, args_tuple, kwargs_or_null]
4436                let args = self.collect_ex_args(vm)?;
4437                self.execute_call(args, vm)
4438            }
4439            Instruction::CallIntrinsic1 { func } => {
4440                let value = self.pop_value();
4441                let result = self.call_intrinsic_1(func.get(arg), value, vm)?;
4442                self.push_value(result);
4443                Ok(None)
4444            }
4445            Instruction::CallIntrinsic2 { func } => {
4446                let value2 = self.pop_value();
4447                let value1 = self.pop_value();
4448                let result = self.call_intrinsic_2(func.get(arg), value1, value2, vm)?;
4449                self.push_value(result);
4450                Ok(None)
4451            }
4452            Instruction::CheckEgMatch => {
4453                let match_type = self.pop_value();
4454                let exc_value = self.pop_value();
4455                let (rest, matched) =
4456                    crate::exceptions::exception_group_match(&exc_value, &match_type, vm)?;
4457
4458                // Set matched exception as current exception (if not None)
4459                // This mirrors CPython's PyErr_SetHandledException(match_o) in CHECK_EG_MATCH
4460                if !vm.is_none(&matched)
4461                    && let Some(exc) = matched.downcast_ref::<PyBaseException>()
4462                {
4463                    vm.set_exception(Some(exc.to_owned()));
4464                }
4465
4466                self.push_value(rest);
4467                self.push_value(matched);
4468                Ok(None)
4469            }
4470            Instruction::CompareOp { opname: op } => {
4471                let op_val = op.get(arg);
4472                self.adaptive(|s, ii, cb| s.specialize_compare_op(vm, op_val, ii, cb));
4473                self.execute_compare(vm, arg)
4474            }
4475            Instruction::ContainsOp { invert } => {
4476                self.adaptive(|s, ii, cb| s.specialize_contains_op(vm, ii, cb));
4477                let b = self.pop_stackref();
4478                let a = self.pop_stackref();
4479
4480                let value = match invert.get(arg) {
4481                    bytecode::Invert::No => self._in(vm, &a, &b)?,
4482                    bytecode::Invert::Yes => self._not_in(vm, &a, &b)?,
4483                };
4484                self.push_bool_or_fused_jump(instruction.cache_entries(), value, vm);
4485                Ok(None)
4486            }
4487            Instruction::ConvertValue { oparg: conversion } => {
4488                self.convert_value(conversion.get(arg), vm)
4489            }
4490            Instruction::Copy { i: index } => {
4491                // CopyItem { index: 1 } copies TOS
4492                // CopyItem { index: 2 } copies second from top
4493                // This is 1-indexed to match CPython
4494                let idx = index.get(arg) as usize;
4495                let stack_len = self.localsplus.stack_len();
4496                debug_assert!(stack_len >= idx, "CopyItem: stack underflow");
4497                let value = self.localsplus.stack_index(stack_len - idx);
4498                self.push_stackref_opt(value.cloned());
4499                Ok(None)
4500            }
4501            Instruction::CopyFreeVars { n } => {
4502                let n = n.get(arg) as usize;
4503                if n > 0 {
4504                    let closure = self
4505                        .func_obj
4506                        .and_then(|f| f.downcast_ref::<PyFunction>())
4507                        .and_then(|f| f.closure.as_ref());
4508                    let nlocalsplus = self.code.localspluskinds.len();
4509                    let freevar_start = nlocalsplus - n;
4510                    let fastlocals = self.localsplus.fastlocals_mut();
4511                    if let Some(closure) = closure {
4512                        for i in 0..n {
4513                            fastlocals[freevar_start + i] =
4514                                Some(closure.as_slice()[i].clone().into());
4515                        }
4516                    }
4517                }
4518                Ok(None)
4519            }
4520            Instruction::DeleteAttr { namei: idx } => self.delete_attr(vm, idx.get(arg)),
4521            Instruction::DeleteDeref { i } => {
4522                self.cell_ref(i.get(arg).as_usize()).set(None);
4523                Ok(None)
4524            }
4525            Instruction::DeleteFast { var_num } => {
4526                self.localsplus
4527                    .debug_audit_local_release(var_num.get(arg).as_usize());
4528                let fastlocals = self.localsplus.fastlocals_mut();
4529                let idx = var_num.get(arg);
4530                if fastlocals[idx].is_none() {
4531                    return Err(vm.new_unbound_local_error(format!(
4532                        "local variable '{}' referenced before assignment",
4533                        self.code.varnames[idx]
4534                    )));
4535                }
4536                fastlocals[idx] = None;
4537                Ok(None)
4538            }
4539            Instruction::DeleteGlobal { namei: idx } => {
4540                let name = self.code.names[idx.get(arg) as usize];
4541                match self.globals.del_item(name, vm) {
4542                    Ok(()) => {}
4543                    Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
4544                        return Err(name_error(name, vm));
4545                    }
4546                    Err(e) => return Err(e),
4547                }
4548                Ok(None)
4549            }
4550            Instruction::DeleteName { namei: idx } => {
4551                let name = self.code.names[idx.get(arg) as usize];
4552                let res = self.locals.mapping(vm).ass_subscript(name, None, vm);
4553
4554                match res {
4555                    Ok(()) => {}
4556                    Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
4557                        return Err(name_error(name, vm));
4558                    }
4559                    Err(e) => return Err(e),
4560                }
4561                Ok(None)
4562            }
4563            Instruction::DeleteSubscr => self.execute_delete_subscript(vm),
4564            Instruction::DictUpdate { i: index } => {
4565                // Stack before: [..., dict, ..., source]  (source at TOS)
4566                // Stack after:  [..., dict, ...]  (source consumed)
4567                // The dict to update is at position TOS-i (before popping source)
4568
4569                let idx = index.get(arg);
4570
4571                // Pop the source from TOS
4572                let source = self.pop_value();
4573
4574                // Get the dict to update (it's now at TOS-(i-1) after popping source)
4575                let dict = if idx <= 1 {
4576                    // DICT_UPDATE 0 or 1: dict is at TOS (after popping source)
4577                    self.top_value()
4578                } else {
4579                    // DICT_UPDATE n: dict is at TOS-(n-1)
4580                    self.nth_value(idx - 1)
4581                };
4582
4583                let dict = dict.downcast_ref::<PyDict>().expect("exact dict expected");
4584
4585                // For dictionary unpacking {**x}, x must be a mapping
4586                // Check if the object has the mapping protocol (keys method)
4587                if vm
4588                    .get_method(source.clone(), vm.ctx.intern_str("keys"))
4589                    .is_none()
4590                {
4591                    return Err(vm.new_type_error(format!(
4592                        "'{}' object is not a mapping",
4593                        source.class().name()
4594                    )));
4595                }
4596
4597                dict.merge_object(source, vm)?;
4598                Ok(None)
4599            }
4600            Instruction::DictMerge { i: index } => {
4601                let source = self.pop_value();
4602                let idx = index.get(arg);
4603
4604                // Get the dict to merge into (same logic as DICT_UPDATE)
4605                let dict_ref = if idx <= 1 {
4606                    self.top_value()
4607                } else {
4608                    self.nth_value(idx - 1)
4609                };
4610
4611                let dict: &Py<PyDict> = unsafe { dict_ref.downcast_unchecked_ref() };
4612
4613                // Get callable for error messages
4614                // Stack: [callable, self_or_null, args_tuple, kwargs_dict]
4615                let callable = self.nth_value(idx + 2);
4616                let func_str = Self::object_function_str(callable, vm);
4617
4618                // Fast path: source is an exact dict (not a subclass, which may
4619                // override `keys`/`__getitem__`). Iterate its entries natively
4620                // instead of going through the mapping protocol, mirroring
4621                // CPython's `PyDict_Merge` fast path for `PyDict_Check(other)`.
4622                let source = if source.class().is(vm.ctx.types.dict_type) {
4623                    let src_dict = source
4624                        .downcast_ref::<PyDict>()
4625                        .expect("exact dict must have a PyDict payload");
4626                    // Snapshot under a single read lock so a mutation of `source`
4627                    // triggered by `dict.set_item` (e.g. via a target subclass, or
4628                    // aliasing) can't be observed mid-iteration.
4629                    for (key, value) in src_dict.items_vec() {
4630                        if dict.contains_key(&*key, vm) {
4631                            let key_str = key.str(vm)?;
4632                            return Err(vm.new_type_error(format!(
4633                                "{} got multiple values for keyword argument '{}'",
4634                                func_str,
4635                                key_str.as_wtf8()
4636                            )));
4637                        }
4638                        dict.set_item(&*key, value, vm)?;
4639                    }
4640                    return Ok(None);
4641                } else {
4642                    source
4643                };
4644
4645                // Check if source is a mapping
4646                if vm
4647                    .get_method(source.clone(), vm.ctx.intern_str("keys"))
4648                    .is_none()
4649                {
4650                    return Err(vm.new_type_error(format!(
4651                        "{} argument after ** must be a mapping, not {}",
4652                        func_str,
4653                        source.class().name()
4654                    )));
4655                }
4656
4657                // Merge keys, checking for duplicates
4658                let keys_iter = vm.call_method(&source, "keys", ())?;
4659                for key in keys_iter.try_to_value::<Vec<PyObjectRef>>(vm)? {
4660                    if dict.contains_key(&*key, vm) {
4661                        let key_str = key.str(vm)?;
4662                        return Err(vm.new_type_error(format!(
4663                            "{} got multiple values for keyword argument '{}'",
4664                            func_str,
4665                            key_str.as_wtf8()
4666                        )));
4667                    }
4668                    let value = vm.call_method(&source, "__getitem__", (key.clone(),))?;
4669                    dict.set_item(&*key, value, vm)?;
4670                }
4671                Ok(None)
4672            }
4673            Instruction::EndAsyncFor => {
4674                // Pops (awaitable, exc) from stack.
4675                // If exc is StopAsyncIteration, clears it (normal loop end).
4676                // Otherwise re-raises.
4677                let exc = self.pop_value();
4678                let _awaitable = self.pop_value();
4679
4680                let exc = exc
4681                    .downcast::<PyBaseException>()
4682                    .expect("EndAsyncFor expects exception on stack");
4683
4684                if exc.fast_isinstance(vm.ctx.exceptions.stop_async_iteration) {
4685                    // StopAsyncIteration - normal end of async for loop
4686                    vm.set_exception(None);
4687                    Ok(None)
4688                } else {
4689                    // Other exception - re-raise
4690                    Err(exc)
4691                }
4692            }
4693            Instruction::ExtendedArg => {
4694                *extend_arg = true;
4695                Ok(None)
4696            }
4697            Instruction::ForIter { .. } => {
4698                // Relative forward jump: target = lasti + caches + delta
4699                let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
4700                self.adaptive(|s, ii, cb| s.specialize_for_iter(vm, u32::from(arg), ii, cb));
4701                self.execute_for_iter(vm, target)?;
4702                Ok(None)
4703            }
4704            Instruction::FormatSimple => {
4705                let value = self.pop_value();
4706                let formatted = vm.format(&value, vm.ctx.new_str(""))?;
4707                self.push_value(formatted.into());
4708
4709                Ok(None)
4710            }
4711            Instruction::FormatWithSpec => {
4712                let spec = self.pop_value();
4713                let value = self.pop_value();
4714                let formatted = vm.format(&value, spec.downcast::<PyStr>().unwrap())?;
4715                self.push_value(formatted.into());
4716
4717                Ok(None)
4718            }
4719            Instruction::GetAiter => {
4720                let aiterable = self.pop_value();
4721                let aiter = match vm.get_special_method(&aiterable, identifier!(vm, __aiter__))? {
4722                    Some(meth) => meth.invoke((), vm)?,
4723                    None => {
4724                        return Err(vm.new_type_error(format!(
4725                            "'async for' requires an object with __aiter__ method, got {}",
4726                            aiterable.class().name()
4727                        )));
4728                    }
4729                };
4730                if vm
4731                    .get_special_method(&aiter, identifier!(vm, __anext__))?
4732                    .is_none()
4733                {
4734                    return Err(vm.new_type_error(format!(
4735                        "'async for' received an object from __aiter__ that does not implement __anext__: {}",
4736                        aiter.class().name()
4737                    )));
4738                }
4739                self.push_value(aiter);
4740                Ok(None)
4741            }
4742            Instruction::GetAnext => {
4743                #[cfg(debug_assertions)] // remove when GetAnext is fully implemented
4744                let orig_stack_len = self.localsplus.stack_len();
4745
4746                let aiter = self.top_value();
4747                let awaitable = if aiter.class().is(vm.ctx.types.async_generator) {
4748                    vm.call_special_method(aiter, identifier!(vm, __anext__), ())?
4749                } else {
4750                    if vm
4751                        .get_special_method(aiter, identifier!(vm, __anext__))?
4752                        .is_none()
4753                    {
4754                        let msg = format!(
4755                            "'async for' requires an iterator with __anext__ method, got {:.100}",
4756                            aiter.class().name()
4757                        );
4758                        return Err(vm.new_type_error(msg));
4759                    }
4760                    let next_iter =
4761                        vm.call_special_method(aiter, identifier!(vm, __anext__), ())?;
4762                    crate::coroutine::get_awaitable_iter(next_iter.clone(), vm).map_err(|e| {
4763                        let err = vm.new_type_error(format!(
4764                            "'async for' received an invalid object from __anext__: {:.200}",
4765                            next_iter.class().name()
4766                        ));
4767                        err.set_cause(Some(e));
4768                        err
4769                    })?
4770                };
4771                self.push_value(awaitable);
4772                #[cfg(debug_assertions)]
4773                debug_assert_eq!(orig_stack_len + 1, self.localsplus.stack_len());
4774                Ok(None)
4775            }
4776            Instruction::GetAwaitable { r#where: oparg } => {
4777                let iterable = self.pop_value();
4778
4779                let iter = match crate::coroutine::get_awaitable_iter(iterable.clone(), vm) {
4780                    Ok(iter) => iter,
4781                    Err(e) => {
4782                        // _PyEval_FormatAwaitableError: override error for async with
4783                        // when the type doesn't have __await__
4784                        let oparg_val = oparg.get(arg);
4785                        if vm
4786                            .get_method(iterable.clone(), identifier!(vm, __await__))
4787                            .is_none()
4788                        {
4789                            if oparg_val == 1 {
4790                                return Err(vm.new_type_error(format!(
4791                                    "'async with' received an object from __aenter__ \
4792                                     that does not implement __await__: {}",
4793                                    iterable.class().name()
4794                                )));
4795                            } else if oparg_val == 2 {
4796                                return Err(vm.new_type_error(format!(
4797                                    "'async with' received an object from __aexit__ \
4798                                     that does not implement __await__: {}",
4799                                    iterable.class().name()
4800                                )));
4801                            }
4802                        }
4803                        return Err(e);
4804                    }
4805                };
4806
4807                // Check if coroutine is already being awaited
4808                if let Some(coro) = iter.downcast_ref::<PyCoroutine>()
4809                    && coro
4810                        .as_coro()
4811                        .frame_opt()
4812                        .and_then(|f| f.yield_from_target())
4813                        .is_some()
4814                {
4815                    return Err(vm.new_runtime_error("coroutine is being awaited already"));
4816                }
4817
4818                self.push_value(iter);
4819                Ok(None)
4820            }
4821            Instruction::GetIter => {
4822                let iterated_obj = self.pop_value();
4823                let iter_obj = PyIter::try_from_object(vm, iterated_obj)?;
4824                self.push_value(iter_obj.into());
4825                Ok(None)
4826            }
4827            Instruction::GetYieldFromIter => {
4828                // GET_YIELD_FROM_ITER: prepare iterator for yield from
4829                // If iterable is a coroutine, ensure we're in a coroutine context
4830                // If iterable is a generator, use it directly
4831                // Otherwise, call iter() on it
4832                let iterable = self.pop_value();
4833                let iter = if iterable.class().is(vm.ctx.types.coroutine_type) {
4834                    // Coroutine requires CO_COROUTINE or CO_ITERABLE_COROUTINE flag
4835                    if !self.code.flags.intersects(
4836                        bytecode::CodeFlags::COROUTINE | bytecode::CodeFlags::ITERABLE_COROUTINE,
4837                    ) {
4838                        return Err(vm.new_type_error(
4839                            "cannot 'yield from' a coroutine object in a non-coroutine generator",
4840                        ));
4841                    }
4842                    iterable
4843                } else if iterable.class().is(vm.ctx.types.generator_type) {
4844                    // Generator can be used directly
4845                    iterable
4846                } else {
4847                    // Otherwise, get iterator
4848                    PyIter::try_from_object(vm, iterable)?.into()
4849                };
4850                self.push_value(iter);
4851                Ok(None)
4852            }
4853            Instruction::GetLen => {
4854                // STACK.append(len(STACK[-1]))
4855                let obj = self.top_value();
4856                let len = obj.length(vm)?;
4857                self.push_value(vm.ctx.new_int(len).into());
4858                Ok(None)
4859            }
4860            Instruction::ImportFrom { namei: idx } => {
4861                let obj = self.import_from(vm, idx.get(arg))?;
4862                self.push_value(obj);
4863                Ok(None)
4864            }
4865            Instruction::ImportName { namei: idx } => {
4866                self.import(vm, Some(self.code.names[idx.get(arg) as usize]))?;
4867                Ok(None)
4868            }
4869            Instruction::IsOp { invert } => {
4870                let b = self.pop_stackref();
4871                let a = self.pop_stackref();
4872                let res = a.is(b.as_object());
4873
4874                let value = match invert.get(arg) {
4875                    bytecode::Invert::No => res,
4876                    bytecode::Invert::Yes => !res,
4877                };
4878                self.push_bool_or_fused_jump(instruction.cache_entries(), value, vm);
4879                Ok(None)
4880            }
4881            Instruction::JumpForward { .. } => {
4882                self.jump_relative_forward(u32::from(arg), 0);
4883                Ok(None)
4884            }
4885            Instruction::JumpBackward { .. } => {
4886                // CPython rewrites JUMP_BACKWARD to JUMP_BACKWARD_NO_JIT
4887                // when JIT is unavailable.
4888                let instr_idx = self.lasti() as usize - 1;
4889                unsafe {
4890                    self.code
4891                        .instructions
4892                        .replace_op(instr_idx, Instruction::JumpBackwardNoJit);
4893                }
4894                self.jump_relative_backward_and_trace_line(u32::from(arg), 1, vm)?;
4895                Ok(None)
4896            }
4897            Instruction::JumpBackwardJit | Instruction::JumpBackwardNoJit => {
4898                self.jump_relative_backward_and_trace_line(u32::from(arg), 1, vm)?;
4899                Ok(None)
4900            }
4901            Instruction::JumpBackwardNoInterrupt { .. } => {
4902                self.jump_relative_backward_and_trace_line(u32::from(arg), 0, vm)?;
4903                Ok(None)
4904            }
4905            Instruction::ListAppend { i } => {
4906                let item = self.pop_value();
4907                let obj = self.nth_value(i.get(arg) - 1);
4908                let list: &Py<PyList> = unsafe {
4909                    // SAFETY: trust compiler
4910                    obj.downcast_unchecked_ref()
4911                };
4912                list.append(item);
4913                Ok(None)
4914            }
4915            Instruction::ListExtend { i } => {
4916                let iterable = self.pop_value();
4917                let obj = self.nth_value(i.get(arg) - 1);
4918                let list: &Py<PyList> = unsafe {
4919                    // SAFETY: compiler guarantees correct type
4920                    obj.downcast_unchecked_ref()
4921                };
4922                let type_name = iterable.class().name().to_owned();
4923                // Only rewrite the error if the type is truly not iterable
4924                // (no __iter__ and no __getitem__). Preserve original TypeError
4925                // from custom iterables that raise during iteration.
4926                let not_iterable = iterable.class().slots().iter.load().is_none()
4927                    && iterable
4928                        .get_class_attr(vm.ctx.intern_str("__getitem__"))
4929                        .is_none();
4930                list.extend(iterable, vm).map_err(|e| {
4931                    if not_iterable && e.class().is(vm.ctx.exceptions.type_error) {
4932                        vm.new_type_error(format!(
4933                            "Value after * must be an iterable, not {type_name}"
4934                        ))
4935                    } else {
4936                        e
4937                    }
4938                })?;
4939                Ok(None)
4940            }
4941            Instruction::LoadAttr { namei: idx } => self.load_attr(vm, idx.get(arg)),
4942            Instruction::LoadSuperAttr { namei: idx } => {
4943                let idx_val = idx.get(arg);
4944                self.adaptive(|s, ii, cb| s.specialize_load_super_attr(vm, idx_val, ii, cb));
4945                self.load_super_attr(vm, idx_val)
4946            }
4947            Instruction::LoadBuildClass => {
4948                let build_class = if let Some(builtins_dict) = self.builtins_dict {
4949                    builtins_dict
4950                        .get_item_opt(identifier!(vm, __build_class__), vm)?
4951                        .ok_or_else(|| {
4952                            vm.new_name_error(
4953                                "__build_class__ not found",
4954                                identifier!(vm, __build_class__).to_owned(),
4955                            )
4956                        })?
4957                } else {
4958                    self.builtins
4959                        .get_item(identifier!(vm, __build_class__), vm)
4960                        .map_err(|e| {
4961                            if e.fast_isinstance(vm.ctx.exceptions.key_error) {
4962                                vm.new_name_error(
4963                                    "__build_class__ not found",
4964                                    identifier!(vm, __build_class__).to_owned(),
4965                                )
4966                            } else {
4967                                e
4968                            }
4969                        })?
4970                };
4971                self.push_value(build_class);
4972                Ok(None)
4973            }
4974            Instruction::LoadLocals => {
4975                // Push the locals dict onto the stack
4976                let locals = self.locals.into_object(vm);
4977                self.push_value(locals);
4978                Ok(None)
4979            }
4980            Instruction::LoadFromDictOrDeref { i } => {
4981                // Pop dict from stack (locals or classdict depending on context)
4982                let class_dict = self.pop_value();
4983                let idx = i.get(arg).as_usize();
4984                let name = self.localsplus_name(idx);
4985                let value = self.mapping_get_optional(&class_dict, name, vm)?;
4986                self.push_value(match value {
4987                    Some(v) => v,
4988                    None => self
4989                        .cell_ref(idx)
4990                        .get()
4991                        .ok_or_else(|| self.unbound_cell_exception(idx, vm))?,
4992                });
4993                Ok(None)
4994            }
4995            Instruction::LoadFromDictOrGlobals { i: idx } => {
4996                // PEP 649: Pop dict from stack (classdict), check there first, then globals
4997                let dict = self.pop_value();
4998                let name = self.code.names[idx.get(arg) as usize];
4999                let value = self.mapping_get_optional(&dict, name, vm)?;
5000
5001                self.push_value(match value {
5002                    Some(v) => v,
5003                    None => self.load_global_or_builtin(name, vm)?,
5004                });
5005                Ok(None)
5006            }
5007            Instruction::LoadConst { consti } => {
5008                self.push_value(self.code.constants[consti.get(arg)].clone().into());
5009                // Mirror CPython's LOAD_CONST family transition. RustPython does
5010                // not currently distinguish immortal constants at runtime.
5011                let instr_idx = self.lasti() as usize - 1;
5012                unsafe {
5013                    self.code
5014                        .instructions
5015                        .replace_op(instr_idx, Instruction::LoadConstMortal);
5016                }
5017                Ok(None)
5018            }
5019            Instruction::LoadConstMortal | Instruction::LoadConstImmortal => {
5020                self.push_value(self.code.constants[u32::from(arg).into()].clone().into());
5021                Ok(None)
5022            }
5023            Instruction::LoadCommonConstant { idx } => {
5024                use bytecode::CommonConstant;
5025                let value = match idx.get(arg) {
5026                    CommonConstant::AssertionError => {
5027                        vm.ctx.exceptions.assertion_error.to_owned().into()
5028                    }
5029                    CommonConstant::NotImplementedError => {
5030                        vm.ctx.exceptions.not_implemented_error.to_owned().into()
5031                    }
5032                    CommonConstant::BuiltinTuple => vm.ctx.types.tuple_type.to_owned().into(),
5033                    CommonConstant::BuiltinAll => vm
5034                        .callable_cache
5035                        .builtin_all
5036                        .clone()
5037                        .expect("builtin_all not initialized"),
5038                    CommonConstant::BuiltinAny => vm
5039                        .callable_cache
5040                        .builtin_any
5041                        .clone()
5042                        .expect("builtin_any not initialized"),
5043                    CommonConstant::BuiltinList => vm.ctx.types.list_type.to_owned().into(),
5044                    CommonConstant::BuiltinSet => vm.ctx.types.set_type.to_owned().into(),
5045                };
5046                self.push_value(value);
5047                Ok(None)
5048            }
5049            Instruction::LoadSmallInt { i: idx } => {
5050                // Cached small integers live for the whole Context, so the value stack can
5051                // borrow them without touching the refcount.
5052                let value = vm.ctx.cached_int(idx.get(arg) as i32);
5053                unsafe { self.push_borrowed(value.as_object()) };
5054                Ok(None)
5055            }
5056            Instruction::LoadDeref { i } => {
5057                let idx = i.get(arg).as_usize();
5058                let x = self
5059                    .cell_ref(idx)
5060                    .get()
5061                    .ok_or_else(|| self.unbound_cell_exception(idx, vm))?;
5062                self.push_value(x);
5063                Ok(None)
5064            }
5065            Instruction::LoadFast { var_num } => {
5066                #[cold]
5067                fn reference_error(
5068                    varname: &'static PyStrInterned,
5069                    vm: &VirtualMachine,
5070                ) -> PyBaseExceptionRef {
5071                    vm.new_unbound_local_error(format!(
5072                        "local variable '{varname}' referenced before assignment"
5073                    ))
5074                }
5075                let idx = var_num.get(arg);
5076                let x = self.localsplus.fastlocals()[idx]
5077                    .clone()
5078                    .ok_or_else(|| reference_error(self.code.varnames[idx], vm))?;
5079                self.push_value(x);
5080                Ok(None)
5081            }
5082            Instruction::LoadFastAndClear { var_num } => {
5083                // Save current slot value and clear it (for inlined comprehensions).
5084                // Pushes NULL (None at Option level) if slot was empty, so that
5085                // StoreFast can restore the empty state after the comprehension.
5086                let idx = var_num.get(arg);
5087                let x = self.localsplus.fastlocals_mut()[idx].take();
5088                self.push_value_opt(x);
5089                Ok(None)
5090            }
5091            Instruction::LoadFastCheck { var_num } => {
5092                // Same as LoadFast but explicitly checks for unbound locals
5093                // (LoadFast in RustPython already does this check)
5094                let idx = var_num.get(arg);
5095                let x = self.localsplus.fastlocals()[idx].clone().ok_or_else(|| {
5096                    vm.new_unbound_local_error(format!(
5097                        "local variable '{}' referenced before assignment",
5098                        self.code.varnames[idx]
5099                    ))
5100                })?;
5101                self.push_value(x);
5102                Ok(None)
5103            }
5104            Instruction::LoadFastLoadFast { var_nums } => {
5105                // Load two local variables at once
5106                // oparg encoding: (idx1 << 4) | idx2
5107                let oparg = var_nums.get(arg);
5108                let (idx1, idx2) = oparg.indexes();
5109                let fastlocals = self.localsplus.fastlocals();
5110                let x1 = fastlocals[idx1].clone().ok_or_else(|| {
5111                    vm.new_unbound_local_error(format!(
5112                        "local variable '{}' referenced before assignment",
5113                        self.code.varnames[idx1]
5114                    ))
5115                })?;
5116                let x2 = fastlocals[idx2].clone().ok_or_else(|| {
5117                    vm.new_unbound_local_error(format!(
5118                        "local variable '{}' referenced before assignment",
5119                        self.code.varnames[idx2]
5120                    ))
5121                })?;
5122                self.push_value(x1);
5123                self.push_value(x2);
5124                Ok(None)
5125            }
5126            Instruction::LoadFastBorrow { var_num } => {
5127                let idx = var_num.get(arg);
5128                self.push_local(idx.as_usize(), vm)?;
5129                Ok(None)
5130            }
5131            Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
5132                let oparg = var_nums.get(arg);
5133                let (idx1, idx2) = oparg.indexes();
5134                self.push_local(idx1.as_usize(), vm)?;
5135                self.push_local(idx2.as_usize(), vm)?;
5136                Ok(None)
5137            }
5138            Instruction::LoadGlobal { namei: idx } => {
5139                let oparg = idx.get(arg);
5140                self.adaptive(|s, ii, cb| s.specialize_load_global(vm, oparg, ii, cb));
5141                let name = &self.code.names[(oparg >> 1) as usize];
5142                let x = self.load_global_or_builtin(name, vm)?;
5143                self.push_value(x);
5144                if (oparg & 1) != 0 {
5145                    self.push_value_opt(None);
5146                }
5147                Ok(None)
5148            }
5149            Instruction::LoadName { namei: idx } => {
5150                let name = self.code.names[idx.get(arg) as usize];
5151                let result = self.locals.mapping(vm).subscript(name, vm);
5152                match result {
5153                    Ok(x) => self.push_value(x),
5154                    Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
5155                        self.push_value(self.load_global_or_builtin(name, vm)?);
5156                    }
5157                    Err(e) => return Err(e),
5158                }
5159                Ok(None)
5160            }
5161            Instruction::LoadSpecial { method } => {
5162                // Pops obj, pushes (callable, self_or_null) for CALL convention.
5163                // Push order: callable first (deeper), self_or_null on top.
5164                use crate::vm::PyMethod;
5165
5166                let obj = self.pop_value();
5167                let oparg = method.get(arg);
5168                let method_name = get_special_method_name(oparg, vm);
5169
5170                match vm.get_special_method(&obj, method_name)? {
5171                    Some(PyMethod::Function { target, func }) => {
5172                        self.push_value(func); // callable (deeper)
5173                        self.push_value(target); // self (TOS)
5174                    }
5175                    Some(PyMethod::Attribute(bound)) => {
5176                        self.push_value(bound); // callable (deeper)
5177                        self.push_null(); // NULL (TOS)
5178                    }
5179                    None => {
5180                        return Err(vm.new_type_error(get_special_method_error_msg(
5181                            oparg,
5182                            &obj.class().name(),
5183                            special_method_can_suggest(&obj, oparg, vm)?,
5184                        )));
5185                    }
5186                };
5187                Ok(None)
5188            }
5189            Instruction::MakeFunction => self.execute_make_function(vm),
5190            Instruction::MakeCell { i } => {
5191                // Wrap the current slot value (if any) in a new PyCell.
5192                // For merged cells (LOCAL|CELL), this wraps the argument value.
5193                // For non-merged cells, this creates an empty cell.
5194                let idx = i.get(arg).as_usize();
5195                let fastlocals = self.localsplus.fastlocals_mut();
5196                let initial = fastlocals[idx].take();
5197                let cell = PyCell::new(initial).into_ref(&vm.ctx).into();
5198                fastlocals[idx] = Some(cell);
5199                Ok(None)
5200            }
5201            Instruction::MapAdd { i } => {
5202                let value = self.pop_value();
5203                let key = self.pop_value();
5204                let obj = self.nth_value(i.get(arg) - 1);
5205                let dict: &Py<PyDict> = unsafe {
5206                    // SAFETY: trust compiler
5207                    obj.downcast_unchecked_ref()
5208                };
5209                dict.set_item(&*key, value, vm)?;
5210                Ok(None)
5211            }
5212            Instruction::MatchClass { count: nargs } => {
5213                // STACK[-1] is a tuple of keyword attribute names, STACK[-2] is the class being matched against, and STACK[-3] is the match subject.
5214                // nargs is the number of positional sub-patterns.
5215                let kwd_attrs = self.pop_value();
5216                let kwd_attrs = kwd_attrs.downcast_ref::<PyTuple>().unwrap();
5217                let cls = self.pop_value();
5218                let subject = self.pop_value();
5219                let nargs_val = nargs.get(arg) as usize;
5220
5221                let Some(cls_type) = cls.downcast_ref::<PyType>() else {
5222                    return Err(vm.new_type_error("called match pattern must be a class"));
5223                };
5224                // Only the error paths need the class name; compute it lazily so a
5225                // successful match does not take the name lock or allocate.
5226                let type_name = || cls_type.name().to_string();
5227
5228                // Check if subject is an instance of cls
5229                if subject.is_instance(cls.as_ref(), vm)? {
5230                    let mut extracted = vec![];
5231                    let seen_attrs = PySet::default().into_ref(&vm.ctx);
5232
5233                    // Get __match_args__ for positional arguments if nargs > 0
5234                    if nargs_val > 0 {
5235                        // Get __match_args__ from the class
5236                        let match_args =
5237                            vm.get_attribute_opt(&cls, identifier!(vm, __match_args__))?;
5238
5239                        if let Some(match_args) = match_args {
5240                            // Convert to tuple
5241                            let match_args = match match_args.downcast_exact::<PyTuple>(vm) {
5242                                Ok(tuple) => tuple,
5243                                Err(match_args) => {
5244                                    // __match_args__ must be a tuple
5245                                    let type_name = type_name();
5246                                    let match_args_type_name = match_args.class().__name__(vm);
5247                                    return Err(vm.new_type_error(format!(
5248                                        "{type_name}.__match_args__ must be a tuple (got {match_args_type_name})"
5249                                    )));
5250                                }
5251                            };
5252
5253                            // Check if we have enough match args
5254                            if match_args.as_slice().len() < nargs_val {
5255                                let type_name = type_name();
5256                                let plural = if match_args.as_slice().len() == 1 {
5257                                    ""
5258                                } else {
5259                                    "s"
5260                                };
5261                                return Err(vm.new_type_error(format!(
5262                                    "{type_name}() accepts {} positional sub-pattern{} ({} given)",
5263                                    match_args.as_slice().len(),
5264                                    plural,
5265                                    nargs_val
5266                                )));
5267                            }
5268
5269                            // Extract positional attributes
5270                            for i in 0..nargs_val {
5271                                let attr_name = &match_args.as_slice()[i];
5272                                let attr_name_str = match attr_name.downcast_ref::<PyStr>() {
5273                                    Some(s) => s,
5274                                    None => {
5275                                        let attr_type_name = attr_name.class().name();
5276                                        return Err(vm.new_type_error(format!(
5277                                            "__match_args__ elements must be strings (got {attr_type_name})"
5278                                        )));
5279                                    }
5280                                };
5281                                if seen_attrs.contains(attr_name.as_object(), vm)? {
5282                                    let type_name = type_name();
5283                                    let attr_repr = attr_name.as_object().repr(vm)?;
5284                                    return Err(vm.new_type_error(format!(
5285                                        "{type_name}() got multiple sub-patterns for attribute {attr_repr}"
5286                                    )));
5287                                }
5288                                seen_attrs.add(attr_name.clone(), vm)?;
5289                                match subject.get_attr(attr_name_str, vm) {
5290                                    Ok(value) => extracted.push(value),
5291                                    Err(e)
5292                                        if e.fast_isinstance(vm.ctx.exceptions.attribute_error) =>
5293                                    {
5294                                        // Missing attribute → non-match
5295                                        self.push_value(vm.ctx.none());
5296                                        return Ok(None);
5297                                    }
5298                                    Err(e) => return Err(e),
5299                                }
5300                            }
5301                        } else {
5302                            // No __match_args__, check if this is a type with MATCH_SELF behavior
5303                            // For built-in types like bool, int, str, list, tuple, dict, etc.
5304                            // they match the subject itself as the single positional argument
5305                            let is_match_self_type =
5306                                cls_type.slots.flags.has_feature(PyTypeFlags::_MATCH_SELF);
5307
5308                            if is_match_self_type {
5309                                if nargs_val == 1 {
5310                                    // Match the subject itself as the single positional argument
5311                                    extracted.push(subject.clone());
5312                                } else if nargs_val > 1 {
5313                                    // Too many positional arguments for MATCH_SELF
5314                                    let type_name = type_name();
5315                                    return Err(vm.new_type_error(format!(
5316                                        "{type_name}() accepts 1 positional sub-pattern ({nargs_val} given)"
5317                                    )));
5318                                }
5319                            } else {
5320                                // No __match_args__ and not a MATCH_SELF type
5321                                if nargs_val > 0 {
5322                                    let type_name = type_name();
5323                                    return Err(vm.new_type_error(format!(
5324                                        "{type_name}() accepts 0 positional sub-patterns ({nargs_val} given)"
5325                                    )));
5326                                }
5327                            }
5328                        }
5329                    }
5330
5331                    // Extract keyword attributes
5332                    for name in kwd_attrs {
5333                        let name_str = name.downcast_ref::<PyStr>().unwrap();
5334                        if seen_attrs.contains(name_str.as_object(), vm)? {
5335                            let type_name = type_name();
5336                            let attr_repr = name.as_object().repr(vm)?;
5337                            return Err(vm.new_type_error(format!(
5338                                "{type_name}() got multiple sub-patterns for attribute {attr_repr}"
5339                            )));
5340                        }
5341                        seen_attrs.add(name.clone(), vm)?;
5342                        match subject.get_attr(name_str, vm) {
5343                            Ok(value) => extracted.push(value),
5344                            Err(e) if e.fast_isinstance(vm.ctx.exceptions.attribute_error) => {
5345                                self.push_value(vm.ctx.none());
5346                                return Ok(None);
5347                            }
5348                            Err(e) => return Err(e),
5349                        }
5350                    }
5351
5352                    self.push_value(vm.ctx.new_tuple(extracted).into());
5353                } else {
5354                    // Not an instance, push None
5355                    self.push_value(vm.ctx.none());
5356                }
5357                Ok(None)
5358            }
5359            Instruction::MatchKeys => {
5360                // MATCH_KEYS doesn't pop subject and keys, only reads them
5361                let keys_tuple = self.top_value(); // stack[-1]
5362                let subject = self.nth_value(1); // stack[-2]
5363
5364                // Check if subject is a mapping and extract values for keys
5365                if subject
5366                    .class()
5367                    .has_patma_collection_flag(PyTypeFlags::MAPPING)
5368                {
5369                    let keys = keys_tuple.downcast_ref::<PyTuple>().unwrap();
5370                    let mut values = Vec::new();
5371                    let mut all_match = true;
5372                    let seen_keys = PySet::default().into_ref(&vm.ctx);
5373
5374                    // We use the two argument form of map.get(key, default) for two reasons:
5375                    // - Atomically check for a key and get its value without error handling.
5376                    // - Don't cause key creation or resizing in dict subclasses like
5377                    //   collections.defaultdict that define __missing__ (or similar).
5378                    // See CPython's _PyEval_MatchKeys
5379
5380                    if let Some(get_method) = vm
5381                        .get_method(subject.to_owned(), vm.ctx.intern_str("get"))
5382                        .transpose()?
5383                    {
5384                        let dummy = vm
5385                            .ctx
5386                            .new_base_object(vm.ctx.types.object_type.to_owned(), None);
5387
5388                        for key in keys {
5389                            if seen_keys.contains(key.as_object(), vm)? {
5390                                return Err(vm.new_value_error(format!(
5391                                    "mapping pattern checks duplicate key ({})",
5392                                    key.as_object().repr(vm)?
5393                                )));
5394                            }
5395                            seen_keys.add(key.as_object().to_owned(), vm)?;
5396                            // value = map.get(key, dummy)
5397                            {
5398                                let value =
5399                                    get_method.call((key.as_object(), dummy.clone()), vm)?;
5400                                // if value == dummy: key not in map!
5401                                if value.is(&dummy) {
5402                                    all_match = false;
5403                                    break;
5404                                }
5405                                values.push(value);
5406                            }
5407                        }
5408                    } else {
5409                        // Fallback if .get() method is not available (shouldn't happen for mappings)
5410                        for key in keys {
5411                            if seen_keys.contains(key.as_object(), vm)? {
5412                                return Err(vm.new_value_error(format!(
5413                                    "mapping pattern checks duplicate key ({})",
5414                                    key.as_object().repr(vm)?
5415                                )));
5416                            }
5417                            seen_keys.add(key.as_object().to_owned(), vm)?;
5418                            match subject.get_item(key.as_object(), vm) {
5419                                Ok(value) => values.push(value),
5420                                Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
5421                                    all_match = false;
5422                                    break;
5423                                }
5424                                Err(e) => return Err(e),
5425                            }
5426                        }
5427                    }
5428
5429                    if all_match {
5430                        // Push values tuple on successful match
5431                        self.push_value(vm.ctx.new_tuple(values).into());
5432                    } else {
5433                        // No match - push None
5434                        self.push_value(vm.ctx.none());
5435                    }
5436                } else {
5437                    // Not a mapping - push None
5438                    self.push_value(vm.ctx.none());
5439                }
5440                Ok(None)
5441            }
5442            Instruction::MatchMapping => {
5443                // Pop and push back the subject to keep it on stack
5444                let subject = self.pop_value();
5445
5446                // Check if the type has the MAPPING flag
5447                let is_mapping = subject
5448                    .class()
5449                    .has_patma_collection_flag(PyTypeFlags::MAPPING);
5450
5451                self.push_value(subject);
5452                self.push_value(vm.ctx.new_bool(is_mapping).into());
5453                Ok(None)
5454            }
5455            Instruction::MatchSequence => {
5456                // Pop and push back the subject to keep it on stack
5457                let subject = self.pop_value();
5458
5459                // Check if the type has the SEQUENCE flag
5460                let is_sequence = subject
5461                    .class()
5462                    .has_patma_collection_flag(PyTypeFlags::SEQUENCE);
5463
5464                self.push_value(subject);
5465                self.push_value(vm.ctx.new_bool(is_sequence).into());
5466                Ok(None)
5467            }
5468            Instruction::Nop => Ok(None),
5469            // NOT_TAKEN is a branch prediction hint - functionally a NOP
5470            Instruction::NotTaken => Ok(None),
5471            // CACHE is used by adaptive interpreter for inline caching - NOP for us
5472            Instruction::Cache => Ok(None),
5473            Instruction::ReturnGenerator => {
5474                // In RustPython, generators/coroutines are created in function.rs
5475                // before the frame starts executing. The RETURN_GENERATOR instruction
5476                // pushes None so that the following POP_TOP has something to consume.
5477                // This matches CPython's semantics where the sent value (None for first call)
5478                // is on the stack when the generator resumes.
5479                self.push_value(vm.ctx.none());
5480                Ok(None)
5481            }
5482            Instruction::PopExcept => {
5483                // Pop prev_exc from value stack and restore it
5484                let prev_exc = self.pop_value();
5485                if vm.is_none(&prev_exc) {
5486                    vm.set_exception(None);
5487                } else if let Ok(exc) = prev_exc.downcast::<PyBaseException>() {
5488                    vm.set_exception(Some(exc));
5489                }
5490
5491                // NOTE: We do NOT clear the traceback of the exception that was just handled.
5492                // Python preserves exception tracebacks even after the exception is no longer
5493                // the "current exception". This is important for code that catches an exception,
5494                // stores it, and later inspects its traceback.
5495                // Reference cycles (Exception → Traceback → FrameObject → locals) are handled by
5496                // Python's garbage collector which can detect and break cycles.
5497
5498                Ok(None)
5499            }
5500            Instruction::PopJumpIfFalse { .. } => self.pop_jump_if_relative(vm, arg, 1, false),
5501            Instruction::PopJumpIfTrue { .. } => self.pop_jump_if_relative(vm, arg, 1, true),
5502            Instruction::PopJumpIfNone { .. } => {
5503                let value = self.pop_stackref();
5504                if vm.is_none(&value) {
5505                    self.jump_relative_forward(u32::from(arg), 1);
5506                } else {
5507                    self.skip_fallthrough_not_taken(vm, 1);
5508                }
5509                Ok(None)
5510            }
5511            Instruction::PopJumpIfNotNone { .. } => {
5512                let value = self.pop_stackref();
5513                if !vm.is_none(&value) {
5514                    self.jump_relative_forward(u32::from(arg), 1);
5515                } else {
5516                    self.skip_fallthrough_not_taken(vm, 1);
5517                }
5518                Ok(None)
5519            }
5520            Instruction::PopTop => {
5521                // Pop value from stack and ignore.
5522                self.pop_stackref();
5523                Ok(None)
5524            }
5525            Instruction::EndFor => {
5526                // Pop the next value from stack (cleanup after loop body)
5527                self.pop_stackref();
5528                Ok(None)
5529            }
5530            Instruction::PopIter => {
5531                // Pop the iterator from stack (end of for loop)
5532                self.pop_stackref();
5533                Ok(None)
5534            }
5535            Instruction::PushNull => {
5536                // Push NULL for self_or_null slot in call protocol
5537                self.push_null();
5538                Ok(None)
5539            }
5540            Instruction::RaiseVarargs { argc: kind } => self.execute_raise(vm, kind.get(arg)),
5541            Instruction::Resume { .. } | Instruction::ResumeCheck => {
5542                // Lazy quickening: initialize adaptive counters on first execution.
5543                // Read before the swap so that the steady state — every call after
5544                // the first — costs a load rather than a read-modify-write.
5545                if !self.code.quickened.load(atomic::Ordering::Relaxed)
5546                    && !self.code.quickened.swap(true, atomic::Ordering::Relaxed)
5547                {
5548                    self.code.instructions.quicken();
5549                    atomic::fence(atomic::Ordering::Release);
5550                }
5551                // Check if bytecode needs re-instrumentation
5552                let global_ver = vm
5553                    .state
5554                    .instrumentation_version
5555                    .load(atomic::Ordering::Acquire);
5556                let code_ver = self
5557                    .code
5558                    .instrumentation_version
5559                    .load(atomic::Ordering::Acquire);
5560                if code_ver != global_ver {
5561                    let events = {
5562                        let state = vm.state.monitoring.lock();
5563                        state.events_for_code(self.code.get_id())
5564                    };
5565                    monitoring::instrument_code(self.code, events);
5566                    self.code
5567                        .instrumentation_version
5568                        .store(global_ver, atomic::Ordering::Release);
5569                    // Re-execute this instruction (it may now be INSTRUMENTED_RESUME)
5570                    self.update_lasti(|i| *i -= 1);
5571                } else {
5572                    self.trace_call_from_resume(vm, u32::from(arg))?;
5573                }
5574                Ok(None)
5575            }
5576            Instruction::ReturnValue => {
5577                let value = self.pop_value();
5578                if vm.use_tracing.get() {
5579                    vm.trace_event_what(
5580                        crate::protocol::TraceEvent::Return,
5581                        monitoring::MonitoringEvent::PyReturn,
5582                        Some(value.clone()),
5583                    )?;
5584                }
5585                self.unwind_blocks(vm, UnwindReason::Returning { value })
5586            }
5587            Instruction::SetAdd { i } => {
5588                let item = self.pop_value();
5589                let obj = self.nth_value(i.get(arg) - 1);
5590                let set: &Py<PySet> = unsafe {
5591                    // SAFETY: trust compiler
5592                    obj.downcast_unchecked_ref()
5593                };
5594                set.add(item, vm)?;
5595                Ok(None)
5596            }
5597            Instruction::SetUpdate { i } => {
5598                let iterable = self.pop_value();
5599                let obj = self.nth_value(i.get(arg) - 1);
5600                let set: &Py<PySet> = unsafe {
5601                    // SAFETY: compiler guarantees correct type
5602                    obj.downcast_unchecked_ref()
5603                };
5604                let iter = PyIter::try_from_object(vm, iterable)?;
5605                while let PyIterReturn::Return(item) = iter.next(vm)? {
5606                    set.add(item, vm)?;
5607                }
5608                Ok(None)
5609            }
5610            Instruction::PushExcInfo => {
5611                // Stack: [exc] -> [prev_exc, exc]
5612                let exc = self.pop_value();
5613                let prev_exc = vm
5614                    .current_exception()
5615                    .map_or_else(|| vm.ctx.none(), |e| e.into());
5616
5617                // Set exc as the current exception
5618                if let Some(exc_ref) = exc.downcast_ref::<PyBaseException>() {
5619                    vm.set_exception(Some(exc_ref.to_owned()));
5620                }
5621
5622                self.push_value(prev_exc);
5623                self.push_value(exc);
5624                Ok(None)
5625            }
5626            Instruction::CheckExcMatch => {
5627                // Stack: [exc, type] -> [exc, bool]
5628                let exc_type = self.pop_value();
5629                let exc = self.top_value();
5630
5631                // Validate that exc_type inherits from BaseException
5632                if let Some(tuple_of_exceptions) = exc_type.downcast_ref::<PyTuple>() {
5633                    for exception in tuple_of_exceptions {
5634                        if !exception
5635                            .is_subclass(vm.ctx.exceptions.base_exception_type.into(), vm)?
5636                        {
5637                            return Err(vm.new_type_error(
5638                                "catching classes that do not inherit from BaseException is not allowed",
5639                            ));
5640                        }
5641                    }
5642                } else if !exc_type.is_subclass(vm.ctx.exceptions.base_exception_type.into(), vm)? {
5643                    return Err(vm.new_type_error(
5644                        "catching classes that do not inherit from BaseException is not allowed",
5645                    ));
5646                }
5647
5648                let result = exc.is_instance(&exc_type, vm)?;
5649                self.push_value(vm.ctx.new_bool(result).into());
5650                Ok(None)
5651            }
5652            Instruction::Reraise { depth } => {
5653                // inst(RERAISE, (values[oparg], exc -- values[oparg]))
5654                //
5655                // Pops only `exc`. When oparg != 0, values[0] is the lasti
5656                // pushed by the exception table; restore it before unwind so
5657                // a later handler that also pushes lasti records the original
5658                // raise.
5659                let oparg = depth.get(arg);
5660                let exc = self.pop_value();
5661                self.restore_reraise_lasti(oparg);
5662
5663                if let Some(exc_ref) = exc.downcast_ref::<PyBaseException>() {
5664                    Err(exc_ref.to_owned())
5665                } else {
5666                    // Fallback: use current exception if TOS is not an exception
5667                    let exc = vm
5668                        .topmost_exception()
5669                        .ok_or_else(|| vm.new_runtime_error("No active exception to re-raise"))?;
5670                    Err(exc)
5671                }
5672            }
5673            Instruction::SetFunctionAttribute { flag: attr } => {
5674                self.execute_set_function_attribute(vm, attr.get(arg))
5675            }
5676            Instruction::SetupAnnotations => self.setup_annotations(vm),
5677            Instruction::StoreAttr { namei: idx } => {
5678                let idx_val = idx.get(arg);
5679                self.adaptive(|s, ii, cb| s.specialize_store_attr(vm, idx_val, ii, cb));
5680                self.store_attr(vm, idx_val)
5681            }
5682            Instruction::StoreDeref { i } => {
5683                let value = self.pop_value();
5684                self.cell_ref(i.get(arg).as_usize()).set(Some(value));
5685                Ok(None)
5686            }
5687            Instruction::StoreFast { var_num } => {
5688                // pop_value_opt: allows NULL from LoadFastAndClear restore path
5689                let value = self.pop_value_opt();
5690                let idx = var_num.get(arg);
5691                self.localsplus.debug_audit_local_release(idx.as_usize());
5692                let fastlocals = self.localsplus.fastlocals_mut();
5693                fastlocals[idx] = value;
5694                Ok(None)
5695            }
5696            Instruction::StoreFastLoadFast { var_nums } => {
5697                // pop_value_opt: allows NULL from LoadFastAndClear restore paths.
5698                let value = self.pop_value_opt();
5699                let oparg = var_nums.get(arg);
5700                let (store_idx, load_idx) = oparg.indexes();
5701                self.localsplus
5702                    .debug_audit_local_release(store_idx.as_usize());
5703                let load_value = {
5704                    let locals = self.localsplus.fastlocals_mut();
5705                    locals[store_idx] = value;
5706                    locals[load_idx].clone()
5707                };
5708                self.push_value_opt(load_value);
5709                Ok(None)
5710            }
5711            Instruction::StoreFastStoreFast { var_nums } => {
5712                let oparg = var_nums.get(arg);
5713                let (idx1, idx2) = oparg.indexes();
5714                // pop_value_opt: allows NULL from LoadFastAndClear restore path
5715                let value1 = self.pop_value_opt();
5716                let value2 = self.pop_value_opt();
5717                self.localsplus.debug_audit_local_release(idx1.as_usize());
5718                self.localsplus.debug_audit_local_release(idx2.as_usize());
5719                let fastlocals = self.localsplus.fastlocals_mut();
5720                fastlocals[idx1] = value1;
5721                fastlocals[idx2] = value2;
5722                Ok(None)
5723            }
5724            Instruction::StoreGlobal { namei: idx } => {
5725                let value = self.pop_value();
5726                self.globals
5727                    .set_item(self.code.names[idx.get(arg) as usize], value, vm)?;
5728                Ok(None)
5729            }
5730            Instruction::StoreName { namei: idx } => {
5731                let name = self.code.names[idx.get(arg) as usize];
5732                let value = self.pop_value();
5733                self.locals
5734                    .mapping(vm)
5735                    .ass_subscript(name, Some(value), vm)?;
5736                Ok(None)
5737            }
5738            Instruction::StoreSlice => {
5739                // Stack: [value, container, start, stop] -> []
5740                let stop = self.pop_value();
5741                let start = self.pop_value();
5742                let container = self.pop_value();
5743                let value = self.pop_value();
5744                let slice: PyObjectRef = PySlice {
5745                    start: Some(start),
5746                    stop,
5747                    step: None,
5748                }
5749                .into_ref(&vm.ctx)
5750                .into();
5751                container.set_item(&*slice, value, vm)?;
5752                Ok(None)
5753            }
5754            Instruction::StoreSubscr => {
5755                self.adaptive(|s, ii, cb| s.specialize_store_subscr(vm, ii, cb));
5756                self.execute_store_subscript(vm)
5757            }
5758            Instruction::Swap { i: index } => {
5759                let len = self.localsplus.stack_len();
5760                debug_assert!(len > 0, "stack underflow in SWAP");
5761                let i = len - 1; // TOS index
5762                let index_val = index.get(arg) as usize;
5763                // CPython: SWAP(n) swaps TOS with PEEK(n) where PEEK(n) = stack_pointer[-n]
5764                // This means swap TOS with the element at index (len - n)
5765                debug_assert!(
5766                    index_val <= len,
5767                    "SWAP index {index_val} exceeds stack size {len}"
5768                );
5769                let j = len - index_val;
5770                self.localsplus.stack_swap(i, j);
5771                Ok(None)
5772            }
5773            Instruction::ToBool => {
5774                self.adaptive(|s, ii, cb| s.specialize_to_bool(vm, ii, cb));
5775                let obj = self.pop_stackref();
5776                let bool_val = obj.try_to_bool(vm)?;
5777                self.push_bool_or_fused_jump(instruction.cache_entries(), bool_val, vm);
5778                Ok(None)
5779            }
5780            Instruction::UnpackEx { counts: args } => {
5781                let args = args.get(arg);
5782                self.execute_unpack_ex(vm, args.before, args.after)
5783            }
5784            Instruction::UnpackSequence { count: size } => {
5785                let expected = size.get(arg);
5786                self.adaptive(|s, ii, cb| s.specialize_unpack_sequence(vm, expected, ii, cb));
5787                self.unpack_sequence(expected, vm)
5788            }
5789            Instruction::WithExceptStart => {
5790                // Stack: [..., exit_func, self_or_null, lasti, prev_exc, exc]
5791                // exit_func at TOS-4, self_or_null at TOS-3
5792                let exc = vm.current_exception();
5793
5794                let stack_len = self.localsplus.stack_len();
5795                let exit_func = expect_unchecked(
5796                    self.localsplus.stack_index(stack_len - 5),
5797                    "WithExceptStart: exit_func is NULL",
5798                );
5799                let self_or_null = self.localsplus.stack_index(stack_len - 4);
5800
5801                let (tp, val, tb) = if let Some(ref exc) = exc {
5802                    vm.split_exception(exc.clone())
5803                } else {
5804                    (vm.ctx.none(), vm.ctx.none(), vm.ctx.none())
5805                };
5806
5807                let exit_res = if let Some(self_exit) = self_or_null {
5808                    exit_func.call((self_exit.clone().to_pyobj(), tp, val, tb), vm)?
5809                } else {
5810                    exit_func.call((tp, val, tb), vm)?
5811                };
5812                self.push_value(exit_res);
5813
5814                Ok(None)
5815            }
5816            Instruction::YieldValue { .. } => {
5817                // The frame outlives this block from here on, so nothing it
5818                // still holds may be a borrow of something else's slot.
5819                self.localsplus.promote_stack();
5820                debug_assert!(
5821                    self.localsplus
5822                        .stack_as_slice()
5823                        .iter()
5824                        .flatten()
5825                        .all(|sr| !sr.is_borrowed()),
5826                    "borrowed refs on stack at yield point"
5827                );
5828                // _YIELD_VALUE_EVENT: if f_lineno jumped, DISPATCH to the
5829                // new instruction instead of suspending.
5830                if self.yield_value_event(vm)? {
5831                    return Ok(None);
5832                }
5833                Ok(Some(ExecutionResult::Yield(self.pop_value())))
5834            }
5835            Instruction::Send { .. } => {
5836                // (receiver, v -- receiver, retval)
5837                self.adaptive(|s, ii, cb| s.specialize_send(vm, ii, cb));
5838                let exit_label = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
5839                let receiver = self.nth_value(1);
5840                let can_fast_send = !self.specialization_eval_frame_active(vm)
5841                    && (receiver.downcast_ref_if_exact::<PyGenerator>(vm).is_some()
5842                        || receiver.downcast_ref_if_exact::<PyCoroutine>(vm).is_some())
5843                    && self
5844                        .builtin_coro(receiver)
5845                        .is_some_and(|coro| !coro.running() && !coro.closed());
5846                let val = self.pop_value();
5847                let receiver = self.top_value();
5848                let ret = if can_fast_send {
5849                    let coro = self.builtin_coro(receiver).unwrap();
5850                    if vm.is_none(&val) {
5851                        coro.send_none(receiver, vm)?
5852                    } else {
5853                        coro.send(receiver, val, vm)?
5854                    }
5855                } else {
5856                    self._send(receiver, val, vm)?
5857                };
5858                match ret {
5859                    PyIterReturn::Return(value) => {
5860                        self.push_value(value);
5861                        Ok(None)
5862                    }
5863                    PyIterReturn::StopIteration(value) => {
5864                        if vm.use_tracing.get() && self.trace_is_set(vm) {
5865                            let stop_exc = vm.new_stop_iteration(value.clone());
5866                            self.fire_exception_trace(&stop_exc, vm)?;
5867                        }
5868                        let value = vm.unwrap_or_none(value);
5869                        self.push_value(value);
5870                        self.jump(exit_label);
5871                        Ok(None)
5872                    }
5873                }
5874            }
5875            Instruction::SendGen => {
5876                let exit_label = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
5877                // Stack: [receiver, val] — peek receiver before popping
5878                let receiver = self.nth_value(1);
5879                let mut started = false;
5880                let can_fast_send = !self.specialization_eval_frame_active(vm)
5881                    && (receiver.downcast_ref_if_exact::<PyGenerator>(vm).is_some()
5882                        || receiver.downcast_ref_if_exact::<PyCoroutine>(vm).is_some())
5883                    && self.builtin_coro(receiver).is_some_and(|coro| {
5884                        started = coro.started();
5885                        !coro.running() && !coro.closed()
5886                    });
5887                let val = self.pop_value();
5888
5889                if can_fast_send {
5890                    let receiver = self.top_value();
5891                    // Hand an already-suspended generator to the trampoline,
5892                    // which runs its frame in this same eval loop and re-yields
5893                    // for this frame — no Rust frame and no instruction per
5894                    // level of a `yield from` / `await` chain.
5895                    if self.flatten != Flatten::Nothing
5896                        && started
5897                        && let Some(cont) = self.send_yield_from_cont(receiver, exit_label)
5898                    {
5899                        vm.set_pending_gen_resume(receiver.to_owned(), val, cont);
5900                        return Ok(Some(ExecutionResult::GenResume));
5901                    }
5902                    let coro = self.builtin_coro(receiver).unwrap();
5903                    let ret = if vm.is_none(&val) {
5904                        coro.send_none(receiver, vm)?
5905                    } else {
5906                        coro.send(receiver, val, vm)?
5907                    };
5908                    match ret {
5909                        PyIterReturn::Return(value) => {
5910                            self.push_value(value);
5911                            return Ok(None);
5912                        }
5913                        PyIterReturn::StopIteration(value) => {
5914                            if vm.use_tracing.get() && self.trace_is_set(vm) {
5915                                let stop_exc = vm.new_stop_iteration(value.clone());
5916                                self.fire_exception_trace(&stop_exc, vm)?;
5917                            }
5918                            let value = vm.unwrap_or_none(value);
5919                            self.push_value(value);
5920                            self.jump(exit_label);
5921                            return Ok(None);
5922                        }
5923                    }
5924                }
5925                let receiver = self.top_value();
5926                match self._send(receiver, val, vm)? {
5927                    PyIterReturn::Return(value) => {
5928                        self.push_value(value);
5929                        Ok(None)
5930                    }
5931                    PyIterReturn::StopIteration(value) => {
5932                        if vm.use_tracing.get() && self.trace_is_set(vm) {
5933                            let stop_exc = vm.new_stop_iteration(value.clone());
5934                            self.fire_exception_trace(&stop_exc, vm)?;
5935                        }
5936                        let value = vm.unwrap_or_none(value);
5937                        self.push_value(value);
5938                        self.jump(exit_label);
5939                        Ok(None)
5940                    }
5941                }
5942            }
5943            Instruction::EndSend => {
5944                // Stack: (receiver, value) -> (value)
5945                // Pops receiver, leaves value
5946                let value = self.pop_value();
5947                self.pop_stackref(); // discard receiver
5948                self.push_value(value);
5949                Ok(None)
5950            }
5951            Instruction::ExitInitCheck => {
5952                // Check that __init__ returned None
5953                let should_be_none = self.pop_value();
5954                if !vm.is_none(&should_be_none) {
5955                    return Err(vm.new_type_error(format!(
5956                        "__init__() should return None, not '{:.200}'",
5957                        should_be_none.class().name()
5958                    )));
5959                }
5960                Ok(None)
5961            }
5962            Instruction::CleanupThrow => {
5963                // CLEANUP_THROW: (sub_iter, last_sent_val, exc) -> (None, value) OR re-raise
5964                // If StopIteration: pop all 3, extract value, push (None, value)
5965                // Otherwise: pop all 3, return Err(exc) for unwind_blocks to handle
5966                //
5967                // Unlike CPython where exception_unwind pops the triple as part of
5968                // stack cleanup to handler depth, RustPython pops here explicitly
5969                // and lets unwind_blocks find outer handlers.
5970                // Compiler sets handler_depth = base + 2 (before exc is pushed).
5971
5972                // First peek at exc_value (top of stack) without popping
5973                let exc = self.top_value();
5974
5975                // Check if it's a StopIteration
5976                if let Some(exc_ref) = exc.downcast_ref::<PyBaseException>()
5977                    && exc_ref.fast_isinstance(vm.ctx.exceptions.stop_iteration)
5978                {
5979                    // Extract value from StopIteration
5980                    let value = exc_ref.get_arg(0).unwrap_or_else(|| vm.ctx.none());
5981                    // Now pop all three
5982                    self.pop_stackref(); // exc
5983                    self.pop_stackref(); // last_sent_val
5984                    self.pop_stackref(); // sub_iter
5985                    self.push_value(vm.ctx.none());
5986                    self.push_value(value);
5987                    return Ok(None);
5988                }
5989
5990                // Re-raise other exceptions: pop all three and return Err(exc)
5991                let exc = self.pop_value(); // exc
5992                self.pop_stackref(); // last_sent_val
5993                self.pop_stackref(); // sub_iter
5994
5995                let exc = exc
5996                    .downcast::<PyBaseException>()
5997                    .map_err(|_| vm.new_type_error("exception expected"))?;
5998                Err(exc)
5999            }
6000            Instruction::UnaryInvert => {
6001                let a = self.pop_value();
6002                let value = vm._invert(&a)?;
6003                self.push_value(value);
6004                Ok(None)
6005            }
6006            Instruction::UnaryNegative => {
6007                let a = self.pop_value();
6008                let value = vm._neg(&a)?;
6009                self.push_value(value);
6010                Ok(None)
6011            }
6012            Instruction::UnaryNot => {
6013                let obj = self.pop_value();
6014                let value = obj.try_to_bool(vm)?;
6015                self.push_value(vm.ctx.new_bool(!value).into());
6016                Ok(None)
6017            }
6018            // Specialized LOAD_ATTR opcodes
6019            Instruction::LoadAttrMethodNoDict => {
6020                let oparg = LoadAttr::from_u32(u32::from(arg));
6021                let cache_base = self.lasti() as usize;
6022
6023                let owner = self.top_value();
6024                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6025
6026                if type_version != 0
6027                    && owner.class().tp_version_tag().load(Acquire) == type_version
6028                    && let Some(func) = self.try_read_cached_descriptor(cache_base, type_version)
6029                {
6030                    let owner = self.pop_stackref();
6031                    self.push_value(func);
6032                    self.push_stackref_opt(Some(owner));
6033                    Ok(None)
6034                } else {
6035                    self.load_attr_slow(vm, oparg)
6036                }
6037            }
6038            Instruction::LoadAttrMethodLazyDict => {
6039                let oparg = LoadAttr::from_u32(u32::from(arg));
6040                let cache_base = self.lasti() as usize;
6041
6042                let owner = self.top_value();
6043                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6044
6045                if type_version != 0
6046                    && owner.class().tp_version_tag().load(Acquire) == type_version
6047                    && !owner.has_instance_dict()
6048                    && let Some(func) = self.try_read_cached_descriptor(cache_base, type_version)
6049                {
6050                    let owner = self.pop_stackref();
6051                    self.push_value(func);
6052                    self.push_stackref_opt(Some(owner));
6053                    Ok(None)
6054                } else {
6055                    self.load_attr_slow(vm, oparg)
6056                }
6057            }
6058            Instruction::LoadAttrMethodWithValues => {
6059                let oparg = LoadAttr::from_u32(u32::from(arg));
6060                let cache_base = self.lasti() as usize;
6061                let attr_name = self.code.names[oparg.name_idx() as usize];
6062
6063                let owner = self.top_value();
6064                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6065
6066                if type_version != 0 && owner.class().tp_version_tag().load(Acquire) == type_version
6067                {
6068                    // Check instance dict doesn't shadow the method.
6069                    let shadowed = match self.shadowing_instance_attr(cache_base, attr_name, vm) {
6070                        Ok(shadowed) => shadowed.is_some(),
6071                        // Dict lookup error -> use safe path.
6072                        Err(_) => return self.load_attr_slow(vm, oparg),
6073                    };
6074
6075                    if !shadowed
6076                        && let Some(func) =
6077                            self.try_read_cached_descriptor(cache_base, type_version)
6078                    {
6079                        let owner = self.pop_stackref();
6080                        self.push_value(func);
6081                        self.push_stackref_opt(Some(owner));
6082                        return Ok(None);
6083                    }
6084                }
6085                self.load_attr_slow(vm, oparg)
6086            }
6087            Instruction::LoadAttrInstanceValue => {
6088                let oparg = LoadAttr::from_u32(u32::from(arg));
6089                let cache_base = self.lasti() as usize;
6090                let attr_name = self.code.names[oparg.name_idx() as usize];
6091
6092                let owner = self.top_value();
6093                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6094
6095                if type_version != 0 && owner.class().tp_version_tag().load(Acquire) == type_version
6096                {
6097                    // Type version matches — no data descriptor for this attr.
6098                    // Try direct dict lookup, skipping full descriptor protocol.
6099                    if let Some(dict) = owner.dict()
6100                        && let Some(value) = dict.get_item_opt(attr_name, vm)?
6101                    {
6102                        self.pop_stackref();
6103                        self.push_value(value);
6104                        return Ok(None);
6105                    }
6106                    // Not in instance dict — fall through to class lookup via slow path
6107                }
6108                self.load_attr_slow(vm, oparg)
6109            }
6110            Instruction::LoadAttrWithHint => {
6111                let oparg = LoadAttr::from_u32(u32::from(arg));
6112                let cache_base = self.lasti() as usize;
6113                let attr_name = self.code.names[oparg.name_idx() as usize];
6114
6115                let owner = self.top_value();
6116                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6117
6118                if type_version != 0
6119                    && owner.class().tp_version_tag().load(Acquire) == type_version
6120                    && let Some(dict) = owner.dict()
6121                {
6122                    // Try the cached entry index first; a hit is an identity
6123                    // check on the entry key instead of a hash probe.
6124                    let hint = self.code.instructions.read_cache_u16(cache_base + 3);
6125                    if let Some((value, refreshed)) =
6126                        dict.get_item_opt_refresh_hint(attr_name, hint, vm)?
6127                    {
6128                        if let Some(new_hint) = refreshed {
6129                            unsafe {
6130                                self.code
6131                                    .instructions
6132                                    .write_cache_u16(cache_base + 3, new_hint);
6133                            }
6134                        }
6135                        self.pop_stackref();
6136                        if oparg.is_method() {
6137                            self.push_value(value);
6138                            self.push_value_opt(None);
6139                        } else {
6140                            self.push_value(value);
6141                        }
6142                        return Ok(None);
6143                    }
6144                }
6145
6146                self.load_attr_slow(vm, oparg)
6147            }
6148            Instruction::LoadAttrModule => {
6149                let oparg = LoadAttr::from_u32(u32::from(arg));
6150                let cache_base = self.lasti() as usize;
6151                let attr_name = self.code.names[oparg.name_idx() as usize];
6152                let owner = self.top_value();
6153                let type_version = self.code.instructions.read_cache_ptr(cache_base + 6);
6154                let keys_version = self.code.instructions.read_cache_ptr(cache_base + 7);
6155                let index = self.code.instructions.read_cache_ptr(cache_base + 8);
6156
6157                if type_version != 0
6158                    && keys_version != 0
6159                    && owner.class().tp_version_tag().load(Acquire) as usize == type_version
6160                    && let Some(module) = owner.downcast_ref_if_exact::<PyModule>(vm)
6161                    && let Some(value) =
6162                        module
6163                            .dict()
6164                            .get_cached_module_attr(attr_name, keys_version, index, vm)
6165                {
6166                    self.pop_stackref();
6167                    if oparg.is_method() {
6168                        self.push_value(value);
6169                        self.push_value_opt(None);
6170                    } else {
6171                        self.push_value(value);
6172                    }
6173                    return Ok(None);
6174                }
6175                self.load_attr_slow(vm, oparg)
6176            }
6177            Instruction::LoadAttrNondescriptorNoDict => {
6178                let oparg = LoadAttr::from_u32(u32::from(arg));
6179                let cache_base = self.lasti() as usize;
6180
6181                let owner = self.top_value();
6182                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6183
6184                if type_version != 0
6185                    && owner.class().tp_version_tag().load(Acquire) == type_version
6186                    && let Some(attr) = self.try_read_cached_descriptor(cache_base, type_version)
6187                {
6188                    self.pop_stackref();
6189                    if oparg.is_method() {
6190                        self.push_value(attr);
6191                        self.push_value_opt(None);
6192                    } else {
6193                        self.push_value(attr);
6194                    }
6195                    return Ok(None);
6196                }
6197                self.load_attr_slow(vm, oparg)
6198            }
6199            Instruction::LoadAttrNondescriptorWithValues => {
6200                let oparg = LoadAttr::from_u32(u32::from(arg));
6201                let cache_base = self.lasti() as usize;
6202                let attr_name = self.code.names[oparg.name_idx() as usize];
6203
6204                let owner = self.top_value();
6205                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6206
6207                if type_version != 0 && owner.class().tp_version_tag().load(Acquire) == type_version
6208                {
6209                    // Instance dict has priority — check if attr is shadowed
6210                    if let Some(value) = self.shadowing_instance_attr(cache_base, attr_name, vm)? {
6211                        self.pop_stackref();
6212                        if oparg.is_method() {
6213                            self.push_value(value);
6214                            self.push_value_opt(None);
6215                        } else {
6216                            self.push_value(value);
6217                        }
6218                        return Ok(None);
6219                    }
6220                    // Not in instance dict — use cached class attr
6221                    let Some(attr) = self.try_read_cached_descriptor(cache_base, type_version)
6222                    else {
6223                        return self.load_attr_slow(vm, oparg);
6224                    };
6225                    self.pop_stackref();
6226                    if oparg.is_method() {
6227                        self.push_value(attr);
6228                        self.push_value_opt(None);
6229                    } else {
6230                        self.push_value(attr);
6231                    }
6232                    return Ok(None);
6233                }
6234                self.load_attr_slow(vm, oparg)
6235            }
6236            Instruction::LoadAttrClass => {
6237                let oparg = LoadAttr::from_u32(u32::from(arg));
6238                let cache_base = self.lasti() as usize;
6239
6240                let owner = self.top_value();
6241                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6242
6243                if type_version != 0
6244                    && let Some(owner_type) = owner.downcast_ref::<PyType>()
6245                    && owner_type.tp_version_tag.load(Acquire) == type_version
6246                    && let Some(attr) = self.try_read_cached_descriptor(cache_base, type_version)
6247                {
6248                    self.pop_stackref();
6249                    if oparg.is_method() {
6250                        self.push_value(attr);
6251                        self.push_value_opt(None);
6252                    } else {
6253                        self.push_value(attr);
6254                    }
6255                    return Ok(None);
6256                }
6257                self.load_attr_slow(vm, oparg)
6258            }
6259            Instruction::LoadAttrClassWithMetaclassCheck => {
6260                let oparg = LoadAttr::from_u32(u32::from(arg));
6261                let cache_base = self.lasti() as usize;
6262
6263                let owner = self.top_value();
6264                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6265                let metaclass_version = self.code.instructions.read_cache_u32(cache_base + 3);
6266
6267                if type_version != 0
6268                    && metaclass_version != 0
6269                    && let Some(owner_type) = owner.downcast_ref::<PyType>()
6270                    && owner_type.tp_version_tag.load(Acquire) == type_version
6271                    && owner.class().tp_version_tag().load(Acquire) == metaclass_version
6272                    && let Some(attr) = self.try_read_cached_descriptor(cache_base, type_version)
6273                {
6274                    self.pop_stackref();
6275                    if oparg.is_method() {
6276                        self.push_value(attr);
6277                        self.push_value_opt(None);
6278                    } else {
6279                        self.push_value(attr);
6280                    }
6281                    return Ok(None);
6282                }
6283                self.load_attr_slow(vm, oparg)
6284            }
6285            Instruction::LoadAttrGetattributeOverridden => {
6286                let oparg = LoadAttr::from_u32(u32::from(arg));
6287                let cache_base = self.lasti() as usize;
6288                let owner = self.top_value();
6289                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6290                let func_version = self.code.instructions.read_cache_u32(cache_base + 3);
6291
6292                if !oparg.is_method()
6293                    && !self.specialization_eval_frame_active(vm)
6294                    && type_version != 0
6295                    && func_version != 0
6296                    && owner.class().tp_version_tag().load(Acquire) == type_version
6297                    && let Some(func_obj) =
6298                        self.try_read_cached_descriptor(cache_base, type_version)
6299                    && let Some(func) = func_obj.downcast_ref_if_exact::<PyFunction>(vm)
6300                    && func.func_version() == func_version
6301                    && self.specialization_has_datastack_space_for_func(vm, func)
6302                {
6303                    debug_assert!(func.has_exact_argcount(2));
6304                    let owner = self.pop_value();
6305                    let attr_name = self.code.names[oparg.name_idx() as usize].to_owned().into();
6306                    let result =
6307                        func.invoke_exact_args_slots(&mut [Some(owner), Some(attr_name)], vm)?;
6308                    self.push_value(result);
6309                    return Ok(None);
6310                }
6311                self.load_attr_slow(vm, oparg)
6312            }
6313            Instruction::LoadAttrSlot => {
6314                let oparg = LoadAttr::from_u32(u32::from(arg));
6315                let cache_base = self.lasti() as usize;
6316
6317                let owner = self.top_value();
6318                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6319
6320                if type_version != 0 && owner.class().tp_version_tag().load(Acquire) == type_version
6321                {
6322                    let slot_offset =
6323                        self.code.instructions.read_cache_u32(cache_base + 3) as i32 as isize;
6324                    if let Some(value) = owner.get_slot(slot_offset) {
6325                        self.pop_stackref();
6326                        if oparg.is_method() {
6327                            self.push_value(value);
6328                            self.push_value_opt(None);
6329                        } else {
6330                            self.push_value(value);
6331                        }
6332                        return Ok(None);
6333                    }
6334                    // Slot is None → AttributeError (fall through to slow path)
6335                }
6336                self.load_attr_slow(vm, oparg)
6337            }
6338            Instruction::LoadAttrProperty => {
6339                let oparg = LoadAttr::from_u32(u32::from(arg));
6340                let cache_base = self.lasti() as usize;
6341
6342                let owner = self.top_value();
6343                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6344
6345                if type_version != 0
6346                    && !self.specialization_eval_frame_active(vm)
6347                    && owner.class().tp_version_tag().load(Acquire) == type_version
6348                    && let Some(fget_obj) =
6349                        self.try_read_cached_descriptor(cache_base, type_version)
6350                    && let Some(func) = fget_obj.downcast_ref_if_exact::<PyFunction>(vm)
6351                    && func.can_specialize_call(1)
6352                    && self.specialization_has_datastack_space_for_func(vm, func)
6353                {
6354                    let owner = self.pop_value();
6355                    let result = func.invoke_exact_args_slots(&mut [Some(owner)], vm)?;
6356                    self.push_value(result);
6357                    return Ok(None);
6358                }
6359                self.load_attr_slow(vm, oparg)
6360            }
6361            Instruction::StoreAttrInstanceValue => {
6362                let attr_idx = u32::from(arg);
6363                let instr_idx = self.lasti() as usize - 1;
6364                let cache_base = instr_idx + 1;
6365                let attr_name = self.code.names[attr_idx as usize];
6366                let owner = self.top_value();
6367                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6368
6369                if type_version != 0
6370                    && owner.class().tp_version_tag().load(Acquire) == type_version
6371                    && let Some(dict) = owner.dict()
6372                {
6373                    self.pop_stackref(); // owner
6374                    let value = self.pop_value();
6375                    // The key was absent at specialization time, but this
6376                    // very store inserts it; hint learning makes later
6377                    // executions replace by entry index.
6378                    self.store_attr_dict_hinted(&dict, attr_name, value, cache_base, vm)?;
6379                    return Ok(None);
6380                }
6381                self.store_attr(vm, attr_idx)
6382            }
6383            Instruction::StoreAttrWithHint => {
6384                let attr_idx = u32::from(arg);
6385                let instr_idx = self.lasti() as usize - 1;
6386                let cache_base = instr_idx + 1;
6387                let attr_name = self.code.names[attr_idx as usize];
6388                let owner = self.top_value();
6389                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6390
6391                if type_version != 0
6392                    && owner.class().tp_version_tag().load(Acquire) == type_version
6393                    && let Some(dict) = owner.dict()
6394                {
6395                    self.pop_stackref(); // owner
6396                    let value = self.pop_value();
6397                    self.store_attr_dict_hinted(&dict, attr_name, value, cache_base, vm)?;
6398                    return Ok(None);
6399                }
6400                self.store_attr(vm, attr_idx)
6401            }
6402            Instruction::StoreAttrSlot => {
6403                let instr_idx = self.lasti() as usize - 1;
6404                let cache_base = instr_idx + 1;
6405                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6406                let version_match = type_version != 0 && {
6407                    let owner = self.top_value();
6408                    owner.class().tp_version_tag().load(Acquire) == type_version
6409                };
6410
6411                if version_match {
6412                    let slot_offset =
6413                        self.code.instructions.read_cache_u16(cache_base + 3) as i16 as isize;
6414                    let owner = self.pop_value();
6415                    let value = self.pop_value();
6416                    owner.set_slot(slot_offset, Some(value));
6417                    return Ok(None);
6418                }
6419                let attr_idx = u32::from(arg);
6420                self.store_attr(vm, attr_idx)
6421            }
6422            Instruction::StoreSubscrListInt => {
6423                // Stack: [value, obj, idx] (TOS=idx, TOS1=obj, TOS2=value)
6424                let idx = self.pop_stackref();
6425                let obj = self.pop_stackref();
6426                let value = self.pop_value();
6427                if let Some(list) = obj.downcast_ref_if_exact::<PyList>(vm)
6428                    && let Some(int_idx) = idx.downcast_ref_if_exact::<PyInt>(vm)
6429                    && let Some(i) = specialization_nonnegative_compact_index(int_idx, vm)
6430                {
6431                    let mut vec = list.borrow_vec_mut();
6432                    if i < vec.len() {
6433                        vec[i] = value;
6434                        return Ok(None);
6435                    }
6436                }
6437                obj.set_item(idx.as_object(), value, vm)?;
6438                Ok(None)
6439            }
6440            Instruction::StoreSubscrDict => {
6441                // Stack: [value, obj, idx] (TOS=idx, TOS1=obj, TOS2=value)
6442                let idx = self.pop_stackref();
6443                let obj = self.pop_stackref();
6444                let value = self.pop_value();
6445                if let Some(dict) = obj.downcast_ref_if_exact::<PyDict>(vm) {
6446                    dict.set_item(idx.as_object(), value, vm)?;
6447                    Ok(None)
6448                } else {
6449                    obj.set_item(idx.as_object(), value, vm)?;
6450                    Ok(None)
6451                }
6452            }
6453            // Specialized BINARY_OP opcodes
6454            Instruction::BinaryOpAddInt => {
6455                self.execute_binary_op_int(vm, Self::int_add, bytecode::BinaryOperator::Add)
6456            }
6457            Instruction::BinaryOpSubtractInt => {
6458                self.execute_binary_op_int(vm, Self::int_sub, bytecode::BinaryOperator::Subtract)
6459            }
6460            Instruction::BinaryOpMultiplyInt => {
6461                self.execute_binary_op_int(vm, Self::int_mul, bytecode::BinaryOperator::Multiply)
6462            }
6463            Instruction::BinaryOpAddFloat => {
6464                self.execute_binary_op_float(vm, |a, b| a + b, bytecode::BinaryOperator::Add)
6465            }
6466            Instruction::BinaryOpSubtractFloat => {
6467                self.execute_binary_op_float(vm, |a, b| a - b, bytecode::BinaryOperator::Subtract)
6468            }
6469            Instruction::BinaryOpMultiplyFloat => {
6470                self.execute_binary_op_float(vm, |a, b| a * b, bytecode::BinaryOperator::Multiply)
6471            }
6472            Instruction::BinaryOpAddUnicode => {
6473                let b = self.top_value();
6474                let a = self.nth_value(1);
6475                if let (Some(a_str), Some(b_str)) = (
6476                    a.downcast_ref_if_exact::<PyStr>(vm),
6477                    b.downcast_ref_if_exact::<PyStr>(vm),
6478                ) {
6479                    let result = a_str.as_wtf8().py_add(b_str.as_wtf8());
6480                    self.pop_stackref();
6481                    self.pop_stackref();
6482                    self.push_value(result.to_pyobject(vm));
6483                    Ok(None)
6484                } else {
6485                    self.execute_bin_op(vm, bytecode::BinaryOperator::Add)
6486                }
6487            }
6488            Instruction::BinaryOpSubscrGetitem => {
6489                let cache_base = self.lasti() as usize;
6490                let type_version = self.code.instructions.read_cache_u32(cache_base + 1);
6491                let owner = self.nth_value(1);
6492                if !self.specialization_eval_frame_active(vm)
6493                    && type_version != 0
6494                    && owner.class().tp_version_tag().load(Acquire) == type_version
6495                    && let Some((func, func_version)) =
6496                        owner.class().get_cached_getitem_for_specialization()
6497                    && func.func_version() == func_version
6498                    && self.specialization_has_datastack_space_for_func(vm, &func)
6499                {
6500                    debug_assert!(func.has_exact_argcount(2));
6501                    let sub = self.pop_value();
6502                    let owner = self.pop_value();
6503                    let result = func.invoke_exact_args_slots(&mut [Some(owner), Some(sub)], vm)?;
6504                    self.push_value(result);
6505                    return Ok(None);
6506                }
6507                self.execute_bin_op(vm, bytecode::BinaryOperator::Subscr)
6508            }
6509            Instruction::BinaryOpExtend => {
6510                let op = self.binary_op_from_arg(arg);
6511                let b = self.top_value();
6512                let a = self.nth_value(1);
6513                let cache_base = self.lasti() as usize;
6514                if let Some(descr) = self.read_cached_binary_op_extend_descr(cache_base)
6515                    && descr.oparg == op
6516                    && (descr.guard)(a, b, vm)
6517                    && let Some(result) = (descr.action)(a, b, vm)
6518                {
6519                    self.pop_stackref();
6520                    self.pop_stackref();
6521                    self.push_value(result);
6522                    Ok(None)
6523                } else {
6524                    self.execute_bin_op(vm, op)
6525                }
6526            }
6527            Instruction::BinaryOpSubscrListInt => {
6528                let b = self.top_value();
6529                let a = self.nth_value(1);
6530                if let (Some(list), Some(idx)) = (
6531                    a.downcast_ref_if_exact::<PyList>(vm),
6532                    b.downcast_ref_if_exact::<PyInt>(vm),
6533                ) && let Some(i) = specialization_nonnegative_compact_index(idx, vm)
6534                {
6535                    let vec = list.borrow_vec();
6536                    if i < vec.len() {
6537                        let value = vec.do_get(i);
6538                        drop(vec);
6539                        self.pop_stackref();
6540                        self.pop_stackref();
6541                        self.push_value(value);
6542                        return Ok(None);
6543                    }
6544                }
6545                self.execute_bin_op(vm, bytecode::BinaryOperator::Subscr)
6546            }
6547            Instruction::BinaryOpSubscrTupleInt => {
6548                let b = self.top_value();
6549                let a = self.nth_value(1);
6550                if let (Some(tuple), Some(idx)) = (
6551                    a.downcast_ref_if_exact::<PyTuple>(vm),
6552                    b.downcast_ref_if_exact::<PyInt>(vm),
6553                ) && let Some(i) = specialization_nonnegative_compact_index(idx, vm)
6554                {
6555                    let elements = tuple.as_slice();
6556                    if i < elements.len() {
6557                        let value = elements[i].clone();
6558                        self.pop_stackref();
6559                        self.pop_stackref();
6560                        self.push_value(value);
6561                        return Ok(None);
6562                    }
6563                }
6564                self.execute_bin_op(vm, bytecode::BinaryOperator::Subscr)
6565            }
6566            Instruction::BinaryOpSubscrDict => {
6567                let b = self.top_value();
6568                let a = self.nth_value(1);
6569                if let Some(dict) = a.downcast_ref_if_exact::<PyDict>(vm) {
6570                    match dict.get_item_opt(b, vm) {
6571                        Ok(Some(value)) => {
6572                            self.pop_stackref();
6573                            self.pop_stackref();
6574                            self.push_value(value);
6575                            return Ok(None);
6576                        }
6577                        Ok(None) => {
6578                            let key = self.pop_value();
6579                            self.pop_stackref();
6580                            return Err(vm.new_key_error(key));
6581                        }
6582                        Err(e) => {
6583                            return Err(e);
6584                        }
6585                    }
6586                }
6587                self.execute_bin_op(vm, bytecode::BinaryOperator::Subscr)
6588            }
6589            Instruction::BinaryOpSubscrStrInt => {
6590                let b = self.top_value();
6591                let a = self.nth_value(1);
6592                if let (Some(a_str), Some(b_int)) = (
6593                    a.downcast_ref_if_exact::<PyStr>(vm),
6594                    b.downcast_ref_if_exact::<PyInt>(vm),
6595                ) && let Some(i) = specialization_nonnegative_compact_index(b_int, vm)
6596                    && let Ok(ch) = a_str.getitem_by_index(vm, i as isize)
6597                    && ch.is_ascii()
6598                {
6599                    let ascii_idx = ch.to_u32() as usize;
6600                    self.pop_stackref();
6601                    self.pop_stackref();
6602                    self.push_value(vm.ctx.ascii_char_cache[ascii_idx].clone().into());
6603                    return Ok(None);
6604                }
6605                self.execute_bin_op(vm, bytecode::BinaryOperator::Subscr)
6606            }
6607            Instruction::BinaryOpSubscrListSlice => {
6608                let b = self.top_value();
6609                let a = self.nth_value(1);
6610                if a.downcast_ref_if_exact::<PyList>(vm).is_some()
6611                    && b.downcast_ref::<PySlice>().is_some()
6612                {
6613                    let b_owned = self.pop_value();
6614                    let a_owned = self.pop_value();
6615                    let result = a_owned.get_item(b_owned.as_object(), vm)?;
6616                    self.push_value(result);
6617                    return Ok(None);
6618                }
6619                self.execute_bin_op(vm, bytecode::BinaryOperator::Subscr)
6620            }
6621            Instruction::CallPyExactArgs => {
6622                let instr_idx = self.lasti() as usize - 1;
6623                let cache_base = instr_idx + 1;
6624                let cached_version = self.code.instructions.read_cache_u32(cache_base + 1);
6625                let nargs: u32 = arg.into();
6626                if self.specialization_eval_frame_active(vm) {
6627                    return self.execute_call_vectorcall(nargs, vm);
6628                }
6629                // Stack: [callable, self_or_null, arg1, ..., argN]
6630                let stack_len = self.localsplus.stack_len();
6631                let self_or_null_is_some = self
6632                    .localsplus
6633                    .stack_index(stack_len - nargs as usize - 1)
6634                    .is_some();
6635                let callable = self.nth_value(nargs + 1);
6636                if let Some(func) = callable.downcast_ref_if_exact::<PyFunction>(vm)
6637                    && func.func_version() == cached_version
6638                    && cached_version != 0
6639                {
6640                    if func.is_jitted() {
6641                        return self.execute_call_vectorcall(nargs, vm);
6642                    }
6643                    let effective_nargs = nargs + u32::from(self_or_null_is_some);
6644                    if !func.has_exact_argcount(effective_nargs) {
6645                        return self.execute_call_vectorcall(nargs, vm);
6646                    }
6647                    if !self.specialization_has_datastack_space_for_func(vm, func) {
6648                        return self.execute_call_vectorcall(nargs, vm);
6649                    }
6650                    if self.specialization_call_recursion_guard(vm) {
6651                        return self.execute_call_vectorcall(nargs, vm);
6652                    }
6653                    if self.flatten == Flatten::CallAndGenResume && !func.is_generator_like() {
6654                        self.tailcall_prepare_frame(nargs, self_or_null_is_some, vm);
6655                        return Ok(Some(ExecutionResult::TailCall));
6656                    }
6657                    // Recursive path: pop args and call.
6658                    let base = usize::from(self_or_null_is_some);
6659                    let mut arg_buf = CallArgBuffer::new(nargs as usize + base);
6660                    let args = arg_buf.slots();
6661                    for (slot, arg) in args[base..]
6662                        .iter_mut()
6663                        .zip(self.pop_multiple(nargs as usize))
6664                    {
6665                        *slot = Some(arg);
6666                    }
6667                    let self_or_null = self.pop_value_opt();
6668                    debug_assert_eq!(self_or_null.is_some(), self_or_null_is_some);
6669                    if self_or_null.is_some() {
6670                        args[0] = self_or_null;
6671                    }
6672                    let callable = self.pop_value();
6673                    let func = callable.downcast_ref_if_exact::<PyFunction>(vm).unwrap();
6674                    let result = func.invoke_exact_args_slots(args, vm)?;
6675                    self.push_value(result);
6676                    Ok(None)
6677                } else {
6678                    self.execute_call_vectorcall(nargs, vm)
6679                }
6680            }
6681            Instruction::CallBoundMethodExactArgs => {
6682                let instr_idx = self.lasti() as usize - 1;
6683                let cache_base = instr_idx + 1;
6684                let cached_version = self.code.instructions.read_cache_u32(cache_base + 1);
6685                let nargs: u32 = arg.into();
6686                if self.specialization_eval_frame_active(vm) {
6687                    return self.execute_call_vectorcall(nargs, vm);
6688                }
6689                // Stack: [callable, self_or_null(NULL), arg1, ..., argN]
6690                let stack_len = self.localsplus.stack_len();
6691                let self_or_null_is_some = self
6692                    .localsplus
6693                    .stack_index(stack_len - nargs as usize - 1)
6694                    .is_some();
6695                let callable = self.nth_value(nargs + 1);
6696                if !self_or_null_is_some
6697                    && let Some(bound_method) = callable.downcast_ref_if_exact::<PyBoundMethod>(vm)
6698                {
6699                    let bound_function = bound_method.function_obj().to_owned();
6700                    let bound_self = bound_method.self_obj().to_owned();
6701                    if let Some(func) = bound_function.downcast_ref_if_exact::<PyFunction>(vm)
6702                        && func.func_version() == cached_version
6703                        && cached_version != 0
6704                    {
6705                        if func.is_jitted() {
6706                            return self.execute_call_vectorcall(nargs, vm);
6707                        }
6708                        if !func.has_exact_argcount(nargs + 1) {
6709                            return self.execute_call_vectorcall(nargs, vm);
6710                        }
6711                        if !self.specialization_has_datastack_space_for_func(vm, func) {
6712                            return self.execute_call_vectorcall(nargs, vm);
6713                        }
6714                        if self.specialization_call_recursion_guard(vm) {
6715                            return self.execute_call_vectorcall(nargs, vm);
6716                        }
6717                        if self.flatten == Flatten::CallAndGenResume && !func.is_generator_like() {
6718                            self.tailcall_prepare_bound_method_frame(
6719                                nargs,
6720                                bound_function,
6721                                bound_self,
6722                                vm,
6723                            );
6724                            return Ok(Some(ExecutionResult::TailCall));
6725                        }
6726                        // Recursive path: stage args without a per-call Vec.
6727                        // [bound_self, arg1, ..., argN]
6728                        let mut arg_buf = CallArgBuffer::new(nargs as usize + 1);
6729                        let args = arg_buf.slots();
6730                        for (slot, arg) in
6731                            args[1..].iter_mut().zip(self.pop_multiple(nargs as usize))
6732                        {
6733                            *slot = Some(arg);
6734                        }
6735                        self.pop_stackref_opt(); // null (self_or_null)
6736                        self.pop_stackref(); // callable (bound method)
6737                        args[0] = Some(bound_self);
6738                        let result = func.invoke_exact_args_slots(args, vm)?;
6739                        self.push_value(result);
6740                        return Ok(None);
6741                    }
6742                }
6743                self.execute_call_vectorcall(nargs, vm)
6744            }
6745            Instruction::CallLen => {
6746                let nargs: u32 = arg.into();
6747                if nargs == 1 {
6748                    // Stack: [callable, null, arg]
6749                    let obj = self.pop_value(); // arg
6750                    let null = self.pop_value_opt();
6751                    let callable = self.pop_value();
6752                    if null.is_none()
6753                        && vm
6754                            .callable_cache
6755                            .len
6756                            .as_ref()
6757                            .is_some_and(|len_callable| callable.is(len_callable))
6758                    {
6759                        let len = obj.length(vm)?;
6760                        self.push_value(vm.ctx.new_int(len).into());
6761                        return Ok(None);
6762                    }
6763                    // Guard failed — re-push and fallback
6764                    self.push_value(callable);
6765                    self.push_value_opt(null);
6766                    self.push_value(obj);
6767                }
6768                self.execute_call_vectorcall(nargs, vm)
6769            }
6770            Instruction::CallIsinstance => {
6771                let nargs: u32 = arg.into();
6772                let stack_len = self.localsplus.stack_len();
6773                let self_or_null_is_some = self
6774                    .localsplus
6775                    .stack_index(stack_len - nargs as usize - 1)
6776                    .is_some();
6777                let effective_nargs = nargs + u32::from(self_or_null_is_some);
6778                if effective_nargs == 2 {
6779                    let callable = self.nth_value(nargs + 1);
6780                    if vm
6781                        .callable_cache
6782                        .isinstance
6783                        .as_ref()
6784                        .is_some_and(|isinstance_callable| callable.is(isinstance_callable))
6785                    {
6786                        // Stack: [callable, self_or_null, args...]; effective_nargs == 2,
6787                        // so the instance is either the first positional arg or self_or_null.
6788                        let cls = self.pop_value();
6789                        let inst = if nargs == 2 {
6790                            let inst = self.pop_value();
6791                            self.pop_stackref_opt(); // null
6792                            inst
6793                        } else {
6794                            self.pop_value() // self_or_null holds the instance
6795                        };
6796                        self.pop_stackref(); // callable
6797                        let result = inst.is_instance(&cls, vm)?;
6798                        self.push_value(vm.ctx.new_bool(result).into());
6799                        return Ok(None);
6800                    }
6801                }
6802                self.execute_call_vectorcall(nargs, vm)
6803            }
6804            Instruction::CallType1 => {
6805                let nargs: u32 = arg.into();
6806                if nargs == 1 {
6807                    // Stack: [callable, null, arg]
6808                    let obj = self.pop_value();
6809                    let null = self.pop_value_opt();
6810                    let callable = self.pop_value();
6811                    if null.is_none() && callable.is(vm.ctx.types.type_type.as_object()) {
6812                        let tp = obj.class().to_owned().into();
6813                        self.push_value(tp);
6814                        return Ok(None);
6815                    }
6816                    // Guard failed — re-push and fallback
6817                    self.push_value(callable);
6818                    self.push_value_opt(null);
6819                    self.push_value(obj);
6820                }
6821                self.execute_call_vectorcall(nargs, vm)
6822            }
6823            Instruction::CallStr1 => {
6824                let nargs: u32 = arg.into();
6825                if nargs == 1 {
6826                    let obj = self.pop_value();
6827                    let null = self.pop_value_opt();
6828                    let callable = self.pop_value();
6829                    if null.is_none() && callable.is(vm.ctx.types.str_type.as_object()) {
6830                        let result = obj.str(vm)?;
6831                        self.push_value(result.into());
6832                        return Ok(None);
6833                    }
6834                    self.push_value(callable);
6835                    self.push_value_opt(null);
6836                    self.push_value(obj);
6837                }
6838                self.execute_call_vectorcall(nargs, vm)
6839            }
6840            Instruction::CallTuple1 => {
6841                let nargs: u32 = arg.into();
6842                if nargs == 1 {
6843                    let obj = self.pop_value();
6844                    let null = self.pop_value_opt();
6845                    let callable = self.pop_value();
6846                    if null.is_none() && callable.is(vm.ctx.types.tuple_type.as_object()) {
6847                        // tuple(x) returns x as-is when x is already an exact tuple
6848                        if let Ok(tuple) = obj.clone().downcast_exact::<PyTuple>(vm) {
6849                            self.push_value(tuple.into_pyref().into());
6850                        } else {
6851                            let elements: Vec<PyObjectRef> = vm.extract_elements_with(&obj, Ok)?;
6852                            self.push_value(vm.ctx.new_tuple(elements).into());
6853                        }
6854                        return Ok(None);
6855                    }
6856                    self.push_value(callable);
6857                    self.push_value_opt(null);
6858                    self.push_value(obj);
6859                }
6860                self.execute_call_vectorcall(nargs, vm)
6861            }
6862            Instruction::CallBuiltinO => {
6863                let nargs: u32 = arg.into();
6864                let stack_len = self.localsplus.stack_len();
6865                let self_or_null_is_some = self
6866                    .localsplus
6867                    .stack_index(stack_len - nargs as usize - 1)
6868                    .is_some();
6869                let effective_nargs = nargs + u32::from(self_or_null_is_some);
6870                let callable = self.nth_value(nargs + 1);
6871                if let Some(native) = callable.downcast_ref_if_exact::<PyNativeFunction>(vm) {
6872                    let call_conv = native.value.flags
6873                        & (PyMethodFlags::VARARGS
6874                            | PyMethodFlags::FASTCALL
6875                            | PyMethodFlags::NOARGS
6876                            | PyMethodFlags::O
6877                            | PyMethodFlags::KEYWORDS);
6878                    if call_conv == PyMethodFlags::O && effective_nargs == 1 {
6879                        let (callable, args_vec) = self.take_call_args(nargs as usize);
6880                        debug_assert_eq!(args_vec.len(), effective_nargs as usize);
6881                        let result =
6882                            callable.vectorcall(args_vec, effective_nargs as usize, None, vm)?;
6883                        self.push_value(result);
6884                        return Ok(None);
6885                    }
6886                }
6887                self.execute_call_vectorcall(nargs, vm)
6888            }
6889            Instruction::CallBuiltinFast => {
6890                let nargs: u32 = arg.into();
6891                let stack_len = self.localsplus.stack_len();
6892                let self_or_null_is_some = self
6893                    .localsplus
6894                    .stack_index(stack_len - nargs as usize - 1)
6895                    .is_some();
6896                let effective_nargs = nargs + u32::from(self_or_null_is_some);
6897                let callable = self.nth_value(nargs + 1);
6898                if let Some(native) = callable.downcast_ref_if_exact::<PyNativeFunction>(vm) {
6899                    let call_conv = native.value.flags
6900                        & (PyMethodFlags::VARARGS
6901                            | PyMethodFlags::FASTCALL
6902                            | PyMethodFlags::NOARGS
6903                            | PyMethodFlags::O
6904                            | PyMethodFlags::KEYWORDS);
6905                    if call_conv == PyMethodFlags::FASTCALL {
6906                        let (callable, args_vec) = self.take_call_args(nargs as usize);
6907                        debug_assert_eq!(args_vec.len(), effective_nargs as usize);
6908                        let result =
6909                            callable.vectorcall(args_vec, effective_nargs as usize, None, vm)?;
6910                        self.push_value(result);
6911                        return Ok(None);
6912                    }
6913                }
6914                self.execute_call_vectorcall(nargs, vm)
6915            }
6916            Instruction::CallPyGeneral => {
6917                let instr_idx = self.lasti() as usize - 1;
6918                let cache_base = instr_idx + 1;
6919                let cached_version = self.code.instructions.read_cache_u32(cache_base + 1);
6920                let nargs: u32 = arg.into();
6921                if self.specialization_eval_frame_active(vm) {
6922                    return self.execute_call_vectorcall(nargs, vm);
6923                }
6924                let callable = self.nth_value(nargs + 1);
6925                if let Some(func) = callable.downcast_ref_if_exact::<PyFunction>(vm)
6926                    && func.func_version() == cached_version
6927                    && cached_version != 0
6928                {
6929                    if func.is_jitted() {
6930                        return self.execute_call_vectorcall(nargs, vm);
6931                    }
6932                    if self.specialization_call_recursion_guard(vm) {
6933                        return self.execute_call_vectorcall(nargs, vm);
6934                    }
6935                    let (callable, args_vec) = self.take_call_args(nargs as usize);
6936                    let effective_nargs = args_vec.len();
6937                    let result =
6938                        vectorcall_function(&callable, args_vec, effective_nargs, None, vm)?;
6939                    self.push_value(result);
6940                    Ok(None)
6941                } else {
6942                    self.execute_call_vectorcall(nargs, vm)
6943                }
6944            }
6945            Instruction::CallBoundMethodGeneral => {
6946                let instr_idx = self.lasti() as usize - 1;
6947                let cache_base = instr_idx + 1;
6948                let cached_version = self.code.instructions.read_cache_u32(cache_base + 1);
6949                let nargs: u32 = arg.into();
6950                if self.specialization_eval_frame_active(vm) {
6951                    return self.execute_call_vectorcall(nargs, vm);
6952                }
6953                let stack_len = self.localsplus.stack_len();
6954                let self_or_null_is_some = self
6955                    .localsplus
6956                    .stack_index(stack_len - nargs as usize - 1)
6957                    .is_some();
6958                let callable = self.nth_value(nargs + 1);
6959                if !self_or_null_is_some
6960                    && let Some(bound_method) = callable.downcast_ref_if_exact::<PyBoundMethod>(vm)
6961                {
6962                    let bound_function = bound_method.function_obj().to_owned();
6963                    let bound_self = bound_method.self_obj().to_owned();
6964                    if let Some(func) = bound_function.downcast_ref_if_exact::<PyFunction>(vm)
6965                        && func.func_version() == cached_version
6966                        && cached_version != 0
6967                    {
6968                        if func.is_jitted() {
6969                            return self.execute_call_vectorcall(nargs, vm);
6970                        }
6971                        if self.specialization_call_recursion_guard(vm) {
6972                            return self.execute_call_vectorcall(nargs, vm);
6973                        }
6974                        let nargs_usize = nargs as usize;
6975                        let mut args_vec = Vec::with_capacity(nargs_usize + 1);
6976                        args_vec.push(bound_self);
6977                        args_vec.extend(self.pop_multiple(nargs_usize));
6978                        self.pop_stackref_opt(); // null (self_or_null)
6979                        self.pop_stackref(); // callable (bound method)
6980                        let result = vectorcall_function(
6981                            &bound_function,
6982                            args_vec,
6983                            nargs_usize + 1,
6984                            None,
6985                            vm,
6986                        )?;
6987                        self.push_value(result);
6988                        return Ok(None);
6989                    }
6990                }
6991                self.execute_call_vectorcall(nargs, vm)
6992            }
6993            Instruction::CallListAppend => {
6994                let nargs: u32 = arg.into();
6995                if nargs == 1 {
6996                    // Stack: [callable, self_or_null, item]
6997                    let stack_len = self.localsplus.stack_len();
6998                    let self_or_null_is_some = self.localsplus.stack_index(stack_len - 2).is_some();
6999                    let callable = self.nth_value(2);
7000                    let self_is_list = self
7001                        .localsplus
7002                        .stack_index(stack_len - 2)
7003                        .as_ref()
7004                        .is_some_and(|obj| obj.downcast_ref::<PyList>().is_some());
7005                    if vm
7006                        .callable_cache
7007                        .list_append
7008                        .as_ref()
7009                        .is_some_and(|list_append| callable.is(list_append))
7010                        && self_or_null_is_some
7011                        && self_is_list
7012                    {
7013                        let item = self.pop_value();
7014                        let self_or_null = self.pop_value_opt();
7015                        let callable = self.pop_value();
7016                        if let Some(list_obj) = self_or_null.as_ref()
7017                            && let Some(list) = list_obj.downcast_ref::<PyList>()
7018                        {
7019                            list.append(item);
7020                            // CALL_LIST_APPEND fuses the following POP_TOP.
7021                            self.jump_relative_forward(
7022                                1,
7023                                Instruction::CallListAppend.cache_entries() as u32,
7024                            );
7025                            return Ok(None);
7026                        }
7027                        self.push_value(callable);
7028                        self.push_value_opt(self_or_null);
7029                        self.push_value(item);
7030                    }
7031                }
7032                self.execute_call_vectorcall(nargs, vm)
7033            }
7034            Instruction::CallMethodDescriptorNoargs => {
7035                let nargs: u32 = arg.into();
7036                let stack_len = self.localsplus.stack_len();
7037                let self_or_null_is_some = self
7038                    .localsplus
7039                    .stack_index(stack_len - nargs as usize - 1)
7040                    .is_some();
7041                let total_nargs = nargs + u32::from(self_or_null_is_some);
7042                if total_nargs == 1 {
7043                    let callable = self.nth_value(nargs + 1);
7044                    let self_index =
7045                        stack_len - nargs as usize - 1 + usize::from(!self_or_null_is_some);
7046                    if let Some(descr) = callable.downcast_ref_if_exact::<PyMethodDescriptor>(vm)
7047                        && (descr.method.flags
7048                            & (PyMethodFlags::VARARGS
7049                                | PyMethodFlags::FASTCALL
7050                                | PyMethodFlags::NOARGS
7051                                | PyMethodFlags::O
7052                                | PyMethodFlags::KEYWORDS))
7053                            == PyMethodFlags::NOARGS
7054                        && self
7055                            .localsplus
7056                            .stack_index(self_index)
7057                            .as_ref()
7058                            .is_some_and(|self_obj| self_obj.class().is(descr.common.typ))
7059                    {
7060                        let func = descr.method.func;
7061                        let callee = Callee::named(descr.method.name).with_instance_arg(true);
7062                        let (_callable, all_args) = self.take_call_args(nargs as usize);
7063                        debug_assert_eq!(all_args.len(), total_nargs as usize);
7064                        let args = FuncArgs {
7065                            args: all_args,
7066                            kwargs: Default::default(),
7067                        };
7068                        let result = func(vm, args, callee)?;
7069                        self.push_value(result);
7070                        return Ok(None);
7071                    }
7072                }
7073                self.execute_call_vectorcall(nargs, vm)
7074            }
7075            Instruction::CallMethodDescriptorO => {
7076                let nargs: u32 = arg.into();
7077                let stack_len = self.localsplus.stack_len();
7078                let self_or_null_is_some = self
7079                    .localsplus
7080                    .stack_index(stack_len - nargs as usize - 1)
7081                    .is_some();
7082                let total_nargs = nargs + u32::from(self_or_null_is_some);
7083                if total_nargs == 2 {
7084                    let callable = self.nth_value(nargs + 1);
7085                    let self_index =
7086                        stack_len - nargs as usize - 1 + usize::from(!self_or_null_is_some);
7087                    if let Some(descr) = callable.downcast_ref_if_exact::<PyMethodDescriptor>(vm)
7088                        && (descr.method.flags
7089                            & (PyMethodFlags::VARARGS
7090                                | PyMethodFlags::FASTCALL
7091                                | PyMethodFlags::NOARGS
7092                                | PyMethodFlags::O
7093                                | PyMethodFlags::KEYWORDS))
7094                            == PyMethodFlags::O
7095                        && self
7096                            .localsplus
7097                            .stack_index(self_index)
7098                            .as_ref()
7099                            .is_some_and(|self_obj| self_obj.class().is(descr.common.typ))
7100                    {
7101                        let func = descr.method.func;
7102                        let callee = Callee::named(descr.method.name).with_instance_arg(true);
7103                        let (_callable, all_args) = self.take_call_args(nargs as usize);
7104                        debug_assert_eq!(all_args.len(), total_nargs as usize);
7105                        let args = FuncArgs {
7106                            args: all_args,
7107                            kwargs: Default::default(),
7108                        };
7109                        let result = func(vm, args, callee)?;
7110                        self.push_value(result);
7111                        return Ok(None);
7112                    }
7113                }
7114                self.execute_call_vectorcall(nargs, vm)
7115            }
7116            Instruction::CallMethodDescriptorFast => {
7117                let nargs: u32 = arg.into();
7118                let stack_len = self.localsplus.stack_len();
7119                let self_or_null_is_some = self
7120                    .localsplus
7121                    .stack_index(stack_len - nargs as usize - 1)
7122                    .is_some();
7123                let total_nargs = nargs + u32::from(self_or_null_is_some);
7124                let callable = self.nth_value(nargs + 1);
7125                let self_index =
7126                    stack_len - nargs as usize - 1 + usize::from(!self_or_null_is_some);
7127                if total_nargs > 0
7128                    && let Some(descr) = callable.downcast_ref_if_exact::<PyMethodDescriptor>(vm)
7129                    && (descr.method.flags
7130                        & (PyMethodFlags::VARARGS
7131                            | PyMethodFlags::FASTCALL
7132                            | PyMethodFlags::NOARGS
7133                            | PyMethodFlags::O
7134                            | PyMethodFlags::KEYWORDS))
7135                        == PyMethodFlags::FASTCALL
7136                    && self
7137                        .localsplus
7138                        .stack_index(self_index)
7139                        .as_ref()
7140                        .is_some_and(|self_obj| self_obj.class().is(descr.common.typ))
7141                {
7142                    let func = descr.method.func;
7143                    let callee = Callee::named(descr.method.name).with_instance_arg(true);
7144                    let (_callable, all_args) = self.take_call_args(nargs as usize);
7145                    debug_assert_eq!(all_args.len(), total_nargs as usize);
7146                    let args = FuncArgs {
7147                        args: all_args,
7148                        kwargs: Default::default(),
7149                    };
7150                    let result = func(vm, args, callee)?;
7151                    self.push_value(result);
7152                    return Ok(None);
7153                }
7154                self.execute_call_vectorcall(nargs, vm)
7155            }
7156            Instruction::CallBuiltinClass => {
7157                let nargs: u32 = arg.into();
7158                let callable = self.nth_value(nargs + 1);
7159                if let Some(cls) = callable.downcast_ref::<PyType>()
7160                    && cls.slots().vectorcall.load().is_some()
7161                {
7162                    let (callable, args_vec) = self.take_call_args(nargs as usize);
7163                    let effective_nargs = args_vec.len();
7164                    let result = callable.vectorcall(args_vec, effective_nargs, None, vm)?;
7165                    self.push_value(result);
7166                    return Ok(None);
7167                }
7168                self.execute_call_vectorcall(nargs, vm)
7169            }
7170            Instruction::CallAllocAndEnterInit => {
7171                let instr_idx = self.lasti() as usize - 1;
7172                let cache_base = instr_idx + 1;
7173                let cached_version = self.code.instructions.read_cache_u32(cache_base + 1);
7174                let nargs: u32 = arg.into();
7175                let callable = self.nth_value(nargs + 1);
7176                let stack_len = self.localsplus.stack_len();
7177                let self_or_null_is_some = self
7178                    .localsplus
7179                    .stack_index(stack_len - nargs as usize - 1)
7180                    .is_some();
7181                if !self.specialization_eval_frame_active(vm)
7182                    && !self_or_null_is_some
7183                    && cached_version != 0
7184                    && let Some(cls) = callable.downcast_ref::<PyType>()
7185                    && cls.tp_version_tag().load(Acquire) == cached_version
7186                    && let Some((init_func, init_func_version)) =
7187                        cls.get_cached_init_for_specialization(cached_version)
7188                    && init_func.func_version() == init_func_version
7189                    && init_func.has_exact_argcount(nargs + 1)
7190                    && let Some(cls_alloc) = cls.slots().alloc.load()
7191                {
7192                    // The specialization runs `__init__` directly with no
7193                    // interpreter-visible trampoline frame. Deopt when the
7194                    // datastack or recursion budget for the `__init__` frame is
7195                    // unavailable.
7196                    if !self.specialization_has_datastack_space_for_func(vm, &init_func) {
7197                        return self.execute_call_vectorcall(nargs, vm);
7198                    }
7199                    if self.specialization_call_recursion_guard(vm) {
7200                        return self.execute_call_vectorcall(nargs, vm);
7201                    }
7202                    // Allocate object directly (tp_new == object.__new__, tp_alloc == generic).
7203                    let cls_ref = cls.to_owned();
7204                    let new_obj = cls_alloc(cls_ref, 0, vm)?;
7205
7206                    // Stage args as [new_obj, arg1, ..., argN]; slot 0 is
7207                    // filled by the init runner.
7208                    let mut arg_buf = CallArgBuffer::new(nargs as usize + 1);
7209                    let args = arg_buf.slots();
7210                    for (slot, arg) in args[1..].iter_mut().zip(self.pop_multiple(nargs as usize)) {
7211                        *slot = Some(arg);
7212                    }
7213                    let _null = self.pop_value_opt(); // self_or_null (None)
7214                    let _callable = self.pop_value(); // callable (type)
7215                    let result = self.specialization_run_init(new_obj, &init_func, args, vm)?;
7216                    self.push_value(result);
7217                    return Ok(None);
7218                }
7219                self.execute_call_vectorcall(nargs, vm)
7220            }
7221            Instruction::CallMethodDescriptorFastWithKeywords => {
7222                // Native function interface is uniform regardless of keyword support
7223                let nargs: u32 = arg.into();
7224                let stack_len = self.localsplus.stack_len();
7225                let self_or_null_is_some = self
7226                    .localsplus
7227                    .stack_index(stack_len - nargs as usize - 1)
7228                    .is_some();
7229                let total_nargs = nargs + u32::from(self_or_null_is_some);
7230                let callable = self.nth_value(nargs + 1);
7231                let self_index =
7232                    stack_len - nargs as usize - 1 + usize::from(!self_or_null_is_some);
7233                if total_nargs > 0
7234                    && let Some(descr) = callable.downcast_ref_if_exact::<PyMethodDescriptor>(vm)
7235                    && (descr.method.flags
7236                        & (PyMethodFlags::VARARGS
7237                            | PyMethodFlags::FASTCALL
7238                            | PyMethodFlags::NOARGS
7239                            | PyMethodFlags::O
7240                            | PyMethodFlags::KEYWORDS))
7241                        == (PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS)
7242                    && self
7243                        .localsplus
7244                        .stack_index(self_index)
7245                        .as_ref()
7246                        .is_some_and(|self_obj| self_obj.class().is(descr.common.typ))
7247                {
7248                    let func = descr.method.func;
7249                    let callee = Callee::named(descr.method.name).with_instance_arg(true);
7250                    let (_callable, all_args) = self.take_call_args(nargs as usize);
7251                    debug_assert_eq!(all_args.len(), total_nargs as usize);
7252                    let args = FuncArgs {
7253                        args: all_args,
7254                        kwargs: Default::default(),
7255                    };
7256                    let result = func(vm, args, callee)?;
7257                    self.push_value(result);
7258                    return Ok(None);
7259                }
7260                self.execute_call_vectorcall(nargs, vm)
7261            }
7262            Instruction::CallBuiltinFastWithKeywords => {
7263                // Native function interface is uniform regardless of keyword support
7264                let nargs: u32 = arg.into();
7265                let stack_len = self.localsplus.stack_len();
7266                let self_or_null_is_some = self
7267                    .localsplus
7268                    .stack_index(stack_len - nargs as usize - 1)
7269                    .is_some();
7270                let effective_nargs = nargs + u32::from(self_or_null_is_some);
7271                let callable = self.nth_value(nargs + 1);
7272                if let Some(native) = callable.downcast_ref_if_exact::<PyNativeFunction>(vm) {
7273                    let call_conv = native.value.flags
7274                        & (PyMethodFlags::VARARGS
7275                            | PyMethodFlags::FASTCALL
7276                            | PyMethodFlags::NOARGS
7277                            | PyMethodFlags::O
7278                            | PyMethodFlags::KEYWORDS);
7279                    if call_conv == (PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS) {
7280                        let (callable, args_vec) = self.take_call_args(nargs as usize);
7281                        debug_assert_eq!(args_vec.len(), effective_nargs as usize);
7282                        let result =
7283                            callable.vectorcall(args_vec, effective_nargs as usize, None, vm)?;
7284                        self.push_value(result);
7285                        return Ok(None);
7286                    }
7287                }
7288                self.execute_call_vectorcall(nargs, vm)
7289            }
7290            Instruction::CallNonPyGeneral => {
7291                let nargs: u32 = arg.into();
7292                let stack_len = self.localsplus.stack_len();
7293                let self_or_null_is_some = self
7294                    .localsplus
7295                    .stack_index(stack_len - nargs as usize - 1)
7296                    .is_some();
7297                let callable = self.nth_value(nargs + 1);
7298                if callable.downcast_ref_if_exact::<PyFunction>(vm).is_some()
7299                    || callable
7300                        .downcast_ref_if_exact::<PyBoundMethod>(vm)
7301                        .is_some()
7302                {
7303                    return self.execute_call_vectorcall(nargs, vm);
7304                }
7305                let (callable, args_vec) = self.take_call_args(nargs as usize);
7306                debug_assert_eq!(
7307                    args_vec.len(),
7308                    nargs as usize + usize::from(self_or_null_is_some)
7309                );
7310                let effective_nargs = args_vec.len();
7311                let result = callable.vectorcall(args_vec, effective_nargs, None, vm)?;
7312                self.push_value(result);
7313                Ok(None)
7314            }
7315            Instruction::CallKwPy => {
7316                let instr_idx = self.lasti() as usize - 1;
7317                let cache_base = instr_idx + 1;
7318                let cached_version = self.code.instructions.read_cache_u32(cache_base + 1);
7319                let nargs: u32 = arg.into();
7320                if self.specialization_eval_frame_active(vm) {
7321                    return self.execute_call_kw_vectorcall(nargs, vm);
7322                }
7323                // Stack: [callable, self_or_null, arg1, ..., argN, kwarg_names]
7324                let callable = self.nth_value(nargs + 2);
7325                if let Some(func) = callable.downcast_ref_if_exact::<PyFunction>(vm)
7326                    && func.func_version() == cached_version
7327                    && cached_version != 0
7328                {
7329                    if func.is_jitted() {
7330                        return self.execute_call_kw_vectorcall(nargs, vm);
7331                    }
7332                    if self.specialization_call_recursion_guard(vm) {
7333                        return self.execute_call_kw_vectorcall(nargs, vm);
7334                    }
7335                    let nargs_usize = nargs as usize;
7336                    let kwarg_names_obj = self.pop_value();
7337                    let kwarg_names_tuple = kwarg_names_obj
7338                        .downcast_ref::<PyTuple>()
7339                        .expect("kwarg names should be tuple");
7340                    let kw_count = kwarg_names_tuple.as_slice().len();
7341                    let all_args: Vec<PyObjectRef> = self.pop_multiple(nargs_usize).collect();
7342                    let self_or_null = self.pop_value_opt();
7343                    let callable = self.pop_value();
7344                    let pos_count = nargs_usize - kw_count;
7345                    let (args_vec, effective_nargs) = if let Some(self_val) = self_or_null {
7346                        let mut v = Vec::with_capacity(nargs_usize + 1);
7347                        v.push(self_val);
7348                        v.extend(all_args);
7349                        (v, pos_count + 1)
7350                    } else {
7351                        (all_args, pos_count)
7352                    };
7353                    let kwnames = kwarg_names_tuple.as_slice();
7354                    let result = vectorcall_function(
7355                        &callable,
7356                        args_vec,
7357                        effective_nargs,
7358                        Some(kwnames),
7359                        vm,
7360                    )?;
7361                    self.push_value(result);
7362                    return Ok(None);
7363                }
7364                self.execute_call_kw_vectorcall(nargs, vm)
7365            }
7366            Instruction::CallKwBoundMethod => {
7367                let instr_idx = self.lasti() as usize - 1;
7368                let cache_base = instr_idx + 1;
7369                let cached_version = self.code.instructions.read_cache_u32(cache_base + 1);
7370                let nargs: u32 = arg.into();
7371                if self.specialization_eval_frame_active(vm) {
7372                    return self.execute_call_kw_vectorcall(nargs, vm);
7373                }
7374                // Stack: [callable, self_or_null, arg1, ..., argN, kwarg_names]
7375                let stack_len = self.localsplus.stack_len();
7376                let self_or_null_is_some = self
7377                    .localsplus
7378                    .stack_index(stack_len - nargs as usize - 2)
7379                    .is_some();
7380                let callable = self.nth_value(nargs + 2);
7381                if !self_or_null_is_some
7382                    && let Some(bound_method) = callable.downcast_ref_if_exact::<PyBoundMethod>(vm)
7383                {
7384                    let bound_function = bound_method.function_obj().to_owned();
7385                    let bound_self = bound_method.self_obj().to_owned();
7386                    if let Some(func) = bound_function.downcast_ref_if_exact::<PyFunction>(vm)
7387                        && func.func_version() == cached_version
7388                        && cached_version != 0
7389                    {
7390                        if func.is_jitted() {
7391                            return self.execute_call_kw_vectorcall(nargs, vm);
7392                        }
7393                        let nargs_usize = nargs as usize;
7394                        let kwarg_names_obj = self.pop_value();
7395                        let kwarg_names_tuple = kwarg_names_obj
7396                            .downcast_ref::<PyTuple>()
7397                            .expect("kwarg names should be tuple");
7398                        let kw_count = kwarg_names_tuple.as_slice().len();
7399                        let all_args: Vec<PyObjectRef> = self.pop_multiple(nargs_usize).collect();
7400                        self.pop_stackref_opt(); // null (self_or_null)
7401                        self.pop_stackref(); // callable (bound method)
7402                        let pos_count = nargs_usize - kw_count;
7403                        let mut args_vec = Vec::with_capacity(nargs_usize + 1);
7404                        args_vec.push(bound_self);
7405                        args_vec.extend(all_args);
7406                        let kwnames = kwarg_names_tuple.as_slice();
7407                        let result = vectorcall_function(
7408                            &bound_function,
7409                            args_vec,
7410                            pos_count + 1,
7411                            Some(kwnames),
7412                            vm,
7413                        )?;
7414                        self.push_value(result);
7415                        return Ok(None);
7416                    }
7417                }
7418                self.execute_call_kw_vectorcall(nargs, vm)
7419            }
7420            Instruction::CallKwNonPy => {
7421                let nargs: u32 = arg.into();
7422                let stack_len = self.localsplus.stack_len();
7423                let self_or_null_is_some = self
7424                    .localsplus
7425                    .stack_index(stack_len - nargs as usize - 2)
7426                    .is_some();
7427                let callable = self.nth_value(nargs + 2);
7428                if callable.downcast_ref_if_exact::<PyFunction>(vm).is_some()
7429                    || callable
7430                        .downcast_ref_if_exact::<PyBoundMethod>(vm)
7431                        .is_some()
7432                {
7433                    return self.execute_call_kw_vectorcall(nargs, vm);
7434                }
7435                let nargs_usize = nargs as usize;
7436                let kwarg_names_obj = self.pop_value();
7437                let kwarg_names_tuple = kwarg_names_obj
7438                    .downcast_ref::<PyTuple>()
7439                    .expect("kwarg names should be tuple");
7440                let kw_count = kwarg_names_tuple.as_slice().len();
7441                let all_args: Vec<PyObjectRef> = self.pop_multiple(nargs_usize).collect();
7442                let self_or_null = self.pop_value_opt();
7443                let callable = self.pop_value();
7444                let pos_count = nargs_usize - kw_count;
7445                let mut args_vec =
7446                    Vec::with_capacity(nargs_usize + usize::from(self_or_null_is_some));
7447                if let Some(self_val) = self_or_null {
7448                    args_vec.push(self_val);
7449                }
7450                args_vec.extend(all_args);
7451                let result = callable.vectorcall(
7452                    args_vec,
7453                    pos_count + usize::from(self_or_null_is_some),
7454                    Some(kwarg_names_tuple.as_slice()),
7455                    vm,
7456                )?;
7457                self.push_value(result);
7458                Ok(None)
7459            }
7460            Instruction::LoadSuperAttrAttr => {
7461                let oparg = u32::from(arg);
7462                let attr_name = self.code.names[(oparg >> 2) as usize];
7463                // Stack: [global_super, class, self]
7464                let self_obj = self.top_value();
7465                let class_obj = self.nth_value(1);
7466                let global_super = self.nth_value(2);
7467                // Guard: global_super is builtin super and class is a type
7468                if global_super.is(&vm.ctx.types.super_type.as_object())
7469                    && class_obj.downcast_ref::<PyType>().is_some()
7470                {
7471                    let class = class_obj.downcast_ref::<PyType>().unwrap();
7472                    let start_type = self_obj.class();
7473                    // MRO lookup: skip classes up to and including `class`, then search
7474                    let mro: Vec<PyRef<PyType>> = start_type.mro_map_collect(|x| x.to_owned());
7475                    let mut found = None;
7476                    let mut past_class = false;
7477                    for cls in &mro {
7478                        if !past_class {
7479                            if cls.is(class) {
7480                                past_class = true;
7481                            }
7482                            continue;
7483                        }
7484                        if let Some(descr) = cls.get_direct_attr(attr_name) {
7485                            // Call descriptor __get__ if available
7486                            // Pass None for obj when self IS its own type (classmethod)
7487                            let obj_arg = if self_obj.is(start_type.as_object()) {
7488                                None
7489                            } else {
7490                                Some(self_obj)
7491                            };
7492                            let result = vm
7493                                .call_get_descriptor_specific(
7494                                    &descr,
7495                                    obj_arg,
7496                                    Some(start_type.as_object()),
7497                                )
7498                                .unwrap_or(Ok(descr))?;
7499                            found = Some(result);
7500                            break;
7501                        }
7502                    }
7503                    if let Some(attr) = found {
7504                        self.pop_stackref(); // self
7505                        self.pop_stackref(); // class
7506                        self.pop_stackref(); // super
7507                        self.push_value(attr);
7508                        return Ok(None);
7509                    }
7510                }
7511                let oparg = LoadSuperAttr::from_u32(oparg);
7512                self.load_super_attr(vm, oparg)
7513            }
7514            Instruction::LoadSuperAttrMethod => {
7515                let oparg = u32::from(arg);
7516                let attr_name = self.code.names[(oparg >> 2) as usize];
7517                // Stack: [global_super, class, self]
7518                let self_obj = self.top_value();
7519                let class_obj = self.nth_value(1);
7520                let global_super = self.nth_value(2);
7521                // Guard: global_super is builtin super and class is a type
7522                if global_super.is(&vm.ctx.types.super_type.as_object())
7523                    && class_obj.downcast_ref::<PyType>().is_some()
7524                {
7525                    let class = class_obj.downcast_ref::<PyType>().unwrap();
7526                    let self_val = self_obj.to_owned();
7527                    let start_type = self_obj.class();
7528                    // MRO lookup
7529                    let mro: Vec<PyRef<PyType>> = start_type.mro_map_collect(|x| x.to_owned());
7530                    let mut found = None;
7531                    let mut past_class = false;
7532                    for cls in &mro {
7533                        if !past_class {
7534                            if cls.is(class) {
7535                                past_class = true;
7536                            }
7537                            continue;
7538                        }
7539                        if let Some(descr) = cls.get_direct_attr(attr_name) {
7540                            let descr_cls = descr.class();
7541                            if descr_cls
7542                                .slots()
7543                                .flags
7544                                .has_feature(PyTypeFlags::METHOD_DESCRIPTOR)
7545                            {
7546                                // Method descriptor: push unbound func + self
7547                                // CALL will prepend self as first positional arg
7548                                found = Some((descr, true));
7549                            } else if let Some(descr_get) = descr_cls.slots().descr_get.load() {
7550                                // Has __get__ but not METHOD_DESCRIPTOR: bind it
7551                                let bound = descr_get(
7552                                    &descr,
7553                                    Some(&self_val),
7554                                    Some(start_type.as_object()),
7555                                    vm,
7556                                )?;
7557                                found = Some((bound, false));
7558                            } else {
7559                                // Plain attribute
7560                                found = Some((descr, false));
7561                            }
7562                            break;
7563                        }
7564                    }
7565                    if let Some((attr, is_method)) = found {
7566                        self.pop_stackref(); // self
7567                        self.pop_stackref(); // class
7568                        self.pop_stackref(); // super
7569                        self.push_value(attr);
7570                        if is_method {
7571                            self.push_value(self_val);
7572                        } else {
7573                            self.push_null();
7574                        }
7575                        return Ok(None);
7576                    }
7577                }
7578                let oparg = LoadSuperAttr::from_u32(oparg);
7579                self.load_super_attr(vm, oparg)
7580            }
7581            Instruction::CompareOpInt => {
7582                let b = self.top_value();
7583                let a = self.nth_value(1);
7584                if let (Some(a_int), Some(b_int)) = (
7585                    a.downcast_ref_if_exact::<PyInt>(vm),
7586                    b.downcast_ref_if_exact::<PyInt>(vm),
7587                ) && let (Some(a_val), Some(b_val)) = (
7588                    specialization_compact_int_value(a_int),
7589                    specialization_compact_int_value(b_int),
7590                ) {
7591                    let op = self.compare_op_from_arg(arg);
7592                    let result = op.eval_ord(a_val.cmp(&b_val));
7593                    self.pop_stackref();
7594                    self.pop_stackref();
7595                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7596                    Ok(None)
7597                } else {
7598                    self.execute_compare(vm, arg)
7599                }
7600            }
7601            Instruction::CompareOpFloat => {
7602                let b = self.top_value();
7603                let a = self.nth_value(1);
7604                if let (Some(a_f), Some(b_f)) = (
7605                    a.downcast_ref_if_exact::<PyFloat>(vm),
7606                    b.downcast_ref_if_exact::<PyFloat>(vm),
7607                ) {
7608                    let op = self.compare_op_from_arg(arg);
7609                    let (a, b) = (a_f.to_f64(), b_f.to_f64());
7610                    // Use Rust's IEEE 754 float comparison which handles NaN correctly
7611                    let result = match a.partial_cmp(&b) {
7612                        Some(ord) => op.eval_ord(ord),
7613                        None => op == PyComparisonOp::Ne, // NaN != anything is true
7614                    };
7615                    self.pop_stackref();
7616                    self.pop_stackref();
7617                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7618                    Ok(None)
7619                } else {
7620                    self.execute_compare(vm, arg)
7621                }
7622            }
7623            Instruction::CompareOpStr => {
7624                let b = self.top_value();
7625                let a = self.nth_value(1);
7626                if let (Some(a_str), Some(b_str)) = (
7627                    a.downcast_ref_if_exact::<PyStr>(vm),
7628                    b.downcast_ref_if_exact::<PyStr>(vm),
7629                ) {
7630                    let op = self.compare_op_from_arg(arg);
7631                    // The same two shortcuts the unspecialized comparison takes:
7632                    // one object is equal to itself, and equality answers two
7633                    // strings of different length without reading either.
7634                    let Some(result) = op.eval_eq(|| a.is(b) || a_str.as_wtf8() == b_str.as_wtf8())
7635                    else {
7636                        return self.execute_compare(vm, arg);
7637                    };
7638                    self.pop_stackref();
7639                    self.pop_stackref();
7640                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7641                    Ok(None)
7642                } else {
7643                    self.execute_compare(vm, arg)
7644                }
7645            }
7646            Instruction::ToBoolBool => {
7647                let obj = self.top_value();
7648                if obj.class().is(vm.ctx.types.bool_type) {
7649                    // Already a bool, so normally a no-op — but when a
7650                    // POP_JUMP_IF_* follows, branching on it here retires both
7651                    // instructions in one dispatch.
7652                    if let Some(jump) = self.fused_bool_jump(instruction.cache_entries(), vm) {
7653                        let result = obj.is(&vm.ctx.true_value);
7654                        self.pop_stackref();
7655                        self.take_fused_bool_jump(jump, result);
7656                    }
7657                    Ok(None)
7658                } else {
7659                    let obj = self.pop_stackref();
7660                    let result = obj.try_to_bool(vm)?;
7661                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7662                    Ok(None)
7663                }
7664            }
7665            Instruction::ToBoolInt => {
7666                let obj = self.top_value();
7667                if let Some(int_val) = obj.downcast_ref_if_exact::<PyInt>(vm) {
7668                    let result = !int_val.as_bigint().is_zero();
7669                    self.pop_stackref();
7670                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7671                    Ok(None)
7672                } else {
7673                    let obj = self.pop_stackref();
7674                    let result = obj.try_to_bool(vm)?;
7675                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7676                    Ok(None)
7677                }
7678            }
7679            Instruction::ToBoolNone => {
7680                let obj = self.top_value();
7681                if obj.class().is(vm.ctx.types.none_type) {
7682                    self.pop_stackref();
7683                    self.push_bool_or_fused_jump(instruction.cache_entries(), false, vm);
7684                    Ok(None)
7685                } else {
7686                    let obj = self.pop_stackref();
7687                    let result = obj.try_to_bool(vm)?;
7688                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7689                    Ok(None)
7690                }
7691            }
7692            Instruction::ToBoolList => {
7693                let obj = self.top_value();
7694                if let Some(list) = obj.downcast_ref_if_exact::<PyList>(vm) {
7695                    let result = !list.borrow_vec().is_empty();
7696                    self.pop_stackref();
7697                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7698                    Ok(None)
7699                } else {
7700                    let obj = self.pop_stackref();
7701                    let result = obj.try_to_bool(vm)?;
7702                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7703                    Ok(None)
7704                }
7705            }
7706            Instruction::ToBoolStr => {
7707                let obj = self.top_value();
7708                if let Some(s) = obj.downcast_ref_if_exact::<PyStr>(vm) {
7709                    let result = !s.is_empty();
7710                    self.pop_stackref();
7711                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7712                    Ok(None)
7713                } else {
7714                    let obj = self.pop_stackref();
7715                    let result = obj.try_to_bool(vm)?;
7716                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7717                    Ok(None)
7718                }
7719            }
7720            Instruction::ToBoolAlwaysTrue => {
7721                // Objects without __bool__ or __len__ are always True.
7722                // Guard: check type version hasn't changed.
7723                let instr_idx = self.lasti() as usize - 1;
7724                let cache_base = instr_idx + 1;
7725                let obj = self.top_value();
7726                let cached_version = self.code.instructions.read_cache_u32(cache_base + 1);
7727                if cached_version != 0
7728                    && obj.class().tp_version_tag().load(Acquire) == cached_version
7729                {
7730                    self.pop_stackref();
7731                    self.push_bool_or_fused_jump(instruction.cache_entries(), true, vm);
7732                    Ok(None)
7733                } else {
7734                    let obj = self.pop_stackref();
7735                    let result = obj.try_to_bool(vm)?;
7736                    self.push_bool_or_fused_jump(instruction.cache_entries(), result, vm);
7737                    Ok(None)
7738                }
7739            }
7740            Instruction::ContainsOpDict => {
7741                let b = self.top_value(); // haystack
7742                if let Some(dict) = b.downcast_ref_if_exact::<PyDict>(vm) {
7743                    let a = self.nth_value(1); // needle
7744                    let found = dict.get_item_opt(a, vm)?.is_some();
7745                    self.pop_stackref();
7746                    self.pop_stackref();
7747                    let invert = bytecode::Invert::try_from(u32::from(arg) as u8)
7748                        .unwrap_or(bytecode::Invert::No);
7749                    let value = match invert {
7750                        bytecode::Invert::No => found,
7751                        bytecode::Invert::Yes => !found,
7752                    };
7753                    self.push_bool_or_fused_jump(instruction.cache_entries(), value, vm);
7754                    Ok(None)
7755                } else {
7756                    let b = self.pop_value();
7757                    let a = self.pop_value();
7758                    let invert = bytecode::Invert::try_from(u32::from(arg) as u8)
7759                        .unwrap_or(bytecode::Invert::No);
7760                    let value = match invert {
7761                        bytecode::Invert::No => self._in(vm, &a, &b)?,
7762                        bytecode::Invert::Yes => self._not_in(vm, &a, &b)?,
7763                    };
7764                    self.push_bool_or_fused_jump(instruction.cache_entries(), value, vm);
7765                    Ok(None)
7766                }
7767            }
7768            Instruction::ContainsOpSet => {
7769                let b = self.top_value(); // haystack
7770                if b.downcast_ref_if_exact::<PySet>(vm).is_some()
7771                    || b.downcast_ref_if_exact::<PyFrozenSet>(vm).is_some()
7772                {
7773                    let a = self.nth_value(1); // needle
7774                    let found = vm._contains(b, a)?;
7775                    self.pop_stackref();
7776                    self.pop_stackref();
7777                    let invert = bytecode::Invert::try_from(u32::from(arg) as u8)
7778                        .unwrap_or(bytecode::Invert::No);
7779                    let value = match invert {
7780                        bytecode::Invert::No => found,
7781                        bytecode::Invert::Yes => !found,
7782                    };
7783                    self.push_bool_or_fused_jump(instruction.cache_entries(), value, vm);
7784                    Ok(None)
7785                } else {
7786                    let b = self.pop_value();
7787                    let a = self.pop_value();
7788                    let invert = bytecode::Invert::try_from(u32::from(arg) as u8)
7789                        .unwrap_or(bytecode::Invert::No);
7790                    let value = match invert {
7791                        bytecode::Invert::No => self._in(vm, &a, &b)?,
7792                        bytecode::Invert::Yes => self._not_in(vm, &a, &b)?,
7793                    };
7794                    self.push_bool_or_fused_jump(instruction.cache_entries(), value, vm);
7795                    Ok(None)
7796                }
7797            }
7798            Instruction::UnpackSequenceTwoTuple => {
7799                let obj = self.top_value();
7800                if let Some(tuple) = obj.downcast_ref_if_exact::<PyTuple>(vm) {
7801                    let elements = tuple.as_slice();
7802                    if elements.len() == 2 {
7803                        let e0 = elements[0].clone();
7804                        let e1 = elements[1].clone();
7805                        self.pop_stackref();
7806                        self.push_value(e1);
7807                        self.push_value(e0);
7808                        return Ok(None);
7809                    }
7810                }
7811                let size = u32::from(arg);
7812                self.unpack_sequence(size, vm)
7813            }
7814            Instruction::UnpackSequenceTuple => {
7815                let size = u32::from(arg) as usize;
7816                let obj = self.top_value();
7817                if let Some(tuple) = obj.downcast_ref_if_exact::<PyTuple>(vm) {
7818                    let elements = tuple.as_slice();
7819                    if elements.len() == size {
7820                        let elems: Vec<_> = elements.to_vec();
7821                        self.pop_stackref();
7822                        for elem in elems.into_iter().rev() {
7823                            self.push_value(elem);
7824                        }
7825                        return Ok(None);
7826                    }
7827                }
7828                self.unpack_sequence(size as u32, vm)
7829            }
7830            Instruction::UnpackSequenceList => {
7831                let size = u32::from(arg) as usize;
7832                let obj = self.top_value();
7833                if let Some(list) = obj.downcast_ref_if_exact::<PyList>(vm) {
7834                    let vec = list.borrow_vec();
7835                    if vec.len() == size {
7836                        let elems: Vec<_> = vec.to_vec();
7837                        drop(vec);
7838                        self.pop_stackref();
7839                        for elem in elems.into_iter().rev() {
7840                            self.push_value(elem);
7841                        }
7842                        return Ok(None);
7843                    }
7844                }
7845                self.unpack_sequence(size as u32, vm)
7846            }
7847            Instruction::ForIterRange => {
7848                let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
7849                let iter = self.top_value();
7850                if let Some(range_iter) = iter.downcast_ref_if_exact::<PyRangeIterator>(vm) {
7851                    if let Some(value) = range_iter.fast_next() {
7852                        self.push_value(vm.ctx.new_int(value).into());
7853                    } else {
7854                        self.for_iter_jump_on_exhausted(target);
7855                    }
7856                    Ok(None)
7857                } else {
7858                    self.execute_for_iter(vm, target)?;
7859                    Ok(None)
7860                }
7861            }
7862            Instruction::ForIterList => {
7863                let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
7864                let iter = self.top_value();
7865                if let Some(list_iter) = iter.downcast_ref_if_exact::<PyListIterator>(vm) {
7866                    if let Some(value) = list_iter.fast_next() {
7867                        self.push_value(value);
7868                    } else {
7869                        self.for_iter_jump_on_exhausted(target);
7870                    }
7871                    Ok(None)
7872                } else {
7873                    self.execute_for_iter(vm, target)?;
7874                    Ok(None)
7875                }
7876            }
7877            Instruction::ForIterTuple => {
7878                let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
7879                let iter = self.top_value();
7880                if let Some(tuple_iter) = iter.downcast_ref_if_exact::<PyTupleIterator>(vm) {
7881                    if let Some(value) = tuple_iter.fast_next() {
7882                        self.push_value(value);
7883                    } else {
7884                        self.for_iter_jump_on_exhausted(target);
7885                    }
7886                    Ok(None)
7887                } else {
7888                    self.execute_for_iter(vm, target)?;
7889                    Ok(None)
7890                }
7891            }
7892            Instruction::ForIterGen => {
7893                let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
7894                let iter = self.top_value();
7895                if self.specialization_eval_frame_active(vm) {
7896                    self.execute_for_iter(vm, target)?;
7897                    return Ok(None);
7898                }
7899                if let Some(generator) = iter.downcast_ref_if_exact::<PyGenerator>(vm) {
7900                    if generator.as_coro().running() || generator.as_coro().closed() {
7901                        self.execute_for_iter(vm, target)?;
7902                        return Ok(None);
7903                    }
7904                    match generator.as_coro().send_none(iter, vm) {
7905                        Ok(PyIterReturn::Return(value)) => {
7906                            self.push_value(value);
7907                        }
7908                        Ok(PyIterReturn::StopIteration(value)) => {
7909                            // FOR_ITER_GEN returns through END_FOR, which
7910                            // fires STOP_ITERATION rather than RAISE.
7911                            if vm.state.monitoring_events.load()
7912                                & MonitoringEvent::StopIteration.mask()
7913                                != 0
7914                            {
7915                                let offset = (self.lasti() - 1) * 2;
7916                                let val = vm.unwrap_or_none(value.clone());
7917                                monitoring::fire_stop_iteration(vm, self.code, offset, &val)?;
7918                            }
7919                            if vm.use_tracing.get() && self.trace_is_set(vm) {
7920                                let stop_exc = vm.new_stop_iteration(value);
7921                                self.fire_exception_trace(&stop_exc, vm)?;
7922                            }
7923                            self.for_iter_jump_on_exhausted(target);
7924                        }
7925                        Err(e) => return Err(e),
7926                    }
7927                    Ok(None)
7928                } else {
7929                    self.execute_for_iter(vm, target)?;
7930                    Ok(None)
7931                }
7932            }
7933            Instruction::LoadGlobalModule => {
7934                let oparg = u32::from(arg);
7935                let cache_base = self.lasti() as usize;
7936                // Keep specialized opcode on guard miss (JUMP_TO_PREDICTED behavior).
7937                let cached_version = self.code.instructions.read_cache_u16(cache_base + 1);
7938                let cached_index = self.code.instructions.read_cache_u16(cache_base + 3);
7939                if cached_version != 0
7940                    && let Some(x) = self
7941                        .globals
7942                        .get_item_by_index_and_keys_version(cached_version, cached_index)
7943                {
7944                    self.push_value(x);
7945                    if (oparg & 1) != 0 {
7946                        self.push_value_opt(None);
7947                    }
7948                    return Ok(None);
7949                }
7950                let name = self.code.names[(oparg >> 1) as usize];
7951                let x = self.load_global_or_builtin(name, vm)?;
7952                self.push_value(x);
7953                if (oparg & 1) != 0 {
7954                    self.push_value_opt(None);
7955                }
7956                Ok(None)
7957            }
7958            Instruction::LoadGlobalBuiltin => {
7959                let oparg = u32::from(arg);
7960                let cache_base = self.lasti() as usize;
7961                let cached_globals_ver = self.code.instructions.read_cache_u16(cache_base + 1);
7962                let cached_builtins_ver = self.code.instructions.read_cache_u16(cache_base + 2);
7963                let cached_index = self.code.instructions.read_cache_u16(cache_base + 3);
7964                if cached_globals_ver != 0
7965                    && cached_builtins_ver != 0
7966                    && let Ok(current_globals_ver) = u16::try_from(self.globals.keys_version())
7967                    && cached_globals_ver == current_globals_ver
7968                    && let Some(builtins_dict) = self.builtins.downcast_ref_if_exact::<PyDict>(vm)
7969                    && let Some(x) = builtins_dict
7970                        .get_item_by_index_and_keys_version(cached_builtins_ver, cached_index)
7971                {
7972                    self.push_value(x);
7973                    if (oparg & 1) != 0 {
7974                        self.push_value_opt(None);
7975                    }
7976                    return Ok(None);
7977                }
7978                let name = self.code.names[(oparg >> 1) as usize];
7979                let x = self.load_global_or_builtin(name, vm)?;
7980                self.push_value(x);
7981                if (oparg & 1) != 0 {
7982                    self.push_value_opt(None);
7983                }
7984                Ok(None)
7985            }
7986            // All INSTRUMENTED_* opcodes delegate to a cold function to keep
7987            // the hot instruction loop free of monitoring overhead.
7988            _ => self.execute_instrumented(instruction, arg, vm),
7989        }
7990    }
7991
7992    /// Handle all INSTRUMENTED_* opcodes. This function is cold — it only
7993    /// runs when sys.monitoring has rewritten the bytecode.
7994    #[cold]
7995    fn execute_instrumented(
7996        &mut self,
7997        instruction: Instruction,
7998        arg: bytecode::OpArg,
7999        vm: &VirtualMachine,
8000    ) -> FrameResult {
8001        debug_assert!(
8002            instruction.is_instrumented(),
8003            "execute_instrumented called with non-instrumented opcode {instruction:?}"
8004        );
8005        // Update prev_line so InstrumentedLine's own change-detection (see
8006        // below) stays in sync. `prev_line` is no longer read for
8007        // `f_lineno` -- that's now derived lazily from `lasti` -- this
8008        // write only exists to dedup LINE events. The main bytecode loop
8009        // skips instrumented opcodes to avoid interfering with that
8010        // de-duplication in InstrumentedLine, so it's updated here instead,
8011        // except for RESUME (prev_line must stay 0 for the first LINE
8012        // event) and InstrumentedLine (manages prev_line in its own
8013        // handler).
8014        if !matches!(
8015            instruction,
8016            Instruction::InstrumentedResume | Instruction::InstrumentedLine
8017        ) {
8018            let idx = self.lasti() as usize - 1;
8019            if let Some((loc, _)) = self.code.locations.get(idx) {
8020                self.prev_line.set(loc.line.get() as u32);
8021            }
8022        }
8023        self.monitoring_mask = vm.state.monitoring_events.load();
8024        match instruction {
8025            Instruction::InstrumentedResume => {
8026                // Version check: re-instrument if stale
8027                let global_ver = vm
8028                    .state
8029                    .instrumentation_version
8030                    .load(atomic::Ordering::Acquire);
8031                let code_ver = self
8032                    .code
8033                    .instrumentation_version
8034                    .load(atomic::Ordering::Acquire);
8035                if code_ver != global_ver {
8036                    let events = {
8037                        let state = vm.state.monitoring.lock();
8038                        state.events_for_code(self.code.get_id())
8039                    };
8040                    monitoring::instrument_code(self.code, events);
8041                    self.code
8042                        .instrumentation_version
8043                        .store(global_ver, atomic::Ordering::Release);
8044                    // Re-execute (may have been de-instrumented to base Resume)
8045                    self.update_lasti(|i| *i -= 1);
8046                    return Ok(None);
8047                }
8048                let resume_type = u32::from(arg);
8049                let offset = (self.lasti() - 1) * 2;
8050                if resume_type == 0 {
8051                    if self.monitoring_mask & MonitoringEvent::PyStart.mask() != 0 {
8052                        monitoring::fire_py_start(vm, self.code, offset)?;
8053                    }
8054                } else if self.monitoring_mask & MonitoringEvent::PyResume.mask() != 0 {
8055                    monitoring::fire_py_resume(vm, self.code, offset)?;
8056                }
8057                self.trace_call_from_resume(vm, resume_type)?;
8058                Ok(None)
8059            }
8060            Instruction::InstrumentedReturnValue => {
8061                let value = self.pop_value();
8062                if vm.use_tracing.get() {
8063                    vm.trace_event_what(
8064                        crate::protocol::TraceEvent::Return,
8065                        monitoring::MonitoringEvent::PyReturn,
8066                        Some(value.clone()),
8067                    )?;
8068                }
8069                if self.monitoring_mask & MonitoringEvent::PyReturn.mask() != 0 {
8070                    let offset = (self.lasti() - 1) * 2;
8071                    monitoring::fire_py_return(vm, self.code, offset, &value)?;
8072                }
8073                self.unwind_blocks(vm, UnwindReason::Returning { value })
8074            }
8075            Instruction::InstrumentedYieldValue => {
8076                self.localsplus.promote_stack();
8077                debug_assert!(
8078                    self.localsplus
8079                        .stack_as_slice()
8080                        .iter()
8081                        .flatten()
8082                        .all(|sr| !sr.is_borrowed()),
8083                    "borrowed refs on stack at yield point"
8084                );
8085                if self.yield_value_event(vm)? {
8086                    return Ok(None);
8087                }
8088                Ok(Some(ExecutionResult::Yield(self.pop_value())))
8089            }
8090            Instruction::InstrumentedCall => {
8091                let args = self.collect_positional_args(u32::from(arg));
8092                self.execute_call_instrumented(args, vm)
8093            }
8094            Instruction::InstrumentedCallKw => {
8095                let args = self.collect_keyword_args(u32::from(arg));
8096                self.execute_call_instrumented(args, vm)
8097            }
8098            Instruction::InstrumentedCallFunctionEx => {
8099                let args = self.collect_ex_args(vm)?;
8100                self.execute_call_instrumented(args, vm)
8101            }
8102            Instruction::InstrumentedLoadSuperAttr => {
8103                let oparg = bytecode::LoadSuperAttr::from(u32::from(arg));
8104                let offset = (self.lasti() - 1) * 2;
8105                // Fire CALL event before super() call
8106                let call_args = if self.monitoring_mask & MonitoringEvent::Call.mask() != 0 {
8107                    let global_super: PyObjectRef = self.nth_value(2).to_owned();
8108                    let arg0 = if oparg.has_class() {
8109                        self.nth_value(1).to_owned()
8110                    } else {
8111                        monitoring::get_missing(vm)
8112                    };
8113                    monitoring::fire_call(vm, self.code, offset, &global_super, arg0.clone())?;
8114                    Some((global_super, arg0))
8115                } else {
8116                    None
8117                };
8118                match self.load_super_attr(vm, oparg) {
8119                    Ok(result) => {
8120                        // Fire C_RETURN on success
8121                        if let Some((global_super, arg0)) = call_args {
8122                            monitoring::fire_c_return(vm, self.code, offset, &global_super, arg0)?;
8123                        }
8124                        Ok(result)
8125                    }
8126                    Err(exc) => {
8127                        // Fire C_RAISE on failure
8128                        let exc = if let Some((global_super, arg0)) = call_args {
8129                            match monitoring::fire_c_raise(
8130                                vm,
8131                                self.code,
8132                                offset,
8133                                &global_super,
8134                                arg0,
8135                            ) {
8136                                Ok(()) => exc,
8137                                Err(monitor_exc) => monitor_exc,
8138                            }
8139                        } else {
8140                            exc
8141                        };
8142                        Err(exc)
8143                    }
8144                }
8145            }
8146            Instruction::InstrumentedJumpForward => {
8147                let src_offset = (self.lasti() - 1) * 2;
8148                let target_idx = self.lasti() + u32::from(arg);
8149                let target = bytecode::Label::from_u32(target_idx);
8150                self.jump(target);
8151                if self.monitoring_mask & MonitoringEvent::Jump.mask() != 0 {
8152                    monitoring::fire_jump(vm, self.code, src_offset, target.as_u32() * 2)?;
8153                }
8154                Ok(None)
8155            }
8156            Instruction::InstrumentedJumpBackward => {
8157                let src_offset = (self.lasti() - 1) * 2;
8158                let from_idx = self.lasti().saturating_sub(1) as usize;
8159                let target_idx = self.lasti() + 1 - u32::from(arg);
8160                let target = bytecode::Label::from_u32(target_idx);
8161                self.jump(target);
8162                if self.monitoring_mask & MonitoringEvent::Jump.mask() != 0 {
8163                    monitoring::fire_jump(vm, self.code, src_offset, target.as_u32() * 2)?;
8164                }
8165                self.trace_backward_same_line(from_idx, vm)?;
8166                Ok(None)
8167            }
8168            Instruction::InstrumentedForIter => {
8169                let src_offset = (self.lasti() - 1) * 2;
8170                let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
8171                let continued = self.execute_for_iter(vm, target)?;
8172                if continued {
8173                    if self.monitoring_mask & MonitoringEvent::BranchLeft.mask() != 0 {
8174                        let dest_offset = (self.lasti() + 1) * 2; // after caches
8175                        monitoring::fire_branch_left(vm, self.code, src_offset, dest_offset)?;
8176                    }
8177                } else if self.monitoring_mask & MonitoringEvent::BranchRight.mask() != 0 {
8178                    // INSTRUMENTED_POP_ITER: dest is the instruction after
8179                    // POP_ITER (FOR_ITER jumps over END_FOR onto POP_ITER).
8180                    let dest_offset = (self.lasti() + 1) * 2;
8181                    monitoring::fire_branch_right(vm, self.code, src_offset, dest_offset)?;
8182                }
8183                Ok(None)
8184            }
8185            Instruction::InstrumentedEndFor => {
8186                // Stack: [value, receiver(iter), ...]
8187                // PyGen_Check: only fire STOP_ITERATION for generators
8188                let is_gen = self
8189                    .nth_value(1)
8190                    .downcast_ref::<crate::builtins::PyGenerator>()
8191                    .is_some();
8192                let value = self.pop_value();
8193                if is_gen && self.monitoring_mask & MonitoringEvent::StopIteration.mask() != 0 {
8194                    let offset = (self.lasti() - 1) * 2;
8195                    monitoring::fire_stop_iteration(vm, self.code, offset, &value)?;
8196                }
8197                Ok(None)
8198            }
8199            Instruction::InstrumentedEndSend => {
8200                let value = self.pop_value();
8201                let receiver = self.pop_value();
8202                // PyGen_Check || PyCoro_CheckExact
8203                let is_gen_or_coro = receiver
8204                    .downcast_ref::<crate::builtins::PyGenerator>()
8205                    .is_some()
8206                    || receiver
8207                        .downcast_ref::<crate::builtins::PyCoroutine>()
8208                        .is_some();
8209                if is_gen_or_coro
8210                    && self.monitoring_mask & MonitoringEvent::StopIteration.mask() != 0
8211                {
8212                    let offset = (self.lasti() - 1) * 2;
8213                    monitoring::fire_stop_iteration(vm, self.code, offset, &value)?;
8214                }
8215                self.push_value(value);
8216                Ok(None)
8217            }
8218            Instruction::InstrumentedPopJumpIfTrue => {
8219                let src_offset = (self.lasti() - 1) * 2;
8220                let target_idx = self.lasti() + 1 + u32::from(arg);
8221                let obj = self.pop_value();
8222                let value = obj.try_to_bool(vm)?;
8223                if value {
8224                    self.jump(bytecode::Label::from_u32(target_idx));
8225                    if self.monitoring_mask & MonitoringEvent::BranchRight.mask() != 0 {
8226                        monitoring::fire_branch_right(vm, self.code, src_offset, target_idx * 2)?;
8227                    }
8228                }
8229                Ok(None)
8230            }
8231            Instruction::InstrumentedPopJumpIfFalse => {
8232                let src_offset = (self.lasti() - 1) * 2;
8233                let target_idx = self.lasti() + 1 + u32::from(arg);
8234                let obj = self.pop_value();
8235                let value = obj.try_to_bool(vm)?;
8236                if !value {
8237                    self.jump(bytecode::Label::from_u32(target_idx));
8238                    if self.monitoring_mask & MonitoringEvent::BranchRight.mask() != 0 {
8239                        monitoring::fire_branch_right(vm, self.code, src_offset, target_idx * 2)?;
8240                    }
8241                }
8242                Ok(None)
8243            }
8244            Instruction::InstrumentedPopJumpIfNone => {
8245                let src_offset = (self.lasti() - 1) * 2;
8246                let target_idx = self.lasti() + 1 + u32::from(arg);
8247                let value = self.pop_value();
8248                if vm.is_none(&value) {
8249                    self.jump(bytecode::Label::from_u32(target_idx));
8250                    if self.monitoring_mask & MonitoringEvent::BranchRight.mask() != 0 {
8251                        monitoring::fire_branch_right(vm, self.code, src_offset, target_idx * 2)?;
8252                    }
8253                }
8254                Ok(None)
8255            }
8256            Instruction::InstrumentedPopJumpIfNotNone => {
8257                let src_offset = (self.lasti() - 1) * 2;
8258                let target_idx = self.lasti() + 1 + u32::from(arg);
8259                let value = self.pop_value();
8260                if !vm.is_none(&value) {
8261                    self.jump(bytecode::Label::from_u32(target_idx));
8262                    if self.monitoring_mask & MonitoringEvent::BranchRight.mask() != 0 {
8263                        monitoring::fire_branch_right(vm, self.code, src_offset, target_idx * 2)?;
8264                    }
8265                }
8266                Ok(None)
8267            }
8268            Instruction::InstrumentedNotTaken => {
8269                if self.monitoring_mask & MonitoringEvent::BranchLeft.mask() != 0 {
8270                    let not_taken_idx = self.lasti() as usize - 1;
8271                    // Scan backwards past CACHE entries to find the branch instruction
8272                    let mut branch_idx = not_taken_idx.saturating_sub(1);
8273                    while branch_idx > 0
8274                        && matches!(
8275                            self.code.instructions.read_op(branch_idx),
8276                            Instruction::Cache
8277                        )
8278                    {
8279                        branch_idx -= 1;
8280                    }
8281                    let src_offset = (branch_idx as u32) * 2;
8282                    let dest_offset = self.lasti() * 2;
8283                    monitoring::fire_branch_left(vm, self.code, src_offset, dest_offset)?;
8284                }
8285                Ok(None)
8286            }
8287            Instruction::InstrumentedPopIter => {
8288                // BRANCH_RIGHT is fired by InstrumentedForIter, not here.
8289                self.pop_stackref();
8290                Ok(None)
8291            }
8292            Instruction::InstrumentedEndAsyncFor => {
8293                if self.monitoring_mask & MonitoringEvent::BranchRight.mask() != 0 {
8294                    let oparg_val = u32::from(arg);
8295                    // src = next_instr - oparg (END_SEND position)
8296                    let src_offset = (self.lasti() - oparg_val) * 2;
8297                    // dest = this_instr + 1
8298                    let dest_offset = self.lasti() * 2;
8299                    monitoring::fire_branch_right(vm, self.code, src_offset, dest_offset)?;
8300                }
8301                let exc = self.pop_value();
8302                let _awaitable = self.pop_value();
8303                let exc = exc
8304                    .downcast::<PyBaseException>()
8305                    .expect("EndAsyncFor expects exception on stack");
8306                if exc.fast_isinstance(vm.ctx.exceptions.stop_async_iteration) {
8307                    vm.set_exception(None);
8308                    Ok(None)
8309                } else {
8310                    Err(exc)
8311                }
8312            }
8313            Instruction::InstrumentedLine => {
8314                let idx = self.lasti() as usize - 1;
8315                let offset = idx as u32 * 2;
8316
8317                // Read the full side-table chain before firing any events,
8318                // because a callback may de-instrument and clear the tables.
8319                let (real_op_byte, also_instruction) = {
8320                    let data = self.code.monitoring_data.lock();
8321                    let line_op = data.as_ref().map_or(0, |d| d.line_opcodes[idx]);
8322                    if line_op == u8::from(Instruction::InstrumentedInstruction) {
8323                        // LINE wraps INSTRUCTION: resolve the INSTRUCTION side-table too
8324                        let inst_op = data.as_ref().map_or(0, |d| d.per_instruction_opcodes[idx]);
8325                        (inst_op, true)
8326                    } else {
8327                        (line_op, false)
8328                    }
8329                };
8330                debug_assert!(
8331                    real_op_byte != 0,
8332                    "INSTRUMENTED_LINE at {idx} without stored opcode"
8333                );
8334
8335                // Fire LINE event only if line changed
8336                if let Some((loc, _)) = self.code.locations.get(idx) {
8337                    let line = loc.line.get() as u32;
8338                    if line != self.prev_line.get() && line > 0 {
8339                        self.prev_line.set(line);
8340                        monitoring::fire_line(vm, self.code, offset, line)?;
8341                    }
8342                }
8343
8344                // If the LINE position also had INSTRUCTION, fire that event too
8345                if also_instruction {
8346                    monitoring::fire_instruction(vm, self.code, offset)?;
8347                }
8348
8349                // NOTE: prev_line is already up to date here -- the
8350                // dedup check above (`if line != self.prev_line.get() ...`)
8351                // reads the same `idx`/`loc` and, when the line changed,
8352                // already set `prev_line` to that same value; no further
8353                // write is needed (a prior unconditional re-write here was
8354                // a dead duplicate of that one, and also isn't needed for
8355                // `f_lineno`, which is derived lazily from `lasti`).
8356
8357                // Re-dispatch to the real original opcode
8358                let original_op = Instruction::try_from(real_op_byte)
8359                    .expect("invalid opcode in side-table chain");
8360                let lasti_before_dispatch = self.lasti();
8361                let result = if original_op.to_base().is_some() {
8362                    self.execute_instrumented(original_op, arg, vm)
8363                } else {
8364                    let mut do_extend_arg = false;
8365                    self.execute_instruction(original_op, arg, &mut do_extend_arg, vm)
8366                };
8367                let orig_caches = original_op.to_base().unwrap_or(original_op).cache_entries();
8368                if orig_caches > 0 && self.lasti() == lasti_before_dispatch {
8369                    self.update_lasti(|i| *i += orig_caches as u32);
8370                }
8371                result
8372            }
8373            Instruction::InstrumentedInstruction => {
8374                let idx = self.lasti() as usize - 1;
8375                let offset = idx as u32 * 2;
8376
8377                // Get original opcode from side-table
8378                let original_op_byte = {
8379                    let data = self.code.monitoring_data.lock();
8380                    data.as_ref().map_or(0, |d| d.per_instruction_opcodes[idx])
8381                };
8382                debug_assert!(
8383                    original_op_byte != 0,
8384                    "INSTRUMENTED_INSTRUCTION at {idx} without stored opcode"
8385                );
8386
8387                // Fire INSTRUCTION event
8388                monitoring::fire_instruction(vm, self.code, offset)?;
8389
8390                // Re-dispatch to original opcode
8391                let original_op = Instruction::try_from(original_op_byte)
8392                    .expect("invalid opcode in instruction side-table");
8393                let lasti_before_dispatch = self.lasti();
8394                let result = if original_op.to_base().is_some() {
8395                    self.execute_instrumented(original_op, arg, vm)
8396                } else {
8397                    let mut do_extend_arg = false;
8398                    self.execute_instruction(original_op, arg, &mut do_extend_arg, vm)
8399                };
8400                let orig_caches = original_op.to_base().unwrap_or(original_op).cache_entries();
8401                if orig_caches > 0 && self.lasti() == lasti_before_dispatch {
8402                    self.update_lasti(|i| *i += orig_caches as u32);
8403                }
8404                result
8405            }
8406            _ => {
8407                unreachable!("{instruction:?} instruction should not be executed")
8408            }
8409        }
8410    }
8411
8412    #[inline]
8413    fn mapping_get_optional(
8414        &self,
8415        mapping: &PyObject,
8416        name: &Py<PyStr>,
8417        vm: &VirtualMachine,
8418    ) -> PyResult<Option<PyObjectRef>> {
8419        if mapping.class().is(vm.ctx.types.dict_type) {
8420            let dict = mapping
8421                .downcast_ref::<PyDict>()
8422                .expect("exact dict must have a PyDict payload");
8423            dict.get_item_opt(name, vm)
8424        } else {
8425            match mapping.get_item(name, vm) {
8426                Ok(value) => Ok(Some(value)),
8427                Err(err) if err.fast_isinstance(vm.ctx.exceptions.key_error) => Ok(None),
8428                Err(err) => Err(err),
8429            }
8430        }
8431    }
8432
8433    #[inline]
8434    fn load_global_or_builtin(&self, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
8435        if let Some(builtins_dict) = self.builtins_dict {
8436            // Fast path: both globals and builtins are exact dicts
8437            // SAFETY: builtins_dict is only set when globals is also exact dict
8438            let globals_exact = unsafe { PyExact::ref_unchecked(self.globals) };
8439            globals_exact
8440                .get_chain_exact(builtins_dict, name, vm)?
8441                .ok_or_else(|| {
8442                    vm.new_name_error(format!("name '{name}' is not defined"), name.to_owned())
8443                })
8444        } else {
8445            // Slow path: builtins is not a dict, use generic __getitem__
8446            if let Some(value) = self.globals.get_item_opt(name, vm)? {
8447                return Ok(value);
8448            }
8449            self.builtins.get_item(name, vm).map_err(|e| {
8450                if e.fast_isinstance(vm.ctx.exceptions.key_error) {
8451                    vm.new_name_error(format!("name '{name}' is not defined"), name.to_owned())
8452                } else {
8453                    e
8454                }
8455            })
8456        }
8457    }
8458
8459    #[cfg_attr(feature = "flame-it", flame("FrameObject"))]
8460    fn import(&mut self, vm: &VirtualMachine, module_name: Option<&Py<PyStr>>) -> PyResult<()> {
8461        let module_name = module_name.unwrap_or(vm.ctx.empty_str);
8462        let from_list = self.pop_value();
8463        let level = usize::try_from_object(vm, self.pop_value())?;
8464
8465        let module = vm.import_from(module_name, from_list, level)?;
8466
8467        self.push_value(module);
8468        Ok(())
8469    }
8470
8471    #[cfg_attr(feature = "flame-it", flame("FrameObject"))]
8472    fn import_from(&mut self, vm: &VirtualMachine, idx: bytecode::NameIdx) -> PyResult {
8473        let module = self.top_value();
8474        let name = self.code.names[idx as usize];
8475
8476        // Load attribute, and transform any error into import error.
8477        if let Some(obj) = vm.get_attribute_opt(module, name)? {
8478            return Ok(obj);
8479        }
8480        // fallback to importing '{module.__name__}.{name}' from sys.modules
8481        let fallback_module = (|| {
8482            let mod_name = module.get_attr(identifier!(vm, __name__), vm).ok()?;
8483            let mod_name = mod_name.downcast_ref::<PyUtf8Str>()?;
8484            let full_mod_name = vm.ctx.new_utf8_str(format!("{}.{name}", mod_name.as_str()));
8485            let sys_modules = vm.sys_module.get_attr("modules", vm).ok()?;
8486            sys_modules.get_item(&*full_mod_name, vm).ok()
8487        })();
8488
8489        if let Some(sub_module) = fallback_module {
8490            return Ok(sub_module);
8491        }
8492
8493        use crate::import::{
8494            get_spec_file_origin, is_possibly_shadowing_path, is_stdlib_module_name,
8495        };
8496
8497        // Get module name for the error message
8498        let mod_name_obj = module.get_attr(identifier!(vm, __name__), vm).ok();
8499        let mod_name = mod_name_obj
8500            .as_ref()
8501            .and_then(|n| n.downcast_ref::<PyUtf8Str>());
8502        let module_name = mod_name.map_or("<unknown module name>", |s| s.as_str());
8503
8504        let spec = module
8505            .get_attr("__spec__", vm)
8506            .ok()
8507            .filter(|s| !vm.is_none(s));
8508
8509        let origin = get_spec_file_origin(spec.as_deref(), vm);
8510
8511        let is_possibly_shadowing = origin
8512            .as_ref()
8513            .is_some_and(|o| is_possibly_shadowing_path(o, vm));
8514        let is_possibly_shadowing_stdlib = if is_possibly_shadowing {
8515            if let Some(ref mod_name) = mod_name_obj {
8516                is_stdlib_module_name(mod_name, vm)?
8517            } else {
8518                false
8519            }
8520        } else {
8521            false
8522        };
8523
8524        let msg = if is_possibly_shadowing_stdlib {
8525            let origin = origin.as_ref().unwrap();
8526            format!(
8527                "cannot import name '{name}' from '{module_name}' \
8528                 (consider renaming '{origin}' since it has the same \
8529                 name as the standard library module named '{module_name}' \
8530                 and prevents importing that standard library module)"
8531            )
8532        } else {
8533            let is_init = is_module_initializing(module, vm);
8534            if is_init {
8535                if is_possibly_shadowing {
8536                    let origin = origin.as_ref().unwrap();
8537                    format!(
8538                        "cannot import name '{name}' from '{module_name}' \
8539                         (consider renaming '{origin}' if it has the same name \
8540                         as a library you intended to import)"
8541                    )
8542                } else if let Some(ref path) = origin {
8543                    format!(
8544                        "cannot import name '{name}' from partially initialized module \
8545                         '{module_name}' (most likely due to a circular import) ({path})"
8546                    )
8547                } else {
8548                    format!(
8549                        "cannot import name '{name}' from partially initialized module \
8550                         '{module_name}' (most likely due to a circular import)"
8551                    )
8552                }
8553            } else if let Some(ref path) = origin {
8554                format!("cannot import name '{name}' from '{module_name}' ({path})")
8555            } else {
8556                format!("cannot import name '{name}' from '{module_name}' (unknown location)")
8557            }
8558        };
8559        let err = vm.new_import_error(
8560            msg,
8561            match mod_name {
8562                Some(s) => s.to_owned().into_wtf8(),
8563                None => vm.ctx.new_utf8_str("<unknown module name>").into_wtf8(),
8564            },
8565        );
8566
8567        if let Some(ref path) = origin {
8568            let _ignore = err
8569                .as_object()
8570                .set_attr("path", vm.ctx.new_str(path.as_str()), vm);
8571        }
8572
8573        // name_from = the attribute name that failed to import (best-effort metadata)
8574        let _ignore = err.as_object().set_attr("name_from", name.to_owned(), vm);
8575
8576        Err(err)
8577    }
8578
8579    #[cfg_attr(feature = "flame-it", flame("FrameObject"))]
8580    fn import_star(&mut self, vm: &VirtualMachine) -> PyResult<()> {
8581        let module = self.pop_value();
8582
8583        let Some(dict) = module.dict() else {
8584            return Ok(());
8585        };
8586
8587        let mod_name = module
8588            .get_attr(identifier!(vm, __name__), vm)
8589            .ok()
8590            .and_then(|n| n.downcast::<PyStr>().ok());
8591
8592        let require_str = |obj: PyObjectRef, attr: &str| -> PyResult<PyRef<PyStr>> {
8593            obj.downcast().map_err(|obj: PyObjectRef| {
8594                let source = if let Some(ref mod_name) = mod_name {
8595                    format!("{}.{attr}", mod_name.as_wtf8())
8596                } else {
8597                    attr.to_owned()
8598                };
8599                let repr = obj.repr(vm).unwrap_or_else(|_| vm.ctx.new_str("?"));
8600                vm.new_type_error(format!(
8601                    "{} in {} must be str, not {}",
8602                    repr.as_wtf8(),
8603                    source,
8604                    obj.class().name()
8605                ))
8606            })
8607        };
8608
8609        let locals_map = self.locals.mapping(vm);
8610        if let Ok(all) = dict.get_item(identifier!(vm, __all__), vm) {
8611            let items: Vec<PyObjectRef> = all.try_to_value(vm)?;
8612            for item in items {
8613                let name = require_str(item, "__all__")?;
8614                let value = module.get_attr(&*name, vm)?;
8615                locals_map.ass_subscript(&name, Some(value), vm)?;
8616            }
8617        } else {
8618            for (k, v) in dict {
8619                let k = require_str(k, "__dict__")?;
8620                if !k.as_bytes().starts_with(b"_") {
8621                    locals_map.ass_subscript(&k, Some(v), vm)?;
8622                }
8623            }
8624        }
8625        Ok(())
8626    }
8627
8628    /// Unwind blocks.
8629    /// The reason for unwinding gives a hint on what to do when
8630    /// unwinding a block.
8631    /// Optionally returns an exception.
8632    #[cfg_attr(feature = "flame-it", flame("FrameObject"))]
8633    fn unwind_blocks(&mut self, vm: &VirtualMachine, reason: UnwindReason) -> FrameResult {
8634        // use exception table for exception handling
8635        match reason {
8636            UnwindReason::Raising { exception, offset } => {
8637                // Look up handler from the raising instruction. lasti may
8638                // already have been restored by RERAISE to the original raise.
8639                if let Some(entry) =
8640                    bytecode::find_exception_handler(&self.code.exceptiontable, offset)
8641                {
8642                    // Fire EXCEPTION_HANDLED before setting up handler.
8643                    // If the callback raises, the handler is NOT set up and the
8644                    // new exception propagates instead.
8645                    if vm.state.monitoring_events.load() & MonitoringEvent::ExceptionHandled.mask()
8646                        != 0
8647                    {
8648                        let byte_offset = offset * 2;
8649                        let exc_obj: PyObjectRef = exception.clone().into();
8650                        monitoring::fire_exception_handled(vm, self.code, byte_offset, &exc_obj)?;
8651                    }
8652
8653                    // 1. Pop stack to entry.depth
8654                    while self.localsplus.stack_len() > entry.depth as usize {
8655                        let _ = self.localsplus.stack_pop();
8656                    }
8657
8658                    // 2. If push_lasti=true, push the current lasti (which
8659                    // RERAISE may have restored to the original raise).
8660                    if entry.push_lasti {
8661                        let lasti = self.lasti().saturating_sub(1);
8662                        self.push_value(vm.ctx.new_int(lasti as i32).into());
8663                    }
8664
8665                    // 3. Push exception onto stack
8666                    // always push exception, PUSH_EXC_INFO transforms [exc] -> [prev_exc, exc]
8667                    // Do NOT call vm.set_exception here! PUSH_EXC_INFO will do it.
8668                    // PUSH_EXC_INFO needs to get prev_exc from vm.current_exception() BEFORE setting the new one.
8669                    self.push_value(exception.into());
8670
8671                    // 4. Jump to handler
8672                    self.jump(bytecode::Label::from_u32(entry.target));
8673
8674                    Ok(None)
8675                } else {
8676                    // No handler found, propagate exception
8677                    Err(exception)
8678                }
8679            }
8680            UnwindReason::Returning { value } => Ok(Some(ExecutionResult::Return(value))),
8681        }
8682    }
8683
8684    /// Restore lasti from the exception-table value still on the stack.
8685    ///
8686    /// `oparg` is RERAISE's operand: values[0] is lasti when oparg != 0.
8687    /// lasti is stored as the next instruction index, so the saved index is
8688    /// incremented by one.
8689    fn restore_reraise_lasti(&mut self, oparg: u32) {
8690        if oparg == 0 {
8691            return;
8692        }
8693        let stack_len = self.localsplus.stack_len();
8694        if stack_len < oparg as usize {
8695            return;
8696        }
8697        let Some(lasti_ref) = self.localsplus.stack_index(stack_len - oparg as usize) else {
8698            return;
8699        };
8700        let Some(int) = lasti_ref.as_object().downcast_ref::<PyInt>() else {
8701            return;
8702        };
8703        let Some(idx) = int.as_bigint().to_u32() else {
8704            return;
8705        };
8706        self.lasti.store(idx.saturating_add(1), Relaxed);
8707    }
8708
8709    fn execute_store_subscript(&mut self, vm: &VirtualMachine) -> FrameResult {
8710        let idx = self.pop_stackref();
8711        let obj = self.pop_stackref();
8712        let value = self.pop_value();
8713        obj.set_item(idx.as_object(), value, vm)?;
8714        Ok(None)
8715    }
8716
8717    fn execute_delete_subscript(&mut self, vm: &VirtualMachine) -> FrameResult {
8718        let idx = self.pop_stackref();
8719        let obj = self.pop_stackref();
8720        obj.del_item(idx.as_object(), vm)?;
8721        Ok(None)
8722    }
8723
8724    fn execute_build_map(&mut self, vm: &VirtualMachine, size: u32) -> FrameResult {
8725        let size = size as usize;
8726        let map_obj = vm.ctx.new_dict();
8727        for (key, value) in self.pop_multiple(2 * size).tuples() {
8728            map_obj.set_item(&*key, value, vm)?;
8729        }
8730
8731        self.push_value(map_obj.into());
8732        Ok(None)
8733    }
8734
8735    fn execute_build_slice(
8736        &mut self,
8737        vm: &VirtualMachine,
8738        argc: bytecode::BuildSliceArgCount,
8739    ) -> Option<ExecutionResult> {
8740        let step = match argc {
8741            bytecode::BuildSliceArgCount::Two => None,
8742            bytecode::BuildSliceArgCount::Three => Some(self.pop_value()),
8743        };
8744        let stop = self.pop_value();
8745        let start = self.pop_value();
8746
8747        let obj = PySlice {
8748            start: Some(start),
8749            stop,
8750            step,
8751        }
8752        .into_ref(&vm.ctx);
8753        self.push_value(obj.into());
8754        None
8755    }
8756
8757    fn collect_positional_args(&mut self, nargs: u32) -> FuncArgs {
8758        FuncArgs {
8759            args: self.pop_multiple(nargs as usize).collect(),
8760            ..Default::default()
8761        }
8762    }
8763
8764    fn collect_keyword_args(&mut self, nargs: u32) -> FuncArgs {
8765        let kwarg_names = self
8766            .pop_value()
8767            .downcast::<PyTuple>()
8768            .expect("kwarg names should be tuple of strings");
8769        let args = self.pop_multiple(nargs as usize);
8770
8771        let kwarg_names = kwarg_names.as_slice().iter().map(|pyobj| {
8772            pyobj
8773                .downcast_ref::<PyUtf8Str>()
8774                .unwrap()
8775                .as_str()
8776                .to_owned()
8777        });
8778        FuncArgs::with_kwargs_names(args, kwarg_names)
8779    }
8780
8781    fn collect_ex_args(&mut self, vm: &VirtualMachine) -> PyResult<FuncArgs> {
8782        let kwargs_or_null = self.pop_value_opt();
8783        let mut kwargs = KwArgs::default();
8784        if let Some(kw_obj) = kwargs_or_null {
8785            // Stack: [callable, self_or_null, args_tuple]
8786            let callable = self.nth_value(2);
8787            let func_str = Self::object_function_str(callable, vm);
8788
8789            Self::iterate_mapping_keys(vm, &kw_obj, &func_str, |key, value| {
8790                // `PyStr`, not `PyUtf8Str`: CPython only checks that the key is a
8791                // `str`, not that it is valid UTF-8, so surrogate keys are accepted.
8792                let key_str = key
8793                    .downcast_ref::<PyStr>()
8794                    .ok_or_else(|| vm.new_type_error("keywords must be strings"))?;
8795                kwargs.insert(key_str.as_wtf8().to_owned(), value);
8796                Ok(())
8797            })?
8798        };
8799
8800        let args_obj = self.pop_value();
8801        let args = if let Some(tuple) = args_obj.downcast_ref::<PyTuple>() {
8802            tuple.as_slice().to_vec()
8803        } else {
8804            // Single *arg passed directly; convert to sequence at runtime.
8805            // Stack: [callable, self_or_null]
8806            let callable = self.nth_value(1);
8807            let func_str = Self::object_function_str(callable, vm);
8808            let not_iterable = args_obj.class().slots().iter.load().is_none()
8809                && args_obj
8810                    .get_class_attr(vm.ctx.intern_str("__getitem__"))
8811                    .is_none();
8812            args_obj.try_to_value::<Vec<PyObjectRef>>(vm).map_err(|e| {
8813                if not_iterable && e.class().is(vm.ctx.exceptions.type_error) {
8814                    vm.new_type_error(format!(
8815                        "{} argument after * must be an iterable, not {}",
8816                        func_str,
8817                        args_obj.class().slot_name()
8818                    ))
8819                } else {
8820                    e
8821                }
8822            })?
8823        };
8824        Ok(FuncArgs { args, kwargs })
8825    }
8826
8827    /// Returns a display string for a callable object for use in error messages.
8828    /// For objects with `__qualname__`, returns "module.qualname()" or "qualname()".
8829    /// For other objects, returns repr(obj).
8830    fn object_function_str(obj: &PyObject, vm: &VirtualMachine) -> Wtf8Buf {
8831        let repr_fallback = || {
8832            obj.repr(vm)
8833                .as_ref()
8834                .map_or_else(|_| "?".as_ref(), |s| s.as_wtf8())
8835                .to_owned()
8836        };
8837        let Ok(qualname) = obj.get_attr(vm.ctx.intern_str("__qualname__"), vm) else {
8838            return repr_fallback();
8839        };
8840        let Some(qualname_str) = qualname.downcast_ref::<PyStr>() else {
8841            return repr_fallback();
8842        };
8843        if let Ok(module) = obj.get_attr(vm.ctx.intern_str("__module__"), vm)
8844            && let Some(module_str) = module.downcast_ref::<PyStr>()
8845            && module_str.as_bytes() != b"builtins"
8846        {
8847            return wtf8_concat!(module_str.as_wtf8(), ".", qualname_str.as_wtf8(), "()");
8848        }
8849        wtf8_concat!(qualname_str.as_wtf8(), "()")
8850    }
8851
8852    /// Helper function to iterate over mapping keys using the keys() method.
8853    /// This ensures proper order preservation for OrderedDict and other custom mappings.
8854    fn iterate_mapping_keys<F>(
8855        vm: &VirtualMachine,
8856        mapping: &PyObject,
8857        func_str: &Wtf8,
8858        mut key_handler: F,
8859    ) -> PyResult<()>
8860    where
8861        F: FnMut(PyObjectRef, PyObjectRef) -> PyResult<()>,
8862    {
8863        // Fast path: exact dict (e.g. built by DICT_MERGE), not a subclass which
8864        // may override `keys`/`__getitem__`. Iterate its entries natively,
8865        // mirroring CPython's `PyDict_Check(kwargs)` fast path in `CALL_FUNCTION_EX`.
8866        if mapping.class().is(vm.ctx.types.dict_type) {
8867            let dict = mapping
8868                .downcast_ref::<PyDict>()
8869                .expect("exact dict must have a PyDict payload");
8870            // Snapshot under a single read lock: safe against mutation of
8871            // `mapping` from within `key_handler`.
8872            for (key, value) in dict.items_vec() {
8873                key_handler(key, value)?;
8874            }
8875            return Ok(());
8876        }
8877
8878        let Some(keys_method) = vm.get_method(mapping.to_owned(), vm.ctx.intern_str("keys")) else {
8879            return Err(vm.new_type_error(format!(
8880                "{} argument after ** must be a mapping, not {}",
8881                func_str,
8882                mapping.class().name()
8883            )));
8884        };
8885
8886        let keys = PyIter::try_from_object(vm, keys_method?.call((), vm)?)?;
8887        while let PyIterReturn::Return(key) = keys.next(vm)? {
8888            let value = mapping.get_item(&*key, vm)?;
8889            key_handler(key, value)?;
8890        }
8891        Ok(())
8892    }
8893
8894    /// Vectorcall dispatch for Instruction::Call (positional args only).
8895    /// Uses vectorcall slot if available, otherwise falls back to FuncArgs.
8896    #[inline]
8897    fn execute_call_vectorcall(&mut self, nargs: u32, vm: &VirtualMachine) -> FrameResult {
8898        let nargs_usize = nargs as usize;
8899        let stack_len = self.localsplus.stack_len();
8900        debug_assert!(
8901            stack_len >= nargs_usize + 2,
8902            "CALL stack underflow: need callable + self_or_null + {nargs_usize} args, have {stack_len}"
8903        );
8904        let callable_idx = stack_len - nargs_usize - 2;
8905        let self_or_null_idx = stack_len - nargs_usize - 1;
8906        let args_start = stack_len - nargs_usize;
8907
8908        // Build args: [self?, arg1, ..., argN]
8909        let self_or_null = self
8910            .localsplus
8911            .stack_index_mut(self_or_null_idx)
8912            .take()
8913            .map(|sr| sr.to_pyobj());
8914        let has_self = self_or_null.is_some();
8915
8916        let effective_nargs = if has_self {
8917            nargs_usize + 1
8918        } else {
8919            nargs_usize
8920        };
8921        let mut args_vec = Vec::with_capacity(effective_nargs);
8922        if let Some(self_val) = self_or_null {
8923            args_vec.push(self_val);
8924        }
8925        for stack_idx in args_start..stack_len {
8926            let val = self
8927                .localsplus
8928                .stack_index_mut(stack_idx)
8929                .take()
8930                .unwrap()
8931                .to_pyobj();
8932            args_vec.push(val);
8933        }
8934
8935        let callable_obj = self
8936            .localsplus
8937            .stack_index_mut(callable_idx)
8938            .take()
8939            .unwrap()
8940            .to_pyobj();
8941        self.localsplus.stack_truncate(callable_idx);
8942
8943        // invoke_vectorcall falls back to FuncArgs if no vectorcall slot
8944        let result = callable_obj.vectorcall(args_vec, effective_nargs, None, vm)?;
8945        self.push_value(result);
8946        Ok(None)
8947    }
8948
8949    /// Vectorcall dispatch for Instruction::CallKw (positional + keyword args).
8950    #[inline]
8951    fn execute_call_kw_vectorcall(&mut self, nargs: u32, vm: &VirtualMachine) -> FrameResult {
8952        let nargs_usize = nargs as usize;
8953
8954        // Pop kwarg_names tuple from top of stack
8955        let kwarg_names_obj = self.pop_value();
8956        let kwarg_names_tuple = kwarg_names_obj
8957            .downcast_ref::<PyTuple>()
8958            .expect("kwarg names should be tuple");
8959        let kw_count = kwarg_names_tuple.as_slice().len();
8960        debug_assert!(kw_count <= nargs_usize, "CALL_KW kw_count exceeds nargs");
8961
8962        let stack_len = self.localsplus.stack_len();
8963        debug_assert!(
8964            stack_len >= nargs_usize + 2,
8965            "CALL_KW stack underflow: need callable + self_or_null + {nargs_usize} args, have {stack_len}"
8966        );
8967        let callable_idx = stack_len - nargs_usize - 2;
8968        let self_or_null_idx = stack_len - nargs_usize - 1;
8969        let args_start = stack_len - nargs_usize;
8970
8971        // Build args: [self?, pos_arg1, ..., pos_argM, kw_val1, ..., kw_valK]
8972        let self_or_null = self
8973            .localsplus
8974            .stack_index_mut(self_or_null_idx)
8975            .take()
8976            .map(|sr| sr.to_pyobj());
8977        let has_self = self_or_null.is_some();
8978
8979        let pos_count = nargs_usize
8980            .checked_sub(kw_count)
8981            .expect("CALL_KW: kw_count exceeds nargs");
8982        let effective_nargs = if has_self { pos_count + 1 } else { pos_count };
8983
8984        // Build the full args slice: positional (including self) + kwarg values
8985        let total_args = effective_nargs + kw_count;
8986        let mut args_vec = Vec::with_capacity(total_args);
8987        if let Some(self_val) = self_or_null {
8988            args_vec.push(self_val);
8989        }
8990        for stack_idx in args_start..stack_len {
8991            let val = self
8992                .localsplus
8993                .stack_index_mut(stack_idx)
8994                .take()
8995                .unwrap()
8996                .to_pyobj();
8997            args_vec.push(val);
8998        }
8999
9000        let callable_obj = self
9001            .localsplus
9002            .stack_index_mut(callable_idx)
9003            .take()
9004            .unwrap()
9005            .to_pyobj();
9006        self.localsplus.stack_truncate(callable_idx);
9007
9008        // invoke_vectorcall falls back to FuncArgs if no vectorcall slot
9009        let kwnames = kwarg_names_tuple.as_slice();
9010        let result = callable_obj.vectorcall(args_vec, effective_nargs, Some(kwnames), vm)?;
9011        self.push_value(result);
9012        Ok(None)
9013    }
9014
9015    #[inline]
9016    fn execute_call(&mut self, args: FuncArgs, vm: &VirtualMachine) -> FrameResult {
9017        // Stack: [callable, self_or_null, ...]
9018        let self_or_null = self.pop_value_opt(); // Option<PyObjectRef>
9019        let callable = self.pop_value();
9020
9021        let final_args = if let Some(self_val) = self_or_null {
9022            let mut args = args;
9023            args.prepend_arg(self_val);
9024            args
9025        } else {
9026            args
9027        };
9028
9029        let value = callable.call(final_args, vm)?;
9030        self.push_value(value);
9031        Ok(None)
9032    }
9033
9034    /// Instrumented version of execute_call: fires CALL, C_RETURN, and C_RAISE events.
9035    fn execute_call_instrumented(&mut self, args: FuncArgs, vm: &VirtualMachine) -> FrameResult {
9036        let self_or_null = self.pop_value_opt();
9037        let callable = self.pop_value();
9038
9039        let final_args = if let Some(self_val) = self_or_null {
9040            let mut args = args;
9041            args.prepend_arg(self_val);
9042            args
9043        } else {
9044            args
9045        };
9046
9047        let is_python_call = callable.downcast_ref_if_exact::<PyFunction>(vm).is_some();
9048
9049        // Fire CALL event
9050        let call_arg0 = if self.monitoring_mask & MonitoringEvent::Call.mask() != 0 {
9051            let arg0 = final_args
9052                .args
9053                .first()
9054                .cloned()
9055                .unwrap_or_else(|| monitoring::get_missing(vm));
9056            let offset = (self.lasti() - 1) * 2;
9057            monitoring::fire_call(vm, self.code, offset, &callable, arg0.clone())?;
9058            Some(arg0)
9059        } else {
9060            None
9061        };
9062
9063        match callable.call(final_args, vm) {
9064            Ok(value) => {
9065                if let Some(arg0) = call_arg0
9066                    && !is_python_call
9067                {
9068                    let offset = (self.lasti() - 1) * 2;
9069                    monitoring::fire_c_return(vm, self.code, offset, &callable, arg0)?;
9070                }
9071                self.push_value(value);
9072                Ok(None)
9073            }
9074            Err(exc) => {
9075                let exc = if let Some(arg0) = call_arg0
9076                    && !is_python_call
9077                {
9078                    let offset = (self.lasti() - 1) * 2;
9079                    match monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0) {
9080                        Ok(()) => exc,
9081                        Err(monitor_exc) => monitor_exc,
9082                    }
9083                } else {
9084                    exc
9085                };
9086                Err(exc)
9087            }
9088        }
9089    }
9090
9091    fn execute_raise(&mut self, vm: &VirtualMachine, kind: bytecode::RaiseKind) -> FrameResult {
9092        let cause = match kind {
9093            bytecode::RaiseKind::RaiseCause => {
9094                let val = self.pop_value();
9095                Some(if vm.is_none(&val) {
9096                    // if the cause arg is none, we clear the cause
9097                    None
9098                } else {
9099                    // if the cause arg is an exception, we overwrite it
9100                    let ctor = ExceptionCtor::try_from_object(vm, val).map_err(|_| {
9101                        vm.new_type_error("exception causes must derive from BaseException")
9102                    })?;
9103                    Some(ctor.instantiate(vm)?)
9104                })
9105            }
9106            // if there's no cause arg, we keep the cause as is
9107            _ => None,
9108        };
9109        let exception = match kind {
9110            bytecode::RaiseKind::RaiseCause | bytecode::RaiseKind::Raise => {
9111                ExceptionCtor::try_from_object(vm, self.pop_value())?.instantiate(vm)?
9112            }
9113            bytecode::RaiseKind::BareRaise => {
9114                // RAISE_VARARGS 0: bare `raise` gets exception from VM state
9115                vm.topmost_exception()
9116                    .ok_or_else(|| vm.new_runtime_error("No active exception to reraise"))?
9117            }
9118            bytecode::RaiseKind::ReraiseFromStack => {
9119                // RERAISE: gets exception from stack top
9120                // Used in cleanup blocks where exception is on stack after COPY 3
9121                let exc = self.pop_value();
9122                exc.downcast::<PyBaseException>().map_err(|obj| {
9123                    vm.new_type_error(format!(
9124                        "exceptions must derive from BaseException, not {}",
9125                        obj.class().name()
9126                    ))
9127                })?
9128            }
9129        };
9130        #[cfg(debug_assertions)]
9131        debug!("Exception raised: {exception:?} with cause: {cause:?}");
9132        if let Some(cause) = cause {
9133            exception.set_cause(cause);
9134        }
9135        Err(exception)
9136    }
9137
9138    /// The continuation for parking this frame at the `SEND` it is executing,
9139    /// if handing `receiver` to the trampoline is worth it: see `GenCont` for
9140    /// the shape this needs, and `is_delegating` for why one lone level of
9141    /// delegation keeps the recursive path.
9142    #[inline(never)]
9143    fn send_yield_from_cont(
9144        &self,
9145        receiver: &PyObject,
9146        exit_label: bytecode::Label,
9147    ) -> Option<GenCont> {
9148        let next_idx = self.lasti() as usize + 1;
9149        let unit = self.code.instructions.get(next_idx)?;
9150        if !matches!(unit.op, Instruction::YieldValue { .. }) || u8::from(unit.arg) < 1 {
9151            return None;
9152        }
9153        if !self.builtin_coro(receiver).is_some_and(is_delegating) {
9154            return None;
9155        }
9156        Some(GenCont {
9157            exit: exit_label.as_u32(),
9158            resumed_at: next_idx as u32 + 1,
9159        })
9160    }
9161
9162    fn builtin_coro<'a>(&self, coro: &'a PyObject) -> Option<&'a Coro> {
9163        match_class!(match coro {
9164            ref g @ PyGenerator => Some(g.as_coro()),
9165            ref c @ PyCoroutine => Some(c.as_coro()),
9166            _ => None,
9167        })
9168    }
9169
9170    fn _send(
9171        &self,
9172        jen: &PyObject,
9173        val: PyObjectRef,
9174        vm: &VirtualMachine,
9175    ) -> PyResult<PyIterReturn> {
9176        match self.builtin_coro(jen) {
9177            Some(coro) => coro.send(jen, val, vm),
9178            // TODO: turn return type to PyResult<PyIterReturn> then ExecutionResult will be simplified
9179            None if vm.is_none(&val) => PyIter::new(jen).next(vm),
9180            None => {
9181                let meth = jen.get_attr("send", vm)?;
9182                PyIterReturn::from_pyresult(meth.call((val,), vm), vm)
9183            }
9184        }
9185    }
9186
9187    fn execute_unpack_ex(&mut self, vm: &VirtualMachine, before: u8, after: u32) -> FrameResult {
9188        let (before, after) = (before as usize, after as usize);
9189        let value = self.pop_value();
9190        let not_iterable = value.class().slots().iter.load().is_none()
9191            && value
9192                .get_class_attr(vm.ctx.intern_str("__getitem__"))
9193                .is_none();
9194        let elements: Vec<_> = value.try_to_value(vm).map_err(|e| {
9195            if not_iterable && e.class().is(vm.ctx.exceptions.type_error) {
9196                vm.new_type_error(format!(
9197                    "cannot unpack non-iterable {} object",
9198                    value.class().name()
9199                ))
9200            } else {
9201                e
9202            }
9203        })?;
9204        let min_expected = before + after;
9205
9206        let middle = elements.len().checked_sub(min_expected).ok_or_else(|| {
9207            vm.new_value_error(format!(
9208                "not enough values to unpack (expected at least {}, got {})",
9209                min_expected,
9210                elements.len()
9211            ))
9212        })?;
9213
9214        let mut elements = elements;
9215        // Elements on stack from right-to-left:
9216        self.localsplus.stack_extend(
9217            elements
9218                .drain(before + middle..)
9219                .rev()
9220                .map(|e| Some(PyStackRef::new_owned(e))),
9221        );
9222
9223        let middle_elements = elements.drain(before..).collect();
9224        let t = vm.ctx.new_list(middle_elements);
9225        self.push_value(t.into());
9226
9227        // Lastly the first reversed values:
9228        self.localsplus.stack_extend(
9229            elements
9230                .into_iter()
9231                .rev()
9232                .map(|e| Some(PyStackRef::new_owned(e))),
9233        );
9234
9235        Ok(None)
9236    }
9237
9238    #[inline]
9239    fn jump(&mut self, label: bytecode::Label) {
9240        let target_pc = label.as_u32();
9241        vm_trace!("jump from {:?} to {:?}", self.lasti(), target_pc);
9242        self.update_lasti(|i| *i = target_pc);
9243    }
9244
9245    /// Jump forward by `delta` code units from after instruction + caches.
9246    /// lasti is already at instruction_index + 1, so after = lasti + caches.
9247    ///
9248    /// Unchecked arithmetic is intentional: the compiler guarantees valid
9249    /// targets, and debug builds will catch overflow via Rust's default checks.
9250    #[inline]
9251    fn jump_relative_forward(&mut self, delta: u32, caches: u32) {
9252        let target = self.lasti() + caches + delta;
9253        self.update_lasti(|i| *i = target);
9254    }
9255
9256    /// Jump backward by `delta` code units from after instruction + caches.
9257    ///
9258    /// Unchecked arithmetic is intentional: the compiler guarantees valid
9259    /// targets, and debug builds will catch underflow via Rust's default checks.
9260    #[inline]
9261    fn jump_relative_backward(&mut self, delta: u32, caches: u32) {
9262        let target = self.lasti() + caches - delta;
9263        self.update_lasti(|i| *i = target);
9264    }
9265
9266    /// JUMP_BACKWARD plus the settrace LINE event for a backward edge that
9267    /// stays on the same source line (`sys_trace_jump_func`).
9268    fn jump_relative_backward_and_trace_line(
9269        &mut self,
9270        delta: u32,
9271        caches: u32,
9272        vm: &VirtualMachine,
9273    ) -> PyResult<()> {
9274        let from_idx = self.lasti().saturating_sub(1) as usize;
9275        self.jump_relative_backward(delta, caches);
9276        self.trace_backward_same_line(from_idx, vm)
9277    }
9278
9279    /// sys.settrace generates line events for all backward edges, even if on
9280    /// the same line.
9281    fn trace_backward_same_line(&mut self, from_idx: usize, vm: &VirtualMachine) -> PyResult<()> {
9282        if !(vm.use_tracing.get() && self.trace_is_set(vm) && self.trace_lines_is_set()) {
9283            return Ok(());
9284        }
9285        let to_idx = self.lasti() as usize;
9286        let from_line = self.code.addr2line(from_idx as i32 * 2);
9287        let to_line = self.code.addr2line(to_idx as i32 * 2);
9288        if to_line >= 0 && to_line == from_line {
9289            self.prev_line.set(to_line as u32);
9290            // lasti currently names the destination instruction; f_lineno
9291            // reads lasti-1 (the in-flight opcode), so point past dest for
9292            // the duration of the callback.
9293            let dest = self.lasti();
9294            self.update_lasti(|i| *i = dest + 1);
9295            let result = vm.trace_event(crate::protocol::TraceEvent::Line, None);
9296            self.update_lasti(|i| *i = dest);
9297            result?;
9298        }
9299        Ok(())
9300    }
9301
9302    /// Step over the `NOT_TAKEN` marker on a conditional jump's fall-through edge.
9303    ///
9304    /// The compiler plants `NOT_TAKEN` after every conditional jump purely as the
9305    /// anchor `sys.monitoring` swaps for `INSTRUMENTED_NOT_TAKEN` when branch
9306    /// events are on. Left alone it is a no-op, so land past it instead of
9307    /// spending a whole dispatch on it. `caches` is the jump's own inline-cache
9308    /// count, which the fall-through path would otherwise leave for the dispatch
9309    /// loop to add.
9310    ///
9311    /// Skipped while tracing, where the dispatch loop is what emits per-opcode
9312    /// `sys.settrace` events and every executed instruction has to be seen.
9313    #[inline]
9314    fn skip_fallthrough_not_taken(&mut self, vm: &VirtualMachine, caches: u32) {
9315        if self.specialization_eval_frame_active(vm) {
9316            return;
9317        }
9318        let next = self.lasti() + caches;
9319        if (next as usize) < self.code.instructions.len()
9320            && matches!(
9321                self.code.instructions.read_op(next as usize),
9322                Instruction::NotTaken
9323            )
9324        {
9325            self.update_lasti(|i| *i = next + 1);
9326        }
9327    }
9328
9329    #[inline]
9330    fn pop_jump_if_relative(
9331        &mut self,
9332        vm: &VirtualMachine,
9333        arg: bytecode::OpArg,
9334        caches: u32,
9335        flag: bool,
9336    ) -> FrameResult {
9337        let obj = self.pop_stackref();
9338        let value = obj.try_to_bool(vm)?;
9339        if value == flag {
9340            self.jump_relative_forward(u32::from(arg), caches);
9341        } else {
9342            self.skip_fallthrough_not_taken(vm, caches);
9343        }
9344        Ok(None)
9345    }
9346
9347    /// `_PyEval_MonitorRaise` for a StopIteration produced by FOR_ITER.
9348    /// Sequence iterators match FOR_ITER_LIST/RANGE/TUPLE and do not raise.
9349    /// Generators still report RAISE and the settrace exception event
9350    /// (Internal StopIteration).
9351    fn monitor_for_iter_stop(
9352        &self,
9353        value: Option<PyObjectRef>,
9354        vm: &VirtualMachine,
9355    ) -> PyResult<()> {
9356        let iter = self.top_value();
9357        if iter.downcast_ref_if_exact::<PyListIterator>(vm).is_some()
9358            || iter.downcast_ref_if_exact::<PyRangeIterator>(vm).is_some()
9359            || iter.downcast_ref_if_exact::<PyTupleIterator>(vm).is_some()
9360        {
9361            return Ok(());
9362        }
9363        let need_raise = vm.state.monitoring_events.load() & MonitoringEvent::Raise.mask() != 0;
9364        let need_trace = vm.use_tracing.get() && self.trace_is_set(vm);
9365        if !need_raise && !need_trace {
9366            return Ok(());
9367        }
9368        let stop_exc = vm.new_stop_iteration(value);
9369        if need_raise {
9370            let offset = (self.lasti() - 1) * 2;
9371            let exc_obj: PyObjectRef = stop_exc.clone().into();
9372            monitoring::fire_raise(vm, self.code, offset, &exc_obj)?;
9373        }
9374        if need_trace {
9375            self.fire_exception_trace(&stop_exc, vm)?;
9376        }
9377        Ok(())
9378    }
9379
9380    /// Advance the iterator on top of stack.
9381    /// Returns `true` if iteration continued (item pushed), `false` if exhausted (jumped).
9382    fn execute_for_iter(
9383        &mut self,
9384        vm: &VirtualMachine,
9385        target: bytecode::Label,
9386    ) -> Result<bool, PyBaseExceptionRef> {
9387        let top = self.top_value();
9388
9389        // FOR_ITER_RANGE: bypass generic iterator protocol for range iterators
9390        if let Some(range_iter) = top.downcast_ref_if_exact::<PyRangeIterator>(vm) {
9391            if let Some(value) = range_iter.fast_next() {
9392                self.push_value(vm.ctx.new_int(value).into());
9393                return Ok(true);
9394            }
9395            if vm.use_tracing.get() && self.trace_is_set(vm) {
9396                let stop_exc = vm.new_stop_iteration(None);
9397                self.fire_exception_trace(&stop_exc, vm)?;
9398            }
9399            self.jump(self.for_iter_jump_target(target));
9400            return Ok(false);
9401        }
9402
9403        let top_of_stack = PyIter::new(top);
9404        let next_obj = top_of_stack.next(vm);
9405
9406        match next_obj {
9407            Ok(PyIterReturn::Return(value)) => {
9408                self.push_value(value);
9409                Ok(true)
9410            }
9411            Ok(PyIterReturn::StopIteration(value)) => {
9412                // _FOR_ITER / INSTRUMENTED_FOR_ITER: _PyEval_MonitorRaise
9413                // then clear the StopIteration and jump over END_FOR.
9414                self.monitor_for_iter_stop(value, vm)?;
9415                self.jump(self.for_iter_jump_target(target));
9416                Ok(false)
9417            }
9418            Err(next_error) => {
9419                self.pop_stackref();
9420                Err(next_error)
9421            }
9422        }
9423    }
9424
9425    /// Compute the jump target for FOR_ITER exhaustion: skip END_FOR and jump to POP_ITER.
9426    fn for_iter_jump_target(&self, target: bytecode::Label) -> bytecode::Label {
9427        let target_idx = target.as_usize();
9428        if let Some(unit) = self.code.instructions.get(target_idx)
9429            && matches!(
9430                unit.op,
9431                bytecode::Instruction::EndFor | bytecode::Instruction::InstrumentedEndFor
9432            )
9433        {
9434            return bytecode::Label::from_u32(target.as_u32() + 1);
9435        }
9436        target
9437    }
9438    fn execute_make_function(&mut self, vm: &VirtualMachine) -> FrameResult {
9439        // MakeFunction only takes code object, no flags
9440        let code_obj: PyRef<PyCode> = self
9441            .pop_value()
9442            .downcast()
9443            .expect("Stack value should be code object");
9444
9445        // Create function with minimal attributes
9446        let func_obj = PyFunction::new(code_obj, self.globals.to_owned(), vm)?.into_pyobject(vm);
9447
9448        self.push_value(func_obj);
9449        Ok(None)
9450    }
9451
9452    fn execute_set_function_attribute(
9453        &mut self,
9454        vm: &VirtualMachine,
9455        attr: bytecode::MakeFunctionFlag,
9456    ) -> FrameResult {
9457        // SET_FUNCTION_ATTRIBUTE sets attributes on a function
9458        // Stack: [..., attr_value, func] -> [..., func]
9459        // Stack order: func is at -1, attr_value is at -2
9460
9461        let func = self.pop_value_opt();
9462        let attr_value = expect_unchecked(self.replace_top(func), "attr_value must not be null");
9463
9464        let func = self.top_value();
9465        // Get the function reference and call the new method
9466        let func_ref = func
9467            .downcast_ref_if_exact::<PyFunction>(vm)
9468            .expect("SET_FUNCTION_ATTRIBUTE expects function on stack");
9469
9470        let payload: &PyFunction = func_ref.payload();
9471        // SetFunctionAttribute always follows MakeFunction, so at this point
9472        // there are no other references to func. It is therefore safe to treat it as mutable.
9473        unsafe {
9474            let payload_ptr = payload as *const PyFunction as *mut PyFunction;
9475            (*payload_ptr).set_function_attribute(attr, attr_value, vm)?;
9476        };
9477
9478        Ok(None)
9479    }
9480
9481    #[cfg_attr(feature = "flame-it", flame("FrameObject"))]
9482    fn execute_bin_op(&mut self, vm: &VirtualMachine, op: bytecode::BinaryOperator) -> FrameResult {
9483        let b = self.pop_stackref();
9484        let a = self.pop_stackref();
9485        let (a_ref, b_ref) = (a.as_object(), b.as_object());
9486        let value = match op {
9487            // Exact-int fast paths for +, -, *, //, %: bypass binary_op1
9488            // dispatch and use i64 arithmetic when possible to avoid BigInt
9489            // heap allocation, falling back to the slow path otherwise.
9490            bytecode::BinaryOperator::Add | bytecode::BinaryOperator::InplaceAdd => {
9491                if let (Some(a), Some(b)) = (
9492                    a_ref.downcast_ref_if_exact::<PyInt>(vm),
9493                    b_ref.downcast_ref_if_exact::<PyInt>(vm),
9494                ) {
9495                    Ok(Self::int_add(a, b, vm))
9496                } else if matches!(op, bytecode::BinaryOperator::Add) {
9497                    vm._add(a_ref, b_ref)
9498                } else {
9499                    vm._iadd(a_ref, b_ref)
9500                }
9501            }
9502            bytecode::BinaryOperator::Subtract | bytecode::BinaryOperator::InplaceSubtract => {
9503                if let (Some(a), Some(b)) = (
9504                    a_ref.downcast_ref_if_exact::<PyInt>(vm),
9505                    b_ref.downcast_ref_if_exact::<PyInt>(vm),
9506                ) {
9507                    Ok(Self::int_sub(a, b, vm))
9508                } else if matches!(op, bytecode::BinaryOperator::Subtract) {
9509                    vm._sub(a_ref, b_ref)
9510                } else {
9511                    vm._isub(a_ref, b_ref)
9512                }
9513            }
9514            bytecode::BinaryOperator::Multiply | bytecode::BinaryOperator::InplaceMultiply => {
9515                if let (Some(a), Some(b)) = (
9516                    a_ref.downcast_ref_if_exact::<PyInt>(vm),
9517                    b_ref.downcast_ref_if_exact::<PyInt>(vm),
9518                ) {
9519                    Ok(Self::int_mul(a, b, vm))
9520                } else if matches!(op, bytecode::BinaryOperator::Multiply) {
9521                    vm._mul(a_ref, b_ref)
9522                } else {
9523                    vm._imul(a_ref, b_ref)
9524                }
9525            }
9526            bytecode::BinaryOperator::MatrixMultiply => vm._matmul(a_ref, b_ref),
9527            bytecode::BinaryOperator::Power => vm._pow(a_ref, b_ref, vm.ctx.none.as_object()),
9528            bytecode::BinaryOperator::TrueDivide => vm._truediv(a_ref, b_ref),
9529            bytecode::BinaryOperator::FloorDivide
9530            | bytecode::BinaryOperator::InplaceFloorDivide => {
9531                if let (Some(a), Some(b)) = (
9532                    a_ref.downcast_ref_if_exact::<PyInt>(vm),
9533                    b_ref.downcast_ref_if_exact::<PyInt>(vm),
9534                ) && let Some(result) = Self::int_floordiv(a.as_bigint(), b.as_bigint(), vm)
9535                {
9536                    Ok(result)
9537                } else if matches!(op, bytecode::BinaryOperator::FloorDivide) {
9538                    vm._floordiv(a_ref, b_ref)
9539                } else {
9540                    vm._ifloordiv(a_ref, b_ref)
9541                }
9542            }
9543            bytecode::BinaryOperator::Remainder | bytecode::BinaryOperator::InplaceRemainder => {
9544                if let (Some(a), Some(b)) = (
9545                    a_ref.downcast_ref_if_exact::<PyInt>(vm),
9546                    b_ref.downcast_ref_if_exact::<PyInt>(vm),
9547                ) && let Some(result) = Self::int_mod(a.as_bigint(), b.as_bigint(), vm)
9548                {
9549                    Ok(result)
9550                } else if matches!(op, bytecode::BinaryOperator::Remainder) {
9551                    vm._mod(a_ref, b_ref)
9552                } else {
9553                    vm._imod(a_ref, b_ref)
9554                }
9555            }
9556            bytecode::BinaryOperator::Lshift => vm._lshift(a_ref, b_ref),
9557            bytecode::BinaryOperator::Rshift => vm._rshift(a_ref, b_ref),
9558            bytecode::BinaryOperator::Xor => vm._xor(a_ref, b_ref),
9559            bytecode::BinaryOperator::Or => vm._or(a_ref, b_ref),
9560            bytecode::BinaryOperator::And => vm._and(a_ref, b_ref),
9561            bytecode::BinaryOperator::InplaceMatrixMultiply => vm._imatmul(a_ref, b_ref),
9562            bytecode::BinaryOperator::InplacePower => {
9563                vm._ipow(a_ref, b_ref, vm.ctx.none.as_object())
9564            }
9565            bytecode::BinaryOperator::InplaceTrueDivide => vm._itruediv(a_ref, b_ref),
9566            bytecode::BinaryOperator::InplaceLshift => vm._ilshift(a_ref, b_ref),
9567            bytecode::BinaryOperator::InplaceRshift => vm._irshift(a_ref, b_ref),
9568            bytecode::BinaryOperator::InplaceXor => vm._ixor(a_ref, b_ref),
9569            bytecode::BinaryOperator::InplaceOr => vm._ior(a_ref, b_ref),
9570            bytecode::BinaryOperator::InplaceAnd => vm._iand(a_ref, b_ref),
9571            bytecode::BinaryOperator::Subscr => a_ref.get_item(b_ref.as_object(), vm),
9572        }?;
9573
9574        self.push_value(value);
9575        Ok(None)
9576    }
9577
9578    /// Int binary op with an i64 fast path to avoid BigInt heap allocation.
9579    /// `checked` computes the i64 result; on `None` (either operand does not
9580    /// fit i64, or the op overflows i64) it falls through to `fallback` on the
9581    /// full BigInt values. Result boxing always goes through `new_int` so the
9582    /// small-int cache is consulted identically.
9583    #[inline]
9584    fn int_fast_op(
9585        a: &Py<PyInt>,
9586        b: &Py<PyInt>,
9587        vm: &VirtualMachine,
9588        checked: fn(i64, i64) -> Option<i64>,
9589        fallback: impl FnOnce(&BigInt, &BigInt) -> BigInt,
9590    ) -> PyObjectRef {
9591        if let (Some(av), Some(bv)) = (a.try_to_i64_fast(), b.try_to_i64_fast())
9592            && let Some(result) = checked(av, bv)
9593        {
9594            return vm.ctx.new_int(result).into();
9595        }
9596        vm.ctx
9597            .new_int(fallback(a.as_bigint(), b.as_bigint()))
9598            .into()
9599    }
9600
9601    /// Int addition with i64 fast path to avoid BigInt heap allocation.
9602    #[inline]
9603    fn int_add(a: &Py<PyInt>, b: &Py<PyInt>, vm: &VirtualMachine) -> PyObjectRef {
9604        Self::int_fast_op(a, b, vm, i64::checked_add, |a, b| a + b)
9605    }
9606
9607    /// Int subtraction with i64 fast path to avoid BigInt heap allocation.
9608    #[inline]
9609    fn int_sub(a: &Py<PyInt>, b: &Py<PyInt>, vm: &VirtualMachine) -> PyObjectRef {
9610        Self::int_fast_op(a, b, vm, i64::checked_sub, |a, b| a - b)
9611    }
9612
9613    /// Int multiplication with i64 fast path to avoid BigInt heap allocation.
9614    #[inline]
9615    fn int_mul(a: &Py<PyInt>, b: &Py<PyInt>, vm: &VirtualMachine) -> PyObjectRef {
9616        Self::int_fast_op(a, b, vm, i64::checked_mul, |a, b| a * b)
9617    }
9618
9619    /// Int divide/remainder i64 fast path. Returns `None` to signal the caller
9620    /// to fall through to the slow path when either operand does not fit i64
9621    /// or `compute` reports a case it cannot handle (zero divisor or i64
9622    /// overflow). Result boxing goes through `new_int` so the small-int cache
9623    /// is consulted identically.
9624    #[inline]
9625    fn int_div_fast_op(
9626        a: &BigInt,
9627        b: &BigInt,
9628        vm: &VirtualMachine,
9629        compute: fn(i64, i64) -> Option<i64>,
9630    ) -> Option<PyObjectRef> {
9631        use num_traits::ToPrimitive;
9632        let (av, bv) = (a.to_i64()?, b.to_i64()?);
9633        compute(av, bv).map(|r| vm.ctx.new_int(r).into())
9634    }
9635
9636    /// Floor division of two i64 values with floor (toward negative infinity)
9637    /// semantics. `None` when `b == 0` or the quotient overflows i64
9638    /// (`i64::MIN / -1`).
9639    #[inline]
9640    fn floordiv_i64(a: i64, b: i64) -> Option<i64> {
9641        if b == 0 {
9642            return None;
9643        }
9644        let q = a.checked_div(b)?;
9645        let r = a % b;
9646        Some(if r != 0 && (r < 0) != (b < 0) {
9647            q - 1
9648        } else {
9649            q
9650        })
9651    }
9652
9653    /// Remainder of two i64 values, taking the sign of the divisor. `None`
9654    /// when `b == 0` or the operation overflows i64 (`i64::MIN % -1`).
9655    #[inline]
9656    fn mod_i64(a: i64, b: i64) -> Option<i64> {
9657        if b == 0 {
9658            return None;
9659        }
9660        let r = a.checked_rem(b)?;
9661        Some(if r != 0 && (r < 0) != (b < 0) {
9662            r + b
9663        } else {
9664            r
9665        })
9666    }
9667
9668    /// Int floor division with i64 fast path. `None` falls through to the
9669    /// slow path (bigint operands, zero divisor, or i64 overflow).
9670    #[inline]
9671    fn int_floordiv(a: &BigInt, b: &BigInt, vm: &VirtualMachine) -> Option<PyObjectRef> {
9672        Self::int_div_fast_op(a, b, vm, Self::floordiv_i64)
9673    }
9674
9675    /// Int remainder with i64 fast path. `None` falls through to the slow
9676    /// path (bigint operands, zero divisor, or i64 overflow).
9677    #[inline]
9678    fn int_mod(a: &BigInt, b: &BigInt, vm: &VirtualMachine) -> Option<PyObjectRef> {
9679        Self::int_div_fast_op(a, b, vm, Self::mod_i64)
9680    }
9681
9682    #[cold]
9683    fn setup_annotations(&mut self, vm: &VirtualMachine) -> FrameResult {
9684        let __annotations__ = identifier!(vm, __annotations__);
9685        let locals_obj = self.locals.as_object(vm);
9686        // Try using locals as dict first, if not, fallback to generic method.
9687        let has_annotations = if let Some(d) = locals_obj.downcast_ref_if_exact::<PyDict>(vm) {
9688            d.contains_key(__annotations__, vm)
9689        } else {
9690            self._in(vm, __annotations__.as_object(), locals_obj)?
9691        };
9692        if !has_annotations {
9693            locals_obj.set_item(__annotations__, vm.ctx.new_dict().into(), vm)?;
9694        }
9695        Ok(None)
9696    }
9697
9698    /// _PyEval_UnpackIterableStackRef
9699    fn unpack_sequence(&mut self, size: u32, vm: &VirtualMachine) -> FrameResult {
9700        let value = self.pop_stackref();
9701        let size = size as usize;
9702
9703        // Fast path for exact tuple/list types (not subclasses) — push
9704        // elements directly from the slice without intermediate Vec allocation,
9705        // matching UNPACK_SEQUENCE_TUPLE / UNPACK_SEQUENCE_LIST specializations.
9706        let cls = value.class();
9707        if cls.is(vm.ctx.types.tuple_type) {
9708            let tuple = value.downcast_ref::<PyTuple>().unwrap();
9709            return self.unpack_fast(tuple.as_slice(), size, vm);
9710        }
9711        if cls.is(vm.ctx.types.list_type) {
9712            let list = value.downcast_ref::<PyList>().unwrap();
9713            let borrowed = list.borrow_vec();
9714            return self.unpack_fast(&borrowed, size, vm);
9715        }
9716
9717        // General path — iterate up to `size + 1` elements to avoid
9718        // consuming the entire iterator (fixes hang on infinite sequences).
9719        let not_iterable = value.class().slots().iter.load().is_none()
9720            && value
9721                .get_class_attr(vm.ctx.intern_str("__getitem__"))
9722                .is_none();
9723        let iter = PyIter::try_from_object(vm, value.as_object().to_owned()).map_err(|e| {
9724            if not_iterable && e.class().is(vm.ctx.exceptions.type_error) {
9725                vm.new_type_error(format!(
9726                    "cannot unpack non-iterable {} object",
9727                    value.class().name()
9728                ))
9729            } else {
9730                e
9731            }
9732        })?;
9733
9734        let mut elements = Vec::with_capacity(size);
9735        for _ in 0..size {
9736            match iter.next(vm)? {
9737                PyIterReturn::Return(item) => elements.push(item),
9738                PyIterReturn::StopIteration(_) => {
9739                    return Err(vm.new_value_error(format!(
9740                        "not enough values to unpack (expected {size}, got {})",
9741                        elements.len()
9742                    )));
9743                }
9744            }
9745        }
9746
9747        // Check that the iterator is exhausted.
9748        match iter.next(vm)? {
9749            PyIterReturn::Return(_) => {
9750                // For exact dict types, show "got N" using the container's
9751                // size (PyDict_Size). Exact tuple/list are handled by the
9752                // fast path above and never reach here.
9753                let msg = if value.class().is(vm.ctx.types.dict_type) {
9754                    if let Ok(got) = value.length(vm) {
9755                        if got > size {
9756                            format!("too many values to unpack (expected {size}, got {got})")
9757                        } else {
9758                            format!("too many values to unpack (expected {size})")
9759                        }
9760                    } else {
9761                        format!("too many values to unpack (expected {size})")
9762                    }
9763                } else {
9764                    format!("too many values to unpack (expected {size})")
9765                };
9766                Err(vm.new_value_error(msg))
9767            }
9768            PyIterReturn::StopIteration(_) => {
9769                self.localsplus.stack_extend(
9770                    elements
9771                        .into_iter()
9772                        .rev()
9773                        .map(|e| Some(PyStackRef::new_owned(e))),
9774                );
9775                Ok(None)
9776            }
9777        }
9778    }
9779
9780    fn unpack_fast(
9781        &mut self,
9782        elements: &[PyObjectRef],
9783        size: usize,
9784        vm: &VirtualMachine,
9785    ) -> FrameResult {
9786        match elements.len().cmp(&size) {
9787            core::cmp::Ordering::Equal => {
9788                for elem in elements.iter().rev() {
9789                    self.push_value(elem.clone());
9790                }
9791                Ok(None)
9792            }
9793            core::cmp::Ordering::Greater => Err(vm.new_value_error(format!(
9794                "too many values to unpack (expected {size}, got {})",
9795                elements.len()
9796            ))),
9797            core::cmp::Ordering::Less => Err(vm.new_value_error(format!(
9798                "not enough values to unpack (expected {size}, got {})",
9799                elements.len()
9800            ))),
9801        }
9802    }
9803
9804    fn convert_value(
9805        &mut self,
9806        conversion: bytecode::ConvertValueOparg,
9807        vm: &VirtualMachine,
9808    ) -> FrameResult {
9809        use bytecode::ConvertValueOparg;
9810        let value = self.pop_value();
9811        let value = match conversion {
9812            ConvertValueOparg::Str => value.str(vm)?.into(),
9813            ConvertValueOparg::Repr => value.repr(vm)?.into(),
9814            ConvertValueOparg::Ascii => builtins::ascii(value, vm)?.into(),
9815            ConvertValueOparg::None => value,
9816        };
9817
9818        self.push_value(value);
9819        Ok(None)
9820    }
9821
9822    fn _in(&self, vm: &VirtualMachine, needle: &PyObject, haystack: &PyObject) -> PyResult<bool> {
9823        let found = vm._contains(haystack, needle)?;
9824        Ok(found)
9825    }
9826
9827    #[inline(always)]
9828    fn _not_in(
9829        &self,
9830        vm: &VirtualMachine,
9831        needle: &PyObject,
9832        haystack: &PyObject,
9833    ) -> PyResult<bool> {
9834        Ok(!self._in(vm, needle, haystack)?)
9835    }
9836
9837    #[cfg_attr(feature = "flame-it", flame("FrameObject"))]
9838    fn execute_compare(&mut self, vm: &VirtualMachine, arg: bytecode::OpArg) -> FrameResult {
9839        let op = bytecode::ComparisonOperator::try_from(u32::from(arg))
9840            .unwrap_or(bytecode::ComparisonOperator::Equal);
9841        let b = self.pop_stackref();
9842        let a = self.pop_stackref();
9843        let cmp_op: PyComparisonOp = op.into();
9844        let force_bool = u32::from(arg) & bytecode::oparg::COMPARE_OP_BOOL_MASK != 0;
9845
9846        // COMPARE_OP_INT: leaf type, cannot recurse — skip rich_compare dispatch
9847        if let (Some(a_int), Some(b_int)) = (
9848            a.downcast_ref_if_exact::<PyInt>(vm),
9849            b.downcast_ref_if_exact::<PyInt>(vm),
9850        ) {
9851            let result = cmp_op.eval_ord(a_int.as_bigint().cmp(b_int.as_bigint()));
9852            self.push_value(vm.ctx.new_bool(result).into());
9853            return Ok(None);
9854        }
9855        // COMPARE_OP_FLOAT: leaf type, cannot recurse — skip rich_compare dispatch.
9856        // Falls through on NaN (partial_cmp returns None) for correct != semantics.
9857        if let (Some(a_f), Some(b_f)) = (
9858            a.downcast_ref_if_exact::<PyFloat>(vm),
9859            b.downcast_ref_if_exact::<PyFloat>(vm),
9860        ) && let Some(ord) = a_f.to_f64().partial_cmp(&b_f.to_f64())
9861        {
9862            let result = cmp_op.eval_ord(ord);
9863            self.push_value(vm.ctx.new_bool(result).into());
9864            return Ok(None);
9865        }
9866
9867        let value = a.rich_compare(b.as_object(), cmp_op, vm)?;
9868        let value = if force_bool {
9869            let bool_val = value.try_to_bool(vm)?;
9870            vm.ctx.new_bool(bool_val).into()
9871        } else {
9872            value
9873        };
9874        self.push_value(value);
9875        Ok(None)
9876    }
9877
9878    /// Store an instance attribute through the cached entry index at
9879    /// `cache_base + 3`, refreshing the cache when the hint missed.
9880    fn store_attr_dict_hinted(
9881        &mut self,
9882        dict: &Py<PyDict>,
9883        attr_name: &'static PyStrInterned,
9884        value: PyObjectRef,
9885        cache_base: usize,
9886        vm: &VirtualMachine,
9887    ) -> PyResult<()> {
9888        let hint = self.code.instructions.read_cache_u16(cache_base + 3);
9889        if let Some(new_hint) = dict.set_item_with_hint(attr_name, hint, value, vm)? {
9890            unsafe {
9891                self.code
9892                    .instructions
9893                    .write_cache_u16(cache_base + 3, new_hint);
9894            }
9895        }
9896        Ok(())
9897    }
9898
9899    /// Shadow check for method/nondescriptor loads: return the instance
9900    /// attribute shadowing the cached class attr, or `None` if not shadowed.
9901    ///
9902    /// A keys-version stamp of the instance dict is kept in the pointer cache
9903    /// at `cache_base + 3`. While the dict reports the same stamp, its key
9904    /// set is unchanged since the name was last verified absent, so the probe
9905    /// is skipped. On a verified-absent probe the current stamp is recorded
9906    /// for the next execution.
9907    fn shadowing_instance_attr(
9908        &self,
9909        cache_base: usize,
9910        attr_name: &'static PyStrInterned,
9911        vm: &VirtualMachine,
9912    ) -> PyResult<Option<PyObjectRef>> {
9913        let stamp = self.code.instructions.read_cache_ptr(cache_base + 3);
9914        // Take the stamp check first, on a borrowed dict: a hit is the whole
9915        // fast path, and cloning the dict for it would cost more than the
9916        // comparison it exists to make.
9917        let stamped = self.top_value().with_instance_dict(|dict| {
9918            dict.is_some_and(|d| stamp != 0 && stamp == d.keys_version() as usize)
9919        });
9920        if stamped {
9921            return Ok(None);
9922        }
9923        let Some(dict) = self.top_value().dict() else {
9924            return Ok(None);
9925        };
9926        // Take the stamp before probing so it attests the probed key set.
9927        let stamp = dict.assign_keys_version(vm);
9928        if let Some(value) = dict.get_item_opt(attr_name, vm)? {
9929            return Ok(Some(value));
9930        }
9931        unsafe {
9932            self.code
9933                .instructions
9934                .write_cache_ptr(cache_base + 3, stamp as usize);
9935        }
9936        Ok(None)
9937    }
9938
9939    /// Read a cached descriptor pointer and validate it against the expected
9940    /// type version, using a lock-free double-check pattern:
9941    ///   1. read pointer  →  incref (try_to_owned)
9942    ///   2. re-read version + pointer and confirm they still match
9943    ///
9944    /// This matches the read-side pattern used in LOAD_ATTR_METHOD_WITH_VALUES
9945    /// and friends: no read-side lock, relying on the write side to invalidate
9946    /// the version tag before swapping the pointer.
9947    #[inline]
9948    fn try_read_cached_descriptor(
9949        &self,
9950        cache_base: usize,
9951        expected_type_version: u32,
9952    ) -> Option<PyObjectRef> {
9953        let descr_ptr = self.code.instructions.read_cache_ptr(cache_base + 5);
9954        if descr_ptr == 0 {
9955            return None;
9956        }
9957        // SAFETY: `descr_ptr` was a valid `*mut PyObject` when the writer
9958        // stored it, and the writer keeps a strong reference alive in
9959        // `InlineCacheEntry`.  `try_to_owned_from_ptr` performs a
9960        // conditional incref that fails if the object is already freed.
9961        let cloned = unsafe { PyObject::try_to_owned_from_ptr(descr_ptr as *mut PyObject) }?;
9962        // Double-check: version tag still matches AND pointer unchanged.
9963        if self.code.instructions.read_cache_u32(cache_base + 1) == expected_type_version
9964            && self.code.instructions.read_cache_ptr(cache_base + 5) == descr_ptr
9965        {
9966            Some(cloned)
9967        } else {
9968            drop(cloned);
9969            None
9970        }
9971    }
9972
9973    #[inline]
9974    unsafe fn write_cached_descriptor(
9975        &self,
9976        cache_base: usize,
9977        type_version: u32,
9978        descr_ptr: usize,
9979    ) {
9980        // Publish descriptor cache with version-invalidation protocol:
9981        // invalidate version first, then write payload, then publish version.
9982        // Reader double-checks version+ptr after incref, so no writer lock needed.
9983        unsafe {
9984            self.code.instructions.write_cache_u32(cache_base + 1, 0);
9985            self.code
9986                .instructions
9987                .write_cache_ptr(cache_base + 5, descr_ptr);
9988            self.code
9989                .instructions
9990                .write_cache_u32(cache_base + 1, type_version);
9991        }
9992    }
9993
9994    #[inline]
9995    unsafe fn write_cached_descriptor_with_metaclass(
9996        &self,
9997        cache_base: usize,
9998        type_version: u32,
9999        metaclass_version: u32,
10000        descr_ptr: usize,
10001    ) {
10002        unsafe {
10003            self.code.instructions.write_cache_u32(cache_base + 1, 0);
10004            self.code
10005                .instructions
10006                .write_cache_u32(cache_base + 3, metaclass_version);
10007            self.code
10008                .instructions
10009                .write_cache_ptr(cache_base + 5, descr_ptr);
10010            self.code
10011                .instructions
10012                .write_cache_u32(cache_base + 1, type_version);
10013        }
10014    }
10015
10016    #[inline]
10017    unsafe fn write_cached_binary_op_extend_descr(
10018        &self,
10019        cache_base: usize,
10020        descr: Option<&'static BinaryOpExtendSpecializationDescr>,
10021    ) {
10022        let ptr = descr.map_or(0, |d| {
10023            d as *const BinaryOpExtendSpecializationDescr as usize
10024        });
10025        unsafe {
10026            self.code
10027                .instructions
10028                .write_cache_ptr(cache_base + BINARY_OP_EXTEND_EXTERNAL_CACHE_OFFSET, ptr);
10029        }
10030    }
10031
10032    #[inline]
10033    fn read_cached_binary_op_extend_descr(
10034        &self,
10035        cache_base: usize,
10036    ) -> Option<&'static BinaryOpExtendSpecializationDescr> {
10037        let ptr = self
10038            .code
10039            .instructions
10040            .read_cache_ptr(cache_base + BINARY_OP_EXTEND_EXTERNAL_CACHE_OFFSET);
10041        if ptr == 0 {
10042            return None;
10043        }
10044        // SAFETY: We only store pointers to entries in `BINARY_OP_EXTEND_DESCRIPTORS`.
10045        Some(unsafe { &*(ptr as *const BinaryOpExtendSpecializationDescr) })
10046    }
10047
10048    #[inline]
10049    fn binary_op_extended_specialization(
10050        &self,
10051        op: bytecode::BinaryOperator,
10052        lhs: &PyObject,
10053        rhs: &PyObject,
10054        vm: &VirtualMachine,
10055    ) -> Option<&'static BinaryOpExtendSpecializationDescr> {
10056        BINARY_OP_EXTEND_DESCRIPTORS
10057            .iter()
10058            .find(|d| d.oparg == op && (d.guard)(lhs, rhs, vm))
10059    }
10060
10061    fn load_attr(&mut self, vm: &VirtualMachine, oparg: LoadAttr) -> FrameResult {
10062        self.adaptive(|s, ii, cb| s.specialize_load_attr(vm, oparg, ii, cb));
10063        self.load_attr_slow(vm, oparg)
10064    }
10065
10066    fn specialize_load_attr(
10067        &mut self,
10068        _vm: &VirtualMachine,
10069        oparg: LoadAttr,
10070        instr_idx: usize,
10071        cache_base: usize,
10072    ) {
10073        // Pre-check: bail if already specialized by another thread
10074        if !matches!(
10075            self.code.instructions.read_op(instr_idx),
10076            Instruction::LoadAttr { .. }
10077        ) {
10078            return;
10079        }
10080        let obj = self.top_value();
10081        let cls = obj.class();
10082
10083        // Check if this is a type object (class attribute access)
10084        if obj.downcast_ref::<PyType>().is_some() {
10085            self.specialize_class_load_attr(_vm, oparg, instr_idx, cache_base);
10086            return;
10087        }
10088
10089        // Capture the version before inspecting getattro and the MRO so a
10090        // concurrently installed __getattribute__/__getattr__ invalidates the
10091        // version this specialization is cached against.
10092        let type_version = cls.version_for_specialization(_vm);
10093        if type_version == 0 {
10094            unsafe {
10095                self.code.instructions.write_adaptive_counter(
10096                    cache_base,
10097                    bytecode::adaptive_counter_backoff(
10098                        self.code.instructions.read_adaptive_counter(cache_base),
10099                    ),
10100                );
10101            }
10102            return;
10103        }
10104
10105        let attr_name = self.code.names[oparg.name_idx() as usize];
10106
10107        // Match CPython: only specialize module attribute loads when the
10108        // current module dict has no __getattr__ override and the attribute is
10109        // already present. Modules have their own getattro, so this comes first.
10110        // The module type must not define the name either, so reading the dict
10111        // entry directly gives what the generic lookup would.
10112        if let Some(module) = obj.downcast_ref_if_exact::<PyModule>(_vm) {
10113            let module_dict = module.dict();
10114            if cls.get_attr(attr_name).is_none()
10115                && let Some((keys_version, index)) = module_dict.module_attr_cache(attr_name, _vm)
10116            {
10117                // Keep module guards atomic and separate from every other LOAD_ATTR
10118                // payload: a concurrent reader may already be executing another kind.
10119                unsafe {
10120                    self.code
10121                        .instructions
10122                        .write_cache_ptr(cache_base + 6, type_version as usize);
10123                    self.code
10124                        .instructions
10125                        .write_cache_ptr(cache_base + 7, keys_version as usize);
10126                    self.code
10127                        .instructions
10128                        .write_cache_ptr(cache_base + 8, usize::from(index));
10129                }
10130                self.specialize_at(instr_idx, cache_base, Instruction::LoadAttrModule);
10131            } else {
10132                self.cooldown_adaptive_at(cache_base);
10133            }
10134            return;
10135        }
10136
10137        // Only specialize if getattro is the default (PyBaseObject::getattro)
10138        let is_default_getattro = cls.slots().getattro.load().is_some_and(|f| {
10139            crate::types::fn_addr(f)
10140                == crate::types::fn_addr(PyBaseObject::getattro as crate::types::GetattroFunc)
10141        });
10142        if !is_default_getattro {
10143            let getattribute = cls.get_attr(identifier!(_vm, __getattribute__));
10144            if !oparg.is_method()
10145                && !self.specialization_eval_frame_active(_vm)
10146                && cls.get_attr(identifier!(_vm, __getattr__)).is_none()
10147                && let Some(getattribute) = getattribute
10148                && let Some(func) = getattribute.downcast_ref_if_exact::<PyFunction>(_vm)
10149                && func.can_specialize_call(2)
10150            {
10151                let func_version = func.get_version_for_current_state();
10152                if func_version != 0 {
10153                    let func_ptr = &*getattribute as *const PyObject as usize;
10154                    unsafe {
10155                        self.code
10156                            .instructions
10157                            .write_cache_u32(cache_base + 3, func_version);
10158                        self.write_cached_descriptor(cache_base, type_version, func_ptr);
10159                    }
10160                    self.specialize_at(
10161                        instr_idx,
10162                        cache_base,
10163                        Instruction::LoadAttrGetattributeOverridden,
10164                    );
10165                    return;
10166                }
10167            }
10168            unsafe {
10169                self.code.instructions.write_adaptive_counter(
10170                    cache_base,
10171                    bytecode::adaptive_counter_backoff(
10172                        self.code.instructions.read_adaptive_counter(cache_base),
10173                    ),
10174                );
10175            }
10176            return;
10177        }
10178
10179        let cls_attr = cls.get_attr(attr_name);
10180        let class_has_dict = cls.slots().flags.has_feature(PyTypeFlags::HAS_DICT);
10181
10182        if oparg.is_method() {
10183            // Method specialization
10184            if let Some(ref descr) = cls_attr
10185                && descr
10186                    .class()
10187                    .slots
10188                    .flags
10189                    .has_feature(PyTypeFlags::METHOD_DESCRIPTOR)
10190            {
10191                let descr_ptr = &**descr as *const PyObject as usize;
10192                unsafe {
10193                    self.write_cached_descriptor(cache_base, type_version, descr_ptr);
10194                }
10195
10196                let new_op = if !class_has_dict {
10197                    Instruction::LoadAttrMethodNoDict
10198                } else if obj.dict().is_none() {
10199                    Instruction::LoadAttrMethodLazyDict
10200                } else {
10201                    Instruction::LoadAttrMethodWithValues
10202                };
10203                self.specialize_at(instr_idx, cache_base, new_op);
10204                return;
10205            }
10206            // Can't specialize this method call
10207            unsafe {
10208                self.code.instructions.write_adaptive_counter(
10209                    cache_base,
10210                    bytecode::adaptive_counter_backoff(
10211                        self.code.instructions.read_adaptive_counter(cache_base),
10212                    ),
10213                );
10214            }
10215        } else {
10216            // Regular attribute access
10217            let has_data_descr = cls_attr.as_ref().is_some_and(|descr| {
10218                let descr_cls = descr.class();
10219                descr_cls.slots().descr_get.load().is_some()
10220                    && descr_cls.slots().descr_set.load().is_some()
10221            });
10222            let has_descr_get = cls_attr
10223                .as_ref()
10224                .is_some_and(|descr| descr.class().slots().descr_get.load().is_some());
10225
10226            if has_data_descr {
10227                // Check for member descriptor (slot access)
10228                // The slot offset only means anything on the layout the
10229                // descriptor was defined for; the specialized instruction
10230                // guards on the type version alone, so what descr_get()
10231                // checks on every access has to be checked here instead.
10232                if let Some(ref descr) = cls_attr
10233                    && let Some(member_descr) = descr.downcast_ref::<PyMemberDescriptor>()
10234                    && let Some(offset) = member_descr.slot_offset()
10235                    && !member_descr.member.audit_read()
10236                    && cls.fast_issubclass(&member_descr.common.typ)
10237                {
10238                    unsafe {
10239                        self.code
10240                            .instructions
10241                            .write_cache_u32(cache_base + 1, type_version);
10242                        self.code
10243                            .instructions
10244                            .write_cache_u32(cache_base + 3, offset as u32);
10245                    }
10246                    self.specialize_at(instr_idx, cache_base, Instruction::LoadAttrSlot);
10247                } else if let Some(ref descr) = cls_attr
10248                    && let Some(prop) = descr.downcast_ref::<PyProperty>()
10249                    && let Some(fget) = prop.get_fget()
10250                    && let Some(func) = fget.downcast_ref_if_exact::<PyFunction>(_vm)
10251                    && func.can_specialize_call(1)
10252                    && !self.specialization_eval_frame_active(_vm)
10253                {
10254                    // Property specialization caches fget directly.
10255                    let fget_ptr = &*fget as *const PyObject as usize;
10256                    unsafe {
10257                        self.write_cached_descriptor(cache_base, type_version, fget_ptr);
10258                    }
10259                    self.specialize_at(instr_idx, cache_base, Instruction::LoadAttrProperty);
10260                } else {
10261                    unsafe {
10262                        self.code.instructions.write_adaptive_counter(
10263                            cache_base,
10264                            bytecode::adaptive_counter_backoff(
10265                                self.code.instructions.read_adaptive_counter(cache_base),
10266                            ),
10267                        );
10268                    }
10269                }
10270            } else if has_descr_get {
10271                // Non-data descriptor with __get__ — can't specialize
10272                unsafe {
10273                    self.code.instructions.write_adaptive_counter(
10274                        cache_base,
10275                        bytecode::adaptive_counter_backoff(
10276                            self.code.instructions.read_adaptive_counter(cache_base),
10277                        ),
10278                    );
10279                }
10280            } else if class_has_dict {
10281                if let Some(ref descr) = cls_attr {
10282                    // Plain class attr + class supports dict — check dict first, fallback
10283                    let descr_ptr = &**descr as *const PyObject as usize;
10284                    unsafe {
10285                        self.write_cached_descriptor(cache_base, type_version, descr_ptr);
10286                    }
10287                    self.specialize_at(
10288                        instr_idx,
10289                        cache_base,
10290                        Instruction::LoadAttrNondescriptorWithValues,
10291                    );
10292                } else {
10293                    // Match CPython ABSENT/no-shadow behavior: if the
10294                    // attribute is missing on both the class and the current
10295                    // instance, keep the generic opcode and just enter
10296                    // cooldown instead of specializing a repeated miss path.
10297                    // A present attribute always specializes; when no entry
10298                    // index is representable the hint degrades to 0 and the
10299                    // handler simply keeps taking its full-probe fallback.
10300                    let instance_attr_hint = if let Some(dict) = obj.dict() {
10301                        match dict.get_item_opt_refresh_hint(attr_name, 0, _vm) {
10302                            Ok(present) => present.map(|(_, refreshed)| refreshed.unwrap_or(0)),
10303                            Err(_) => {
10304                                unsafe {
10305                                    self.code.instructions.write_adaptive_counter(
10306                                        cache_base,
10307                                        bytecode::adaptive_counter_backoff(
10308                                            self.code
10309                                                .instructions
10310                                                .read_adaptive_counter(cache_base),
10311                                        ),
10312                                    );
10313                                }
10314                                return;
10315                            }
10316                        }
10317                    } else {
10318                        None
10319                    };
10320                    if let Some(hint) = instance_attr_hint {
10321                        unsafe {
10322                            self.code
10323                                .instructions
10324                                .write_cache_u32(cache_base + 1, type_version);
10325                            self.code.instructions.write_cache_u16(cache_base + 3, hint);
10326                        }
10327                        self.specialize_at(instr_idx, cache_base, Instruction::LoadAttrWithHint);
10328                    } else {
10329                        self.cooldown_adaptive_at(cache_base);
10330                    }
10331                }
10332            } else if let Some(ref descr) = cls_attr {
10333                // No dict support, plain class attr — cache directly
10334                let descr_ptr = &**descr as *const PyObject as usize;
10335                unsafe {
10336                    self.write_cached_descriptor(cache_base, type_version, descr_ptr);
10337                }
10338                self.specialize_at(
10339                    instr_idx,
10340                    cache_base,
10341                    Instruction::LoadAttrNondescriptorNoDict,
10342                );
10343            } else {
10344                // No dict and no class attr: repeated miss path, so cooldown.
10345                self.cooldown_adaptive_at(cache_base);
10346            }
10347        }
10348    }
10349
10350    fn specialize_class_load_attr(
10351        &mut self,
10352        _vm: &VirtualMachine,
10353        oparg: LoadAttr,
10354        instr_idx: usize,
10355        cache_base: usize,
10356    ) {
10357        let obj = self.top_value();
10358        let owner_type = obj.downcast_ref::<PyType>().unwrap();
10359        let attr_name = self.code.names[oparg.name_idx() as usize];
10360
10361        // Check metaclass: ensure no data descriptor on metaclass for this name
10362        let mcl = obj.class();
10363        let (mcl_attr, mut metaclass_version) = mcl.lookup_ref_and_version_interned(attr_name, _vm);
10364        if let Some(ref attr) = mcl_attr {
10365            let attr_class = attr.class();
10366            if attr_class.slots().descr_set.load().is_some() {
10367                // Data descriptor on metaclass — can't specialize
10368                unsafe {
10369                    self.code.instructions.write_adaptive_counter(
10370                        cache_base,
10371                        bytecode::adaptive_counter_backoff(
10372                            self.code.instructions.read_adaptive_counter(cache_base),
10373                        ),
10374                    );
10375                }
10376                return;
10377            }
10378        }
10379        if !mcl.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
10380            if metaclass_version == 0 {
10381                unsafe {
10382                    self.code.instructions.write_adaptive_counter(
10383                        cache_base,
10384                        bytecode::adaptive_counter_backoff(
10385                            self.code.instructions.read_adaptive_counter(cache_base),
10386                        ),
10387                    );
10388                }
10389                return;
10390            }
10391        } else {
10392            metaclass_version = 0;
10393        }
10394
10395        let (cls_attr, type_version) = owner_type.lookup_ref_and_version_interned(attr_name, _vm);
10396        if type_version == 0 {
10397            unsafe {
10398                self.code.instructions.write_adaptive_counter(
10399                    cache_base,
10400                    bytecode::adaptive_counter_backoff(
10401                        self.code.instructions.read_adaptive_counter(cache_base),
10402                    ),
10403                );
10404            }
10405            return;
10406        }
10407        if let Some(ref descr) = cls_attr {
10408            let descr_class = descr.class();
10409            let has_descr_get = descr_class.slots.descr_get.load().is_some();
10410            if !has_descr_get {
10411                // METHOD or NON_DESCRIPTOR — can cache directly
10412                let descr_ptr = &**descr as *const PyObject as usize;
10413                let new_op = if metaclass_version == 0 {
10414                    Instruction::LoadAttrClass
10415                } else {
10416                    Instruction::LoadAttrClassWithMetaclassCheck
10417                };
10418                unsafe {
10419                    if metaclass_version == 0 {
10420                        self.write_cached_descriptor(cache_base, type_version, descr_ptr);
10421                    } else {
10422                        self.write_cached_descriptor_with_metaclass(
10423                            cache_base,
10424                            type_version,
10425                            metaclass_version,
10426                            descr_ptr,
10427                        );
10428                    }
10429                }
10430                self.specialize_at(instr_idx, cache_base, new_op);
10431                return;
10432            }
10433        }
10434
10435        // Can't specialize
10436        unsafe {
10437            self.code.instructions.write_adaptive_counter(
10438                cache_base,
10439                bytecode::adaptive_counter_backoff(
10440                    self.code.instructions.read_adaptive_counter(cache_base),
10441                ),
10442            );
10443        }
10444    }
10445
10446    fn load_attr_slow(&mut self, vm: &VirtualMachine, oparg: LoadAttr) -> FrameResult {
10447        let attr_name = self.code.names[oparg.name_idx() as usize];
10448
10449        if !oparg.is_method() {
10450            // Regular attribute access: `get_attr` reads through the receiver,
10451            // so a borrowed entry never has to become an owned one.
10452            let parent = self.pop_stackref();
10453            let obj = parent.get_attr(attr_name, vm)?;
10454            self.push_value(obj);
10455            return Ok(None);
10456        }
10457
10458        // Method call: push [method, self_or_null]. `PyMethod::get` binds the
10459        // receiver, so this path does need it owned.
10460        let parent = self.pop_value();
10461        match PyMethod::get(parent.clone(), attr_name, vm)? {
10462            PyMethod::Function { target: _, func } => {
10463                self.push_value(func);
10464                self.push_value(parent);
10465            }
10466            PyMethod::Attribute(val) => {
10467                self.push_value(val);
10468                self.push_null();
10469            }
10470        }
10471        Ok(None)
10472    }
10473
10474    fn specialize_binary_op(
10475        &mut self,
10476        vm: &VirtualMachine,
10477        op: bytecode::BinaryOperator,
10478        instr_idx: usize,
10479        cache_base: usize,
10480    ) {
10481        if !matches!(
10482            self.code.instructions.read_op(instr_idx),
10483            Instruction::BinaryOp { .. }
10484        ) {
10485            return;
10486        }
10487        let b = self.top_value();
10488        let a = self.nth_value(1);
10489        // `external_cache` in _PyBinaryOpCache is used only by BINARY_OP_EXTEND.
10490        unsafe {
10491            self.write_cached_binary_op_extend_descr(cache_base, None);
10492        }
10493        let mut cached_extend_descr = None;
10494
10495        let new_op = match op {
10496            bytecode::BinaryOperator::Add => {
10497                if a.downcast_ref_if_exact::<PyInt>(vm).is_some()
10498                    && b.downcast_ref_if_exact::<PyInt>(vm).is_some()
10499                {
10500                    Some(Instruction::BinaryOpAddInt)
10501                } else if a.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10502                    && b.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10503                {
10504                    Some(Instruction::BinaryOpAddFloat)
10505                } else if a.downcast_ref_if_exact::<PyStr>(vm).is_some()
10506                    && b.downcast_ref_if_exact::<PyStr>(vm).is_some()
10507                {
10508                    if self
10509                        .binary_op_inplace_unicode_target_local(cache_base, a)
10510                        .is_some()
10511                    {
10512                        Some(Instruction::BinaryOpInplaceAddUnicode)
10513                    } else {
10514                        Some(Instruction::BinaryOpAddUnicode)
10515                    }
10516                } else if let Some(descr) = self.binary_op_extended_specialization(op, a, b, vm) {
10517                    cached_extend_descr = Some(descr);
10518                    Some(Instruction::BinaryOpExtend)
10519                } else {
10520                    None
10521                }
10522            }
10523            bytecode::BinaryOperator::Subtract => {
10524                if a.downcast_ref_if_exact::<PyInt>(vm).is_some()
10525                    && b.downcast_ref_if_exact::<PyInt>(vm).is_some()
10526                {
10527                    Some(Instruction::BinaryOpSubtractInt)
10528                } else if a.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10529                    && b.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10530                {
10531                    Some(Instruction::BinaryOpSubtractFloat)
10532                } else if let Some(descr) = self.binary_op_extended_specialization(op, a, b, vm) {
10533                    cached_extend_descr = Some(descr);
10534                    Some(Instruction::BinaryOpExtend)
10535                } else {
10536                    None
10537                }
10538            }
10539            bytecode::BinaryOperator::Multiply => {
10540                if a.downcast_ref_if_exact::<PyInt>(vm).is_some()
10541                    && b.downcast_ref_if_exact::<PyInt>(vm).is_some()
10542                {
10543                    Some(Instruction::BinaryOpMultiplyInt)
10544                } else if a.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10545                    && b.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10546                {
10547                    Some(Instruction::BinaryOpMultiplyFloat)
10548                } else if let Some(descr) = self.binary_op_extended_specialization(op, a, b, vm) {
10549                    cached_extend_descr = Some(descr);
10550                    Some(Instruction::BinaryOpExtend)
10551                } else {
10552                    None
10553                }
10554            }
10555            bytecode::BinaryOperator::TrueDivide => {
10556                if let Some(descr) = self.binary_op_extended_specialization(op, a, b, vm) {
10557                    cached_extend_descr = Some(descr);
10558                    Some(Instruction::BinaryOpExtend)
10559                } else {
10560                    None
10561                }
10562            }
10563            bytecode::BinaryOperator::Subscr => {
10564                let b_is_nonnegative_int = b
10565                    .downcast_ref_if_exact::<PyInt>(vm)
10566                    .is_some_and(|i| specialization_nonnegative_compact_index(i, vm).is_some());
10567                if a.downcast_ref_if_exact::<PyList>(vm).is_some() && b_is_nonnegative_int {
10568                    Some(Instruction::BinaryOpSubscrListInt)
10569                } else if a.downcast_ref_if_exact::<PyTuple>(vm).is_some() && b_is_nonnegative_int {
10570                    Some(Instruction::BinaryOpSubscrTupleInt)
10571                } else if a.downcast_ref_if_exact::<PyDict>(vm).is_some() {
10572                    Some(Instruction::BinaryOpSubscrDict)
10573                } else if a.downcast_ref_if_exact::<PyStr>(vm).is_some() && b_is_nonnegative_int {
10574                    Some(Instruction::BinaryOpSubscrStrInt)
10575                } else if a.downcast_ref_if_exact::<PyList>(vm).is_some()
10576                    && b.downcast_ref::<PySlice>().is_some()
10577                {
10578                    Some(Instruction::BinaryOpSubscrListSlice)
10579                } else {
10580                    let cls = a.class();
10581                    // Check the cheap gates before the __getitem__ lookup, which
10582                    // takes the global type lock and may allocate a version tag.
10583                    if cls.slots().flags.has_feature(PyTypeFlags::HEAPTYPE)
10584                        && !self.specialization_eval_frame_active(vm)
10585                    {
10586                        let (getitem, type_version) =
10587                            cls.lookup_ref_and_version_interned(identifier!(vm, __getitem__), vm);
10588                        if type_version != 0
10589                            && let Some(getitem) = getitem
10590                            && let Some(func) = getitem.downcast_ref_if_exact::<PyFunction>(vm)
10591                            && func.can_specialize_call(2)
10592                            && cls.cache_getitem_for_specialization(
10593                                func.to_owned(),
10594                                type_version,
10595                                vm,
10596                            )
10597                        {
10598                            // Record the type version so the specialized handler
10599                            // can revalidate before using the cached __getitem__.
10600                            unsafe {
10601                                self.code
10602                                    .instructions
10603                                    .write_cache_u32(cache_base + 1, type_version);
10604                            }
10605                            Some(Instruction::BinaryOpSubscrGetitem)
10606                        } else {
10607                            None
10608                        }
10609                    } else {
10610                        None
10611                    }
10612                }
10613            }
10614            bytecode::BinaryOperator::InplaceAdd => {
10615                if a.downcast_ref_if_exact::<PyStr>(vm).is_some()
10616                    && b.downcast_ref_if_exact::<PyStr>(vm).is_some()
10617                {
10618                    if self
10619                        .binary_op_inplace_unicode_target_local(cache_base, a)
10620                        .is_some()
10621                    {
10622                        Some(Instruction::BinaryOpInplaceAddUnicode)
10623                    } else {
10624                        Some(Instruction::BinaryOpAddUnicode)
10625                    }
10626                } else if a.downcast_ref_if_exact::<PyInt>(vm).is_some()
10627                    && b.downcast_ref_if_exact::<PyInt>(vm).is_some()
10628                {
10629                    Some(Instruction::BinaryOpAddInt)
10630                } else if a.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10631                    && b.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10632                {
10633                    Some(Instruction::BinaryOpAddFloat)
10634                } else {
10635                    None
10636                }
10637            }
10638            bytecode::BinaryOperator::InplaceSubtract => {
10639                if a.downcast_ref_if_exact::<PyInt>(vm).is_some()
10640                    && b.downcast_ref_if_exact::<PyInt>(vm).is_some()
10641                {
10642                    Some(Instruction::BinaryOpSubtractInt)
10643                } else if a.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10644                    && b.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10645                {
10646                    Some(Instruction::BinaryOpSubtractFloat)
10647                } else {
10648                    None
10649                }
10650            }
10651            bytecode::BinaryOperator::InplaceMultiply => {
10652                if a.downcast_ref_if_exact::<PyInt>(vm).is_some()
10653                    && b.downcast_ref_if_exact::<PyInt>(vm).is_some()
10654                {
10655                    Some(Instruction::BinaryOpMultiplyInt)
10656                } else if a.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10657                    && b.downcast_ref_if_exact::<PyFloat>(vm).is_some()
10658                {
10659                    Some(Instruction::BinaryOpMultiplyFloat)
10660                } else {
10661                    None
10662                }
10663            }
10664            bytecode::BinaryOperator::And
10665            | bytecode::BinaryOperator::Or
10666            | bytecode::BinaryOperator::Xor
10667            | bytecode::BinaryOperator::InplaceAnd
10668            | bytecode::BinaryOperator::InplaceOr
10669            | bytecode::BinaryOperator::InplaceXor => {
10670                if let Some(descr) = self.binary_op_extended_specialization(op, a, b, vm) {
10671                    cached_extend_descr = Some(descr);
10672                    Some(Instruction::BinaryOpExtend)
10673                } else {
10674                    None
10675                }
10676            }
10677            _ => None,
10678        };
10679
10680        if matches!(new_op, Some(Instruction::BinaryOpExtend)) {
10681            unsafe {
10682                self.write_cached_binary_op_extend_descr(cache_base, cached_extend_descr);
10683            }
10684        }
10685        self.commit_specialization(instr_idx, cache_base, new_op);
10686    }
10687
10688    #[inline]
10689    fn binary_op_inplace_unicode_target_local(
10690        &self,
10691        cache_base: usize,
10692        left: &PyObject,
10693    ) -> Option<usize> {
10694        let next_idx = cache_base + Instruction::from(Opcode::BinaryOp).cache_entries();
10695        let unit = self.code.instructions.get(next_idx)?;
10696        let next_op = unit.op.to_base().unwrap_or(unit.op);
10697        if !matches!(next_op, Instruction::StoreFast { .. }) {
10698            return None;
10699        }
10700        let local_idx = usize::from(u8::from(unit.arg));
10701        self.localsplus
10702            .fastlocals()
10703            .get(local_idx)
10704            .and_then(|slot| slot.as_ref())
10705            .filter(|local| local.is(left))
10706            .map(|_| local_idx)
10707    }
10708
10709    /// Adaptive counter: trigger specialization at zero, otherwise advance countdown.
10710    #[inline]
10711    fn adaptive(&mut self, specialize: impl FnOnce(&mut Self, usize, usize)) {
10712        let instr_idx = self.lasti() as usize - 1;
10713        let cache_base = instr_idx + 1;
10714        let counter = self.code.instructions.read_adaptive_counter(cache_base);
10715        if bytecode::adaptive_counter_triggers(counter) {
10716            specialize(self, instr_idx, cache_base);
10717        } else {
10718            unsafe {
10719                self.code.instructions.write_adaptive_counter(
10720                    cache_base,
10721                    bytecode::advance_adaptive_counter(counter),
10722                );
10723            }
10724        }
10725    }
10726
10727    /// Install a specialized opcode and set adaptive cooldown bits.
10728    #[inline]
10729    fn specialize_at(&mut self, instr_idx: usize, cache_base: usize, new_op: Instruction) {
10730        unsafe {
10731            self.code
10732                .instructions
10733                .write_adaptive_counter(cache_base, ADAPTIVE_COOLDOWN_VALUE);
10734            self.code.instructions.replace_op(instr_idx, new_op);
10735        }
10736    }
10737
10738    #[inline]
10739    fn cooldown_adaptive_at(&mut self, cache_base: usize) {
10740        unsafe {
10741            self.code
10742                .instructions
10743                .write_adaptive_counter(cache_base, ADAPTIVE_COOLDOWN_VALUE);
10744        }
10745    }
10746
10747    /// Commit a specialization result: replace op on success, backoff on failure.
10748    #[inline]
10749    fn commit_specialization(
10750        &mut self,
10751        instr_idx: usize,
10752        cache_base: usize,
10753        new_op: Option<Instruction>,
10754    ) {
10755        if let Some(new_op) = new_op {
10756            self.specialize_at(instr_idx, cache_base, new_op);
10757        } else {
10758            unsafe {
10759                self.code.instructions.write_adaptive_counter(
10760                    cache_base,
10761                    bytecode::adaptive_counter_backoff(
10762                        self.code.instructions.read_adaptive_counter(cache_base),
10763                    ),
10764                );
10765            }
10766        }
10767    }
10768
10769    /// Execute a specialized binary op on two int operands.
10770    /// Fallback to generic binary op if either operand is not an exact int.
10771    #[inline]
10772    fn execute_binary_op_int(
10773        &mut self,
10774        vm: &VirtualMachine,
10775        op: impl FnOnce(&Py<PyInt>, &Py<PyInt>, &VirtualMachine) -> PyObjectRef,
10776        deopt_op: bytecode::BinaryOperator,
10777    ) -> FrameResult {
10778        let b = self.top_value();
10779        let a = self.nth_value(1);
10780        if let (Some(a_int), Some(b_int)) = (
10781            a.downcast_ref_if_exact::<PyInt>(vm),
10782            b.downcast_ref_if_exact::<PyInt>(vm),
10783        ) {
10784            let result = op(a_int, b_int, vm);
10785            self.pop_stackref();
10786            self.pop_stackref();
10787            self.push_value(result);
10788            Ok(None)
10789        } else {
10790            self.execute_bin_op(vm, deopt_op)
10791        }
10792    }
10793
10794    /// Execute a specialized binary op on two float operands.
10795    /// Fallback to generic binary op if either operand is not an exact float.
10796    #[inline]
10797    fn execute_binary_op_float(
10798        &mut self,
10799        vm: &VirtualMachine,
10800        op: impl FnOnce(f64, f64) -> f64,
10801        deopt_op: bytecode::BinaryOperator,
10802    ) -> FrameResult {
10803        let b = self.top_value();
10804        let a = self.nth_value(1);
10805        if let (Some(a_f), Some(b_f)) = (
10806            a.downcast_ref_if_exact::<PyFloat>(vm),
10807            b.downcast_ref_if_exact::<PyFloat>(vm),
10808        ) {
10809            let result = op(a_f.to_f64(), b_f.to_f64());
10810            self.pop_stackref();
10811            self.pop_stackref();
10812            self.push_value(vm.ctx.new_float(result).into());
10813            Ok(None)
10814        } else {
10815            self.execute_bin_op(vm, deopt_op)
10816        }
10817    }
10818
10819    fn specialize_call(
10820        &mut self,
10821        vm: &VirtualMachine,
10822        nargs: u32,
10823        instr_idx: usize,
10824        cache_base: usize,
10825    ) {
10826        if !matches!(
10827            self.code.instructions.read_op(instr_idx),
10828            Instruction::Call { .. }
10829        ) {
10830            return;
10831        }
10832        // Stack: [callable, self_or_null, arg1, ..., argN]
10833        // callable is at position nargs + 1 from top
10834        // self_or_null is at position nargs from top
10835        let stack_len = self.localsplus.stack_len();
10836        let self_or_null_is_some = self
10837            .localsplus
10838            .stack_index(stack_len - nargs as usize - 1)
10839            .is_some();
10840        let callable = self.nth_value(nargs + 1);
10841
10842        if let Some(func) = callable.downcast_ref_if_exact::<PyFunction>(vm) {
10843            if self.specialization_eval_frame_active(vm) || func.is_jitted() {
10844                unsafe {
10845                    self.code.instructions.write_adaptive_counter(
10846                        cache_base,
10847                        bytecode::adaptive_counter_backoff(
10848                            self.code.instructions.read_adaptive_counter(cache_base),
10849                        ),
10850                    );
10851                }
10852                return;
10853            }
10854            if !func.is_optimized_for_call_specialization() {
10855                unsafe {
10856                    self.code.instructions.write_adaptive_counter(
10857                        cache_base,
10858                        bytecode::adaptive_counter_backoff(
10859                            self.code.instructions.read_adaptive_counter(cache_base),
10860                        ),
10861                    );
10862                }
10863                return;
10864            }
10865            let version = func.get_version_for_current_state();
10866            if version == 0 {
10867                unsafe {
10868                    self.code.instructions.write_adaptive_counter(
10869                        cache_base,
10870                        bytecode::adaptive_counter_backoff(
10871                            self.code.instructions.read_adaptive_counter(cache_base),
10872                        ),
10873                    );
10874                }
10875                return;
10876            }
10877
10878            let effective_nargs = if self_or_null_is_some {
10879                nargs + 1
10880            } else {
10881                nargs
10882            };
10883
10884            let new_op = if func.can_specialize_call(effective_nargs) {
10885                Instruction::CallPyExactArgs
10886            } else {
10887                Instruction::CallPyGeneral
10888            };
10889            unsafe {
10890                self.code
10891                    .instructions
10892                    .write_cache_u32(cache_base + 1, version);
10893            }
10894            self.specialize_at(instr_idx, cache_base, new_op);
10895            return;
10896        }
10897
10898        // Bound Python method object (`method`) specialization.
10899        if !self_or_null_is_some
10900            && let Some(bound_method) = callable.downcast_ref_if_exact::<PyBoundMethod>(vm)
10901        {
10902            if let Some(func) = bound_method
10903                .function_obj()
10904                .downcast_ref_if_exact::<PyFunction>(vm)
10905            {
10906                if self.specialization_eval_frame_active(vm) || func.is_jitted() {
10907                    unsafe {
10908                        self.code.instructions.write_adaptive_counter(
10909                            cache_base,
10910                            bytecode::adaptive_counter_backoff(
10911                                self.code.instructions.read_adaptive_counter(cache_base),
10912                            ),
10913                        );
10914                    }
10915                    return;
10916                }
10917                if !func.is_optimized_for_call_specialization() {
10918                    unsafe {
10919                        self.code.instructions.write_adaptive_counter(
10920                            cache_base,
10921                            bytecode::adaptive_counter_backoff(
10922                                self.code.instructions.read_adaptive_counter(cache_base),
10923                            ),
10924                        );
10925                    }
10926                    return;
10927                }
10928                let version = func.get_version_for_current_state();
10929                if version == 0 {
10930                    unsafe {
10931                        self.code.instructions.write_adaptive_counter(
10932                            cache_base,
10933                            bytecode::adaptive_counter_backoff(
10934                                self.code.instructions.read_adaptive_counter(cache_base),
10935                            ),
10936                        );
10937                    }
10938                    return;
10939                }
10940
10941                let new_op = if func.can_specialize_call(nargs + 1) {
10942                    Instruction::CallBoundMethodExactArgs
10943                } else {
10944                    Instruction::CallBoundMethodGeneral
10945                };
10946                unsafe {
10947                    self.code
10948                        .instructions
10949                        .write_cache_u32(cache_base + 1, version);
10950                }
10951                self.specialize_at(instr_idx, cache_base, new_op);
10952            } else {
10953                // Match CPython: bound methods wrapping non-Python callables
10954                // are not specialized as CALL_NON_PY_GENERAL.
10955                unsafe {
10956                    self.code.instructions.write_adaptive_counter(
10957                        cache_base,
10958                        bytecode::adaptive_counter_backoff(
10959                            self.code.instructions.read_adaptive_counter(cache_base),
10960                        ),
10961                    );
10962                }
10963            }
10964            return;
10965        }
10966
10967        // Try to specialize method descriptor calls
10968        if let Some(descr) = callable.downcast_ref_if_exact::<PyMethodDescriptor>(vm) {
10969            let call_cache_entries = Instruction::CallListAppend.cache_entries();
10970            let next_idx = cache_base + call_cache_entries;
10971            let next_is_pop_top = if next_idx < self.code.instructions.len() {
10972                let next_op = self.code.instructions.read_op(next_idx);
10973                matches!(next_op.to_base().unwrap_or(next_op), Instruction::PopTop)
10974            } else {
10975                false
10976            };
10977
10978            let call_conv = descr.method.flags
10979                & (PyMethodFlags::VARARGS
10980                    | PyMethodFlags::FASTCALL
10981                    | PyMethodFlags::NOARGS
10982                    | PyMethodFlags::O
10983                    | PyMethodFlags::KEYWORDS);
10984            let total_nargs = nargs + u32::from(self_or_null_is_some);
10985
10986            let new_op = if call_conv == PyMethodFlags::NOARGS {
10987                if total_nargs != 1 {
10988                    unsafe {
10989                        self.code.instructions.write_adaptive_counter(
10990                            cache_base,
10991                            bytecode::adaptive_counter_backoff(
10992                                self.code.instructions.read_adaptive_counter(cache_base),
10993                            ),
10994                        );
10995                    }
10996                    return;
10997                }
10998                Instruction::CallMethodDescriptorNoargs
10999            } else if call_conv == PyMethodFlags::O {
11000                if total_nargs != 2 {
11001                    unsafe {
11002                        self.code.instructions.write_adaptive_counter(
11003                            cache_base,
11004                            bytecode::adaptive_counter_backoff(
11005                                self.code.instructions.read_adaptive_counter(cache_base),
11006                            ),
11007                        );
11008                    }
11009                    return;
11010                }
11011                if self_or_null_is_some
11012                    && nargs == 1
11013                    && next_is_pop_top
11014                    && vm
11015                        .callable_cache
11016                        .list_append
11017                        .as_ref()
11018                        .is_some_and(|list_append| callable.is(list_append))
11019                {
11020                    Instruction::CallListAppend
11021                } else {
11022                    Instruction::CallMethodDescriptorO
11023                }
11024            } else if call_conv == PyMethodFlags::FASTCALL {
11025                Instruction::CallMethodDescriptorFast
11026            } else if call_conv == (PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS) {
11027                Instruction::CallMethodDescriptorFastWithKeywords
11028            } else {
11029                Instruction::CallNonPyGeneral
11030            };
11031            self.specialize_at(instr_idx, cache_base, new_op);
11032            return;
11033        }
11034
11035        // Try to specialize builtin calls
11036        if let Some(native) = callable.downcast_ref_if_exact::<PyNativeFunction>(vm) {
11037            let effective_nargs = nargs + u32::from(self_or_null_is_some);
11038            let call_conv = native.value.flags
11039                & (PyMethodFlags::VARARGS
11040                    | PyMethodFlags::FASTCALL
11041                    | PyMethodFlags::NOARGS
11042                    | PyMethodFlags::O
11043                    | PyMethodFlags::KEYWORDS);
11044            let new_op = if call_conv == PyMethodFlags::O {
11045                if effective_nargs != 1 {
11046                    unsafe {
11047                        self.code.instructions.write_adaptive_counter(
11048                            cache_base,
11049                            bytecode::adaptive_counter_backoff(
11050                                self.code.instructions.read_adaptive_counter(cache_base),
11051                            ),
11052                        );
11053                    }
11054                    return;
11055                }
11056                if native.zelf.is_none()
11057                    && nargs == 1
11058                    && vm
11059                        .callable_cache
11060                        .len
11061                        .as_ref()
11062                        .is_some_and(|len_callable| callable.is(len_callable))
11063                {
11064                    Instruction::CallLen
11065                } else {
11066                    Instruction::CallBuiltinO
11067                }
11068            } else if call_conv == PyMethodFlags::FASTCALL {
11069                if native.zelf.is_none()
11070                    && effective_nargs == 2
11071                    && vm
11072                        .callable_cache
11073                        .isinstance
11074                        .as_ref()
11075                        .is_some_and(|isinstance_callable| callable.is(isinstance_callable))
11076                {
11077                    Instruction::CallIsinstance
11078                } else {
11079                    Instruction::CallBuiltinFast
11080                }
11081            } else if call_conv == (PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS) {
11082                Instruction::CallBuiltinFastWithKeywords
11083            } else {
11084                Instruction::CallNonPyGeneral
11085            };
11086            self.specialize_at(instr_idx, cache_base, new_op);
11087            return;
11088        }
11089
11090        // type/str/tuple(x) and class-call specializations
11091        if let Some(cls) = callable.downcast_ref::<PyType>() {
11092            if cls.slots().flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
11093                if !self_or_null_is_some && nargs == 1 {
11094                    let new_op = if callable.is(&vm.ctx.types.type_type.as_object()) {
11095                        Some(Instruction::CallType1)
11096                    } else if callable.is(&vm.ctx.types.str_type.as_object()) {
11097                        Some(Instruction::CallStr1)
11098                    } else if callable.is(&vm.ctx.types.tuple_type.as_object()) {
11099                        Some(Instruction::CallTuple1)
11100                    } else {
11101                        None
11102                    };
11103                    if let Some(new_op) = new_op {
11104                        self.specialize_at(instr_idx, cache_base, new_op);
11105                        return;
11106                    }
11107                }
11108                if cls.slots().vectorcall.load().is_some() {
11109                    self.specialize_at(instr_idx, cache_base, Instruction::CallBuiltinClass);
11110                    return;
11111                }
11112                self.specialize_at(instr_idx, cache_base, Instruction::CallNonPyGeneral);
11113                return;
11114            }
11115
11116            // CPython only considers CALL_ALLOC_AND_ENTER_INIT for types whose
11117            // metaclass is exactly `type`.
11118            if !callable.class().is(vm.ctx.types.type_type) {
11119                self.specialize_at(instr_idx, cache_base, Instruction::CallNonPyGeneral);
11120                return;
11121            }
11122
11123            // CallAllocAndEnterInit: heap type with default __new__
11124            if !self_or_null_is_some && cls.slots().flags.has_feature(PyTypeFlags::HEAPTYPE) {
11125                // Capture the version before inspecting tp_new/tp_alloc so a
11126                // concurrently installed __new__ invalidates the version this
11127                // specialization is cached against.
11128                let type_version = cls.version_for_specialization(vm);
11129                let object_new = vm.ctx.types.object_type.slots().new.load();
11130                let cls_new = cls.slots().new.load();
11131                let object_alloc = vm.ctx.types.object_type.slots().alloc.load();
11132                let cls_alloc = cls.slots().alloc.load();
11133                if let (Some(cls_new_fn), Some(obj_new_fn), Some(cls_alloc_fn), Some(obj_alloc_fn)) =
11134                    (cls_new, object_new, cls_alloc, object_alloc)
11135                    && crate::types::fn_addr(cls_new_fn) == crate::types::fn_addr(obj_new_fn)
11136                    && crate::types::fn_addr(cls_alloc_fn) == crate::types::fn_addr(obj_alloc_fn)
11137                {
11138                    if type_version == 0 {
11139                        unsafe {
11140                            self.code.instructions.write_adaptive_counter(
11141                                cache_base,
11142                                bytecode::adaptive_counter_backoff(
11143                                    self.code.instructions.read_adaptive_counter(cache_base),
11144                                ),
11145                            );
11146                        }
11147                        return;
11148                    }
11149                    let init = cls.get_attr(identifier!(vm, __init__));
11150                    if let Some(init) = init
11151                        && let Some(init_func) = init.downcast_ref_if_exact::<PyFunction>(vm)
11152                        && init_func.can_specialize_call(nargs + 1)
11153                        && !init_func.is_generator_like()
11154                        && cls.cache_init_for_specialization(init_func.to_owned(), type_version, vm)
11155                    {
11156                        unsafe {
11157                            self.code
11158                                .instructions
11159                                .write_cache_u32(cache_base + 1, type_version);
11160                        }
11161                        self.specialize_at(
11162                            instr_idx,
11163                            cache_base,
11164                            Instruction::CallAllocAndEnterInit,
11165                        );
11166                        return;
11167                    }
11168                }
11169            }
11170            self.specialize_at(instr_idx, cache_base, Instruction::CallNonPyGeneral);
11171            return;
11172        }
11173
11174        // General fallback: specialized non-Python callable path
11175        self.specialize_at(instr_idx, cache_base, Instruction::CallNonPyGeneral);
11176    }
11177
11178    fn specialize_call_kw(
11179        &mut self,
11180        vm: &VirtualMachine,
11181        nargs: u32,
11182        instr_idx: usize,
11183        cache_base: usize,
11184    ) {
11185        if !matches!(
11186            self.code.instructions.read_op(instr_idx),
11187            Instruction::CallKw { .. }
11188        ) {
11189            return;
11190        }
11191        // Stack: [callable, self_or_null, arg1, ..., argN, kwarg_names]
11192        // callable is at position nargs + 2 from top
11193        let stack_len = self.localsplus.stack_len();
11194        let self_or_null_is_some = self
11195            .localsplus
11196            .stack_index(stack_len - nargs as usize - 2)
11197            .is_some();
11198        let callable = self.nth_value(nargs + 2);
11199
11200        if let Some(func) = callable.downcast_ref_if_exact::<PyFunction>(vm) {
11201            if self.specialization_eval_frame_active(vm) || func.is_jitted() {
11202                unsafe {
11203                    self.code.instructions.write_adaptive_counter(
11204                        cache_base,
11205                        bytecode::adaptive_counter_backoff(
11206                            self.code.instructions.read_adaptive_counter(cache_base),
11207                        ),
11208                    );
11209                }
11210                return;
11211            }
11212            if !func.is_optimized_for_call_specialization() {
11213                unsafe {
11214                    self.code.instructions.write_adaptive_counter(
11215                        cache_base,
11216                        bytecode::adaptive_counter_backoff(
11217                            self.code.instructions.read_adaptive_counter(cache_base),
11218                        ),
11219                    );
11220                }
11221                return;
11222            }
11223            let version = func.get_version_for_current_state();
11224            if version == 0 {
11225                unsafe {
11226                    self.code.instructions.write_adaptive_counter(
11227                        cache_base,
11228                        bytecode::adaptive_counter_backoff(
11229                            self.code.instructions.read_adaptive_counter(cache_base),
11230                        ),
11231                    );
11232                }
11233                return;
11234            }
11235
11236            unsafe {
11237                self.code
11238                    .instructions
11239                    .write_cache_u32(cache_base + 1, version);
11240            }
11241            self.specialize_at(instr_idx, cache_base, Instruction::CallKwPy);
11242            return;
11243        }
11244
11245        if !self_or_null_is_some
11246            && let Some(bound_method) = callable.downcast_ref_if_exact::<PyBoundMethod>(vm)
11247        {
11248            if let Some(func) = bound_method
11249                .function_obj()
11250                .downcast_ref_if_exact::<PyFunction>(vm)
11251            {
11252                if self.specialization_eval_frame_active(vm) || func.is_jitted() {
11253                    unsafe {
11254                        self.code.instructions.write_adaptive_counter(
11255                            cache_base,
11256                            bytecode::adaptive_counter_backoff(
11257                                self.code.instructions.read_adaptive_counter(cache_base),
11258                            ),
11259                        );
11260                    }
11261                    return;
11262                }
11263                if !func.is_optimized_for_call_specialization() {
11264                    unsafe {
11265                        self.code.instructions.write_adaptive_counter(
11266                            cache_base,
11267                            bytecode::adaptive_counter_backoff(
11268                                self.code.instructions.read_adaptive_counter(cache_base),
11269                            ),
11270                        );
11271                    }
11272                    return;
11273                }
11274                let version = func.get_version_for_current_state();
11275                if version == 0 {
11276                    unsafe {
11277                        self.code.instructions.write_adaptive_counter(
11278                            cache_base,
11279                            bytecode::adaptive_counter_backoff(
11280                                self.code.instructions.read_adaptive_counter(cache_base),
11281                            ),
11282                        );
11283                    }
11284                    return;
11285                }
11286                unsafe {
11287                    self.code
11288                        .instructions
11289                        .write_cache_u32(cache_base + 1, version);
11290                }
11291                self.specialize_at(instr_idx, cache_base, Instruction::CallKwBoundMethod);
11292            } else {
11293                // Match CPython: bound methods wrapping non-Python callables
11294                // are not specialized as CALL_KW_NON_PY.
11295                unsafe {
11296                    self.code.instructions.write_adaptive_counter(
11297                        cache_base,
11298                        bytecode::adaptive_counter_backoff(
11299                            self.code.instructions.read_adaptive_counter(cache_base),
11300                        ),
11301                    );
11302                }
11303            }
11304            return;
11305        }
11306
11307        // General fallback: specialized non-Python callable path
11308        self.specialize_at(instr_idx, cache_base, Instruction::CallKwNonPy);
11309    }
11310
11311    fn specialize_send(&mut self, vm: &VirtualMachine, instr_idx: usize, cache_base: usize) {
11312        if !matches!(
11313            self.code.instructions.read_op(instr_idx),
11314            Instruction::Send { .. }
11315        ) {
11316            return;
11317        }
11318        // Stack: [receiver, val] — receiver is at position 1
11319        let receiver = self.nth_value(1);
11320        let is_exact_gen_or_coro = receiver.downcast_ref_if_exact::<PyGenerator>(vm).is_some()
11321            || receiver.downcast_ref_if_exact::<PyCoroutine>(vm).is_some();
11322        if is_exact_gen_or_coro && !self.specialization_eval_frame_active(vm) {
11323            self.specialize_at(instr_idx, cache_base, Instruction::SendGen);
11324        } else {
11325            unsafe {
11326                self.code.instructions.write_adaptive_counter(
11327                    cache_base,
11328                    bytecode::adaptive_counter_backoff(
11329                        self.code.instructions.read_adaptive_counter(cache_base),
11330                    ),
11331                );
11332            }
11333        }
11334    }
11335
11336    fn specialize_load_super_attr(
11337        &mut self,
11338        vm: &VirtualMachine,
11339        oparg: LoadSuperAttr,
11340        instr_idx: usize,
11341        cache_base: usize,
11342    ) {
11343        if !matches!(
11344            self.code.instructions.read_op(instr_idx),
11345            Instruction::LoadSuperAttr { .. }
11346        ) {
11347            return;
11348        }
11349        // Stack: [global_super, class, self]
11350        let global_super = self.nth_value(2);
11351        let class = self.nth_value(1);
11352
11353        if !global_super.is(&vm.ctx.types.super_type.as_object())
11354            || class.downcast_ref::<PyType>().is_none()
11355        {
11356            unsafe {
11357                self.code.instructions.write_adaptive_counter(
11358                    cache_base,
11359                    bytecode::adaptive_counter_backoff(
11360                        self.code.instructions.read_adaptive_counter(cache_base),
11361                    ),
11362                );
11363            }
11364            return;
11365        }
11366
11367        let new_op = if oparg.is_load_method() {
11368            Instruction::LoadSuperAttrMethod
11369        } else {
11370            Instruction::LoadSuperAttrAttr
11371        };
11372        self.specialize_at(instr_idx, cache_base, new_op);
11373    }
11374
11375    fn specialize_compare_op(
11376        &mut self,
11377        vm: &VirtualMachine,
11378        op: bytecode::ComparisonOperator,
11379        instr_idx: usize,
11380        cache_base: usize,
11381    ) {
11382        if !matches!(
11383            self.code.instructions.read_op(instr_idx),
11384            Instruction::CompareOp { .. }
11385        ) {
11386            return;
11387        }
11388        let b = self.top_value();
11389        let a = self.nth_value(1);
11390
11391        let new_op = if let (Some(a_int), Some(b_int)) = (
11392            a.downcast_ref_if_exact::<PyInt>(vm),
11393            b.downcast_ref_if_exact::<PyInt>(vm),
11394        ) {
11395            if specialization_compact_int_value(a_int).is_some()
11396                && specialization_compact_int_value(b_int).is_some()
11397            {
11398                Some(Instruction::CompareOpInt)
11399            } else {
11400                None
11401            }
11402        } else if a.downcast_ref_if_exact::<PyFloat>(vm).is_some()
11403            && b.downcast_ref_if_exact::<PyFloat>(vm).is_some()
11404        {
11405            Some(Instruction::CompareOpFloat)
11406        } else if a.downcast_ref_if_exact::<PyStr>(vm).is_some()
11407            && b.downcast_ref_if_exact::<PyStr>(vm).is_some()
11408            && (op == bytecode::ComparisonOperator::Equal
11409                || op == bytecode::ComparisonOperator::NotEqual)
11410        {
11411            Some(Instruction::CompareOpStr)
11412        } else {
11413            None
11414        };
11415
11416        self.commit_specialization(instr_idx, cache_base, new_op);
11417    }
11418
11419    /// Recover the ComparisonOperator from the instruction arg byte.
11420    /// `replace_op` preserves the arg byte, so the original op remains accessible.
11421    fn compare_op_from_arg(&self, arg: bytecode::OpArg) -> PyComparisonOp {
11422        bytecode::ComparisonOperator::try_from(u32::from(arg))
11423            .unwrap_or(bytecode::ComparisonOperator::Equal)
11424            .into()
11425    }
11426
11427    /// Execute an immediately following conditional jump without materializing
11428    /// the comparison result as a Python bool. This is the adaptive interpreter
11429    /// equivalent of keeping the result virtual across the two-opcode trace.
11430    #[inline]
11431    /// A `POP_JUMP_IF_TRUE`/`POP_JUMP_IF_FALSE` sitting immediately after the
11432    /// running instruction, resolved so the boolean's producer can branch on it
11433    /// without the jump being dispatched at all.
11434    ///
11435    /// `None` when the successor is anything else -- its instrumented form
11436    /// included, so `sys.monitoring` branch events still fire -- or while
11437    /// tracing, where the dispatch loop has to see every instruction to report
11438    /// it.
11439    fn fused_bool_jump(&self, caches: usize, vm: &VirtualMachine) -> Option<FusedBoolJump> {
11440        if self.specialization_eval_frame_active(vm) {
11441            return None;
11442        }
11443
11444        let jump_idx = self.lasti() as usize + caches;
11445        if jump_idx >= self.code.instructions.len() {
11446            return None;
11447        }
11448
11449        // One Acquire load for opcode and delta together. A jump needing
11450        // EXTENDED_ARG has that prefix at `jump_idx` instead, so the match
11451        // rejects it and this single byte is the whole delta.
11452        let jump = self.code.instructions.read_unit(jump_idx);
11453        let jump_on = match jump.op {
11454            Instruction::PopJumpIfFalse { .. } => false,
11455            Instruction::PopJumpIfTrue { .. } => true,
11456            _ => return None,
11457        };
11458        debug_assert_eq!(jump.op.cache_entries(), 1);
11459        let fallthrough = jump_idx as u32 + 2;
11460        Some(FusedBoolJump {
11461            jump_on,
11462            taken: fallthrough + jump.arg.as_u32(),
11463            fallthrough,
11464        })
11465    }
11466
11467    /// Land on the edge `result` selects, stepping over the fall-through
11468    /// `NOT_TAKEN` marker. The marker is only probed when the branch is not
11469    /// taken, keeping the taken edge to a single instruction-array read.
11470    #[inline]
11471    fn take_fused_bool_jump(&mut self, jump: FusedBoolJump, result: bool) {
11472        let target = if result == jump.jump_on {
11473            jump.taken
11474        } else {
11475            self.not_taken_skipped(jump.fallthrough)
11476        };
11477        self.lasti.store(target, Relaxed);
11478    }
11479
11480    /// Push `result` as the running instruction's boolean output, or branch on it
11481    /// directly when a `POP_JUMP_IF_*` follows. See [`Self::fused_bool_jump`].
11482    #[inline]
11483    fn push_bool_or_fused_jump(&mut self, caches: usize, result: bool, vm: &VirtualMachine) {
11484        match self.fused_bool_jump(caches, vm) {
11485            Some(jump) => self.take_fused_bool_jump(jump, result),
11486            None => self.push_value(vm.ctx.new_bool(result).into()),
11487        }
11488    }
11489
11490    /// `next`, advanced past a `NOT_TAKEN` marker sitting there.
11491    ///
11492    /// The fused-jump paths land directly on the successor, so they step over
11493    /// the marker themselves rather than going through
11494    /// [`Self::skip_fallthrough_not_taken`]; the tracing guard is the caller's,
11495    /// since fusion is already disabled while tracing.
11496    #[inline]
11497    fn not_taken_skipped(&self, next: u32) -> u32 {
11498        if (next as usize) < self.code.instructions.len()
11499            && matches!(
11500                self.code.instructions.read_op(next as usize),
11501                Instruction::NotTaken
11502            )
11503        {
11504            next + 1
11505        } else {
11506            next
11507        }
11508    }
11509
11510    /// Recover the BinaryOperator from the instruction arg byte.
11511    /// `replace_op` preserves the arg byte, so the original op remains accessible.
11512    fn binary_op_from_arg(&self, arg: bytecode::OpArg) -> bytecode::BinaryOperator {
11513        bytecode::BinaryOperator::try_from(u32::from(arg)).unwrap_or(bytecode::BinaryOperator::Add)
11514    }
11515
11516    fn specialize_to_bool(&mut self, vm: &VirtualMachine, instr_idx: usize, cache_base: usize) {
11517        if !matches!(
11518            self.code.instructions.read_op(instr_idx),
11519            Instruction::ToBool
11520        ) {
11521            return;
11522        }
11523        let obj = self.top_value();
11524        let cls = obj.class();
11525
11526        let new_op = if cls.is(vm.ctx.types.bool_type) {
11527            Some(Instruction::ToBoolBool)
11528        } else if cls.is(PyInt::class(&vm.ctx)) {
11529            Some(Instruction::ToBoolInt)
11530        } else if cls.is(vm.ctx.types.none_type) {
11531            Some(Instruction::ToBoolNone)
11532        } else if cls.is(PyList::class(&vm.ctx)) {
11533            Some(Instruction::ToBoolList)
11534        } else if cls.is(PyStr::class(&vm.ctx)) {
11535            Some(Instruction::ToBoolStr)
11536        } else if cls.slots().flags.has_feature(PyTypeFlags::HEAPTYPE) {
11537            // Capture the version before inspecting the bool/len slots so a
11538            // concurrently installed __bool__/__len__ invalidates the version
11539            // the ToBoolAlwaysTrue guard is cached against.
11540            let type_version = cls.version_for_specialization(vm);
11541            let has_bool_or_len = cls.slots().as_number.boolean.load().is_some()
11542                || cls.slots().as_mapping.length.load().is_some()
11543                || cls.slots().as_sequence.length.load().is_some();
11544            if !has_bool_or_len {
11545                if type_version != 0 {
11546                    unsafe {
11547                        self.code
11548                            .instructions
11549                            .write_cache_u32(cache_base + 1, type_version);
11550                    }
11551                    self.specialize_at(instr_idx, cache_base, Instruction::ToBoolAlwaysTrue);
11552                } else {
11553                    unsafe {
11554                        self.code.instructions.write_adaptive_counter(
11555                            cache_base,
11556                            bytecode::adaptive_counter_backoff(
11557                                self.code.instructions.read_adaptive_counter(cache_base),
11558                            ),
11559                        );
11560                    }
11561                }
11562                return;
11563            }
11564            None
11565        } else {
11566            None
11567        };
11568
11569        self.commit_specialization(instr_idx, cache_base, new_op);
11570    }
11571
11572    fn specialize_for_iter(
11573        &mut self,
11574        vm: &VirtualMachine,
11575        jump_delta: u32,
11576        instr_idx: usize,
11577        cache_base: usize,
11578    ) {
11579        if !matches!(
11580            self.code.instructions.read_op(instr_idx),
11581            Instruction::ForIter { .. }
11582        ) {
11583            return;
11584        }
11585        let iter = self.top_value();
11586
11587        let new_op = if iter.downcast_ref_if_exact::<PyRangeIterator>(vm).is_some() {
11588            Some(Instruction::ForIterRange)
11589        } else if iter.downcast_ref_if_exact::<PyListIterator>(vm).is_some() {
11590            Some(Instruction::ForIterList)
11591        } else if iter.downcast_ref_if_exact::<PyTupleIterator>(vm).is_some() {
11592            Some(Instruction::ForIterTuple)
11593        } else if iter.downcast_ref_if_exact::<PyGenerator>(vm).is_some()
11594            && i16::try_from(jump_delta).is_ok()
11595            && self.for_iter_has_end_for_shape(instr_idx, jump_delta)
11596            && !self.specialization_eval_frame_active(vm)
11597        {
11598            Some(Instruction::ForIterGen)
11599        } else {
11600            None
11601        };
11602
11603        self.commit_specialization(instr_idx, cache_base, new_op);
11604    }
11605
11606    #[inline]
11607    fn specialization_eval_frame_active(&self, vm: &VirtualMachine) -> bool {
11608        vm.use_tracing.get()
11609    }
11610
11611    #[inline]
11612    fn specialization_has_datastack_space_for_func(
11613        &self,
11614        vm: &VirtualMachine,
11615        func: &Py<PyFunction>,
11616    ) -> bool {
11617        self.specialization_has_datastack_space_for_func_with_extra(vm, func, 0)
11618    }
11619
11620    #[inline]
11621    fn specialization_has_datastack_space_for_func_with_extra(
11622        &self,
11623        vm: &VirtualMachine,
11624        func: &Py<PyFunction>,
11625        extra_bytes: usize,
11626    ) -> bool {
11627        match func.datastack_frame_size_bytes() {
11628            Some(frame_size) => frame_size
11629                .checked_add(extra_bytes)
11630                .is_some_and(|size| vm.datastack_has_space(size)),
11631            None => extra_bytes == 0 || vm.datastack_has_space(extra_bytes),
11632        }
11633    }
11634
11635    #[inline]
11636    fn specialization_call_recursion_guard(&self, vm: &VirtualMachine) -> bool {
11637        self.specialization_call_recursion_guard_with_extra_frames(vm, 0)
11638    }
11639
11640    #[inline]
11641    fn specialization_call_recursion_guard_with_extra_frames(
11642        &self,
11643        vm: &VirtualMachine,
11644        extra_frames: usize,
11645    ) -> bool {
11646        vm.current_recursion_depth()
11647            .saturating_add(1)
11648            .saturating_add(extra_frames)
11649            >= vm.recursion_limit.get()
11650    }
11651
11652    /// Prepare a callee frame on the datastack for a TailCall.
11653    /// Pops args, self_or_null, and callable from the caller's stack,
11654    /// builds the callee InterpreterFrame, and stores its pointer in
11655    /// `vm.pending_tailcall_frame`.
11656    ///
11657    /// The callable must be at stack position `nargs + 1` (already validated).
11658    fn tailcall_prepare_frame(
11659        &mut self,
11660        nargs: u32,
11661        self_or_null_is_some: bool,
11662        vm: &VirtualMachine,
11663    ) {
11664        let base = usize::from(self_or_null_is_some);
11665        let effective_nargs = nargs as usize + base;
11666
11667        // Peek at the callable (still on the stack) to build the callee
11668        // frame. The callable stays on the caller's stack until we're done
11669        // constructing the callee.
11670        let callable = self.nth_value(nargs + 1);
11671        let func = callable.downcast_ref_if_exact::<PyFunction>(vm).unwrap();
11672
11673        let code: &Py<PyCode> = &func.code;
11674
11675        let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
11676            FrameLocals::lazy()
11677        } else {
11678            FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact(
11679                func.globals.clone(),
11680            ))
11681        };
11682
11683        let callee_iframe = unsafe {
11684            // SAFETY: the callable stays on the caller's stack until it is
11685            // moved into `pending_tailcall_owner`, which keeps `func` alive
11686            // while this frame runs.
11687            InterpreterFrame::new_on_datastack(
11688                code,
11689                &func.globals,
11690                &func.builtins,
11691                Some(func.as_object()),
11692                locals,
11693                func.closure.as_ref().map_or(&[], |c| c.as_slice()),
11694                vm,
11695            )
11696        };
11697
11698        // Move args directly from the caller's stack into callee fastlocals,
11699        // avoiding an intermediate buffer.
11700        {
11701            let fastlocals = callee_iframe.localsplus.fastlocals_mut();
11702            for (dst, arg) in fastlocals[base..effective_nargs]
11703                .iter_mut()
11704                .zip(self.pop_multiple(nargs as usize))
11705            {
11706                *dst = Some(arg);
11707            }
11708            let self_or_null = self.pop_value_opt();
11709            debug_assert_eq!(self_or_null.is_some(), self_or_null_is_some);
11710            if self_or_null.is_some() {
11711                fastlocals[0] = self_or_null;
11712            }
11713        }
11714
11715        // Pop the callable and transfer ownership to the trampoline. This one
11716        // reference keeps every field borrowed by the callee frame alive.
11717        let callable = self.pop_value();
11718        vm.set_pending_tailcall_owner(callable);
11719
11720        vm.set_pending_tailcall(callee_iframe);
11721    }
11722
11723    /// Prepare a callee frame for a bound method TailCall.
11724    /// Pops args, self_or_null (null), and callable from the caller's stack,
11725    /// builds the callee InterpreterFrame with bound_self prepended, and
11726    /// stores its pointer in `vm.pending_tailcall_frame`.
11727    fn tailcall_prepare_bound_method_frame(
11728        &mut self,
11729        nargs: u32,
11730        bound_function: PyObjectRef,
11731        bound_self: PyObjectRef,
11732        vm: &VirtualMachine,
11733    ) {
11734        let effective_nargs = nargs as usize + 1; // +1 for bound_self
11735
11736        let func = bound_function
11737            .downcast_ref_if_exact::<PyFunction>(vm)
11738            .unwrap();
11739        let code: &Py<PyCode> = &func.code;
11740
11741        let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
11742            FrameLocals::lazy()
11743        } else {
11744            FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact(
11745                func.globals.clone(),
11746            ))
11747        };
11748
11749        let callee_iframe = unsafe {
11750            // SAFETY: `bound_function` is held on this stack until the
11751            // callee frame is stored as a pending tailcall, and `func`
11752            // is borrowed from it.
11753            InterpreterFrame::new_on_datastack(
11754                code,
11755                &func.globals,
11756                &func.builtins,
11757                Some(func.as_object()),
11758                locals,
11759                func.closure.as_ref().map_or(&[], |c| c.as_slice()),
11760                vm,
11761            )
11762        };
11763
11764        // Move args directly from the caller's stack into callee fastlocals.
11765        let fastlocals = callee_iframe.localsplus.fastlocals_mut();
11766        for (dst, arg) in fastlocals[1..effective_nargs]
11767            .iter_mut()
11768            .zip(self.pop_multiple(nargs as usize))
11769        {
11770            *dst = Some(arg);
11771        }
11772        self.pop_stackref_opt(); // null (self_or_null)
11773        self.pop_stackref(); // callable (bound method)
11774        fastlocals[0] = Some(bound_self);
11775
11776        // The function owns every field borrowed by the callee frame.
11777        // bound_self is owned by fastlocals; the bound-method object itself is
11778        // no longer needed and was dropped above, matching the recursive path.
11779        vm.set_pending_tailcall_owner(bound_function);
11780
11781        vm.set_pending_tailcall(callee_iframe);
11782    }
11783
11784    #[inline]
11785    fn for_iter_has_end_for_shape(&self, instr_idx: usize, jump_delta: u32) -> bool {
11786        let target_idx = instr_idx
11787            + 1
11788            + Instruction::from(Opcode::ForIter).cache_entries()
11789            + jump_delta as usize;
11790        self.code.instructions.get(target_idx).is_some_and(|unit| {
11791            matches!(
11792                unit.op,
11793                Instruction::EndFor | Instruction::InstrumentedEndFor
11794            )
11795        })
11796    }
11797
11798    /// Handle iterator exhaustion in specialized FOR_ITER handlers.
11799    /// Skips END_FOR if present at target and jumps.
11800    fn for_iter_jump_on_exhausted(&mut self, target: bytecode::Label) {
11801        let target_idx = target.as_usize();
11802        let jump_target = if let Some(unit) = self.code.instructions.get(target_idx) {
11803            if matches!(
11804                unit.op,
11805                bytecode::Instruction::EndFor | bytecode::Instruction::InstrumentedEndFor
11806            ) {
11807                bytecode::Label::from_u32(target.as_u32() + 1)
11808            } else {
11809                target
11810            }
11811        } else {
11812            target
11813        };
11814        self.jump(jump_target);
11815    }
11816
11817    fn specialize_load_global(
11818        &mut self,
11819        vm: &VirtualMachine,
11820        oparg: u32,
11821        instr_idx: usize,
11822        cache_base: usize,
11823    ) {
11824        if !matches!(
11825            self.code.instructions.read_op(instr_idx),
11826            Instruction::LoadGlobal { .. }
11827        ) {
11828            return;
11829        }
11830        let name = self.code.names[(oparg >> 1) as usize];
11831        let Ok(globals_version @ 1..) = u16::try_from(self.globals.assign_keys_version(vm)) else {
11832            unsafe {
11833                self.code.instructions.write_adaptive_counter(
11834                    cache_base,
11835                    bytecode::adaptive_counter_backoff(
11836                        self.code.instructions.read_adaptive_counter(cache_base),
11837                    ),
11838                );
11839            }
11840            return;
11841        };
11842
11843        if let Ok(Some(globals_hint)) = self.globals.hint_for_key(name, vm) {
11844            unsafe {
11845                self.code
11846                    .instructions
11847                    .write_cache_u16(cache_base + 1, globals_version);
11848                self.code.instructions.write_cache_u16(cache_base + 2, 0);
11849                self.code
11850                    .instructions
11851                    .write_cache_u16(cache_base + 3, globals_hint);
11852            }
11853            self.specialize_at(instr_idx, cache_base, Instruction::LoadGlobalModule);
11854            return;
11855        }
11856
11857        if let Some(builtins_dict) = self.builtins.downcast_ref_if_exact::<PyDict>(vm)
11858            && let Ok(Some(builtins_hint)) = builtins_dict.hint_for_key(name, vm)
11859            && let Ok(builtins_version @ 1..) = u16::try_from(builtins_dict.assign_keys_version(vm))
11860        {
11861            unsafe {
11862                self.code
11863                    .instructions
11864                    .write_cache_u16(cache_base + 1, globals_version);
11865                self.code
11866                    .instructions
11867                    .write_cache_u16(cache_base + 2, builtins_version);
11868                self.code
11869                    .instructions
11870                    .write_cache_u16(cache_base + 3, builtins_hint);
11871            }
11872            self.specialize_at(instr_idx, cache_base, Instruction::LoadGlobalBuiltin);
11873            return;
11874        }
11875
11876        unsafe {
11877            self.code.instructions.write_adaptive_counter(
11878                cache_base,
11879                bytecode::adaptive_counter_backoff(
11880                    self.code.instructions.read_adaptive_counter(cache_base),
11881                ),
11882            );
11883        }
11884    }
11885
11886    fn specialize_store_subscr(
11887        &mut self,
11888        vm: &VirtualMachine,
11889        instr_idx: usize,
11890        cache_base: usize,
11891    ) {
11892        if !matches!(
11893            self.code.instructions.read_op(instr_idx),
11894            Instruction::StoreSubscr
11895        ) {
11896            return;
11897        }
11898        // Stack: [value, obj, idx] — obj is TOS-1
11899        let obj = self.nth_value(1);
11900        let idx = self.top_value();
11901
11902        let new_op = if let (Some(list), Some(int_idx)) = (
11903            obj.downcast_ref_if_exact::<PyList>(vm),
11904            idx.downcast_ref_if_exact::<PyInt>(vm),
11905        ) {
11906            let list_len = list.borrow_vec().len();
11907            if specialization_nonnegative_compact_index(int_idx, vm).is_some_and(|i| i < list_len) {
11908                Some(Instruction::StoreSubscrListInt)
11909            } else {
11910                None
11911            }
11912        } else if obj.downcast_ref_if_exact::<PyDict>(vm).is_some() {
11913            Some(Instruction::StoreSubscrDict)
11914        } else {
11915            None
11916        };
11917
11918        self.commit_specialization(instr_idx, cache_base, new_op);
11919    }
11920
11921    fn specialize_contains_op(&mut self, vm: &VirtualMachine, instr_idx: usize, cache_base: usize) {
11922        if !matches!(
11923            self.code.instructions.read_op(instr_idx),
11924            Instruction::ContainsOp { .. }
11925        ) {
11926            return;
11927        }
11928        let haystack = self.top_value(); // b = TOS = haystack
11929        let new_op = if haystack.downcast_ref_if_exact::<PyDict>(vm).is_some() {
11930            Some(Instruction::ContainsOpDict)
11931        } else if haystack.downcast_ref_if_exact::<PySet>(vm).is_some()
11932            || haystack.downcast_ref_if_exact::<PyFrozenSet>(vm).is_some()
11933        {
11934            Some(Instruction::ContainsOpSet)
11935        } else {
11936            None
11937        };
11938
11939        self.commit_specialization(instr_idx, cache_base, new_op);
11940    }
11941
11942    fn specialize_unpack_sequence(
11943        &mut self,
11944        vm: &VirtualMachine,
11945        expected_count: u32,
11946        instr_idx: usize,
11947        cache_base: usize,
11948    ) {
11949        if !matches!(
11950            self.code.instructions.read_op(instr_idx),
11951            Instruction::UnpackSequence { .. }
11952        ) {
11953            return;
11954        }
11955        let obj = self.top_value();
11956        let new_op = if let Some(tuple) = obj.downcast_ref_if_exact::<PyTuple>(vm) {
11957            if tuple.as_slice().len() != expected_count as usize {
11958                None
11959            } else if expected_count == 2 {
11960                Some(Instruction::UnpackSequenceTwoTuple)
11961            } else {
11962                Some(Instruction::UnpackSequenceTuple)
11963            }
11964        } else if let Some(list) = obj.downcast_ref_if_exact::<PyList>(vm) {
11965            if list.borrow_vec().len() == expected_count as usize {
11966                Some(Instruction::UnpackSequenceList)
11967            } else {
11968                None
11969            }
11970        } else {
11971            None
11972        };
11973
11974        self.commit_specialization(instr_idx, cache_base, new_op);
11975    }
11976
11977    fn specialize_store_attr(
11978        &mut self,
11979        vm: &VirtualMachine,
11980        attr_idx: bytecode::NameIdx,
11981        instr_idx: usize,
11982        cache_base: usize,
11983    ) {
11984        if !matches!(
11985            self.code.instructions.read_op(instr_idx),
11986            Instruction::StoreAttr { .. }
11987        ) {
11988            return;
11989        }
11990        // TOS = owner (the object being assigned to)
11991        let owner = self.top_value();
11992        let cls = owner.class();
11993
11994        // Capture the version before inspecting the setattro slot so a
11995        // concurrently installed __setattr__ invalidates the version this
11996        // specialization is cached against.
11997        let type_version = cls.version_for_specialization(vm);
11998        if type_version == 0 {
11999            unsafe {
12000                self.code.instructions.write_adaptive_counter(
12001                    cache_base,
12002                    bytecode::adaptive_counter_backoff(
12003                        self.code.instructions.read_adaptive_counter(cache_base),
12004                    ),
12005                );
12006            }
12007            return;
12008        }
12009
12010        // Only specialize if setattr is the default (generic_setattr)
12011        let is_default_setattr = cls.slots().setattro.load().is_some_and(|f| {
12012            crate::types::fn_addr(f)
12013                == crate::types::fn_addr(PyBaseObject::slot_setattro as crate::types::SetattroFunc)
12014        });
12015        if !is_default_setattr {
12016            unsafe {
12017                self.code.instructions.write_adaptive_counter(
12018                    cache_base,
12019                    bytecode::adaptive_counter_backoff(
12020                        self.code.instructions.read_adaptive_counter(cache_base),
12021                    ),
12022                );
12023            }
12024            return;
12025        }
12026
12027        let attr_name = self.code.names[attr_idx as usize];
12028        let cls_attr = cls.get_attr(attr_name);
12029        let has_data_descr = cls_attr.as_ref().is_some_and(|descr| {
12030            let descr_cls = descr.class();
12031            descr_cls.slots().descr_get.load().is_some()
12032                && descr_cls.slots().descr_set.load().is_some()
12033        });
12034
12035        if has_data_descr {
12036            // Check for member descriptor (slot access)
12037            // As in the load specialization, the offset is only valid for
12038            // instances of the type the descriptor belongs to.
12039            if let Some(ref descr) = cls_attr
12040                && let Some(member_descr) = descr.downcast_ref::<PyMemberDescriptor>()
12041                && let Some(offset) = member_descr
12042                    .slot_offset()
12043                    .filter(|_| !member_descr.member.readonly())
12044                && cls.fast_issubclass(&member_descr.common.typ)
12045            {
12046                unsafe {
12047                    self.code
12048                        .instructions
12049                        .write_cache_u32(cache_base + 1, type_version);
12050                    self.code
12051                        .instructions
12052                        .write_cache_u16(cache_base + 3, offset as u16);
12053                }
12054                self.specialize_at(instr_idx, cache_base, Instruction::StoreAttrSlot);
12055            } else {
12056                unsafe {
12057                    self.code.instructions.write_adaptive_counter(
12058                        cache_base,
12059                        bytecode::adaptive_counter_backoff(
12060                            self.code.instructions.read_adaptive_counter(cache_base),
12061                        ),
12062                    );
12063                }
12064            }
12065        } else if let Some(dict) = owner.dict() {
12066            let hint = match dict.hint_for_key(attr_name, vm) {
12067                Ok(hint) => hint,
12068                Err(_) => {
12069                    unsafe {
12070                        self.code.instructions.write_adaptive_counter(
12071                            cache_base,
12072                            bytecode::adaptive_counter_backoff(
12073                                self.code.instructions.read_adaptive_counter(cache_base),
12074                            ),
12075                        );
12076                    }
12077                    return;
12078                }
12079            };
12080            unsafe {
12081                self.code
12082                    .instructions
12083                    .write_cache_u32(cache_base + 1, type_version);
12084                self.code
12085                    .instructions
12086                    .write_cache_u16(cache_base + 3, hint.unwrap_or(0));
12087            }
12088            self.specialize_at(
12089                instr_idx,
12090                cache_base,
12091                if hint.is_some() {
12092                    Instruction::StoreAttrWithHint
12093                } else {
12094                    Instruction::StoreAttrInstanceValue
12095                },
12096            );
12097        } else {
12098            unsafe {
12099                self.code.instructions.write_adaptive_counter(
12100                    cache_base,
12101                    bytecode::adaptive_counter_backoff(
12102                        self.code.instructions.read_adaptive_counter(cache_base),
12103                    ),
12104                );
12105            }
12106        }
12107    }
12108
12109    fn load_super_attr(&mut self, vm: &VirtualMachine, oparg: LoadSuperAttr) -> FrameResult {
12110        let attr_name = self.code.names[oparg.name_idx() as usize];
12111
12112        // Stack layout (bottom to top): [super, class, self]
12113        // Pop in LIFO order: self, class, super
12114        let self_obj = self.pop_value();
12115        let class = self.pop_value();
12116        let global_super = self.pop_value();
12117
12118        // Create super object - pass args based on has_class flag
12119        // When super is shadowed, has_class=false means call with 0 args
12120        let super_obj = if oparg.has_class() {
12121            global_super.call((class, self_obj.clone()), vm)?
12122        } else {
12123            global_super.call((), vm)?
12124        };
12125
12126        if oparg.is_load_method() {
12127            // Method load: push [method, self_or_null]
12128            let method = PyMethod::get(super_obj, attr_name, vm)?;
12129            match method {
12130                PyMethod::Function { target: _, func } => {
12131                    self.push_value(func);
12132                    self.push_value(self_obj);
12133                }
12134                PyMethod::Attribute(val) => {
12135                    self.push_value(val);
12136                    self.push_null();
12137                }
12138            }
12139        } else {
12140            // Regular attribute access
12141            let obj = super_obj.get_attr(attr_name, vm)?;
12142            self.push_value(obj);
12143        }
12144        Ok(None)
12145    }
12146
12147    fn store_attr(&mut self, vm: &VirtualMachine, attr: bytecode::NameIdx) -> FrameResult {
12148        let attr_name = self.code.names[attr as usize];
12149        let parent = self.pop_stackref();
12150        let value = self.pop_value();
12151        parent.set_attr(attr_name, value, vm)?;
12152        Ok(None)
12153    }
12154
12155    fn delete_attr(&mut self, vm: &VirtualMachine, attr: bytecode::NameIdx) -> FrameResult {
12156        let attr_name = self.code.names[attr as usize];
12157        let parent = self.pop_stackref();
12158        parent.del_attr(attr_name, vm)?;
12159        Ok(None)
12160    }
12161
12162    // Block stack functions removed - exception table handles all exception/cleanup
12163
12164    #[inline(always)]
12165    #[track_caller]
12166    fn push_stackref_opt(&mut self, obj: Option<PyStackRef>) {
12167        match self.localsplus.stack_try_push(obj) {
12168            Ok(()) => {}
12169            Err(_e) => self.fatal("tried to push value onto stack but overflowed max_stackdepth"),
12170        }
12171    }
12172
12173    #[inline(always)]
12174    #[track_caller] // not a real track_caller but push_value is less useful for debugging
12175    fn push_value_opt(&mut self, obj: Option<PyObjectRef>) {
12176        self.push_stackref_opt(obj.map(PyStackRef::new_owned));
12177    }
12178
12179    #[inline(always)]
12180    #[track_caller]
12181    fn push_value(&mut self, obj: PyObjectRef) {
12182        self.push_stackref_opt(Some(PyStackRef::new_owned(obj)));
12183    }
12184
12185    /// Push a borrowed reference onto the stack (no refcount increment).
12186    ///
12187    /// # Safety
12188    /// The object must remain alive until the borrowed ref is consumed.
12189    /// The compiler guarantees consumption within the same basic block.
12190    #[inline]
12191    #[track_caller]
12192    unsafe fn push_borrowed(&mut self, obj: &PyObject) {
12193        self.push_stackref_opt(Some(unsafe { PyStackRef::new_borrowed(obj) }));
12194    }
12195
12196    /// Push the fastlocals slot `idx` for a `LOAD_FAST_BORROW`.
12197    ///
12198    /// With [`BORROW_LOCAL_LOADS`] on this is a borrow: the slot holds the
12199    /// strong count and codegen has proved the entry is consumed before
12200    /// anything can release that slot, so the push and its matching pop cost
12201    /// no atomic traffic at all. With the flag off it is a plain clone, which
12202    /// is what every `LOAD_FAST` does.
12203    #[inline(always)]
12204    fn push_local(&mut self, idx: usize, vm: &VirtualMachine) -> PyResult<()> {
12205        #[cold]
12206        #[inline(never)]
12207        fn unbound(varname: &'static PyStrInterned, vm: &VirtualMachine) -> PyBaseExceptionRef {
12208            vm.new_unbound_local_error(format!(
12209                "local variable '{varname}' referenced before assignment"
12210            ))
12211        }
12212        if BORROW_LOCAL_LOADS {
12213            // Take the address out of the slot before the push needs `&mut
12214            // self`; the borrow of `localsplus` ends with this statement.
12215            let obj: *const PyObject = match self.localsplus.fastlocals()[idx].as_ref() {
12216                Some(obj) => obj.as_object(),
12217                None => return Err(unbound(self.code.varnames[idx], vm)),
12218            };
12219            // SAFETY: the fastlocals slot owns a strong count on `obj` and,
12220            // per the borrow invariant on `PyStackRef`, cannot release it
12221            // before this entry is popped.
12222            unsafe { self.push_borrowed(&*obj) };
12223        } else {
12224            let obj = match self.localsplus.fastlocals()[idx].clone() {
12225                Some(obj) => obj,
12226                None => return Err(unbound(self.code.varnames[idx], vm)),
12227            };
12228            self.push_value(obj);
12229        }
12230        Ok(())
12231    }
12232
12233    #[inline(always)]
12234    fn push_null(&mut self) {
12235        self.push_stackref_opt(None);
12236    }
12237
12238    /// Pop a raw stackref from the stack, returning None if the stack slot is NULL.
12239    #[inline(always)]
12240    fn pop_stackref_opt(&mut self) -> Option<PyStackRef> {
12241        if self.localsplus.stack_is_empty() {
12242            self.fatal("tried to pop from empty stack");
12243        }
12244        self.localsplus.stack_pop()
12245    }
12246
12247    /// Pop a raw stackref from the stack. Panics if NULL.
12248    #[inline(always)]
12249    #[track_caller]
12250    fn pop_stackref(&mut self) -> PyStackRef {
12251        expect_unchecked(
12252            self.pop_stackref_opt(),
12253            "pop stackref but null found. This is a compiler bug.",
12254        )
12255    }
12256
12257    /// Pop a value from the stack, returning None if the stack slot is NULL.
12258    /// Automatically promotes borrowed refs to owned.
12259    #[inline(always)]
12260    fn pop_value_opt(&mut self) -> Option<PyObjectRef> {
12261        self.pop_stackref_opt().map(|sr| sr.to_pyobj())
12262    }
12263
12264    #[inline(always)]
12265    #[track_caller]
12266    fn pop_value(&mut self) -> PyObjectRef {
12267        self.pop_stackref().to_pyobj()
12268    }
12269
12270    fn call_intrinsic_1(
12271        &mut self,
12272        func: bytecode::IntrinsicFunction1,
12273        arg: PyObjectRef,
12274        vm: &VirtualMachine,
12275    ) -> PyResult {
12276        match func {
12277            bytecode::IntrinsicFunction1::Invalid => {
12278                unreachable!("This is a bug in RustPython compiler")
12279            }
12280            bytecode::IntrinsicFunction1::Print => {
12281                let displayhook = vm
12282                    .sys_module
12283                    .get_attr("displayhook", vm)
12284                    .map_err(|_| vm.new_runtime_error("lost sys.displayhook"))?;
12285                displayhook.call((arg,), vm)
12286            }
12287            bytecode::IntrinsicFunction1::ImportStar => {
12288                // arg is the module object
12289                self.push_value(arg); // Push module back on stack for import_star
12290                self.import_star(vm)?;
12291                Ok(vm.ctx.none())
12292            }
12293            bytecode::IntrinsicFunction1::UnaryPositive => vm._pos(&arg),
12294            bytecode::IntrinsicFunction1::SubscriptGeneric => {
12295                // Used for PEP 695: Generic[*type_params]
12296                crate::builtins::genericalias::subscript_generic(arg, vm)
12297            }
12298            bytecode::IntrinsicFunction1::TypeVar => {
12299                let type_var: PyObjectRef =
12300                    _typing::TypeVar::new(vm, arg, vm.ctx.none(), vm.ctx.none())
12301                        .into_ref(&vm.ctx)
12302                        .into();
12303                Ok(type_var)
12304            }
12305            bytecode::IntrinsicFunction1::ParamSpec => {
12306                let param_spec: PyObjectRef =
12307                    _typing::ParamSpec::new(arg, vm).into_ref(&vm.ctx).into();
12308                Ok(param_spec)
12309            }
12310            bytecode::IntrinsicFunction1::TypeVarTuple => {
12311                let type_var_tuple: PyObjectRef =
12312                    _typing::TypeVarTuple::new(arg, vm).into_ref(&vm.ctx).into();
12313                Ok(type_var_tuple)
12314            }
12315            bytecode::IntrinsicFunction1::TypeAlias => {
12316                // TypeAlias receives a tuple of (name, type_params, value)
12317                let tuple: PyTupleRef = arg
12318                    .downcast()
12319                    .map_err(|_| vm.new_type_error("TypeAlias expects a tuple argument"))?;
12320
12321                if tuple.as_slice().len() != 3 {
12322                    return Err(vm.new_type_error(format!(
12323                        "TypeAlias expects exactly 3 arguments, got {}",
12324                        tuple.as_slice().len()
12325                    )));
12326                }
12327
12328                let name = tuple.as_slice()[0].clone();
12329                let type_params_obj = tuple.as_slice()[1].clone();
12330                let compute_value = tuple.as_slice()[2].clone();
12331
12332                let type_params: PyTupleRef = if vm.is_none(&type_params_obj) {
12333                    vm.ctx.empty_tuple.clone()
12334                } else {
12335                    type_params_obj
12336                        .downcast()
12337                        .map_err(|_| vm.new_type_error("Type params must be a tuple."))?
12338                };
12339
12340                let name = name
12341                    .downcast::<crate::builtins::PyStr>()
12342                    .map_err(|_| vm.new_type_error("TypeAliasType name must be a string"))?;
12343                let type_alias = _typing::TypeAliasType::new(name, type_params, compute_value);
12344                Ok(type_alias.into_ref(&vm.ctx).into())
12345            }
12346            bytecode::IntrinsicFunction1::ListToTuple => {
12347                // Convert list to tuple
12348                let list = arg
12349                    .downcast::<PyList>()
12350                    .map_err(|_| vm.new_type_error("LIST_TO_TUPLE expects a list"))?;
12351                Ok(vm.ctx.new_tuple(list.borrow_vec().to_vec()).into())
12352            }
12353            bytecode::IntrinsicFunction1::StopIterationError => {
12354                // Convert StopIteration to RuntimeError (PEP 479)
12355                // Returns the exception object; RERAISE will re-raise it
12356                if arg.fast_isinstance(vm.ctx.exceptions.stop_iteration) {
12357                    let flags = &self.code.flags;
12358                    let msg = if flags.contains(bytecode::CodeFlags::ASYNC_GENERATOR) {
12359                        "async generator raised StopIteration"
12360                    } else if flags.contains(bytecode::CodeFlags::COROUTINE) {
12361                        "coroutine raised StopIteration"
12362                    } else {
12363                        "generator raised StopIteration"
12364                    };
12365                    let err = vm.new_runtime_error(msg);
12366                    // PEP 479 chains both __cause__ and __context__ to the
12367                    // original StopIteration; the explicit cause is what users
12368                    // see in tracebacks (suppress_context becomes true), but
12369                    // assertions that inspect __context__ also expect it set.
12370                    let cause: Option<PyBaseExceptionRef> = arg.downcast().ok();
12371                    err.set_context(cause.clone());
12372                    err.set_cause(cause);
12373                    Ok(err.into())
12374                } else {
12375                    // Not StopIteration, pass through for RERAISE
12376                    Ok(arg)
12377                }
12378            }
12379            bytecode::IntrinsicFunction1::AsyncGenWrap => {
12380                // Wrap value for async generator
12381                // Creates an AsyncGenWrappedValue
12382                Ok(crate::builtins::asyncgenerator::PyAsyncGenWrappedValue(arg)
12383                    .into_ref(&vm.ctx)
12384                    .into())
12385            }
12386        }
12387    }
12388
12389    fn call_intrinsic_2(
12390        &mut self,
12391        func: bytecode::IntrinsicFunction2,
12392        arg1: PyObjectRef,
12393        arg2: PyObjectRef,
12394        vm: &VirtualMachine,
12395    ) -> PyResult {
12396        match func {
12397            bytecode::IntrinsicFunction2::Invalid => {
12398                unreachable!("This is a bug in RustPython compiler")
12399            }
12400            bytecode::IntrinsicFunction2::SetTypeparamDefault => {
12401                crate::stdlib::_typing::set_typeparam_default(arg1, arg2, vm)
12402            }
12403            bytecode::IntrinsicFunction2::SetFunctionTypeParams => {
12404                // arg1 is the function, arg2 is the type params tuple
12405                // Set __type_params__ attribute on the function
12406                arg1.set_attr("__type_params__", arg2, vm)?;
12407                Ok(arg1)
12408            }
12409            bytecode::IntrinsicFunction2::TypeVarWithBound => {
12410                let type_var: PyObjectRef = _typing::TypeVar::new(vm, arg1, arg2, vm.ctx.none())
12411                    .into_ref(&vm.ctx)
12412                    .into();
12413                Ok(type_var)
12414            }
12415            bytecode::IntrinsicFunction2::TypeVarWithConstraint => {
12416                let type_var: PyObjectRef = _typing::TypeVar::new(vm, arg1, vm.ctx.none(), arg2)
12417                    .into_ref(&vm.ctx)
12418                    .into();
12419                Ok(type_var)
12420            }
12421            bytecode::IntrinsicFunction2::PrepReraiseStar => {
12422                // arg1 = orig (original exception)
12423                // arg2 = excs (list of exceptions raised/reraised in except* blocks)
12424                // Returns: exception to reraise, or None if nothing to reraise
12425                crate::exceptions::prep_reraise_star(&arg1, &arg2, vm)
12426            }
12427        }
12428    }
12429
12430    /// Take a call's `[self_or_null, arg1, ..., argN]` off the stack as one
12431    /// vectorcall argument list, along with the callable underneath them.
12432    ///
12433    /// The stack already holds the arguments in vectorcall order, so filling a
12434    /// single vector by index costs one allocation — collecting the positional
12435    /// arguments first and then pushing `self` in front of them costs two plus
12436    /// a copy.
12437    fn take_call_args(&mut self, nargs: usize) -> (PyObjectRef, Vec<PyObjectRef>) {
12438        let stack_len = self.localsplus.stack_len();
12439        debug_assert!(
12440            stack_len >= nargs + 2,
12441            "CALL stack underflow: need callable + self_or_null + {nargs} args, have {stack_len}"
12442        );
12443        let callable_idx = stack_len - nargs - 2;
12444        let self_or_null_idx = callable_idx + 1;
12445
12446        let self_or_null = self
12447            .localsplus
12448            .stack_index_mut(self_or_null_idx)
12449            .take()
12450            .map(|sr| sr.to_pyobj());
12451        let mut args = Vec::with_capacity(nargs + usize::from(self_or_null.is_some()));
12452        args.extend(self_or_null);
12453        for stack_idx in self_or_null_idx + 1..stack_len {
12454            let val = self
12455                .localsplus
12456                .stack_index_mut(stack_idx)
12457                .take()
12458                .unwrap()
12459                .to_pyobj();
12460            args.push(val);
12461        }
12462
12463        let callable = self
12464            .localsplus
12465            .stack_index_mut(callable_idx)
12466            .take()
12467            .unwrap()
12468            .to_pyobj();
12469        self.localsplus.stack_truncate(callable_idx);
12470        (callable, args)
12471    }
12472
12473    /// Pop multiple values from the stack. Panics if any slot is NULL.
12474    fn pop_multiple(&mut self, count: usize) -> impl ExactSizeIterator<Item = PyObjectRef> + '_ {
12475        let stack_len = self.localsplus.stack_len();
12476        if count > stack_len {
12477            let instr = self.code.instructions.get(self.lasti() as usize);
12478            let op_name = instr.map_or_else(|| "None".to_string(), |i| format!("{:?}", i.op));
12479            panic!(
12480                "Stack underflow in pop_multiple: trying to pop {} elements from stack with {} elements. lasti={}, code={}, op={}, source_path={}",
12481                count,
12482                stack_len,
12483                self.lasti(),
12484                self.code.obj_name,
12485                op_name,
12486                self.code.source_path()
12487            );
12488        }
12489        self.localsplus.stack_drain(stack_len - count).map(|obj| {
12490            expect_unchecked(obj, "pop_multiple but null found. This is a compiler bug.").to_pyobj()
12491        })
12492    }
12493
12494    #[inline]
12495    fn replace_top(&mut self, top: Option<PyObjectRef>) -> Option<PyObjectRef> {
12496        let mut slot = top.map(PyStackRef::new_owned);
12497        let last = self.localsplus.stack_last_mut().unwrap();
12498        core::mem::swap(last, &mut slot);
12499        slot.map(|sr| sr.to_pyobj())
12500    }
12501
12502    #[inline(always)]
12503    #[track_caller]
12504    fn top_value(&self) -> &PyObject {
12505        match self.localsplus.stack_last() {
12506            Some(Some(last)) => last.as_object(),
12507            Some(None) => self.fatal("tried to get top of stack but got NULL"),
12508            None => self.fatal("tried to get top of stack but stack is empty"),
12509        }
12510    }
12511
12512    #[inline(always)]
12513    #[track_caller]
12514    fn nth_value(&self, depth: u32) -> &PyObject {
12515        let idx = self.localsplus.stack_len() - depth as usize - 1;
12516        match self.localsplus.stack_index(idx) {
12517            Some(obj) => obj.as_object(),
12518            None => unsafe { core::hint::unreachable_unchecked() },
12519        }
12520    }
12521
12522    #[cold]
12523    #[inline(never)]
12524    #[track_caller]
12525    fn fatal(&self, msg: &'static str) -> ! {
12526        dbg!(self);
12527        panic!("{msg}")
12528    }
12529}
12530
12531impl fmt::Debug for FrameObject {
12532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12533        // SAFETY: Debug is best-effort; concurrent mutation is unlikely
12534        // and would only affect debug output.
12535        let Some(iframe) = (unsafe { &*self.iframe.get() }) else {
12536            return f.write_str("FrameObject Object { cleared }");
12537        };
12538        let stack_str =
12539            iframe
12540                .localsplus
12541                .stack_as_slice()
12542                .iter()
12543                .fold(String::new(), |mut s, slot| {
12544                    match slot {
12545                        Some(elem) if elem.downcastable::<Self>() => {
12546                            s.push_str("\n  > {frame}");
12547                        }
12548                        Some(elem) => {
12549                            core::fmt::write(&mut s, format_args!("\n  > {elem:?}")).unwrap();
12550                        }
12551                        None => {
12552                            s.push_str("\n  > NULL");
12553                        }
12554                    }
12555                    s
12556                });
12557        // TODO: fix this up
12558        write!(
12559            f,
12560            "FrameObject Object {{ \n Stack:{}\n Locals initialized:{}\n}}",
12561            stack_str,
12562            self.iframe().locals.get().is_some()
12563        )
12564    }
12565}
12566
12567/// _PyEval_SpecialMethodCanSuggest
12568fn special_method_can_suggest(
12569    obj: &PyObject,
12570    oparg: SpecialMethod,
12571    vm: &VirtualMachine,
12572) -> PyResult<bool> {
12573    Ok(match oparg {
12574        SpecialMethod::Enter | SpecialMethod::Exit => {
12575            vm.get_special_method(obj, get_special_method_name(SpecialMethod::AEnter, vm))?
12576                .is_some()
12577                && vm
12578                    .get_special_method(obj, get_special_method_name(SpecialMethod::AExit, vm))?
12579                    .is_some()
12580        }
12581        SpecialMethod::AEnter | SpecialMethod::AExit => {
12582            vm.get_special_method(obj, get_special_method_name(SpecialMethod::Enter, vm))?
12583                .is_some()
12584                && vm
12585                    .get_special_method(obj, get_special_method_name(SpecialMethod::Exit, vm))?
12586                    .is_some()
12587        }
12588    })
12589}
12590
12591fn get_special_method_name(oparg: SpecialMethod, vm: &VirtualMachine) -> &'static PyStrInterned {
12592    match oparg {
12593        SpecialMethod::Enter => identifier!(vm, __enter__),
12594        SpecialMethod::Exit => identifier!(vm, __exit__),
12595        SpecialMethod::AEnter => identifier!(vm, __aenter__),
12596        SpecialMethod::AExit => identifier!(vm, __aexit__),
12597    }
12598}
12599
12600/// _Py_SpecialMethod _Py_SpecialMethods
12601fn get_special_method_error_msg(
12602    oparg: SpecialMethod,
12603    class_name: &str,
12604    can_suggest: bool,
12605) -> String {
12606    if can_suggest {
12607        match oparg {
12608            SpecialMethod::Enter => format!(
12609                "'{class_name}' object does not support the context manager protocol (missed __enter__ method) but it supports the asynchronous context manager protocol. Did you mean to use 'async with'?"
12610            ),
12611            SpecialMethod::Exit => format!(
12612                "'{class_name}' object does not support the context manager protocol (missed __exit__ method) but it supports the asynchronous context manager protocol. Did you mean to use 'async with'?"
12613            ),
12614            SpecialMethod::AEnter => format!(
12615                "'{class_name}' object does not support the asynchronous context manager protocol (missed __aenter__ method) but it supports the context manager protocol. Did you mean to use 'with'?"
12616            ),
12617            SpecialMethod::AExit => format!(
12618                "'{class_name}' object does not support the asynchronous context manager protocol (missed __aexit__ method) but it supports the context manager protocol. Did you mean to use 'with'?"
12619            ),
12620        }
12621    } else {
12622        match oparg {
12623            SpecialMethod::Enter => format!(
12624                "'{class_name}' object does not support the context manager protocol (missed __enter__ method)"
12625            ),
12626            SpecialMethod::Exit => format!(
12627                "'{class_name}' object does not support the context manager protocol (missed __exit__ method)"
12628            ),
12629            SpecialMethod::AEnter => format!(
12630                "'{class_name}' object does not support the asynchronous context manager protocol (missed __aenter__ method)"
12631            ),
12632            SpecialMethod::AExit => format!(
12633                "'{class_name}' object does not support the asynchronous context manager protocol (missed __aexit__ method)"
12634            ),
12635        }
12636    }
12637}
12638
12639fn is_module_initializing(module: &PyObject, vm: &VirtualMachine) -> bool {
12640    let Ok(spec) = module.get_attr(&vm.ctx.new_str("__spec__"), vm) else {
12641        return false;
12642    };
12643    if vm.is_none(&spec) {
12644        return false;
12645    }
12646    let Ok(initializing_attr) = spec.get_attr(&vm.ctx.new_str("_initializing"), vm) else {
12647        return false;
12648    };
12649    initializing_attr.try_to_bool(vm).unwrap_or(false)
12650}
12651
12652fn expect_unchecked<T: fmt::Debug>(optional: Option<T>, err_msg: &'static str) -> T {
12653    if cfg!(debug_assertions) {
12654        optional.expect(err_msg)
12655    } else {
12656        unsafe { optional.unwrap_unchecked() }
12657    }
12658}