Files
RustPython/crates/compiler-core/src/bytecode.rs
T

1258 lines
41 KiB
Rust

//! Implement python as a virtual machine with bytecode. This module
//! implements bytecode structure.
use crate::{
marshal::MarshalError,
varint::{read_varint, read_varint_with_start, write_varint, write_varint_with_start},
{OneIndexed, SourceLocation},
};
use alloc::{borrow::ToOwned, boxed::Box, collections::BTreeSet, fmt, string::String, vec::Vec};
use bitflags::bitflags;
use core::{
cell::UnsafeCell,
hash, mem,
ops::{Deref, Index, IndexMut},
sync::atomic::{AtomicU8, AtomicU16, AtomicUsize, Ordering},
};
use itertools::Itertools;
use malachite_bigint::BigInt;
use num_complex::Complex64;
use rustpython_wtf8::{Wtf8, Wtf8Buf};
pub use crate::bytecode::{
instruction::{
AnyInstruction, Arg, Instruction, InstructionMetadata, PseudoInstruction, StackEffect,
},
oparg::{
BinaryOperator, BuildSliceArgCount, CommonConstant, ComparisonOperator, ConvertValueOparg,
IntrinsicFunction1, IntrinsicFunction2, Invert, Label, LoadAttr, LoadSuperAttr,
MakeFunctionFlag, MakeFunctionFlags, NameIdx, OpArg, OpArgByte, OpArgState, OpArgType,
RaiseKind, ResumeType, SpecialMethod, UnpackExArgs,
},
};
mod instruction;
pub mod oparg;
/// Exception table entry for zero-cost exception handling
/// Format: (start, size, target, depth<<1|lasti)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExceptionTableEntry {
/// Start instruction offset (inclusive)
pub start: u32,
/// End instruction offset (exclusive)
pub end: u32,
/// Handler target offset
pub target: u32,
/// Stack depth at handler entry
pub depth: u16,
/// Whether to push lasti before exception
pub push_lasti: bool,
}
impl ExceptionTableEntry {
pub const fn new(start: u32, end: u32, target: u32, depth: u16, push_lasti: bool) -> Self {
Self {
start,
end,
target,
depth,
push_lasti,
}
}
}
/// Encode exception table entries.
/// Uses 6-bit varint encoding with start marker (MSB) and continuation bit.
pub fn encode_exception_table(entries: &[ExceptionTableEntry]) -> alloc::boxed::Box<[u8]> {
let mut data = Vec::new();
for entry in entries {
let size = entry.end.saturating_sub(entry.start);
let depth_lasti = ((entry.depth as u32) << 1) | (entry.push_lasti as u32);
write_varint_with_start(&mut data, entry.start);
write_varint(&mut data, size);
write_varint(&mut data, entry.target);
write_varint(&mut data, depth_lasti);
}
data.into_boxed_slice()
}
/// Find exception handler for given instruction offset.
pub fn find_exception_handler(table: &[u8], offset: u32) -> Option<ExceptionTableEntry> {
let mut pos = 0;
while pos < table.len() {
let start = read_varint_with_start(table, &mut pos)?;
let size = read_varint(table, &mut pos)?;
let target = read_varint(table, &mut pos)?;
let depth_lasti = read_varint(table, &mut pos)?;
let end = start + size;
let depth = (depth_lasti >> 1) as u16;
let push_lasti = (depth_lasti & 1) != 0;
if offset >= start && offset < end {
return Some(ExceptionTableEntry {
start,
end,
target,
depth,
push_lasti,
});
}
}
None
}
/// Decode all exception table entries.
pub fn decode_exception_table(table: &[u8]) -> Vec<ExceptionTableEntry> {
let mut entries = Vec::new();
let mut pos = 0;
while pos < table.len() {
let Some(start) = read_varint_with_start(table, &mut pos) else {
break;
};
let Some(size) = read_varint(table, &mut pos) else {
break;
};
let Some(target) = read_varint(table, &mut pos) else {
break;
};
let Some(depth_lasti) = read_varint(table, &mut pos) else {
break;
};
let Some(end) = start.checked_add(size) else {
break;
};
entries.push(ExceptionTableEntry {
start,
end,
target,
depth: (depth_lasti >> 1) as u16,
push_lasti: (depth_lasti & 1) != 0,
});
}
entries
}
/// CPython 3.11+ linetable location info codes
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum PyCodeLocationInfoKind {
// Short forms are 0 to 9
Short0 = 0,
Short1 = 1,
Short2 = 2,
Short3 = 3,
Short4 = 4,
Short5 = 5,
Short6 = 6,
Short7 = 7,
Short8 = 8,
Short9 = 9,
// One line forms are 10 to 12
OneLine0 = 10,
OneLine1 = 11,
OneLine2 = 12,
NoColumns = 13,
Long = 14,
None = 15,
}
impl PyCodeLocationInfoKind {
pub fn from_code(code: u8) -> Option<Self> {
match code {
0 => Some(Self::Short0),
1 => Some(Self::Short1),
2 => Some(Self::Short2),
3 => Some(Self::Short3),
4 => Some(Self::Short4),
5 => Some(Self::Short5),
6 => Some(Self::Short6),
7 => Some(Self::Short7),
8 => Some(Self::Short8),
9 => Some(Self::Short9),
10 => Some(Self::OneLine0),
11 => Some(Self::OneLine1),
12 => Some(Self::OneLine2),
13 => Some(Self::NoColumns),
14 => Some(Self::Long),
15 => Some(Self::None),
_ => Option::None,
}
}
pub fn is_short(&self) -> bool {
(*self as u8) <= 9
}
pub fn short_column_group(&self) -> Option<u8> {
if self.is_short() {
Some(*self as u8)
} else {
Option::None
}
}
pub fn one_line_delta(&self) -> Option<i32> {
match self {
Self::OneLine0 => Some(0),
Self::OneLine1 => Some(1),
Self::OneLine2 => Some(2),
_ => Option::None,
}
}
}
pub trait Constant: Sized {
type Name: AsRef<str>;
/// Transforms the given Constant to a BorrowedConstant
fn borrow_constant(&self) -> BorrowedConstant<'_, Self>;
}
impl Constant for ConstantData {
type Name = String;
fn borrow_constant(&self) -> BorrowedConstant<'_, Self> {
use BorrowedConstant::*;
match self {
Self::Integer { value } => Integer { value },
Self::Float { value } => Float { value: *value },
Self::Complex { value } => Complex { value: *value },
Self::Boolean { value } => Boolean { value: *value },
Self::Str { value } => Str { value },
Self::Bytes { value } => Bytes { value },
Self::Code { code } => Code { code },
Self::Tuple { elements } => Tuple { elements },
Self::None => None,
Self::Ellipsis => Ellipsis,
}
}
}
/// A Constant Bag
pub trait ConstantBag: Sized + Copy {
type Constant: Constant;
fn make_constant<C: Constant>(&self, constant: BorrowedConstant<'_, C>) -> Self::Constant;
fn make_int(&self, value: BigInt) -> Self::Constant;
fn make_tuple(&self, elements: impl Iterator<Item = Self::Constant>) -> Self::Constant;
fn make_code(&self, code: CodeObject<Self::Constant>) -> Self::Constant;
fn make_name(&self, name: &str) -> <Self::Constant as Constant>::Name;
}
pub trait AsBag {
type Bag: ConstantBag;
#[allow(clippy::wrong_self_convention)]
fn as_bag(self) -> Self::Bag;
}
impl<Bag: ConstantBag> AsBag for Bag {
type Bag = Self;
fn as_bag(self) -> Self {
self
}
}
#[derive(Clone, Copy)]
pub struct BasicBag;
impl ConstantBag for BasicBag {
type Constant = ConstantData;
fn make_constant<C: Constant>(&self, constant: BorrowedConstant<'_, C>) -> Self::Constant {
constant.to_owned()
}
fn make_int(&self, value: BigInt) -> Self::Constant {
ConstantData::Integer { value }
}
fn make_tuple(&self, elements: impl Iterator<Item = Self::Constant>) -> Self::Constant {
ConstantData::Tuple {
elements: elements.collect(),
}
}
fn make_code(&self, code: CodeObject<Self::Constant>) -> Self::Constant {
ConstantData::Code {
code: Box::new(code),
}
}
fn make_name(&self, name: &str) -> <Self::Constant as Constant>::Name {
name.to_owned()
}
}
#[derive(Clone)]
pub struct Constants<C: Constant>(Box<[C]>);
impl<C: Constant> Deref for Constants<C> {
type Target = [C];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<C: Constant> Index<oparg::ConstIdx> for Constants<C> {
type Output = C;
fn index(&self, consti: oparg::ConstIdx) -> &Self::Output {
&self.0[consti.as_usize()]
}
}
impl<C: Constant> FromIterator<C> for Constants<C> {
fn from_iter<T: IntoIterator<Item = C>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
// TODO: Newtype "CodeObject.varnames". Make sure only `oparg:VarNum` can be used as index
impl<T> Index<oparg::VarNum> for [T] {
type Output = T;
fn index(&self, var_num: oparg::VarNum) -> &Self::Output {
&self[var_num.as_usize()]
}
}
// TODO: Newtype "CodeObject.varnames". Make sure only `oparg:VarNum` can be used as index
impl<T> IndexMut<oparg::VarNum> for [T] {
fn index_mut(&mut self, var_num: oparg::VarNum) -> &mut Self::Output {
&mut self[var_num.as_usize()]
}
}
/// Primary container of a single code object. Each python function has
/// a code object. Also a module has a code object.
#[derive(Clone)]
pub struct CodeObject<C: Constant = ConstantData> {
pub instructions: CodeUnits,
pub locations: Box<[(SourceLocation, SourceLocation)]>,
pub flags: CodeFlags,
/// Number of positional-only arguments
pub posonlyarg_count: u32,
pub arg_count: u32,
pub kwonlyarg_count: u32,
pub source_path: C::Name,
pub first_line_number: Option<OneIndexed>,
pub max_stackdepth: u32,
/// Name of the object that created this code object
pub obj_name: C::Name,
/// Qualified name of the object (like CPython's co_qualname)
pub qualname: C::Name,
pub cell2arg: Option<Box<[i32]>>,
pub constants: Constants<C>,
pub names: Box<[C::Name]>,
pub varnames: Box<[C::Name]>,
pub cellvars: Box<[C::Name]>,
pub freevars: Box<[C::Name]>,
/// Line number table (CPython 3.11+ format)
pub linetable: Box<[u8]>,
/// Exception handling table
pub exceptiontable: Box<[u8]>,
}
bitflags! {
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct CodeFlags: u32 {
const OPTIMIZED = 0x0001;
const NEWLOCALS = 0x0002;
const VARARGS = 0x0004;
const VARKEYWORDS = 0x0008;
const GENERATOR = 0x0020;
const COROUTINE = 0x0080;
const ITERABLE_COROUTINE = 0x0100;
/// If a code object represents a function and has a docstring,
/// this bit is set and the first item in co_consts is the docstring.
const HAS_DOCSTRING = 0x4000000;
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct CodeUnit {
pub op: Instruction,
pub arg: OpArgByte,
}
const _: () = assert!(mem::size_of::<CodeUnit>() == 2);
/// Adaptive specialization: number of executions before attempting specialization.
///
/// Matches CPython's `_Py_BackoffCounter` encoding.
pub const ADAPTIVE_WARMUP_VALUE: u16 = adaptive_counter_bits(1, 1);
/// Adaptive specialization: cooldown counter after a successful specialization.
///
/// Value/backoff = (52, 0), matching CPython's ADAPTIVE_COOLDOWN bits.
pub const ADAPTIVE_COOLDOWN_VALUE: u16 = adaptive_counter_bits(52, 0);
/// Initial JUMP_BACKWARD counter bits (value/backoff = 4095/12).
pub const JUMP_BACKWARD_INITIAL_VALUE: u16 = adaptive_counter_bits(4095, 12);
const BACKOFF_BITS: u16 = 4;
const MAX_BACKOFF: u16 = 12;
const UNREACHABLE_BACKOFF: u16 = 15;
/// Encode an adaptive counter as `(value << 4) | backoff`.
pub const fn adaptive_counter_bits(value: u16, backoff: u16) -> u16 {
(value << BACKOFF_BITS) | backoff
}
/// True when the adaptive counter should trigger specialization.
#[inline]
pub const fn adaptive_counter_triggers(counter: u16) -> bool {
counter < UNREACHABLE_BACKOFF
}
/// Decrement adaptive counter by one countdown step.
#[inline]
pub const fn advance_adaptive_counter(counter: u16) -> u16 {
counter.wrapping_sub(1 << BACKOFF_BITS)
}
/// Reset adaptive counter with exponential backoff.
#[inline]
pub const fn adaptive_counter_backoff(counter: u16) -> u16 {
let backoff = counter & ((1 << BACKOFF_BITS) - 1);
if backoff < MAX_BACKOFF {
adaptive_counter_bits((1 << (backoff + 1)) - 1, backoff + 1)
} else {
adaptive_counter_bits((1 << MAX_BACKOFF) - 1, MAX_BACKOFF)
}
}
impl CodeUnit {
pub const fn new(op: Instruction, arg: OpArgByte) -> Self {
Self { op, arg }
}
}
impl TryFrom<&[u8]> for CodeUnit {
type Error = MarshalError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
match value.len() {
2 => Ok(Self::new(value[0].try_into()?, value[1].into())),
_ => Err(Self::Error::InvalidBytecode),
}
}
}
pub struct CodeUnits {
units: UnsafeCell<Box<[CodeUnit]>>,
adaptive_counters: Box<[AtomicU16]>,
/// Pointer-sized cache entries for descriptor pointers.
/// Single atomic load/store prevents torn reads when multiple threads
/// specialize the same instruction concurrently.
pointer_cache: Box<[AtomicUsize]>,
}
// SAFETY: All cache operations use atomic read/write instructions.
// - replace_op / compare_exchange_op: AtomicU8 store/CAS (Release)
// - cache read/write: AtomicU16 load/store (Relaxed)
// - adaptive counter: AtomicU16 load/store (Relaxed)
// Ordering is established by:
// - replace_op (Release) ↔ dispatch loop read_op (Acquire) for cache data visibility
// - tp_version_tag (Acquire) for descriptor pointer validity
unsafe impl Sync for CodeUnits {}
impl Clone for CodeUnits {
fn clone(&self) -> Self {
// SAFETY: No concurrent mutation during clone — cloning is only done
// during code object construction or marshaling, not while instrumented.
let units = unsafe { &*self.units.get() }.clone();
let adaptive_counters = self
.adaptive_counters
.iter()
.map(|c| AtomicU16::new(c.load(Ordering::Relaxed)))
.collect();
let pointer_cache = self
.pointer_cache
.iter()
.map(|c| AtomicUsize::new(c.load(Ordering::Relaxed)))
.collect();
Self {
units: UnsafeCell::new(units),
adaptive_counters,
pointer_cache,
}
}
}
impl fmt::Debug for CodeUnits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// SAFETY: Debug formatting doesn't race with replace_op
let inner = unsafe { &*self.units.get() };
f.debug_tuple("CodeUnits").field(inner).finish()
}
}
impl TryFrom<&[u8]> for CodeUnits {
type Error = MarshalError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if !value.len().is_multiple_of(2) {
return Err(Self::Error::InvalidBytecode);
}
let units = value
.chunks_exact(2)
.map(CodeUnit::try_from)
.collect::<Result<Vec<_>, _>>()?;
Ok(units.into())
}
}
impl<const N: usize> From<[CodeUnit; N]> for CodeUnits {
fn from(value: [CodeUnit; N]) -> Self {
Self::from(Vec::from(value))
}
}
impl From<Vec<CodeUnit>> for CodeUnits {
fn from(value: Vec<CodeUnit>) -> Self {
let units = value.into_boxed_slice();
let len = units.len();
let adaptive_counters = (0..len)
.map(|_| AtomicU16::new(0))
.collect::<Vec<_>>()
.into_boxed_slice();
let pointer_cache = (0..len)
.map(|_| AtomicUsize::new(0))
.collect::<Vec<_>>()
.into_boxed_slice();
Self {
units: UnsafeCell::new(units),
adaptive_counters,
pointer_cache,
}
}
}
impl FromIterator<CodeUnit> for CodeUnits {
fn from_iter<T: IntoIterator<Item = CodeUnit>>(iter: T) -> Self {
Self::from(iter.into_iter().collect::<Vec<_>>())
}
}
impl Deref for CodeUnits {
type Target = [CodeUnit];
fn deref(&self) -> &Self::Target {
// SAFETY: Shared references to the slice are valid even while replace_op
// may update individual opcode bytes — readers tolerate stale opcodes
// (they will re-read on the next iteration).
unsafe { &*self.units.get() }
}
}
impl CodeUnits {
/// Replace the opcode at `index` in-place without changing the arg byte.
/// Uses atomic Release store to ensure prior cache writes are visible
/// to threads that subsequently read the new opcode with Acquire.
///
/// # Safety
/// - `index` must be in bounds.
/// - `new_op` must have the same arg semantics as the original opcode.
pub unsafe fn replace_op(&self, index: usize, new_op: Instruction) {
let units = unsafe { &*self.units.get() };
let ptr = units.as_ptr().wrapping_add(index) as *const AtomicU8;
unsafe { &*ptr }.store(new_op.into(), Ordering::Release);
}
/// Atomically replace opcode only if it still matches `expected`.
/// Returns true on success. Uses Release ordering on success.
///
/// # Safety
/// - `index` must be in bounds.
pub unsafe fn compare_exchange_op(
&self,
index: usize,
expected: Instruction,
new_op: Instruction,
) -> bool {
let units = unsafe { &*self.units.get() };
let ptr = units.as_ptr().wrapping_add(index) as *const AtomicU8;
unsafe { &*ptr }
.compare_exchange(
expected.into(),
new_op.into(),
Ordering::Release,
Ordering::Relaxed,
)
.is_ok()
}
/// Atomically read the opcode at `index` with Acquire ordering.
/// Pairs with `replace_op` (Release) to ensure cache data visibility.
pub fn read_op(&self, index: usize) -> Instruction {
let units = unsafe { &*self.units.get() };
let ptr = units.as_ptr().wrapping_add(index) as *const AtomicU8;
let byte = unsafe { &*ptr }.load(Ordering::Acquire);
// SAFETY: Only valid Instruction values are stored via replace_op/compare_exchange_op.
unsafe { mem::transmute::<u8, Instruction>(byte) }
}
/// Atomically read the arg byte at `index` with Relaxed ordering.
pub fn read_arg(&self, index: usize) -> OpArgByte {
let units = unsafe { &*self.units.get() };
let ptr = units.as_ptr().wrapping_add(index) as *const u8;
let arg_ptr = unsafe { ptr.add(1) } as *const AtomicU8;
OpArgByte::from(unsafe { &*arg_ptr }.load(Ordering::Relaxed))
}
/// Write a u16 value into a CACHE code unit at `index`.
/// Each CodeUnit is 2 bytes (#[repr(C)]: op u8 + arg u8), so one u16 fits exactly.
/// Uses Relaxed atomic store; ordering is provided by replace_op (Release).
///
/// # Safety
/// - `index` must be in bounds and point to a CACHE entry.
pub unsafe fn write_cache_u16(&self, index: usize, value: u16) {
let units = unsafe { &*self.units.get() };
let ptr = units.as_ptr().wrapping_add(index) as *const AtomicU16;
unsafe { &*ptr }.store(value, Ordering::Relaxed);
}
/// Read a u16 value from a CACHE code unit at `index`.
/// Uses Relaxed atomic load; ordering is provided by read_op (Acquire).
///
/// # Panics
/// Panics if `index` is out of bounds.
pub fn read_cache_u16(&self, index: usize) -> u16 {
let units = unsafe { &*self.units.get() };
assert!(index < units.len(), "read_cache_u16: index out of bounds");
let ptr = units.as_ptr().wrapping_add(index) as *const AtomicU16;
unsafe { &*ptr }.load(Ordering::Relaxed)
}
/// Write a u32 value across two consecutive CACHE code units starting at `index`.
///
/// # Safety
/// Same requirements as `write_cache_u16`.
pub unsafe fn write_cache_u32(&self, index: usize, value: u32) {
unsafe {
self.write_cache_u16(index, value as u16);
self.write_cache_u16(index + 1, (value >> 16) as u16);
}
}
/// Read a u32 value from two consecutive CACHE code units starting at `index`.
///
/// # Panics
/// Panics if `index + 1` is out of bounds.
pub fn read_cache_u32(&self, index: usize) -> u32 {
let lo = self.read_cache_u16(index) as u32;
let hi = self.read_cache_u16(index + 1) as u32;
lo | (hi << 16)
}
/// Store a pointer-sized value atomically in the pointer cache at `index`.
///
/// Uses a single `AtomicUsize` store to prevent torn writes when
/// multiple threads specialize the same instruction concurrently.
///
/// # Safety
/// - `index` must be in bounds.
/// - `value` must be `0` or a valid `*const PyObject` encoded as `usize`.
/// - Callers must follow the cache invalidation/upgrade protocol:
/// invalidate the version guard before writing and publish the new
/// version after writing.
pub unsafe fn write_cache_ptr(&self, index: usize, value: usize) {
self.pointer_cache[index].store(value, Ordering::Relaxed);
}
/// Load a pointer-sized value atomically from the pointer cache at `index`.
///
/// Uses a single `AtomicUsize` load to prevent torn reads.
///
/// # Panics
/// Panics if `index` is out of bounds.
pub fn read_cache_ptr(&self, index: usize) -> usize {
self.pointer_cache[index].load(Ordering::Relaxed)
}
/// Read adaptive counter bits for instruction at `index`.
/// Uses Relaxed atomic load.
pub fn read_adaptive_counter(&self, index: usize) -> u16 {
self.adaptive_counters[index].load(Ordering::Relaxed)
}
/// Write adaptive counter bits for instruction at `index`.
/// Uses Relaxed atomic store.
///
/// # Safety
/// - `index` must be in bounds.
pub unsafe fn write_adaptive_counter(&self, index: usize, value: u16) {
self.adaptive_counters[index].store(value, Ordering::Relaxed);
}
/// Produce a clean copy of the bytecode suitable for serialization
/// (marshal) and `co_code`. Specialized opcodes are mapped back to their
/// base variants via `deoptimize()` and all CACHE entries are zeroed.
pub fn original_bytes(&self) -> Vec<u8> {
let len = self.len();
let mut out = Vec::with_capacity(len * 2);
let mut i = 0;
while i < len {
let op = self.read_op(i).deoptimize();
let arg = self.read_arg(i);
let caches = op.cache_entries();
out.push(u8::from(op));
out.push(u8::from(arg));
// Zero-fill all CACHE entries (counter + cached data)
for _ in 0..caches {
i += 1;
out.push(0); // op = Cache = 0
out.push(0); // arg = 0
}
i += 1;
}
out
}
/// Initialize adaptive warmup counters for all cacheable instructions.
/// Called lazily at RESUME (first execution of a code object).
/// Counters are stored out-of-line to preserve `op = Instruction::Cache`.
/// All writes are atomic (Relaxed) to avoid data races with concurrent readers.
pub fn quicken(&self) {
let len = self.len();
let mut i = 0;
while i < len {
let op = self.read_op(i);
let caches = op.cache_entries();
if caches > 0 {
// Don't write adaptive counter for instrumented opcodes;
// specialization is skipped while monitoring is active.
if !op.is_instrumented() {
let cache_base = i + 1;
if cache_base < len {
let initial_counter = if matches!(op, Instruction::JumpBackward { .. }) {
JUMP_BACKWARD_INITIAL_VALUE
} else {
ADAPTIVE_WARMUP_VALUE
};
unsafe {
self.write_adaptive_counter(cache_base, initial_counter);
}
}
}
i += 1 + caches;
} else {
i += 1;
}
}
}
}
/// A Constant (which usually encapsulates data within it)
///
/// # Examples
/// ```
/// use rustpython_compiler_core::bytecode::ConstantData;
/// let a = ConstantData::Float {value: 120f64};
/// let b = ConstantData::Boolean {value: false};
/// assert_ne!(a, b);
/// ```
#[derive(Debug, Clone)]
pub enum ConstantData {
Tuple { elements: Vec<ConstantData> },
Integer { value: BigInt },
Float { value: f64 },
Complex { value: Complex64 },
Boolean { value: bool },
Str { value: Wtf8Buf },
Bytes { value: Vec<u8> },
Code { code: Box<CodeObject> },
None,
Ellipsis,
}
impl PartialEq for ConstantData {
fn eq(&self, other: &Self) -> bool {
use ConstantData::*;
match (self, other) {
(Integer { value: a }, Integer { value: b }) => a == b,
// we want to compare floats *by actual value* - if we have the *exact same* float
// already in a constant cache, we want to use that
(Float { value: a }, Float { value: b }) => a.to_bits() == b.to_bits(),
(Complex { value: a }, Complex { value: b }) => {
a.re.to_bits() == b.re.to_bits() && a.im.to_bits() == b.im.to_bits()
}
(Boolean { value: a }, Boolean { value: b }) => a == b,
(Str { value: a }, Str { value: b }) => a == b,
(Bytes { value: a }, Bytes { value: b }) => a == b,
(Code { code: a }, Code { code: b }) => core::ptr::eq(a.as_ref(), b.as_ref()),
(Tuple { elements: a }, Tuple { elements: b }) => a == b,
(None, None) => true,
(Ellipsis, Ellipsis) => true,
_ => false,
}
}
}
impl Eq for ConstantData {}
impl hash::Hash for ConstantData {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
use ConstantData::*;
mem::discriminant(self).hash(state);
match self {
Integer { value } => value.hash(state),
Float { value } => value.to_bits().hash(state),
Complex { value } => {
value.re.to_bits().hash(state);
value.im.to_bits().hash(state);
}
Boolean { value } => value.hash(state),
Str { value } => value.hash(state),
Bytes { value } => value.hash(state),
Code { code } => core::ptr::hash(code.as_ref(), state),
Tuple { elements } => elements.hash(state),
None => {}
Ellipsis => {}
}
}
}
/// A borrowed Constant
pub enum BorrowedConstant<'a, C: Constant> {
Integer { value: &'a BigInt },
Float { value: f64 },
Complex { value: Complex64 },
Boolean { value: bool },
Str { value: &'a Wtf8 },
Bytes { value: &'a [u8] },
Code { code: &'a CodeObject<C> },
Tuple { elements: &'a [C] },
None,
Ellipsis,
}
impl<C: Constant> Copy for BorrowedConstant<'_, C> {}
impl<C: Constant> Clone for BorrowedConstant<'_, C> {
fn clone(&self) -> Self {
*self
}
}
impl<C: Constant> BorrowedConstant<'_, C> {
pub fn fmt_display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BorrowedConstant::Integer { value } => write!(f, "{value}"),
BorrowedConstant::Float { value } => write!(f, "{value}"),
BorrowedConstant::Complex { value } => write!(f, "{value}"),
BorrowedConstant::Boolean { value } => {
write!(f, "{}", if *value { "True" } else { "False" })
}
BorrowedConstant::Str { value } => write!(f, "{value:?}"),
BorrowedConstant::Bytes { value } => write!(f, r#"b"{}""#, value.escape_ascii()),
BorrowedConstant::Code { code } => write!(f, "{code:?}"),
BorrowedConstant::Tuple { elements } => {
write!(f, "(")?;
let mut first = true;
for c in *elements {
if first {
first = false
} else {
write!(f, ", ")?;
}
c.borrow_constant().fmt_display(f)?;
}
write!(f, ")")
}
BorrowedConstant::None => write!(f, "None"),
BorrowedConstant::Ellipsis => write!(f, "..."),
}
}
pub fn to_owned(self) -> ConstantData {
use ConstantData::*;
match self {
BorrowedConstant::Integer { value } => Integer {
value: value.clone(),
},
BorrowedConstant::Float { value } => Float { value },
BorrowedConstant::Complex { value } => Complex { value },
BorrowedConstant::Boolean { value } => Boolean { value },
BorrowedConstant::Str { value } => Str {
value: value.to_owned(),
},
BorrowedConstant::Bytes { value } => Bytes {
value: value.to_owned(),
},
BorrowedConstant::Code { code } => Code {
code: Box::new(code.map_clone_bag(&BasicBag)),
},
BorrowedConstant::Tuple { elements } => Tuple {
elements: elements
.iter()
.map(|c| c.borrow_constant().to_owned())
.collect(),
},
BorrowedConstant::None => None,
BorrowedConstant::Ellipsis => Ellipsis,
}
}
}
/*
Maintain a stack of blocks on the VM.
pub enum BlockType {
Loop,
Except,
}
*/
/// Argument structure
pub struct Arguments<'a, N: AsRef<str>> {
pub posonlyargs: &'a [N],
pub args: &'a [N],
pub vararg: Option<&'a N>,
pub kwonlyargs: &'a [N],
pub varkwarg: Option<&'a N>,
}
impl<N: AsRef<str>> fmt::Debug for Arguments<'_, N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
macro_rules! fmt_slice {
($x:expr) => {
format_args!("[{}]", $x.iter().map(AsRef::as_ref).format(", "))
};
}
f.debug_struct("Arguments")
.field("posonlyargs", &fmt_slice!(self.posonlyargs))
.field("args", &fmt_slice!(self.posonlyargs))
.field("vararg", &self.vararg.map(N::as_ref))
.field("kwonlyargs", &fmt_slice!(self.kwonlyargs))
.field("varkwarg", &self.varkwarg.map(N::as_ref))
.finish()
}
}
impl<C: Constant> CodeObject<C> {
/// Get all arguments of the code object
/// like inspect.getargs
pub fn arg_names(&self) -> Arguments<'_, C::Name> {
let nargs = self.arg_count as usize;
let nkwargs = self.kwonlyarg_count as usize;
let mut varargs_pos = nargs + nkwargs;
let posonlyargs = &self.varnames[..self.posonlyarg_count as usize];
let args = &self.varnames[..nargs];
let kwonlyargs = &self.varnames[nargs..varargs_pos];
let vararg = if self.flags.contains(CodeFlags::VARARGS) {
let vararg = &self.varnames[varargs_pos];
varargs_pos += 1;
Some(vararg)
} else {
None
};
let varkwarg = if self.flags.contains(CodeFlags::VARKEYWORDS) {
Some(&self.varnames[varargs_pos])
} else {
None
};
Arguments {
posonlyargs,
args,
vararg,
kwonlyargs,
varkwarg,
}
}
/// Return the labels targeted by the instructions of this CodeObject
pub fn label_targets(&self) -> BTreeSet<Label> {
let mut label_targets = BTreeSet::new();
let mut arg_state = OpArgState::default();
for instruction in &*self.instructions {
let (instruction, arg) = arg_state.get(*instruction);
if let Some(l) = instruction.label_arg() {
label_targets.insert(l.get(arg));
}
}
label_targets
}
fn display_inner(
&self,
f: &mut fmt::Formatter<'_>,
expand_code_objects: bool,
level: usize,
) -> fmt::Result {
let label_targets = self.label_targets();
let line_digits = (3).max(self.locations.last().unwrap().0.line.digits().get());
let offset_digits = (4).max(1 + self.instructions.len().ilog10() as usize);
let mut last_line = OneIndexed::MAX;
let mut arg_state = OpArgState::default();
for (offset, &instruction) in self.instructions.iter().enumerate() {
let (instruction, arg) = arg_state.get(instruction);
// optional line number
let line = self.locations[offset].0.line;
if line != last_line {
if last_line != OneIndexed::MAX {
writeln!(f)?;
}
last_line = line;
write!(f, "{line:line_digits$}")?;
} else {
for _ in 0..line_digits {
write!(f, " ")?;
}
}
write!(f, " ")?;
// level indent
for _ in 0..level {
write!(f, " ")?;
}
// arrow and offset
let arrow = if label_targets.contains(&Label::new(offset as u32)) {
">>"
} else {
" "
};
write!(f, "{arrow} {offset:offset_digits$} ")?;
// instruction
instruction.fmt_dis(arg, f, self, expand_code_objects, 21, level)?;
writeln!(f)?;
}
Ok(())
}
/// Recursively display this CodeObject
pub fn display_expand_code_objects(&self) -> impl fmt::Display + '_ {
struct Display<'a, C: Constant>(&'a CodeObject<C>);
impl<C: Constant> fmt::Display for Display<'_, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.display_inner(f, true, 1)
}
}
Display(self)
}
/// Map this CodeObject to one that holds a Bag::Constant
pub fn map_bag<Bag: ConstantBag>(self, bag: Bag) -> CodeObject<Bag::Constant> {
let map_names = |names: Box<[C::Name]>| {
names
.iter()
.map(|x| bag.make_name(x.as_ref()))
.collect::<Box<[_]>>()
};
CodeObject {
constants: self
.constants
.iter()
.map(|x| bag.make_constant(x.borrow_constant()))
.collect(),
names: map_names(self.names),
varnames: map_names(self.varnames),
cellvars: map_names(self.cellvars),
freevars: map_names(self.freevars),
source_path: bag.make_name(self.source_path.as_ref()),
obj_name: bag.make_name(self.obj_name.as_ref()),
qualname: bag.make_name(self.qualname.as_ref()),
instructions: self.instructions,
locations: self.locations,
flags: self.flags,
posonlyarg_count: self.posonlyarg_count,
arg_count: self.arg_count,
kwonlyarg_count: self.kwonlyarg_count,
first_line_number: self.first_line_number,
max_stackdepth: self.max_stackdepth,
cell2arg: self.cell2arg,
linetable: self.linetable,
exceptiontable: self.exceptiontable,
}
}
/// Same as `map_bag` but clones `self`
pub fn map_clone_bag<Bag: ConstantBag>(&self, bag: &Bag) -> CodeObject<Bag::Constant> {
let map_names =
|names: &[C::Name]| names.iter().map(|x| bag.make_name(x.as_ref())).collect();
CodeObject {
constants: self
.constants
.iter()
.map(|x| bag.make_constant(x.borrow_constant()))
.collect(),
names: map_names(&self.names),
varnames: map_names(&self.varnames),
cellvars: map_names(&self.cellvars),
freevars: map_names(&self.freevars),
source_path: bag.make_name(self.source_path.as_ref()),
obj_name: bag.make_name(self.obj_name.as_ref()),
qualname: bag.make_name(self.qualname.as_ref()),
instructions: self.instructions.clone(),
locations: self.locations.clone(),
flags: self.flags,
posonlyarg_count: self.posonlyarg_count,
arg_count: self.arg_count,
kwonlyarg_count: self.kwonlyarg_count,
first_line_number: self.first_line_number,
max_stackdepth: self.max_stackdepth,
cell2arg: self.cell2arg.clone(),
linetable: self.linetable.clone(),
exceptiontable: self.exceptiontable.clone(),
}
}
}
impl<C: Constant> fmt::Display for CodeObject<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.display_inner(f, false, 1)?;
for constant in &*self.constants {
if let BorrowedConstant::Code { code } = constant.borrow_constant() {
writeln!(f, "\nDisassembly of {code:?}")?;
code.fmt(f)?;
}
}
Ok(())
}
}
pub trait InstrDisplayContext {
type Constant: Constant;
fn get_constant(&self, consti: oparg::ConstIdx) -> &Self::Constant;
fn get_name(&self, i: usize) -> &str;
fn get_varname(&self, var_num: oparg::VarNum) -> &str;
fn get_cell_name(&self, i: usize) -> &str;
}
impl<C: Constant> InstrDisplayContext for CodeObject<C> {
type Constant = C;
fn get_constant(&self, consti: oparg::ConstIdx) -> &C {
&self.constants[consti]
}
fn get_name(&self, i: usize) -> &str {
self.names[i].as_ref()
}
fn get_varname(&self, var_num: oparg::VarNum) -> &str {
self.varnames[var_num].as_ref()
}
fn get_cell_name(&self, i: usize) -> &str {
self.cellvars
.get(i)
.unwrap_or_else(|| &self.freevars[i - self.cellvars.len()])
.as_ref()
}
}
impl fmt::Display for ConstantData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.borrow_constant().fmt_display(f)
}
}
impl<C: Constant> fmt::Debug for CodeObject<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"<code object {} at ??? file {:?}, line {}>",
self.obj_name.as_ref(),
self.source_path.as_ref(),
self.first_line_number.map_or(-1, |x| x.get() as i32)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{vec, vec::Vec};
#[test]
fn test_exception_table_encode_decode() {
let entries = vec![
ExceptionTableEntry::new(0, 10, 20, 2, false),
ExceptionTableEntry::new(15, 25, 30, 1, true),
];
let encoded = encode_exception_table(&entries);
// Find handler at offset 5 (in range [0, 10))
let handler = find_exception_handler(&encoded, 5);
assert!(handler.is_some());
let handler = handler.unwrap();
assert_eq!(handler.start, 0);
assert_eq!(handler.end, 10);
assert_eq!(handler.target, 20);
assert_eq!(handler.depth, 2);
assert!(!handler.push_lasti);
// Find handler at offset 20 (in range [15, 25))
let handler = find_exception_handler(&encoded, 20);
assert!(handler.is_some());
let handler = handler.unwrap();
assert_eq!(handler.start, 15);
assert_eq!(handler.end, 25);
assert_eq!(handler.target, 30);
assert_eq!(handler.depth, 1);
assert!(handler.push_lasti);
// No handler at offset 12 (not in any range)
let handler = find_exception_handler(&encoded, 12);
assert!(handler.is_none());
// No handler at offset 30 (past all ranges)
let handler = find_exception_handler(&encoded, 30);
assert!(handler.is_none());
}
#[test]
fn test_exception_table_empty() {
let entries: Vec<ExceptionTableEntry> = vec![];
let encoded = encode_exception_table(&entries);
assert!(encoded.is_empty());
assert!(find_exception_handler(&encoded, 0).is_none());
}
#[test]
fn test_exception_table_single_entry() {
let entries = vec![ExceptionTableEntry::new(5, 15, 100, 3, true)];
let encoded = encode_exception_table(&entries);
// Inside range
let handler = find_exception_handler(&encoded, 10);
assert!(handler.is_some());
let handler = handler.unwrap();
assert_eq!(handler.target, 100);
assert_eq!(handler.depth, 3);
assert!(handler.push_lasti);
// At start boundary (inclusive)
assert!(find_exception_handler(&encoded, 5).is_some());
// At end boundary (exclusive)
assert!(find_exception_handler(&encoded, 15).is_none());
}
}