Skip to main content

rustpython_vm/builtins/
frame.rs

1/*! The python `frame` type.
2
3*/
4
5use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyIntRef};
6use crate::{
7    AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
8    class::PyClassImpl,
9    frame::{FrameObject, FrameOwner},
10    function::PySetterValue,
11    types::{Destructor, Representable},
12};
13use core::sync::atomic::Ordering::Relaxed;
14use num_traits::Zero;
15#[allow(unused_imports)]
16use rustpython_common::atomic::Radium;
17use rustpython_compiler_core::bytecode::{self, Constant, Instruction, StackEffect};
18use stack_analysis::*;
19
20/// Stack state analysis for safe line-number jumps.
21///
22/// Models the evaluation stack as a 64-bit integer, encoding the kind of each
23/// stack entry in 3-bit blocks. Used by `set_f_lineno` to verify that a jump
24/// is safe and to determine how many values need to be popped.
25pub(crate) mod stack_analysis {
26    use super::*;
27
28    const BITS_PER_BLOCK: u32 = 3;
29    const MASK: i64 = (1 << BITS_PER_BLOCK) - 1; // 0b111
30    const MAX_STACK_ENTRIES: u32 = 63 / BITS_PER_BLOCK; // 21
31    const WILL_OVERFLOW: u64 = 1u64 << ((MAX_STACK_ENTRIES - 1) * BITS_PER_BLOCK);
32
33    pub(crate) const EMPTY_STACK: i64 = 0;
34    pub(crate) const UNINITIALIZED: i64 = -2;
35    pub(crate) const OVERFLOWED: i64 = -1;
36
37    /// Kind of a stack entry.
38    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
39    #[repr(i64)]
40    pub(crate) enum Kind {
41        Iterator = 1,
42        Except = 2,
43        Object = 3,
44        Null = 4,
45        Lasti = 5,
46    }
47
48    impl Kind {
49        const fn from_i64(v: i64) -> Option<Self> {
50            Some(match v {
51                1 => Self::Iterator,
52                2 => Self::Except,
53                3 => Self::Object,
54                4 => Self::Null,
55                5 => Self::Lasti,
56                _ => return None,
57            })
58        }
59    }
60
61    pub(crate) const fn push_value(stack: i64, kind: i64) -> i64 {
62        if (stack as u64) >= WILL_OVERFLOW {
63            OVERFLOWED
64        } else {
65            (stack << BITS_PER_BLOCK) | kind
66        }
67    }
68
69    pub(crate) const fn pop_value(stack: i64) -> i64 {
70        stack >> BITS_PER_BLOCK
71    }
72
73    pub(crate) const fn top_of_stack(stack: i64) -> i64 {
74        stack & MASK
75    }
76
77    const fn peek(stack: i64, n: u32) -> i64 {
78        debug_assert!(n >= 1);
79        (stack >> (BITS_PER_BLOCK * (n - 1))) & MASK
80    }
81
82    const fn stack_swap(stack: i64, n: u32) -> i64 {
83        debug_assert!(n >= 1);
84        let to_swap = peek(stack, n);
85        let top = top_of_stack(stack);
86        let shift = BITS_PER_BLOCK * (n - 1);
87        let replaced_low = (stack & !(MASK << shift)) | (top << shift);
88        (replaced_low & !MASK) | to_swap
89    }
90
91    const fn pop_to_level(mut stack: i64, level: u32) -> i64 {
92        if level == 0 {
93            return EMPTY_STACK;
94        }
95        let max_item: i64 = (1 << BITS_PER_BLOCK) - 1;
96        let level_max_stack = max_item << ((level - 1) * BITS_PER_BLOCK);
97        while stack > level_max_stack {
98            stack = pop_value(stack);
99        }
100        stack
101    }
102
103    #[must_use]
104    const fn compatible_kind(from: i64, to: i64) -> bool {
105        if to == 0 {
106            false
107        } else if to == Kind::Object as i64 {
108            from != Kind::Null as i64
109        } else if to == Kind::Null as i64 {
110            true
111        } else {
112            from == to
113        }
114    }
115
116    #[must_use]
117    pub(crate) const fn compatible_stack(from_stack: i64, to_stack: i64) -> bool {
118        if from_stack < 0 || to_stack < 0 {
119            return false;
120        }
121        let mut from = from_stack;
122        let mut to = to_stack;
123        while from > to {
124            from = pop_value(from);
125        }
126        while from != 0 {
127            let from_top = top_of_stack(from);
128            let to_top = top_of_stack(to);
129            if !compatible_kind(from_top, to_top) {
130                return false;
131            }
132            from = pop_value(from);
133            to = pop_value(to);
134        }
135        to == 0
136    }
137
138    pub(crate) const fn explain_incompatible_stack(to_stack: i64) -> &'static str {
139        debug_assert!(to_stack != 0);
140
141        if to_stack == OVERFLOWED {
142            return "stack is too deep to analyze";
143        }
144
145        if to_stack == UNINITIALIZED {
146            return "can't jump into an exception handler, or code may be unreachable";
147        }
148
149        match Kind::from_i64(top_of_stack(to_stack)) {
150            Some(Kind::Except) => "can't jump into an 'except' block as there's no exception",
151            Some(Kind::Lasti) => "can't jump into a re-raising block as there's no location",
152            Some(Kind::Iterator) => "can't jump into the body of a for loop",
153            _ => "incompatible stacks",
154        }
155    }
156
157    /// Analyze bytecode and compute the stack state at each instruction index.
158    pub(crate) fn mark_stacks<C: Constant>(code: &bytecode::CodeObject<C>) -> Vec<i64> {
159        let instructions = &*code.instructions;
160        let len = instructions.len();
161
162        let mut stacks = vec![UNINITIALIZED; len + 1];
163        stacks[0] = EMPTY_STACK;
164
165        let mut todo = true;
166        while todo {
167            todo = false;
168
169            let mut i = 0;
170            while i < len {
171                let mut next_stack = stacks[i];
172                let mut opcode = instructions[i].op;
173                let mut oparg: u32 = 0;
174
175                // Accumulate EXTENDED_ARG prefixes
176                while matches!(opcode, Instruction::ExtendedArg) {
177                    oparg = (oparg << 8) | u32::from(u8::from(instructions[i].arg));
178                    i += 1;
179                    if i >= len {
180                        break;
181                    }
182                    stacks[i] = next_stack;
183                    opcode = instructions[i].op;
184                }
185                if i >= len {
186                    break;
187                }
188                oparg = (oparg << 8) | u32::from(u8::from(instructions[i].arg));
189
190                // De-instrument and de-specialize: get the underlying base instruction
191                let opcode = opcode.to_base().unwrap_or(opcode).deoptimize();
192
193                let caches = opcode.cache_entries();
194                let next_i = i + 1 + caches;
195
196                if next_stack == UNINITIALIZED {
197                    i = next_i;
198                    continue;
199                }
200
201                match opcode {
202                    Instruction::PopJumpIfFalse { .. }
203                    | Instruction::PopJumpIfTrue { .. }
204                    | Instruction::PopJumpIfNone { .. }
205                    | Instruction::PopJumpIfNotNone { .. } => {
206                        // Relative forward: target = after_caches + delta
207                        let j = next_i + oparg as usize;
208                        next_stack = pop_value(next_stack);
209                        let target_stack = next_stack;
210                        if j < stacks.len() && stacks[j] == UNINITIALIZED {
211                            stacks[j] = target_stack;
212                        }
213                        if next_i < stacks.len() {
214                            stacks[next_i] = next_stack;
215                        }
216                    }
217                    Instruction::Send { .. } => {
218                        // Relative forward: target = after_caches + delta
219                        let j = next_i + oparg as usize;
220                        if j < stacks.len() && stacks[j] == UNINITIALIZED {
221                            stacks[j] = next_stack;
222                        }
223                        if next_i < stacks.len() {
224                            stacks[next_i] = next_stack;
225                        }
226                    }
227                    Instruction::JumpForward { .. } => {
228                        // Relative forward: target = after_caches + delta
229                        let j = next_i + oparg as usize;
230                        if j < stacks.len() && stacks[j] == UNINITIALIZED {
231                            stacks[j] = next_stack;
232                        }
233                    }
234                    Instruction::JumpBackward { .. }
235                    | Instruction::JumpBackwardNoInterrupt { .. } => {
236                        // Relative backward: target = after_caches - delta
237                        let j = next_i - oparg as usize;
238                        if j < stacks.len() && stacks[j] == UNINITIALIZED {
239                            stacks[j] = next_stack;
240                            if j < i {
241                                todo = true;
242                            }
243                        }
244                    }
245                    Instruction::GetIter | Instruction::GetAiter => {
246                        next_stack = push_value(pop_value(next_stack), Kind::Iterator as i64);
247                        if next_i < stacks.len() {
248                            stacks[next_i] = next_stack;
249                        }
250                    }
251                    Instruction::ForIter { .. } => {
252                        // Fall-through (iteration continues): pushes the next value
253                        let body_stack = push_value(next_stack, Kind::Object as i64);
254                        if next_i < stacks.len() {
255                            stacks[next_i] = body_stack;
256                        }
257                        // Exhaustion path: relative forward from after_caches
258                        let mut j = next_i + oparg as usize;
259                        if j < instructions.len() {
260                            let target_op =
261                                instructions[j].op.to_base().unwrap_or(instructions[j].op);
262                            if matches!(target_op, Instruction::EndFor) {
263                                j += 1;
264                            }
265                        }
266                        if j < stacks.len() && stacks[j] == UNINITIALIZED {
267                            stacks[j] = next_stack;
268                        }
269                    }
270                    Instruction::EndAsyncFor => {
271                        next_stack = pop_value(pop_value(next_stack));
272                        if next_i < stacks.len() {
273                            stacks[next_i] = next_stack;
274                        }
275                    }
276                    Instruction::PushExcInfo => {
277                        next_stack = push_value(next_stack, Kind::Except as i64);
278                        if next_i < stacks.len() {
279                            stacks[next_i] = next_stack;
280                        }
281                    }
282                    Instruction::PopExcept => {
283                        next_stack = pop_value(next_stack);
284                        if next_i < stacks.len() {
285                            stacks[next_i] = next_stack;
286                        }
287                    }
288                    Instruction::ReturnValue => {
289                        // End of block, no fall-through
290                    }
291                    Instruction::RaiseVarargs { .. } => {
292                        // End of block, no fall-through
293                    }
294                    Instruction::Reraise { .. } => {
295                        // End of block, no fall-through
296                    }
297                    Instruction::PushNull => {
298                        next_stack = push_value(next_stack, Kind::Null as i64);
299                        if next_i < stacks.len() {
300                            stacks[next_i] = next_stack;
301                        }
302                    }
303                    Instruction::LoadGlobal { .. } => {
304                        next_stack = push_value(next_stack, Kind::Object as i64);
305                        if oparg & 1 != 0 {
306                            next_stack = push_value(next_stack, Kind::Null as i64);
307                        }
308                        if next_i < stacks.len() {
309                            stacks[next_i] = next_stack;
310                        }
311                    }
312                    Instruction::LoadAttr { .. } => {
313                        // LoadAttr: pops object, pushes result
314                        // If oparg & 1, it also pushes Null (method load)
315                        let attr_oparg = oparg;
316                        if attr_oparg & 1 != 0 {
317                            next_stack = pop_value(next_stack);
318                            next_stack = push_value(next_stack, Kind::Object as i64);
319                            next_stack = push_value(next_stack, Kind::Null as i64);
320                        }
321                        // else: default stack_effect handles it
322                        else {
323                            let effect: StackEffect = opcode.stack_effect_info(oparg);
324                            let popped = effect.popped() as i64;
325                            let pushed = effect.pushed() as i64;
326                            for _ in 0..popped {
327                                next_stack = pop_value(next_stack);
328                            }
329                            for _ in 0..pushed {
330                                next_stack = push_value(next_stack, Kind::Object as i64);
331                            }
332                        }
333                        if next_i < stacks.len() {
334                            stacks[next_i] = next_stack;
335                        }
336                    }
337                    Instruction::Swap { .. } => {
338                        let n = oparg;
339                        next_stack = stack_swap(next_stack, n);
340                        if next_i < stacks.len() {
341                            stacks[next_i] = next_stack;
342                        }
343                    }
344                    Instruction::Copy { .. } => {
345                        let n = oparg;
346                        next_stack = push_value(next_stack, peek(next_stack, n));
347                        if next_i < stacks.len() {
348                            stacks[next_i] = next_stack;
349                        }
350                    }
351                    _ => {
352                        // PyCompile_OpcodeStackEffect: apply the net delta so
353                        // overlapping in/out (GET_ANEXT: aiter -- aiter, awaitable)
354                        // keep the original kind of the surviving entries.
355                        let mut delta = opcode.stack_effect(oparg);
356                        while delta < 0 {
357                            next_stack = pop_value(next_stack);
358                            delta += 1;
359                        }
360                        while delta > 0 {
361                            next_stack = push_value(next_stack, Kind::Object as i64);
362                            delta -= 1;
363                        }
364                        if next_i < stacks.len() {
365                            stacks[next_i] = next_stack;
366                        }
367                    }
368                }
369                i = next_i;
370            }
371
372            // Scan exception table
373            let exception_table = bytecode::decode_exception_table(&code.exceptiontable);
374            for entry in &exception_table {
375                let start_offset = entry.start as usize;
376                let handler = entry.target as usize;
377                let level = entry.depth as u32;
378                let has_lasti = entry.push_lasti;
379
380                if start_offset < stacks.len()
381                    && stacks[start_offset] != UNINITIALIZED
382                    && handler < stacks.len()
383                    && stacks[handler] == UNINITIALIZED
384                {
385                    todo = true;
386                    let mut target_stack = pop_to_level(stacks[start_offset], level);
387                    if has_lasti {
388                        target_stack = push_value(target_stack, Kind::Lasti as i64);
389                    }
390                    target_stack = push_value(target_stack, Kind::Except as i64);
391                    stacks[handler] = target_stack;
392                }
393            }
394        }
395
396        stacks
397    }
398
399    /// Build a mapping from instruction index to line number.
400    /// Returns -1 for indices with no line start.
401    pub(crate) fn mark_lines<C: Constant>(code: &bytecode::CodeObject<C>) -> Vec<i32> {
402        let len = code.instructions.len();
403        let mut line_starts = vec![-1i32; len];
404        let mut last_line: i32 = -1;
405
406        for (i, (loc, _)) in code.locations.iter().enumerate() {
407            if i >= len {
408                break;
409            }
410            let line = loc.line.get() as i32;
411            if line != last_line && line > 0 {
412                line_starts[i] = line;
413                last_line = line;
414            }
415        }
416        line_starts
417    }
418
419    /// Find the first line number >= `line` that has code.
420    pub(crate) fn first_line_not_before(lines: &[i32], line: i32) -> i32 {
421        let mut result = i32::MAX;
422        for &l in lines {
423            if l >= line && l < result {
424                result = l;
425            }
426        }
427        if result == i32::MAX { -1 } else { result }
428    }
429}
430
431pub(crate) fn init(context: &'static Context) {
432    FrameObject::extend_class(context, context.types.frame_type);
433}
434
435impl Representable for FrameObject {
436    fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
437        let code = zelf.iframe().code();
438        let file_repr = code.source_path().to_owned().as_object().repr(vm)?;
439        let lineno = zelf.lineno();
440        let name = code.code.obj_name.as_wtf8();
441        let ptr = zelf as *const Py<Self> as usize;
442        Ok(format!(
443            "<frame at {ptr:#x}, file {}, line {lineno}, code {name}>",
444            file_repr.as_wtf8(),
445        ))
446    }
447}
448
449impl FrameObject {
450    /// Find the live source InterpreterFrame on the TLS chain for a
451    /// materialized FrameObject. Returns the raw pointer if found, or null
452    /// if this FrameObject has no live source (already returned or not
453    /// currently executing on this thread).
454    pub(crate) fn find_live_source_iframe(&self) -> *const crate::frame::InterpreterFrame {
455        let self_py_ptr = unsafe { Py::<Self>::from_payload_ptr(self) } as usize;
456        let mut cur = crate::vm::thread::get_current_frame();
457        while !cur.is_null() {
458            let materialized = unsafe { (*cur).materialized.load(Relaxed) };
459            if materialized == self_py_ptr {
460                return cur;
461            }
462            cur = unsafe { &*cur }.previous();
463        }
464        core::ptr::null()
465    }
466}
467
468impl FrameObject {
469    /// Current line, or -1 when the linetable has no line for lasti.
470    pub fn lineno(&self) -> i32 {
471        self.f_code().addr2line(self.f_lasti() as i32)
472    }
473
474    pub fn f_code(&self) -> PyRef<PyCode> {
475        self.iframe().code().to_owned()
476    }
477    fn f_lasti(&self) -> u32 {
478        // Byte offset of the current opcode. lasti is stored as the next
479        // instruction index (see FrameObject::run), so the executing unit
480        // is lasti-1.
481        let live = self.find_live_source_iframe();
482        let val = if !live.is_null() {
483            unsafe { (*live).lasti.load(Relaxed) }
484        } else {
485            self.lasti()
486        };
487        if val == 0 { 0 } else { (val - 1) * 2 }
488    }
489}
490
491#[pyclass(
492    itemsize = core::mem::size_of::<crate::PyObjectRef>(),
493    flags(DISALLOW_INSTANTIATION),
494    with(Py, Representable)
495)]
496impl FrameObject {}
497
498#[pyclass]
499impl Py<FrameObject> {
500    #[pymethod]
501    // = frame_clear_impl
502    fn clear(&self, vm: &VirtualMachine) -> PyResult<()> {
503        // Materialized stack frames are FrameObject-owned even while their
504        // source iframe is still executing. TLS lookup below only sees the
505        // calling thread, so attached_tid is the cross-thread-safe execution
506        // state check.
507        if self.iframe().attached_tid() != 0 {
508            return Err(vm.new_runtime_error("cannot clear an executing frame"));
509        }
510        let owner = FrameOwner::from_i8(
511            self.iframe()
512                .owner
513                .load(core::sync::atomic::Ordering::Acquire),
514        );
515        match owner {
516            FrameOwner::Generator => {
517                // FRAME_SUSPENDED (lasti > 0) cannot be cleared. FRAME_CREATED
518                // and finished frames go through the owner finalizer.
519                if self.lasti() != 0 {
520                    return Err(vm.new_runtime_error("cannot clear a suspended frame"));
521                }
522                if let Some(owner) = self.iframe().generator.to_owned() {
523                    if let Some(coro) = owner.downcast_ref::<PyCoroutine>() {
524                        let _ = PyCoroutine::del(coro, vm);
525                    } else if let Some(async_gen) = owner.downcast_ref::<PyAsyncGen>() {
526                        let _ = PyAsyncGen::del(async_gen, vm);
527                    } else if let Some(generator) = owner.downcast_ref::<PyGenerator>() {
528                        let _ = PyGenerator::del(generator, vm);
529                    }
530                }
531                return Ok(());
532            }
533            FrameOwner::Thread => {
534                return Err(vm.new_runtime_error("cannot clear an executing frame"));
535            }
536            FrameOwner::FrameObject => {
537                if !self.find_live_source_iframe().is_null() {
538                    return Err(vm.new_runtime_error("cannot clear an executing frame"));
539                }
540            }
541        }
542
543        // Move references out before dropping them. Their finalizers may
544        // re-enter this frame, so no locals borrow or cold-data lock may be
545        // held while they run.
546        let fastlocals = {
547            // SAFETY: FrameObject is not executing (detached or stopped).
548            let slots = unsafe { self.fastlocals_mut() };
549            slots
550                .iter_mut()
551                .filter_map(Option::take)
552                .collect::<Vec<_>>()
553        };
554
555        // Clear the evaluation stack and cell references
556        self.clear_stack_and_cells();
557
558        let (temporary_refs, extra_locals, locals_cache, overwritten, retained_back) =
559            match self.iframe().cold_opt() {
560                Some(cold) => (
561                    core::mem::take(&mut *cold.temporary_refs.lock()),
562                    cold.f_extra_locals.lock().take(),
563                    cold.f_locals_cache.lock().take(),
564                    core::mem::take(&mut *cold.f_overwritten_fast_locals.lock()),
565                    cold.retained_back.lock().take(),
566                ),
567                None => (Vec::new(), None, None, Vec::new(), None),
568            };
569        drop((
570            fastlocals,
571            temporary_refs,
572            extra_locals,
573            locals_cache,
574            overwritten,
575            retained_back,
576        ));
577
578        Ok(())
579    }
580
581    #[pygetset]
582    pub fn f_locals(&self, vm: &VirtualMachine) -> PyResult {
583        if self.uses_locals_proxy(vm)? {
584            let proxy = crate::builtins::FrameLocalsProxy::new(self.to_owned());
585            Ok(proxy.into_ref(&vm.ctx).into())
586        } else {
587            Ok(self.iframe().locals.clone_mapping(vm).into())
588        }
589    }
590
591    #[pygetset]
592    fn f_generator(&self) -> Option<PyObjectRef> {
593        self.iframe().generator.to_owned()
594    }
595
596    #[pygetset]
597    pub fn f_back(&self, #[allow(unused)] vm: &VirtualMachine) -> Option<PyRef<FrameObject>> {
598        let mut prev = self.previous_iframe();
599
600        // For materialized frames (previous == 0), find the source iframe on
601        // the TLS chain and use its `previous` instead.
602        if prev.is_null() {
603            // materialized stores `*const Py<FrameObject>` as usize.
604            // `self` is `&Py<FrameObject>` — compare addresses directly.
605            let self_py_ptr = self as *const Self as usize;
606            let mut cur = crate::vm::thread::get_current_frame();
607            while !cur.is_null() {
608                let materialized = unsafe { (*cur).materialized.load(Relaxed) };
609                if materialized == self_py_ptr {
610                    // Found the source iframe — use its previous
611                    prev = unsafe { (*cur).previous() };
612                    break;
613                }
614                cur = unsafe { (*cur).previous() };
615            }
616            if prev.is_null() {
617                // Check retained_back for frames whose callers have returned
618                let retained = self.iframe().cold().retained_back.lock().clone();
619                if let Some(frame) = retained {
620                    return Some(frame);
621                }
622                return None;
623            }
624        }
625
626        // Walk the TLS chain to find the prev iframe and materialize it.
627        // This handles both heap-allocated FrameObjects and stack-allocated
628        // iframes that haven't been observed yet.
629        {
630            let mut cur = crate::vm::thread::get_current_frame();
631            while !cur.is_null() {
632                if core::ptr::eq(cur, prev) {
633                    let iframe_ref = unsafe { &*cur };
634                    let fo = iframe_ref.materialize(vm);
635                    return Some(fo.to_owned());
636                }
637                cur = unsafe { (*cur).previous() };
638            }
639        }
640
641        // The caller already returned — check retained_back
642        let retained = self.iframe().cold().retained_back.lock().clone();
643        if let Some(frame) = retained {
644            return Some(frame);
645        }
646
647        // The caller lives on another thread. Use stop-the-world to
648        // safely materialize the cross-thread frame chain.
649        #[cfg(feature = "threading")]
650        {
651            // Enter STW before dereferencing `prev` — the owning thread may
652            // return and free the stack-allocated iframe at any time.
653            vm.state.stop_the_world.stop_the_world(&vm.state);
654            scopeguard::defer! { vm.state.stop_the_world.start_the_world(&vm.state); }
655            let prev_ref = unsafe { &*prev };
656            // Fast path: already materialized.
657            if let Some(fo) = prev_ref.frame_obj() {
658                return Some(fo.to_owned());
659            }
660            // Slow path: copy the whole chain, linked through retained_back.
661            // SAFETY: the world is stopped, so the owning thread is parked.
662            let fo = unsafe { prev_ref.materialize_detached_chain(vm) };
663            return Some(fo);
664        }
665
666        #[allow(unreachable_code)]
667        None
668    }
669
670    #[pygetset]
671    pub fn f_globals(&self) -> PyDictRef {
672        self.iframe().globals().to_owned()
673    }
674
675    #[pygetset]
676    pub fn f_builtins(&self) -> PyObjectRef {
677        self.iframe().builtins().to_owned()
678    }
679
680    #[pygetset]
681    pub fn f_code(&self) -> PyRef<PyCode> {
682        self.payload.f_code()
683    }
684
685    #[pygetset]
686    fn f_lasti(&self) -> u32 {
687        self.payload.f_lasti()
688    }
689
690    #[pygetset]
691    fn f_lineno(&self) -> Option<usize> {
692        let lineno = self.lineno();
693        (lineno >= 0).then_some(lineno as usize)
694    }
695
696    #[pygetset(setter)]
697    fn set_f_lineno(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
698        let l_new_lineno = match value {
699            PySetterValue::Assign(val) => {
700                let line_ref: PyIntRef = val
701                    .downcast()
702                    .map_err(|_| vm.new_value_error("lineno must be an integer"))?;
703                line_ref
704                    .try_to_primitive::<i32>(vm)
705                    .map_err(|_| vm.new_value_error("lineno must be an integer"))?
706            }
707            PySetterValue::Delete => {
708                return Err(vm.new_attribute_error("cannot delete attribute"));
709            }
710        };
711
712        let Some(what_event) = vm.what_event.get() else {
713            return Err(
714                vm.new_value_error("f_lineno can only be set in a trace function".to_owned())
715            );
716        };
717        {
718            use crate::stdlib::sys::monitoring::MonitoringEvent as Ev;
719            match what_event {
720                Ev::PyResume
721                | Ev::Jump
722                | Ev::Branch
723                | Ev::BranchLeft
724                | Ev::BranchRight
725                | Ev::Line
726                | Ev::PyYield => {}
727                Ev::PyStart => {
728                    return Err(vm.new_value_error(
729                        "can't jump from the 'call' trace event of a new frame".to_owned(),
730                    ));
731                }
732                Ev::Call | Ev::CReturn => {
733                    return Err(vm.new_value_error("can't jump during a call".to_owned()));
734                }
735                Ev::PyReturn
736                | Ev::PyUnwind
737                | Ev::PyThrow
738                | Ev::Raise
739                | Ev::CRaise
740                | Ev::Instruction
741                | Ev::ExceptionHandled => {
742                    return Err(
743                        vm.new_value_error("can only jump from a 'line' trace event".to_owned())
744                    );
745                }
746                Ev::StopIteration | Ev::Reraise => {
747                    return Err(vm.new_system_error("unexpected event type".to_owned()));
748                }
749            }
750        }
751
752        let first_line = self
753            .iframe()
754            .code()
755            .first_line_number
756            .map_or(1, |n| n.get() as i32);
757
758        if l_new_lineno < first_line {
759            return Err(vm.new_value_error(format!(
760                "line {l_new_lineno} comes before the current code block"
761            )));
762        }
763
764        let py_code: &Py<PyCode> = self.iframe().code();
765        let code = &py_code.code;
766        let lines = mark_lines(code);
767
768        // Find the first line >= target that has actual code
769        let new_lineno = first_line_not_before(&lines, l_new_lineno);
770        if new_lineno < 0 {
771            return Err(vm.new_value_error(format!(
772                "line {l_new_lineno} comes after the current code block"
773            )));
774        }
775
776        let stacks = mark_stacks(code);
777        let len = self.iframe().code().instructions.len();
778
779        // lasti points past the current instruction (already incremented).
780        // stacks[lasti - 1] gives the stack state before executing the
781        // instruction that triggered this trace event, which is the current
782        // evaluation stack.  Read from the live iframe when available so the
783        // value reflects the actual execution position.
784        let live = self.find_live_source_iframe();
785        let current_lasti = if !live.is_null() {
786            (unsafe { (*live).lasti.load(Relaxed) }) as usize
787        } else {
788            self.lasti() as usize
789        };
790        let start_idx = current_lasti.saturating_sub(1);
791        let mut start_stack = if start_idx < stacks.len() {
792            stacks[start_idx]
793        } else {
794            OVERFLOWED
795        };
796        let mut best_stack = OVERFLOWED;
797        let mut best_addr: i32 = -1;
798        let mut err: i32 = -1;
799        let mut msg = "cannot find bytecode for specified line";
800
801        for i in 0..len {
802            if lines[i] == new_lineno {
803                let target_stack = stacks[i];
804                if compatible_stack(start_stack, target_stack) {
805                    err = 0;
806                    if target_stack > best_stack {
807                        best_stack = target_stack;
808                        best_addr = i as i32;
809                    }
810                } else if err < 0 {
811                    if start_stack == OVERFLOWED {
812                        msg = "stack to deep to analyze";
813                    } else if start_stack == UNINITIALIZED {
814                        msg = "can't jump from unreachable code";
815                    } else {
816                        msg = explain_incompatible_stack(target_stack);
817                        err = 1;
818                    }
819                }
820            }
821        }
822
823        if err != 0 {
824            return Err(vm.new_value_error(msg.to_owned()));
825        }
826
827        // Yield leaves the yielded value on the modeled stack; the eval
828        // loop has already popped it for a suspended generator.
829        let is_suspended = live.is_null()
830            && current_lasti > 0
831            && matches!(
832                FrameOwner::from_i8(self.iframe().owner.load(Relaxed)),
833                FrameOwner::Generator
834            );
835        if is_suspended {
836            start_stack = pop_value(start_stack);
837        }
838
839        // Count how many entries to pop
840        let mut pop_count = 0usize;
841        {
842            let mut s = start_stack;
843            while s > best_stack {
844                pop_count += 1;
845                s = pop_value(s);
846            }
847        }
848
849        // Store the pending unwind and new lasti. When this frame is backed
850        // by a live stack-allocated iframe, write to the live iframe so the
851        // execution loop picks up the jump target.  Reuse `live` from above.
852        let target = if !live.is_null() {
853            unsafe { &*live }
854        } else {
855            self.iframe()
856        };
857        target
858            .cold()
859            .pending_stack_pops
860            .store(pop_count as u32, Relaxed);
861        target
862            .cold()
863            .pending_unwind_from_stack
864            .store(start_stack, Relaxed);
865        // Bind None into any NULL localsplus slots the jump target may
866        // assume exist, rather than leaving LOAD_FAST to raise later.
867        let unbound = {
868            let fastlocals = unsafe {
869                let ptr = target as *const crate::frame::InterpreterFrame
870                    as *mut crate::frame::InterpreterFrame;
871                (*ptr).localsplus.fastlocals()
872            };
873            fastlocals.iter().filter(|slot| slot.is_none()).count()
874        };
875        if unbound > 0 {
876            let s = if unbound == 1 { "" } else { "s" };
877            crate::stdlib::_warnings::warn(
878                vm.ctx.exceptions.runtime_warning,
879                format!("assigning None to {unbound} unbound local{s}"),
880                1,
881                vm,
882            )?;
883            let none = vm.ctx.none();
884            let fastlocals = unsafe {
885                let ptr = target as *const crate::frame::InterpreterFrame
886                    as *mut crate::frame::InterpreterFrame;
887                (*ptr).localsplus.fastlocals_mut()
888            };
889            for slot in fastlocals.iter_mut() {
890                if slot.is_none() {
891                    *slot = Some(none.clone());
892                }
893            }
894        }
895
896        target.lasti.store(best_addr as u32, Relaxed);
897        Ok(())
898    }
899
900    #[pygetset]
901    fn f_trace(&self, vm: &VirtualMachine) -> PyObjectRef {
902        // Read from live source iframe if available.
903        let live = self.find_live_source_iframe();
904        let trace = if !live.is_null() {
905            unsafe { &*live }.cold().trace.lock().clone()
906        } else {
907            self.iframe().cold().trace.lock().clone()
908        };
909        trace.unwrap_or_else(|| vm.ctx.none())
910    }
911
912    #[pygetset(setter)]
913    fn set_f_trace(&self, value: PySetterValue, vm: &VirtualMachine) {
914        let trace = match value {
915            PySetterValue::Assign(v) => {
916                if vm.is_none(&v) {
917                    None
918                } else {
919                    Some(v)
920                }
921            }
922            PySetterValue::Delete => None,
923        };
924        // Whether this assignment turns tracing on for a frame that was
925        // previously untraced (e.g. bdb stepping back into a caller frame
926        // that had no f_trace). If so, `prev_line` may be stale -- it is
927        // only updated on the cold trace-event path -- and must be
928        // synced again to the currently-executing line so the next instruction
929        // doesn't fire a spurious 'line' event. See
930        // `InterpreterFrame::sync_prev_line_from_lasti`.
931        //
932        // Determine the canonical iframe to check/update *before* writing
933        // anything: for a live materialized frame, `self.iframe()` and the
934        // live source iframe found below alias the same underlying storage,
935        // so checking "was it unset" after writing through one of them
936        // would always observe the just-written value through the other.
937        let trace_is_some = trace.is_some();
938        let live = self.find_live_source_iframe();
939        let canonical = if !live.is_null() {
940            unsafe { &*live }
941        } else {
942            self.iframe()
943        };
944        let was_unset = canonical.cold().trace.lock().is_none();
945
946        // Set on the materialized FrameObject.
947        (*self.iframe().cold().trace.lock()).clone_from(&trace);
948        // Also propagate to the live source iframe if this is a
949        // materialized copy of a stack-allocated frame, so pdb's
950        // f_trace assignment takes effect on the executing frame.
951        if !live.is_null() {
952            *unsafe { &*live }.cold().trace.lock() = trace;
953        }
954
955        if was_unset && trace_is_some {
956            canonical.sync_prev_line_from_lasti();
957        }
958    }
959
960    #[pygetset]
961    fn f_trace_opcodes(&self, vm: &VirtualMachine) -> PyObjectRef {
962        let trace_opcodes = self.iframe().cold().trace_opcodes.lock();
963        vm.ctx.new_bool(*trace_opcodes).into()
964    }
965
966    #[pygetset(setter)]
967    fn set_f_trace_opcodes(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
968        match value {
969            PySetterValue::Assign(value) => {
970                let value: PyIntRef = value
971                    .downcast()
972                    .map_err(|_| vm.new_type_error("attribute value type must be bool"))?;
973
974                let val = !value.as_bigint().is_zero();
975                *self.iframe().cold().trace_opcodes.lock() = val;
976                // Propagate to live source iframe.
977                let live = self.find_live_source_iframe();
978                if !live.is_null() {
979                    *unsafe { &*live }.cold().trace_opcodes.lock() = val;
980                }
981
982                // TODO: Implement the equivalent of _PyEval_SetOpcodeTrace()
983
984                Ok(())
985            }
986            PySetterValue::Delete => Err(vm.new_type_error("can't delete numeric/char attribute")),
987        }
988    }
989}