Skip to main content

rustpython_vm/builtins/
code.rs

1//! Infamous code object. The python class `code`
2
3use super::descriptor::{MemberKind, MemberLayout};
4use super::{PyBytesRef, PyStrRef, PyTupleRef, PyType, set::PyFrozenSet};
5use crate::common::lock::PyMutex;
6#[cfg(feature = "host_env")]
7use crate::convert::ToPyException;
8use crate::{
9    AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
10    builtins::PyStrInterned,
11    bytecode::{self, AsBag, BorrowedConstant, CodeFlags, Constant, ConstantBag, Instruction},
12    class::{PyClassImpl, StaticType},
13    convert::ToPyObject,
14    frozen,
15    function::OptionalArg,
16    types::{Comparable, Constructor, Hashable, Representable},
17};
18use alloc::fmt;
19use core::{
20    borrow::Borrow,
21    ops::Deref,
22    sync::atomic::{AtomicPtr, AtomicU64, Ordering},
23};
24use malachite_bigint::BigInt;
25use num_traits::Zero;
26use rustpython_compiler_core::{OneIndexed, bytecode::CodeUnits, bytecode::PyCodeLocationInfoKind};
27
28/// State for iterating through code address ranges
29struct PyCodeAddressRange<'a> {
30    ar_start: i32,
31    ar_end: i32,
32    ar_line: i32,
33    computed_line: i32,
34    reader: LineTableReader<'a>,
35}
36
37impl<'a> PyCodeAddressRange<'a> {
38    fn new(linetable: &'a [u8], first_line: i32) -> Self {
39        PyCodeAddressRange {
40            ar_start: 0,
41            ar_end: 0,
42            ar_line: -1,
43            computed_line: first_line,
44            reader: LineTableReader::new(linetable),
45        }
46    }
47
48    /// Check if this is a NO_LINE marker (code 15)
49    fn is_no_line_marker(byte: u8) -> bool {
50        (byte >> 3) == 0x1f
51    }
52
53    /// Advance to next address range
54    fn advance(&mut self) -> bool {
55        if self.reader.at_end() {
56            return false;
57        }
58
59        let first_byte = match self.reader.read_byte() {
60            Some(b) => b,
61            None => return false,
62        };
63
64        if (first_byte & 0x80) == 0 {
65            return false; // Invalid linetable
66        }
67
68        let code = (first_byte >> 3) & 0x0f;
69        let length = ((first_byte & 0x07) + 1) as i32;
70
71        // Get line delta for this entry
72        let line_delta = self.get_line_delta(code);
73
74        // Update computed line
75        self.computed_line += line_delta;
76
77        // Check for NO_LINE marker
78        if Self::is_no_line_marker(first_byte) {
79            self.ar_line = -1;
80        } else {
81            self.ar_line = self.computed_line;
82        }
83
84        // Update address range
85        self.ar_start = self.ar_end;
86        self.ar_end += length * 2; // sizeof(_Py_CODEUNIT) = 2
87
88        // Skip remaining bytes for this entry
89        while !self.reader.at_end() {
90            if let Some(b) = self.reader.peek_byte() {
91                if (b & 0x80) != 0 {
92                    break;
93                }
94                self.reader.read_byte();
95            } else {
96                break;
97            }
98        }
99
100        true
101    }
102
103    fn get_line_delta(&mut self, code: u8) -> i32 {
104        let kind = match PyCodeLocationInfoKind::from_code(code) {
105            Some(k) => k,
106            None => return 0,
107        };
108
109        match kind {
110            PyCodeLocationInfoKind::None => 0, // NO_LINE marker
111            PyCodeLocationInfoKind::Long => {
112                let delta = self.reader.read_signed_varint();
113                // Skip end_line, col, end_col
114                self.reader.read_varint();
115                self.reader.read_varint();
116                self.reader.read_varint();
117                delta
118            }
119            PyCodeLocationInfoKind::NoColumns => self.reader.read_signed_varint(),
120            PyCodeLocationInfoKind::OneLine0 => {
121                self.reader.read_byte(); // Skip column
122                self.reader.read_byte(); // Skip end column
123                0
124            }
125            PyCodeLocationInfoKind::OneLine1 => {
126                self.reader.read_byte(); // Skip column
127                self.reader.read_byte(); // Skip end column
128                1
129            }
130            PyCodeLocationInfoKind::OneLine2 => {
131                self.reader.read_byte(); // Skip column
132                self.reader.read_byte(); // Skip end column
133                2
134            }
135            _ if kind.is_short() => {
136                self.reader.read_byte(); // Skip column byte
137                0
138            }
139            _ => 0,
140        }
141    }
142}
143
144#[derive(FromArgs)]
145pub struct ReplaceArgs {
146    #[pyarg(named, optional)]
147    co_posonlyargcount: OptionalArg<u32>,
148    #[pyarg(named, optional)]
149    co_argcount: OptionalArg<u32>,
150    #[pyarg(named, optional)]
151    co_kwonlyargcount: OptionalArg<u32>,
152    #[pyarg(named, optional)]
153    co_filename: OptionalArg<PyStrRef>,
154    #[pyarg(named, optional)]
155    co_firstlineno: OptionalArg<u32>,
156    #[pyarg(named, optional)]
157    co_consts: OptionalArg<Vec<PyObjectRef>>,
158    #[pyarg(named, optional)]
159    co_name: OptionalArg<PyStrRef>,
160    #[pyarg(named, optional)]
161    co_names: OptionalArg<Vec<PyObjectRef>>,
162    #[pyarg(named, optional)]
163    co_flags: OptionalArg<u32>,
164    #[pyarg(named, optional)]
165    co_varnames: OptionalArg<Vec<PyObjectRef>>,
166    #[pyarg(named, optional)]
167    co_nlocals: OptionalArg<u32>,
168    #[pyarg(named, optional)]
169    co_stacksize: OptionalArg<u32>,
170    #[pyarg(named, optional)]
171    co_code: OptionalArg<crate::builtins::PyBytesRef>,
172    #[pyarg(named, optional)]
173    co_linetable: OptionalArg<crate::builtins::PyBytesRef>,
174    #[pyarg(named, optional)]
175    co_exceptiontable: OptionalArg<crate::builtins::PyBytesRef>,
176    #[pyarg(named, optional)]
177    co_freevars: OptionalArg<Vec<PyObjectRef>>,
178    #[pyarg(named, optional)]
179    co_cellvars: OptionalArg<Vec<PyObjectRef>>,
180    #[pyarg(named, optional)]
181    co_qualname: OptionalArg<PyStrRef>,
182}
183
184#[derive(Clone)]
185#[repr(transparent)]
186pub struct Literal(PyObjectRef);
187
188impl Borrow<PyObject> for Literal {
189    fn borrow(&self) -> &PyObject {
190        &self.0
191    }
192}
193
194impl From<Literal> for PyObjectRef {
195    fn from(obj: Literal) -> Self {
196        obj.0
197    }
198}
199
200impl From<PyObjectRef> for Literal {
201    fn from(obj: PyObjectRef) -> Self {
202        Self(obj)
203    }
204}
205
206fn borrow_obj_constant(obj: &PyObject) -> BorrowedConstant<'_, Literal> {
207    match_class!(match obj {
208        ref i @ super::int::PyInt => {
209            let value = i.as_bigint();
210            if obj.class().is(super::bool_::PyBool::static_type()) {
211                BorrowedConstant::Boolean {
212                    value: !value.is_zero(),
213                }
214            } else {
215                BorrowedConstant::Integer { value }
216            }
217        }
218        ref f @ super::float::PyFloat => BorrowedConstant::Float { value: f.to_f64() },
219        ref c @ super::complex::PyComplex => BorrowedConstant::Complex {
220            value: c.as_complex()
221        },
222        ref s @ super::pystr::PyStr => BorrowedConstant::Str { value: s.as_wtf8() },
223        ref b @ super::bytes::PyBytes => BorrowedConstant::Bytes {
224            value: b.as_bytes()
225        },
226        ref c @ PyCode => {
227            BorrowedConstant::Code { code: &c.code }
228        }
229        ref t @ super::tuple::PyTuple => {
230            let elements = t.as_slice();
231            // SAFETY: Literal is repr(transparent) over PyObjectRef, and a Literal tuple only ever
232            //         has other literals as elements
233            let elements = unsafe { &*(elements as *const [PyObjectRef] as *const [Literal]) };
234            BorrowedConstant::Tuple { elements }
235        }
236        super::singletons::PyNone => BorrowedConstant::None,
237        super::slice::PyEllipsis => BorrowedConstant::Ellipsis,
238        ref s @ super::slice::PySlice => {
239            // Constant pool slices always store Some() for start/step (even for None).
240            // Box::leak the array so it outlives the borrow. Leak is acceptable since
241            // constant pool objects live for the program's lifetime.
242            let start = s.start.clone().unwrap();
243            let stop = s.stop.clone();
244            let step = s.step.clone().unwrap();
245            let arr = Box::leak(Box::new([Literal(start), Literal(stop), Literal(step)]));
246            BorrowedConstant::Slice { elements: arr }
247        }
248        ref fs @ super::set::PyFrozenSet => {
249            // Box::leak the elements so they outlive the borrow. Leak is acceptable since
250            // constant pool objects live for the program's lifetime.
251            let elems: Vec<Literal> = fs.elements().into_iter().map(Literal).collect();
252            let elements = Box::leak(elems.into_boxed_slice());
253            BorrowedConstant::Frozenset { elements }
254        }
255        _ => panic!("unexpected payload for constant python value"),
256    })
257}
258
259impl Constant for Literal {
260    type Name = &'static PyStrInterned;
261    fn borrow_constant(&self) -> BorrowedConstant<'_, Self> {
262        borrow_obj_constant(&self.0)
263    }
264}
265
266impl<'a> AsBag for &'a Context {
267    type Bag = PyObjBag<'a>;
268    fn as_bag(self) -> PyObjBag<'a> {
269        PyObjBag(self)
270    }
271}
272
273#[derive(Clone, Copy)]
274pub struct PyObjBag<'a>(pub &'a Context);
275
276/// Whether a string constant reads as a name. Those are the ones interned,
277/// the way `all_name_chars` picks them out.
278fn is_name_chars(value: &crate::common::wtf8::Wtf8) -> bool {
279    value
280        .as_bytes()
281        .iter()
282        .all(|&b| b.is_ascii_alphanumeric() || b == b'_')
283}
284
285impl ConstantBag for PyObjBag<'_> {
286    type Constant = Literal;
287
288    fn make_constant<C: Constant>(&self, constant: BorrowedConstant<'_, C>) -> Self::Constant {
289        let ctx = self.0;
290        let obj = match constant {
291            BorrowedConstant::Integer { value } => ctx.new_bigint(value).into(),
292            BorrowedConstant::Float { value } => ctx.new_float(value).into(),
293            BorrowedConstant::Complex { value } => ctx.new_complex(value).into(),
294            BorrowedConstant::Str { value } if is_name_chars(value) => {
295                ctx.intern_str(value).to_object()
296            }
297            BorrowedConstant::Str { value } => ctx.new_str(value).into(),
298            BorrowedConstant::Bytes { value } => ctx.new_bytes(value.to_vec()).into(),
299            BorrowedConstant::Boolean { value } => ctx.new_bool(value).into(),
300            BorrowedConstant::Code { code } => ctx.new_code(code.map_clone_bag(self)).into(),
301            BorrowedConstant::Tuple { elements } => {
302                let elements = elements
303                    .iter()
304                    .map(|constant| self.make_constant(constant.borrow_constant()).0)
305                    .collect();
306                ctx.new_tuple(elements).into()
307            }
308            BorrowedConstant::Slice { elements } => {
309                let [start, stop, step] = elements;
310                let start_obj = self.make_constant(start.borrow_constant()).0;
311                let stop_obj = self.make_constant(stop.borrow_constant()).0;
312                let step_obj = self.make_constant(step.borrow_constant()).0;
313                // Store as PySlice with Some() for all fields (even None values)
314                // so borrow_obj_constant can reference them.
315                use crate::builtins::PySlice;
316                PySlice {
317                    start: Some(start_obj),
318                    stop: stop_obj,
319                    step: Some(step_obj),
320                }
321                .into_ref(ctx)
322                .into()
323            }
324            BorrowedConstant::Frozenset { elements: _ } => {
325                // Creating a frozenset requires VirtualMachine for element hashing.
326                // PyObjBag only has Context, so we cannot construct PyFrozenSet here.
327                // Frozenset constants from .pyc are handled by PyMarshalBag which has VM access.
328                unimplemented!(
329                    "frozenset constant in PyObjBag::make_constant requires VirtualMachine"
330                )
331            }
332            BorrowedConstant::None => ctx.none(),
333            BorrowedConstant::Ellipsis => ctx.ellipsis.clone().into(),
334        };
335
336        Literal(obj)
337    }
338
339    fn make_name(&self, name: &str) -> &'static PyStrInterned {
340        self.0.intern_str(name)
341    }
342
343    fn make_int(&self, value: BigInt) -> Self::Constant {
344        Literal(self.0.new_int(value).into())
345    }
346
347    fn make_tuple(&self, elements: impl Iterator<Item = Self::Constant>) -> Self::Constant {
348        Literal(self.0.new_tuple(elements.map(|lit| lit.0).collect()).into())
349    }
350
351    fn make_code(&self, code: CodeObject) -> Self::Constant {
352        Literal(self.0.new_code(code).into())
353    }
354}
355
356#[derive(Clone, Copy)]
357pub(crate) struct PyVmBag<'a>(pub &'a VirtualMachine);
358
359impl ConstantBag for PyVmBag<'_> {
360    type Constant = Literal;
361
362    fn make_constant<C: Constant>(&self, constant: BorrowedConstant<'_, C>) -> Self::Constant {
363        let vm = self.0;
364        let ctx = &vm.ctx;
365        let obj = match constant {
366            BorrowedConstant::Integer { value } => ctx.new_bigint(value).into(),
367            BorrowedConstant::Float { value } => ctx.new_float(value).into(),
368            BorrowedConstant::Complex { value } => ctx.new_complex(value).into(),
369            BorrowedConstant::Str { value } if is_name_chars(value) => {
370                ctx.intern_str(value).to_object()
371            }
372            BorrowedConstant::Str { value } => ctx.new_str(value).into(),
373            BorrowedConstant::Bytes { value } => ctx.new_bytes(value.to_vec()).into(),
374            BorrowedConstant::Boolean { value } => ctx.new_bool(value).into(),
375            BorrowedConstant::Code { code } => {
376                PyCode::new_ref_with_bag(vm, code.map_clone_bag(self)).into()
377            }
378            BorrowedConstant::Tuple { elements } => {
379                let elements = elements
380                    .iter()
381                    .map(|constant| self.make_constant(constant.borrow_constant()).0)
382                    .collect();
383                ctx.new_tuple(elements).into()
384            }
385            BorrowedConstant::Slice { elements } => {
386                let [start, stop, step] = elements;
387                let start_obj = self.make_constant(start.borrow_constant()).0;
388                let stop_obj = self.make_constant(stop.borrow_constant()).0;
389                let step_obj = self.make_constant(step.borrow_constant()).0;
390                use crate::builtins::PySlice;
391                PySlice {
392                    start: Some(start_obj),
393                    stop: stop_obj,
394                    step: Some(step_obj),
395                }
396                .into_ref(ctx)
397                .into()
398            }
399            BorrowedConstant::Frozenset { elements } => {
400                let elements = elements
401                    .iter()
402                    .map(|constant| self.make_constant(constant.borrow_constant()).0);
403                PyFrozenSet::from_iter(vm, elements)
404                    .unwrap()
405                    .into_ref(ctx)
406                    .into()
407            }
408            BorrowedConstant::None => ctx.none(),
409            BorrowedConstant::Ellipsis => ctx.ellipsis.clone().into(),
410        };
411
412        Literal(obj)
413    }
414
415    fn make_name(&self, name: &str) -> &'static PyStrInterned {
416        self.0.ctx.intern_str(name)
417    }
418
419    fn make_int(&self, value: BigInt) -> Self::Constant {
420        Literal(self.0.ctx.new_int(value).into())
421    }
422
423    fn make_tuple(&self, elements: impl Iterator<Item = Self::Constant>) -> Self::Constant {
424        Literal(
425            self.0
426                .ctx
427                .new_tuple(elements.map(|lit| lit.0).collect())
428                .into(),
429        )
430    }
431
432    fn make_code(&self, code: CodeObject) -> Self::Constant {
433        Literal(PyCode::new_ref_with_bag(self.0, code).into())
434    }
435}
436
437pub(crate) type CodeObject = bytecode::CodeObject<Literal>;
438
439pub trait IntoCodeObject {
440    fn into_code_object(self, ctx: &Context) -> CodeObject;
441}
442
443impl IntoCodeObject for CodeObject {
444    fn into_code_object(self, _ctx: &Context) -> Self {
445        self
446    }
447}
448
449impl IntoCodeObject for bytecode::CodeObject {
450    fn into_code_object(self, ctx: &Context) -> CodeObject {
451        self.map_bag(PyObjBag(ctx))
452    }
453}
454
455impl<B: AsRef<[u8]>> IntoCodeObject for frozen::FrozenCodeObject<B> {
456    fn into_code_object(self, ctx: &Context) -> CodeObject {
457        self.decode(ctx)
458    }
459}
460
461/// Per-code-object monitoring data (_PyCoMonitoringData).
462/// Stores original opcodes displaced by INSTRUMENTED_LINE / INSTRUMENTED_INSTRUCTION.
463pub struct CoMonitoringData {
464    /// Original opcodes at positions with INSTRUMENTED_LINE.
465    /// Indexed by instruction index. 0 = not instrumented for LINE.
466    pub line_opcodes: Vec<u8>,
467
468    /// Original opcodes at positions with INSTRUMENTED_INSTRUCTION.
469    /// Indexed by instruction index. 0 = not instrumented for INSTRUCTION.
470    pub per_instruction_opcodes: Vec<u8>,
471}
472
473#[pyclass(module = false, name = "code")]
474pub struct PyCode {
475    #[pymember(name = "co_argcount", path = "arg_count")]
476    #[pymember(name = "co_posonlyargcount", path = "posonlyarg_count")]
477    #[pymember(name = "co_kwonlyargcount", path = "kwonlyarg_count")]
478    #[pymember(name = "co_stacksize", path = "max_stackdepth")]
479    #[pymember(name = "co_name", path = "obj_name")]
480    #[pymember(name = "co_qualname", path = "qualname")]
481    #[pymember(name = "co_flags", path = "flags")]
482    pub code: CodeObject,
483    /// Slot-indexed names, equivalent to CPython's `co_localsplusnames`.
484    /// Derived once so frame-local proxy operations do not repeatedly scan
485    /// merged cell variables.
486    localsplus_names: Box<[&'static PyStrInterned]>,
487    #[pymember(name = "co_filename")]
488    source_path: AtomicPtr<PyStrInterned>,
489    #[pymember(name = "co_nlocals")]
490    nlocals: i32,
491    /// Version counter for lazy re-instrumentation.
492    /// Compared against `PyGlobalState::instrumentation_version` at RESUME.
493    pub instrumentation_version: AtomicU64,
494    /// Side-table for INSTRUMENTED_LINE / INSTRUMENTED_INSTRUCTION.
495    pub monitoring_data: PyMutex<Option<CoMonitoringData>>,
496    /// Whether adaptive counters have been initialized (lazy quickening).
497    pub quickened: core::sync::atomic::AtomicBool,
498    /// Whether the bytecode contains any instruction that mutates the current
499    /// exc_info slot (`vm.set_exception`). When false, a normal frame call for
500    /// this code cannot leave the slot unbalanced, so `with_frame` skips the
501    /// exc_info save/restore. Computed once by scanning the instruction stream.
502    pub has_exc_handling: bool,
503}
504
505impl Deref for PyCode {
506    type Target = CodeObject;
507    fn deref(&self) -> &Self::Target {
508        &self.code
509    }
510}
511
512fn build_localspluskinds(
513    varnames: &[&'static PyStrInterned],
514    cellvars: &[&'static PyStrInterned],
515    freevars: &[&'static PyStrInterned],
516    arg_counts: (u32, u32, u32),
517    flags: CodeFlags,
518    instructions: &CodeUnits,
519) -> Result<Box<[u8]>, usize> {
520    use rustpython_compiler_core::bytecode::{
521        CO_FAST_ARG_KW, CO_FAST_ARG_POS, CO_FAST_ARG_VAR, CO_FAST_CELL, CO_FAST_FREE,
522        CO_FAST_HIDDEN, CO_FAST_LOCAL, OpArgState,
523    };
524
525    let num_merged_cells = cellvars
526        .iter()
527        .filter(|cell| varnames.iter().any(|local| *local == **cell))
528        .count();
529    let mut kinds = vec![0; varnames.len() + cellvars.len() - num_merged_cells + freevars.len()];
530
531    let (posonlyarg_count, arg_count, kwonlyarg_count) = arg_counts;
532    let positional_only = posonlyarg_count as usize;
533    let positional_or_keyword = arg_count.saturating_sub(posonlyarg_count) as usize;
534    let argument_kinds = [
535        (positional_only, CO_FAST_ARG_POS),
536        (positional_or_keyword, CO_FAST_ARG_POS | CO_FAST_ARG_KW),
537        (kwonlyarg_count as usize, CO_FAST_ARG_KW),
538        (
539            usize::from(flags.contains(CodeFlags::VARARGS)),
540            CO_FAST_ARG_VAR | CO_FAST_ARG_POS,
541        ),
542        (
543            usize::from(flags.contains(CodeFlags::VARKEYWORDS)),
544            CO_FAST_ARG_VAR | CO_FAST_ARG_KW,
545        ),
546        (usize::MAX, 0),
547    ];
548    let mut local_index = 0;
549    let mut argument_end = 0usize;
550    for (count, argument_kind) in argument_kinds {
551        argument_end = argument_end.saturating_add(count);
552        while local_index < argument_end && local_index < varnames.len() {
553            kinds[local_index] = CO_FAST_LOCAL | argument_kind;
554            local_index += 1;
555        }
556    }
557
558    let mut dropped_cells = 0;
559    for (cell_index, cell) in cellvars.iter().enumerate() {
560        if let Some(local_index) = varnames.iter().position(|local| *local == *cell) {
561            kinds[local_index] |= CO_FAST_CELL;
562            dropped_cells += 1;
563        } else {
564            kinds[varnames.len() + cell_index - dropped_cells] = CO_FAST_CELL;
565        }
566    }
567
568    let free_start = varnames.len() + cellvars.len() - num_merged_cells;
569    for kind in kinds.iter_mut().skip(free_start) {
570        *kind = CO_FAST_FREE;
571    }
572
573    if !flags.contains(CodeFlags::OPTIMIZED) {
574        let mut arg_state = OpArgState::default();
575        for unit in instructions.iter().copied() {
576            let (instruction, arg) = arg_state.get(unit);
577            if matches!(instruction, Instruction::LoadFastAndClear { .. }) {
578                let index = u32::from(arg) as usize;
579                let Some(kind) = kinds.get_mut(index) else {
580                    return Err(index);
581                };
582                *kind |= CO_FAST_HIDDEN;
583            }
584        }
585    }
586
587    Ok(kinds.into_boxed_slice())
588}
589
590impl MemberLayout for CodeFlags {
591    const KIND: MemberKind = MemberKind::Int;
592}
593
594impl PyCode {
595    pub fn new(code: CodeObject) -> Self {
596        let sp = code.source_path as *const PyStrInterned as *mut PyStrInterned;
597        let nlocals = i32::try_from(code.varnames.len()).unwrap_or(i32::MAX);
598        let localsplus_names = {
599            let varname_ids = code
600                .varnames
601                .iter()
602                .map(|name| name.get_id())
603                .collect::<std::collections::HashSet<_>>();
604            let names = code
605                .varnames
606                .iter()
607                .chain(
608                    code.cellvars
609                        .iter()
610                        .filter(|name| !varname_ids.contains(&name.get_id())),
611                )
612                .chain(code.freevars.iter())
613                .copied()
614                .collect::<Box<[_]>>();
615            debug_assert_eq!(names.len(), code.localspluskinds.len());
616            names
617        };
618        // The only opcodes that call `vm.set_exception` (mutating the shared
619        // exc_info slot); instrumented variants only replace these base opcodes
620        // in place, so scanning the freshly-built stream is a sound predicate.
621        let has_exc_handling = code.instructions.iter().any(|u| {
622            matches!(
623                u.op,
624                Instruction::PushExcInfo
625                    | Instruction::PopExcept
626                    | Instruction::CheckEgMatch
627                    | Instruction::EndAsyncFor
628                    | Instruction::InstrumentedEndAsyncFor
629            )
630        });
631        Self {
632            code,
633            localsplus_names,
634            source_path: AtomicPtr::new(sp),
635            nlocals,
636            instrumentation_version: AtomicU64::new(0),
637            monitoring_data: PyMutex::new(None),
638            quickened: core::sync::atomic::AtomicBool::new(false),
639            has_exc_handling,
640        }
641    }
642
643    #[inline(always)]
644    pub(crate) fn localsplus_name(&self, index: usize) -> &'static PyStrInterned {
645        // Bytecode operands and frame-proxy indices are validated against
646        // localspluskinds, whose length is kept equal to localsplus_names by
647        // every CodeObject construction path.
648        self.localsplus_names[index]
649    }
650
651    pub fn source_path(&self) -> &'static PyStrInterned {
652        // SAFETY: always points to a valid &'static PyStrInterned (interned strings are never deallocated)
653        unsafe { &*self.source_path.load(Ordering::Relaxed) }
654    }
655
656    pub fn set_source_path(&self, new: &'static PyStrInterned) {
657        self.source_path.store(
658            new as *const PyStrInterned as *mut PyStrInterned,
659            Ordering::Relaxed,
660        );
661    }
662
663    pub fn new_ref_with_bag(vm: &VirtualMachine, code: CodeObject) -> PyRef<Self> {
664        PyRef::new_ref(Self::new(code), vm.ctx.types.code_type.to_owned(), None)
665    }
666
667    pub fn new_ref_from_bytecode(vm: &VirtualMachine, code: bytecode::CodeObject) -> PyRef<Self> {
668        Self::new_ref_with_bag(vm, code.map_bag(PyVmBag(vm)))
669    }
670
671    pub fn new_ref_from_frozen<B: AsRef<[u8]>>(
672        vm: &VirtualMachine,
673        code: frozen::FrozenCodeObject<B>,
674    ) -> PyRef<Self> {
675        let py_code = Self::new_ref_with_bag(vm, code.decode(PyVmBag(vm)));
676        apply_frozen_co_filename(&py_code, vm);
677        py_code
678    }
679
680    #[cfg(feature = "host_env")]
681    pub fn from_pyc_path(path: &std::path::Path, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
682        let name = match path.file_stem() {
683            Some(stem) => stem.display().to_string(),
684            None => "".to_owned(),
685        };
686        let content = crate::host_env::fs::read(path).map_err(|e| e.to_pyexception(vm))?;
687        Self::from_pyc(
688            &content,
689            Some(&name),
690            Some(&path.display().to_string()),
691            Some("<source>"),
692            vm,
693        )
694    }
695    #[cfg(not(feature = "host_env"))]
696    pub fn from_pyc_path(_path: &std::path::Path, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
697        Err(vm.new_runtime_error("loading a pyc file requires the `host_env` feature"))
698    }
699    pub fn from_pyc(
700        pyc_bytes: &[u8],
701        name: Option<&str>,
702        bytecode_path: Option<&str>,
703        source_path: Option<&str>,
704        vm: &VirtualMachine,
705    ) -> PyResult<PyRef<Self>> {
706        if !crate::import::check_pyc_magic_number_bytes(pyc_bytes) {
707            return Err(vm.new_value_error("pyc bytes has wrong MAGIC"));
708        }
709        let bootstrap_external = vm.import("_frozen_importlib_external", 0)?;
710        let compile_bytecode = bootstrap_external.get_attr("_compile_bytecode", vm)?;
711        // 16 is the pyc header length
712        let Some((_, code_bytes)) = pyc_bytes.split_at_checked(16) else {
713            return Err(vm.new_value_error(format!(
714                "pyc_bytes header is broken. 16 bytes expected but {} bytes given.",
715                pyc_bytes.len()
716            )));
717        };
718        let code_bytes_obj = vm.ctx.new_bytes(code_bytes.to_vec());
719        let compiled =
720            compile_bytecode.call((code_bytes_obj, name, bytecode_path, source_path), vm)?;
721        compiled.try_downcast(vm)
722    }
723}
724
725fn frozen_co_filename(path: &str) -> Option<&'static str> {
726    match path {
727        "_frozen_importlib" => Some("<frozen importlib._bootstrap>"),
728        "_frozen_importlib_external" => Some("<frozen importlib._bootstrap_external>"),
729        _ => None,
730    }
731}
732
733fn apply_frozen_co_filename(code: &PyCode, vm: &VirtualMachine) {
734    let Some(new) = frozen_co_filename(code.source_path().as_str()) else {
735        return;
736    };
737    set_source_path_tree(code, vm.ctx.intern_str(new));
738}
739
740fn set_source_path_tree(code: &PyCode, interned: &'static PyStrInterned) {
741    code.set_source_path(interned);
742    for constant in code.constants.iter() {
743        set_source_path_in_const(&constant.0, interned);
744    }
745}
746
747fn set_source_path_in_const(obj: &PyObject, interned: &'static PyStrInterned) {
748    if let Some(inner) = obj.downcast_ref::<PyCode>() {
749        set_source_path_tree(inner, interned);
750    } else if let Some(tup) = obj.downcast_ref::<super::PyTuple>() {
751        for item in tup {
752            set_source_path_in_const(item, interned);
753        }
754    }
755}
756
757impl fmt::Debug for PyCode {
758    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759        write!(f, "code: {:?}", self.code)
760    }
761}
762
763impl PyCode {
764    /// Line number for a byte offset, or -1 when the linetable has no line.
765    pub fn addr2line(&self, lasti_bytes: i32) -> i32 {
766        if lasti_bytes < 0 {
767            return self.code.first_line_number.map_or(-1, |n| n.get() as i32);
768        }
769        let linetable = self.code.linetable.as_ref();
770        if linetable.is_empty() {
771            return self.code.first_line_number.map_or(-1, |n| n.get() as i32);
772        }
773        let first_line = self.code.first_line_number.map_or(0, |n| n.get() as i32);
774        let mut range = PyCodeAddressRange::new(linetable, first_line);
775        while range.ar_end <= lasti_bytes {
776            if !range.advance() {
777                return -1;
778            }
779        }
780        range.ar_line
781    }
782}
783
784impl PyPayload for PyCode {
785    #[inline]
786    fn class(ctx: &Context) -> &'static Py<PyType> {
787        ctx.types.code_type
788    }
789}
790
791impl Representable for PyCode {
792    #[inline]
793    fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
794        let code = &zelf.code;
795        Ok(format!(
796            "<code object {} at {:#x}, file \"{}\", line {}>",
797            code.obj_name,
798            zelf.get_id(),
799            zelf.source_path().as_str(),
800            code.first_line_number.map_or(-1, |n| n.get() as i32)
801        ))
802    }
803}
804
805impl Comparable for PyCode {
806    fn cmp(
807        zelf: &Py<Self>,
808        other: &PyObject,
809        op: crate::types::PyComparisonOp,
810        vm: &VirtualMachine,
811    ) -> PyResult<crate::function::PyComparisonValue> {
812        op.eq_only(|| {
813            let other = class_or_notimplemented!(Self, other);
814            let a = &zelf.code;
815            let b = &other.code;
816            let eq = a.obj_name == b.obj_name
817                && a.arg_count == b.arg_count
818                && a.posonlyarg_count == b.posonlyarg_count
819                && a.kwonlyarg_count == b.kwonlyarg_count
820                && a.flags == b.flags
821                && a.first_line_number == b.first_line_number
822                && a.instructions.original_bytes() == b.instructions.original_bytes()
823                && a.linetable == b.linetable
824                && a.exceptiontable == b.exceptiontable
825                && a.names == b.names
826                && a.varnames == b.varnames
827                && a.freevars == b.freevars
828                && a.cellvars == b.cellvars
829                && {
830                    let a_consts: Vec<_> = a.constants.iter().map(|c| c.0.clone()).collect();
831                    let b_consts: Vec<_> = b.constants.iter().map(|c| c.0.clone()).collect();
832                    if a_consts.len() != b_consts.len() {
833                        false
834                    } else {
835                        let mut eq = true;
836                        for (ac, bc) in a_consts.iter().zip(b_consts.iter()) {
837                            if !vm.bool_eq(ac, bc)? {
838                                eq = false;
839                                break;
840                            }
841                        }
842                        eq
843                    }
844                };
845            Ok(eq.into())
846        })
847    }
848}
849
850impl Hashable for PyCode {
851    fn hash(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<crate::common::hash::PyHash> {
852        let code = &zelf.code;
853        // Hash a tuple of key attributes, matching CPython's code_hash
854        let tuple = vm.ctx.new_tuple(vec![
855            vm.ctx.new_str(code.obj_name.as_str()).into(),
856            vm.ctx.new_int(code.arg_count).into(),
857            vm.ctx.new_int(code.posonlyarg_count).into(),
858            vm.ctx.new_int(code.kwonlyarg_count).into(),
859            vm.ctx.new_int(code.varnames.len()).into(),
860            vm.ctx.new_int(code.flags.bits()).into(),
861            vm.ctx
862                .new_int(code.first_line_number.map_or(0, |n| n.get()) as i64)
863                .into(),
864            vm.ctx.new_bytes(code.instructions.original_bytes()).into(),
865            {
866                let consts: Vec<_> = code.constants.iter().map(|c| c.0.clone()).collect();
867                vm.ctx.new_tuple(consts).into()
868            },
869        ]);
870        tuple.as_object().hash(vm)
871    }
872}
873
874// Arguments for code object constructor
875#[derive(FromArgs)]
876pub struct PyCodeNewArgs {
877    argcount: u32,
878    posonlyargcount: u32,
879    kwonlyargcount: u32,
880    nlocals: u32,
881    stacksize: u32,
882    flags: u32,
883    co_code: PyBytesRef,
884    consts: PyTupleRef,
885    names: PyTupleRef,
886    varnames: PyTupleRef,
887    filename: PyStrRef,
888    name: PyStrRef,
889    qualname: PyStrRef,
890    firstlineno: i32,
891    linetable: PyBytesRef,
892    exceptiontable: PyBytesRef,
893    freevars: PyTupleRef,
894    cellvars: PyTupleRef,
895}
896
897impl Constructor for PyCode {
898    type Args = PyCodeNewArgs;
899
900    fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
901        // Convert names tuple to vector of interned strings
902        let names: Box<[&'static PyStrInterned]> = args
903            .names
904            .as_slice()
905            .iter()
906            .map(|obj| {
907                let s = obj
908                    .downcast_ref::<super::pystr::PyStr>()
909                    .ok_or_else(|| vm.new_type_error("names must be tuple of strings"))?;
910                Ok(vm.ctx.intern_str(s.as_wtf8()))
911            })
912            .collect::<PyResult<Vec<_>>>()?
913            .into_boxed_slice();
914
915        let varnames: Box<[&'static PyStrInterned]> = args
916            .varnames
917            .as_slice()
918            .iter()
919            .map(|obj| {
920                let s = obj
921                    .downcast_ref::<super::pystr::PyStr>()
922                    .ok_or_else(|| vm.new_type_error("varnames must be tuple of strings"))?;
923                Ok(vm.ctx.intern_str(s.as_wtf8()))
924            })
925            .collect::<PyResult<Vec<_>>>()?
926            .into_boxed_slice();
927
928        let cellvars: Box<[&'static PyStrInterned]> = args
929            .cellvars
930            .as_slice()
931            .iter()
932            .map(|obj| {
933                let s = obj
934                    .downcast_ref::<super::pystr::PyStr>()
935                    .ok_or_else(|| vm.new_type_error("cellvars must be tuple of strings"))?;
936                Ok(vm.ctx.intern_str(s.as_wtf8()))
937            })
938            .collect::<PyResult<Vec<_>>>()?
939            .into_boxed_slice();
940
941        let freevars: Box<[&'static PyStrInterned]> = args
942            .freevars
943            .as_slice()
944            .iter()
945            .map(|obj| {
946                let s = obj
947                    .downcast_ref::<super::pystr::PyStr>()
948                    .ok_or_else(|| vm.new_type_error("freevars must be tuple of strings"))?;
949                Ok(vm.ctx.intern_str(s.as_wtf8()))
950            })
951            .collect::<PyResult<Vec<_>>>()?
952            .into_boxed_slice();
953
954        // Check nlocals matches varnames length
955        if args.nlocals as usize != varnames.len() {
956            return Err(vm.new_value_error(format!(
957                "nlocals ({}) != len(varnames) ({})",
958                args.nlocals,
959                varnames.len()
960            )));
961        }
962
963        // Parse and validate bytecode from bytes
964        let bytecode_bytes = args.co_code.as_bytes();
965        let instructions = CodeUnits::try_from(bytecode_bytes)
966            .map_err(|e| vm.new_value_error(format!("invalid bytecode: {e}")))?;
967
968        // Convert constants
969        let constants = args
970            .consts
971            .as_slice()
972            .iter()
973            .map(|obj| {
974                // Convert PyObject to Literal constant. For now, just wrap it
975                Literal(obj.clone())
976            })
977            .collect();
978
979        // Create locations (start and end pairs)
980        let row = if args.firstlineno > 0 {
981            OneIndexed::new(args.firstlineno as usize).unwrap_or(OneIndexed::MIN)
982        } else {
983            OneIndexed::MIN
984        };
985        let loc = rustpython_compiler_core::SourceLocation {
986            line: row,
987            character_offset: OneIndexed::from_zero_indexed(0),
988        };
989        let locations: Box<
990            [(
991                rustpython_compiler_core::SourceLocation,
992                rustpython_compiler_core::SourceLocation,
993            )],
994        > = vec![(loc, loc); instructions.len()].into_boxed_slice();
995
996        let flags = CodeFlags::from_bits_truncate(args.flags);
997        let localspluskinds = build_localspluskinds(
998            &varnames,
999            &cellvars,
1000            &freevars,
1001            (args.posonlyargcount, args.argcount, args.kwonlyargcount),
1002            flags,
1003            &instructions,
1004        )
1005        .map_err(|index| {
1006            vm.new_value_error(format!(
1007                "code: LOAD_FAST_AND_CLEAR oparg {index} out of range"
1008            ))
1009        })?;
1010
1011        // Build the CodeObject
1012        let code = CodeObject {
1013            instructions,
1014            locations,
1015            flags,
1016            posonlyarg_count: args.posonlyargcount,
1017            arg_count: args.argcount,
1018            kwonlyarg_count: args.kwonlyargcount,
1019            source_path: vm.ctx.intern_str(args.filename.as_wtf8()),
1020            first_line_number: if args.firstlineno > 0 {
1021                OneIndexed::new(args.firstlineno as usize)
1022            } else {
1023                None
1024            },
1025            // Room for one value is always reserved, even where nothing is pushed.
1026            max_stackdepth: args.stacksize.max(1),
1027            obj_name: vm.ctx.intern_str(args.name.as_wtf8()),
1028            qualname: vm.ctx.intern_str(args.qualname.as_wtf8()),
1029            constants,
1030            names,
1031            varnames,
1032            cellvars,
1033            freevars,
1034            localspluskinds,
1035            linetable: args.linetable.as_bytes().to_vec().into_boxed_slice(),
1036            exceptiontable: args.exceptiontable.as_bytes().to_vec().into_boxed_slice(),
1037        };
1038
1039        Ok(Self::new(code))
1040    }
1041}
1042
1043impl PyCode {
1044    pub fn co_filename(&self) -> PyStrRef {
1045        self.source_path().to_owned()
1046    }
1047}
1048
1049#[pyclass(
1050    itemsize = core::mem::size_of::<u16>(),
1051    with(Representable, Constructor, Comparable, Hashable),
1052    flags(HAS_WEAKREF)
1053)]
1054impl Py<PyCode> {
1055    #[pygetset]
1056    pub fn co_cellvars(&self, vm: &VirtualMachine) -> PyTupleRef {
1057        let cellvars = self
1058            .cellvars
1059            .iter()
1060            .map(|name| name.to_pyobject(vm))
1061            .collect();
1062        vm.ctx.new_tuple(cellvars)
1063    }
1064
1065    #[pygetset]
1066    fn co_firstlineno(&self) -> u32 {
1067        self.code.first_line_number.map_or(0, |n| n.get() as _)
1068    }
1069
1070    #[pygetset]
1071    fn co_consts(&self, vm: &VirtualMachine) -> PyTupleRef {
1072        let consts = self.code.constants.iter().map(|x| x.0.clone()).collect();
1073        vm.ctx.new_tuple(consts)
1074    }
1075
1076    #[pygetset]
1077    fn co_names(&self, vm: &VirtualMachine) -> PyTupleRef {
1078        let names = self
1079            .code
1080            .names
1081            .deref()
1082            .iter()
1083            .map(|name| name.to_pyobject(vm))
1084            .collect();
1085        vm.ctx.new_tuple(names)
1086    }
1087
1088    #[pygetset]
1089    pub fn co_varnames(&self, vm: &VirtualMachine) -> PyTupleRef {
1090        let varnames = self.code.varnames.iter().map(|s| s.to_object()).collect();
1091        vm.ctx.new_tuple(varnames)
1092    }
1093
1094    #[pygetset]
1095    pub fn co_code(&self, vm: &VirtualMachine) -> crate::builtins::PyBytesRef {
1096        vm.ctx.new_bytes(self.code.instructions.original_bytes())
1097    }
1098
1099    #[pygetset]
1100    pub fn _co_code_adaptive(&self, vm: &VirtualMachine) -> crate::builtins::PyBytesRef {
1101        // Return current (possibly quickened/specialized) bytecode
1102        let bytes = unsafe {
1103            core::slice::from_raw_parts(
1104                self.code.instructions.as_ptr() as *const u8,
1105                self.code.instructions.len() * 2,
1106            )
1107        };
1108        vm.ctx.new_bytes(bytes.to_vec())
1109    }
1110
1111    #[pygetset]
1112    pub fn co_freevars(&self, vm: &VirtualMachine) -> PyTupleRef {
1113        let names = self
1114            .code
1115            .freevars
1116            .deref()
1117            .iter()
1118            .map(|name| name.to_pyobject(vm))
1119            .collect();
1120        vm.ctx.new_tuple(names)
1121    }
1122
1123    #[pygetset]
1124    pub fn co_linetable(&self, vm: &VirtualMachine) -> crate::builtins::PyBytesRef {
1125        // Return the actual linetable from the code object
1126        vm.ctx.new_bytes(self.code.linetable.to_vec())
1127    }
1128
1129    #[pygetset]
1130    pub fn co_exceptiontable(&self, vm: &VirtualMachine) -> crate::builtins::PyBytesRef {
1131        // Return the actual exception table from the code object
1132        vm.ctx.new_bytes(self.code.exceptiontable.to_vec())
1133    }
1134
1135    // spell-checker: ignore lnotab
1136    // co_lnotab is intentionally not implemented.
1137    // It was deprecated since 3.12 and scheduled for removal in 3.14.
1138    // Use co_lines() or co_linetable instead.
1139
1140    #[pymethod]
1141    pub fn co_lines(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
1142        // TODO: Implement lazy iterator (lineiterator) like CPython for better performance
1143        // Currently returns eager list for simplicity
1144
1145        // Return an iterator over (start_offset, end_offset, lineno) tuples
1146        let linetable = self.code.linetable.as_ref();
1147        let mut lines = Vec::new();
1148
1149        if !linetable.is_empty() {
1150            let first_line = self.code.first_line_number.map_or(0, |n| n.get() as i32);
1151            let mut range = PyCodeAddressRange::new(linetable, first_line);
1152
1153            // Process all address ranges and merge consecutive entries with same line
1154            let mut pending_entry: Option<(i32, i32, i32)> = None;
1155
1156            while range.advance() {
1157                let start = range.ar_start;
1158                let end = range.ar_end;
1159                let line = range.ar_line;
1160
1161                if let Some((prev_start, _, prev_line)) = pending_entry {
1162                    if prev_line == line {
1163                        // Same line, extend the range
1164                        pending_entry = Some((prev_start, end, prev_line));
1165                    } else {
1166                        // Different line, emit the previous entry
1167                        let tuple = if prev_line == -1 {
1168                            vm.ctx.new_tuple(vec![
1169                                vm.ctx.new_int(prev_start).into(),
1170                                vm.ctx.new_int(start).into(),
1171                                vm.ctx.none(),
1172                            ])
1173                        } else {
1174                            vm.ctx.new_tuple(vec![
1175                                vm.ctx.new_int(prev_start).into(),
1176                                vm.ctx.new_int(start).into(),
1177                                vm.ctx.new_int(prev_line).into(),
1178                            ])
1179                        };
1180                        lines.push(tuple.into());
1181                        pending_entry = Some((start, end, line));
1182                    }
1183                } else {
1184                    // First entry
1185                    pending_entry = Some((start, end, line));
1186                }
1187            }
1188
1189            // Emit the last pending entry
1190            if let Some((start, end, line)) = pending_entry {
1191                let tuple = if line == -1 {
1192                    vm.ctx.new_tuple(vec![
1193                        vm.ctx.new_int(start).into(),
1194                        vm.ctx.new_int(end).into(),
1195                        vm.ctx.none(),
1196                    ])
1197                } else {
1198                    vm.ctx.new_tuple(vec![
1199                        vm.ctx.new_int(start).into(),
1200                        vm.ctx.new_int(end).into(),
1201                        vm.ctx.new_int(line).into(),
1202                    ])
1203                };
1204                lines.push(tuple.into());
1205            }
1206        }
1207
1208        let list = vm.ctx.new_list(lines);
1209        vm.call_method(list.as_object(), "__iter__", ())
1210    }
1211
1212    #[pymethod]
1213    pub fn co_positions(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
1214        // Return an iterator over (line, end_line, column, end_column) tuples for each instruction
1215        let linetable = self.code.linetable.as_ref();
1216        let mut positions = Vec::new();
1217
1218        if !linetable.is_empty() {
1219            let mut reader = LineTableReader::new(linetable);
1220            let mut line = self.code.first_line_number.map_or(0, |n| n.get() as i32);
1221
1222            while !reader.at_end() {
1223                let first_byte = match reader.read_byte() {
1224                    Some(b) => b,
1225                    None => break,
1226                };
1227
1228                if (first_byte & 0x80) == 0 {
1229                    break; // Invalid linetable
1230                }
1231
1232                let code = (first_byte >> 3) & 0x0f;
1233                let length = ((first_byte & 0x07) + 1) as i32;
1234
1235                let kind = match PyCodeLocationInfoKind::from_code(code) {
1236                    Some(k) => k,
1237                    None => break, // Invalid code
1238                };
1239
1240                let (line_delta, end_line_delta, column, end_column): (
1241                    i32,
1242                    i32,
1243                    Option<i32>,
1244                    Option<i32>,
1245                ) = match kind {
1246                    PyCodeLocationInfoKind::None => {
1247                        // No location - all values are None
1248                        (0, 0, None, None)
1249                    }
1250                    PyCodeLocationInfoKind::Long => {
1251                        // Long form
1252                        let delta = reader.read_signed_varint();
1253                        let end_line_delta = reader.read_varint() as i32;
1254
1255                        let col = reader.read_varint();
1256                        let column = if col == 0 {
1257                            None
1258                        } else {
1259                            Some((col - 1) as i32)
1260                        };
1261
1262                        let end_col = reader.read_varint();
1263                        let end_column = if end_col == 0 {
1264                            None
1265                        } else {
1266                            Some((end_col - 1) as i32)
1267                        };
1268
1269                        // endline = line + end_line_delta (will be computed after line update)
1270                        (delta, end_line_delta, column, end_column)
1271                    }
1272                    PyCodeLocationInfoKind::NoColumns => {
1273                        // No column form
1274                        let delta = reader.read_signed_varint();
1275                        (delta, 0, None, None) // endline will be same as line (delta = 0)
1276                    }
1277                    PyCodeLocationInfoKind::OneLine0
1278                    | PyCodeLocationInfoKind::OneLine1
1279                    | PyCodeLocationInfoKind::OneLine2 => {
1280                        // One-line form - endline = line
1281                        let col = reader.read_byte().unwrap_or(0) as i32;
1282                        let end_col = reader.read_byte().unwrap_or(0) as i32;
1283                        let delta = kind.one_line_delta().unwrap_or(0);
1284                        (delta, 0, Some(col), Some(end_col)) // endline = line (delta = 0)
1285                    }
1286                    _ if kind.is_short() => {
1287                        // Short form - endline = line
1288                        let col_data = reader.read_byte().unwrap_or(0);
1289                        let col_group = kind.short_column_group().unwrap_or(0);
1290                        let col = ((col_group as i32) << 3) | ((col_data >> 4) as i32);
1291                        let end_col = col + (col_data & 0x0f) as i32;
1292                        (0, 0, Some(col), Some(end_col)) // endline = line (delta = 0)
1293                    }
1294                    _ => (0, 0, None, None),
1295                };
1296
1297                // Update line number
1298                line += line_delta;
1299
1300                // Generate position tuples for each instruction covered by this entry
1301                for _ in 0..length {
1302                    // Handle special case for no location (code 15)
1303                    let final_line = if kind == PyCodeLocationInfoKind::None {
1304                        None
1305                    } else {
1306                        Some(line)
1307                    };
1308
1309                    let final_endline = if kind == PyCodeLocationInfoKind::None {
1310                        None
1311                    } else {
1312                        Some(line + end_line_delta)
1313                    };
1314
1315                    let line_obj = final_line.to_pyobject(vm);
1316                    let end_line_obj = final_endline.to_pyobject(vm);
1317                    let column_obj = column.to_pyobject(vm);
1318                    let end_column_obj = end_column.to_pyobject(vm);
1319
1320                    let tuple =
1321                        vm.ctx
1322                            .new_tuple(vec![line_obj, end_line_obj, column_obj, end_column_obj]);
1323                    positions.push(tuple.into());
1324                }
1325            }
1326        }
1327
1328        let list = vm.ctx.new_list(positions);
1329        vm.call_method(list.as_object(), "__iter__", ())
1330    }
1331
1332    #[pymethod]
1333    pub fn co_branches(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
1334        let instructions = &self.code.instructions;
1335        let mut branches = Vec::new();
1336        let mut extended_arg: u32 = 0;
1337
1338        for (i, unit) in instructions.iter().enumerate() {
1339            // De-instrument: use base opcode for instrumented variants
1340            let op = unit.op.to_base().unwrap_or(unit.op);
1341            let raw_arg = u32::from(u8::from(unit.arg));
1342
1343            if matches!(op, Instruction::ExtendedArg) {
1344                extended_arg = (extended_arg | raw_arg) << 8;
1345                continue;
1346            }
1347
1348            let oparg = extended_arg | raw_arg;
1349            extended_arg = 0;
1350
1351            let caches = op.cache_entries();
1352            let (src, left, right) = match op {
1353                Instruction::ForIter { .. } => {
1354                    // left = fall-through past CACHE entries (continue iteration)
1355                    // right = next_offset + oparg + 2 (skip END_FOR and POP_ITER)
1356                    let after_cache = i + 1 + caches;
1357                    let target = after_cache + oparg as usize;
1358                    let right = if matches!(
1359                        instructions.get(target).map(|u| u.op),
1360                        Some(Instruction::EndFor | Instruction::InstrumentedEndFor)
1361                    ) {
1362                        (target + 2) * 2
1363                    } else {
1364                        target * 2
1365                    };
1366                    (i * 2, after_cache * 2, right)
1367                }
1368                Instruction::PopJumpIfFalse { .. }
1369                | Instruction::PopJumpIfTrue { .. }
1370                | Instruction::PopJumpIfNone { .. }
1371                | Instruction::PopJumpIfNotNone { .. } => {
1372                    // left = fall-through past CACHE entries (skip NOT_TAKEN if present)
1373                    // right = jump target (relative forward from after instruction+caches)
1374                    let after_cache = i + 1 + caches;
1375                    let next_op = instructions
1376                        .get(after_cache)
1377                        .map(|u| u.op.to_base().unwrap_or(u.op));
1378                    let fallthrough = if matches!(next_op, Some(Instruction::NotTaken)) {
1379                        (after_cache + 1) * 2
1380                    } else {
1381                        after_cache * 2
1382                    };
1383                    let right_target = after_cache + oparg as usize;
1384                    (i * 2, fallthrough, right_target * 2)
1385                }
1386                Instruction::EndAsyncFor => {
1387                    // src = END_SEND position (next_i - oparg)
1388                    let next_i = i + 1;
1389                    let Some(src_i) = next_i.checked_sub(oparg as usize) else {
1390                        continue;
1391                    };
1392                    // left = fall-through past NOT_TAKEN
1393                    (src_i * 2, (src_i + 2) * 2, next_i * 2)
1394                }
1395                _ => continue,
1396            };
1397
1398            let tuple = vm.ctx.new_tuple(vec![
1399                vm.ctx.new_int(src).into(),
1400                vm.ctx.new_int(left).into(),
1401                vm.ctx.new_int(right).into(),
1402            ]);
1403            branches.push(tuple.into());
1404        }
1405
1406        let list = vm.ctx.new_list(branches);
1407        vm.call_method(list.as_object(), "__iter__", ())
1408    }
1409
1410    #[pymethod]
1411    pub fn __replace__(&self, args: ReplaceArgs, vm: &VirtualMachine) -> PyResult<PyCode> {
1412        self.replace(args, vm)
1413    }
1414
1415    #[pymethod]
1416    pub fn replace(&self, args: ReplaceArgs, vm: &VirtualMachine) -> PyResult<PyCode> {
1417        let ReplaceArgs {
1418            co_posonlyargcount,
1419            co_argcount,
1420            co_kwonlyargcount,
1421            co_filename,
1422            co_firstlineno,
1423            co_consts,
1424            co_name,
1425            co_names,
1426            co_flags,
1427            co_varnames,
1428            co_nlocals,
1429            co_stacksize,
1430            co_code,
1431            co_linetable,
1432            co_exceptiontable,
1433            co_freevars,
1434            co_cellvars,
1435            co_qualname,
1436        } = args;
1437        let posonlyarg_count = match co_posonlyargcount {
1438            OptionalArg::Present(posonlyarg_count) => posonlyarg_count,
1439            OptionalArg::Missing => self.code.posonlyarg_count,
1440        };
1441
1442        let arg_count = match co_argcount {
1443            OptionalArg::Present(arg_count) => arg_count,
1444            OptionalArg::Missing => self.code.arg_count,
1445        };
1446
1447        let source_path = match co_filename {
1448            OptionalArg::Present(source_path) => source_path,
1449            OptionalArg::Missing => self.source_path().to_owned(),
1450        };
1451
1452        let first_line_number = match co_firstlineno {
1453            OptionalArg::Present(first_line_number) => OneIndexed::new(first_line_number as _),
1454            OptionalArg::Missing => self.code.first_line_number,
1455        };
1456
1457        let kwonlyarg_count = match co_kwonlyargcount {
1458            OptionalArg::Present(kwonlyarg_count) => kwonlyarg_count,
1459            OptionalArg::Missing => self.code.kwonlyarg_count,
1460        };
1461
1462        let constants = match co_consts {
1463            OptionalArg::Present(constants) => constants,
1464            OptionalArg::Missing => self.code.constants.iter().map(|x| x.0.clone()).collect(),
1465        };
1466
1467        let obj_name = match co_name {
1468            OptionalArg::Present(obj_name) => obj_name,
1469            OptionalArg::Missing => self.code.obj_name.to_owned(),
1470        };
1471
1472        let names = match co_names {
1473            OptionalArg::Present(names) => names,
1474            OptionalArg::Missing => self
1475                .code
1476                .names
1477                .deref()
1478                .iter()
1479                .map(|name| name.to_pyobject(vm))
1480                .collect(),
1481        };
1482
1483        let flags = CodeFlags::from_bits_truncate(match co_flags {
1484            OptionalArg::Present(flags) => flags,
1485            OptionalArg::Missing => self.code.flags.bits(),
1486        });
1487
1488        let varname_objects = match co_varnames {
1489            OptionalArg::Present(varnames) => varnames,
1490            OptionalArg::Missing => self.code.varnames.iter().map(|s| s.to_object()).collect(),
1491        };
1492
1493        let qualname = match co_qualname {
1494            OptionalArg::Present(qualname) => qualname,
1495            OptionalArg::Missing => self.code.qualname.to_owned(),
1496        };
1497
1498        // Room for one value is always reserved, even where nothing is pushed.
1499        let max_stackdepth = match co_stacksize {
1500            OptionalArg::Present(stacksize) => stacksize,
1501            OptionalArg::Missing => self.code.max_stackdepth,
1502        }
1503        .max(1);
1504
1505        let instructions = match co_code {
1506            OptionalArg::Present(code_bytes) => {
1507                // Parse and validate bytecode from bytes
1508                CodeUnits::try_from(code_bytes.as_bytes())
1509                    .map_err(|e| vm.new_value_error(format!("invalid bytecode: {e}")))?
1510            }
1511            OptionalArg::Missing => self.code.instructions.clone(),
1512        };
1513
1514        let intern_all = |objs: Vec<PyObjectRef>, field: &str| -> PyResult<Box<[_]>> {
1515            objs.into_iter()
1516                .map(|o| {
1517                    let s = o.downcast_ref::<super::pystr::PyStr>().ok_or_else(|| {
1518                        vm.new_type_error(format!("{field} must be a tuple of strings"))
1519                    })?;
1520                    Ok(vm.ctx.intern_str(s.as_wtf8()))
1521                })
1522                .collect::<PyResult<Vec<_>>>()
1523                .map(Vec::into_boxed_slice)
1524        };
1525
1526        let varnames = intern_all(varname_objects, "co_varnames")?;
1527
1528        let cellvars = match co_cellvars {
1529            OptionalArg::Present(cellvars) => intern_all(cellvars, "co_cellvars")?,
1530            OptionalArg::Missing => self.code.cellvars.clone(),
1531        };
1532
1533        let freevars = match co_freevars {
1534            OptionalArg::Present(freevars) => intern_all(freevars, "co_freevars")?,
1535            OptionalArg::Missing => self.code.freevars.clone(),
1536        };
1537
1538        let nlocals = match co_nlocals {
1539            OptionalArg::Present(nlocals) => nlocals as usize,
1540            OptionalArg::Missing => self.code.varnames.len(),
1541        };
1542        if nlocals != varnames.len() {
1543            return Err(vm.new_value_error(format!(
1544                "co_nlocals ({}) != len(co_varnames) ({})",
1545                nlocals,
1546                varnames.len()
1547            )));
1548        }
1549
1550        let localspluskinds = build_localspluskinds(
1551            &varnames,
1552            &cellvars,
1553            &freevars,
1554            (posonlyarg_count, arg_count, kwonlyarg_count),
1555            flags,
1556            &instructions,
1557        )
1558        .map_err(|index| {
1559            vm.new_value_error(format!(
1560                "code: LOAD_FAST_AND_CLEAR oparg {index} out of range"
1561            ))
1562        })?;
1563
1564        // Handle linetable and exceptiontable
1565        let linetable = match co_linetable {
1566            OptionalArg::Present(linetable) => linetable.as_bytes().to_vec().into_boxed_slice(),
1567            OptionalArg::Missing => self.code.linetable.clone(),
1568        };
1569
1570        let exceptiontable = match co_exceptiontable {
1571            OptionalArg::Present(exceptiontable) => {
1572                exceptiontable.as_bytes().to_vec().into_boxed_slice()
1573            }
1574            OptionalArg::Missing => self.code.exceptiontable.clone(),
1575        };
1576
1577        let new_code = CodeObject {
1578            flags,
1579            posonlyarg_count,
1580            arg_count,
1581            kwonlyarg_count,
1582            source_path: vm.ctx.intern_str(source_path.as_wtf8()),
1583            first_line_number,
1584            obj_name: vm.ctx.intern_str(obj_name.as_wtf8()),
1585            qualname: vm.ctx.intern_str(qualname.as_wtf8()),
1586
1587            max_stackdepth,
1588            instructions,
1589            // FIXME: invalid locations. Actually locations is a duplication of linetable.
1590            // It can be removed once we move every other code to use linetable only.
1591            locations: self.code.locations.clone(),
1592            constants: constants.into_iter().map(Literal).collect(),
1593            names: intern_all(names, "co_names")?,
1594            varnames,
1595            cellvars,
1596            freevars,
1597            localspluskinds,
1598            linetable,
1599            exceptiontable,
1600        };
1601
1602        Ok(PyCode::new(new_code))
1603    }
1604
1605    #[pymethod]
1606    fn _varname_from_oparg(&self, opcode: i32, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
1607        let idx_err = |vm: &VirtualMachine| vm.new_index_error("tuple index out of range");
1608
1609        let idx = usize::try_from(opcode).map_err(|_| idx_err(vm))?;
1610
1611        let varnames_len = self.code.varnames.len();
1612        // Non-parameter cells: cellvars that are NOT also in varnames
1613        let nonparam_cellvars: Vec<_> = self
1614            .code
1615            .cellvars
1616            .iter()
1617            .filter(|s| {
1618                let s_str: &str = s.as_ref();
1619                !self.code.varnames.iter().any(|v| {
1620                    let v_str: &str = v.as_ref();
1621                    v_str == s_str
1622                })
1623            })
1624            .collect();
1625        let nonparam_len = nonparam_cellvars.len();
1626
1627        let name = if idx < varnames_len {
1628            // Index in varnames (includes parameter cells)
1629            self.code.varnames.get(idx).ok_or_else(|| idx_err(vm))?
1630        } else if idx < varnames_len + nonparam_len {
1631            // Index in non-parameter cellvars
1632            *nonparam_cellvars
1633                .get(idx - varnames_len)
1634                .ok_or_else(|| idx_err(vm))?
1635        } else {
1636            // Index in freevars
1637            self.code
1638                .freevars
1639                .get(idx - varnames_len - nonparam_len)
1640                .ok_or_else(|| idx_err(vm))?
1641        };
1642        Ok(name.to_object())
1643    }
1644}
1645
1646impl ToPyObject for CodeObject {
1647    fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
1648        vm.ctx.new_code(self).into()
1649    }
1650}
1651
1652impl ToPyObject for bytecode::CodeObject {
1653    fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
1654        PyCode::new_ref_from_bytecode(vm, self).into()
1655    }
1656}
1657
1658// Helper struct for reading linetable
1659struct LineTableReader<'a> {
1660    data: &'a [u8],
1661    pos: usize,
1662}
1663
1664impl<'a> LineTableReader<'a> {
1665    fn new(data: &'a [u8]) -> Self {
1666        Self { data, pos: 0 }
1667    }
1668
1669    fn read_byte(&mut self) -> Option<u8> {
1670        if self.pos < self.data.len() {
1671            let byte = self.data[self.pos];
1672            self.pos += 1;
1673            Some(byte)
1674        } else {
1675            None
1676        }
1677    }
1678
1679    fn peek_byte(&self) -> Option<u8> {
1680        if self.pos < self.data.len() {
1681            Some(self.data[self.pos])
1682        } else {
1683            None
1684        }
1685    }
1686
1687    fn read_varint(&mut self) -> u32 {
1688        if let Some(first) = self.read_byte() {
1689            let mut val = (first & 0x3f) as u32;
1690            let mut shift = 0;
1691            let mut byte = first;
1692            while (byte & 0x40) != 0 {
1693                if let Some(next) = self.read_byte() {
1694                    shift += 6;
1695                    val |= ((next & 0x3f) as u32) << shift;
1696                    byte = next;
1697                } else {
1698                    break;
1699                }
1700            }
1701            val
1702        } else {
1703            0
1704        }
1705    }
1706
1707    fn read_signed_varint(&mut self) -> i32 {
1708        let uval = self.read_varint();
1709        if uval & 1 != 0 {
1710            -((uval >> 1) as i32)
1711        } else {
1712            (uval >> 1) as i32
1713        }
1714    }
1715
1716    fn at_end(&self) -> bool {
1717        self.pos >= self.data.len()
1718    }
1719}
1720
1721pub(crate) fn init(ctx: &'static Context) {
1722    PyCode::extend_class(ctx, ctx.types.code_type);
1723}