Align codegen passes and opcode metadata with CPython (#7987)

* Share marshal ref table between code object and its internals

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

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

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

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

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

* Apply rustfmt to marshal helpers

* Marshal PySlice from format version 4 instead of 5

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

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

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

This reverts commit 1fc426d0fb5fcdb50d35cad13bbb43e8f6ce1c7f.

* Set marshal FORMAT_VERSION to 5 to match CPython 3.14.5

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

* Thread marshal recursion depth through nested code objects

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

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

* align compiler to CPython

* Align codegen with CPython compile.c

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

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

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

* Align codegen IR with CPython CFG structures

* Match CPython CFG annotation offset arithmetic

* Propagate CPython CFG label translation errors

* Align CPython exception target labeling flow

* Propagate CPython CFG traversal stack allocation errors

* Match CPython optimize_load_fast allocation flow

* Propagate CPython basicblock allocation errors

* Propagate CPython redundant NOP cleanup errors

* Propagate CPython unused const cleanup errors

* Propagate CPython const folding errors

* Propagate CPython swaptimize allocation errors

* Match CPython list-to-tuple fold allocation

* Skip const folding on CPython allocation failures

* Skip subscript folding on CPython allocation failures

* Propagate CPython assembler allocation errors

* Propagate CPython localsplus allocation errors

* Propagate CPython localsplus setup allocation errors

* Propagate CPython jump label map allocation errors

* Propagate CPython instruction sequence allocation errors

* Propagate CPython instruction label allocation errors

* Guard CPython codegen block allocation

* Guard CPython label shadow allocation

* Propagate CPython label shadow allocation errors

* Align CPython c-array allocation updates

* Align CPython ref stack growth

* Match CPython CFG builder debug check

* Avoid Rust-only CFG append clone

* Reuse CPython cleared block slots

* Use CPython block append in copy_basicblock

* Match CPython cfg builder creation order

* Clear label map after CPython apply pass

* Match CPython cfg builder allocation check

* Propagate CPython c-array size errors

* Drop Rust-only label uniqueness check

* Drop Rust-only label shadow debug checks

* Propagate CFG block index overflow

* Propagate CFG label oparg overflow

* Model CPython basicblock instruction storage

* Drop Rust-only recorded CFG precheck

* Model CPython instruction sequence storage

* Propagate instruction sequence offset overflow

* Model CPython instruction sequence labels

* Match CPython jump offset arithmetic

* Match CPython exception table arithmetic

* Match CPython label index arithmetic

* Match CPython instruction offset casts

* Match CPython jump offset indexing

* Match CPython oparg locals casts

* Match CPython localsplus offset arithmetic

* Match CPython cell prefix indexing

* Match CPython C array growth arithmetic

* Match CPython label map allocation arithmetic

* Match CPython label map size tracking

* Use CPython label map size in sequence passes

* Assert CPython label map clearing invariants

* Match CPython label oparg assignment

* Match CPython compiler direct arithmetic

* Match CPython load fast local casts

* Match CPython load fast depth assert

* Match CPython resume depth flagging

* Match CPython stack depth arithmetic

* Drop Rust-only stack overflow error

* Return CPython stack depth directly

* Match CPython C array growth errors

* Match CPython instruction insert asserts

* Match CPython unreachable pseudo jump

* Match CPython CFG size guard

* Match CPython superinstruction assert

* Match CPython redundant jump assert

* Match CPython stackdepth errors

* Match CPython jump offset flow

* Match CPython assembler buffer defaults

* Match CPython bytecode emit growth

* Match CPython assembler entry growth

* Match CPython assembler growth overflow check

* Match CPython remove_unreachable structure

* Match CPython static swap flow

* Inline CPython code unit preprocessing

* Match CPython C array growth checks

* Match CPython label map size guard

* Match CPython load-fast flow

* Simplify CPython CFG condition flow

* Align exception fallthrough propagation

* Match CPython pseudo target table

* Match CPython annotations CFG assert

* Match CPython inverted op assert

* Match CPython many-locals guard

* Reject deopt opcodes in CFG stack effects

* Match CPython invalid stack effect error

* Test CPython deopt stack effect guard

* Match CPython load-fast extended-arg assert

* Match CPython instruction allocation asserts

* Match CPython basicblock last-instr asserts

* Match CPython opcode range asserts

* Assert CPython fallthrough line propagation invariant

* Assert CPython CFG target offset sign

* Assert CPython exception fallthrough invariant

* Assert CPython exception stack bounds

* Assert CPython traversal stack allocation

* Match CPython label-map allocation in shadow

* Mirror CPython label-map sentinel fill

* Match CPython CFG builder allocation asserts

* Match CPython exception stack structure

* Match CPython ref stack structure

* Match CPython CFG traversal stack structure

* Mirror CPython CFG traversal stack pointer

* Use CPython fixed exception handler stack

* Mirror CPython ref stack capacity field

* Match CPython swap optimizer scratch stack

* Align static swap helpers with CPython blocks

* Align swaptimize signature with CPython

* Match CPython redundant pair pass result

* Match CPython inline pass result

* Match CPython redundant NOP pass results

* Fix bytecode metadata after upstream rebase

* Match CPython opcode stack metadata

* Add CPython identifiers to cspell dictionary

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

* Fix CI failures from bytecode-parity work

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

* Resync generated opcode metadata

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

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

* Drop is_block_push has_arg invariant

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

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

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

Forward has_arg/has_const from Instruction and PseudoInstruction to
their opcode, so AnyInstruction can use either_real_pseudo! like the
other has_* accessors instead of an open-coded match.
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-05-28 09:19:11 +09:00
1 parent ca412fce5d
commit 1a959cf7f3
16 files changed
+8488 -15768

No files matched your search

+13
View File
@@ -37,6 +37,7 @@ CFWS
CLASSDEREF
classdict
cmpop
CNOTAB
codedepth
CODEUNIT
CONIN
@@ -98,12 +99,14 @@ HASUNION
heaptype
hexdigit
HIGHRES
ialloc
IFUNC
IMMUTABLETYPE
INCREF
inlinedepth
inplace
inpos
ioffset
isbytecode
ishidden
ismine
@@ -111,6 +114,7 @@ ISPOINTER
isoctal
iteminfo
Itertool
iused
keeped
kwnames
kwonlyarg
@@ -122,6 +126,7 @@ linearise
lineful
lineiterator
linetable
LNOTAB
loadfast
localsplus
localspluskinds
@@ -137,17 +142,22 @@ multibytecodec
nameobj
nameop
nargsf
nblocks
ncells
ncellsused
ncellvars
nconsts
newargs
newfree
NEWLOCALS
newsemlockobject
nextop
nfrees
nkwargs
nkwelts
nlocalsplus
nointerrupt
noffsets
Nondescriptor
noninteger
nops
@@ -155,6 +165,7 @@ noraise
nseen
NSIGNALS
numer
nvars
opname
opnames
orelse
@@ -220,6 +231,7 @@ subparams
subscr
sval
swappedbytes
swaptimize
sysdict
tbstderr
templatelib
@@ -240,6 +252,7 @@ uncollectable
Unhandle
unparse
unparser
untargeted
untracking
VARKEYWORDS
varkwarg
-2
View File
@@ -2580,7 +2580,6 @@ class TestExpressionStackSize(unittest.TestCase):
def test_dict(self):
self.check_stack_size("{" + "x:x, " * self.N + "x:x}")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 102 not less than or equal to 6
def test_func_args(self):
self.check_stack_size("f(" + "x, " * self.N + ")")
@@ -2588,7 +2587,6 @@ class TestExpressionStackSize(unittest.TestCase):
kwargs = (f'a{i}=x' for i in range(self.N))
self.check_stack_size("f(" + ", ".join(kwargs) + ")")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 102 not less than or equal to 6
def test_meth_args(self):
self.check_stack_size("o.m(" + "x, " * self.N + ")")
-2
View File
@@ -1211,14 +1211,12 @@ class DisTests(DisTestBase):
def test_disassemble_fstring(self):
self.do_disassembly_test(_fstring, dis_fstring)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_disassemble_with(self):
self.do_disassembly_test(_with, dis_with)
def test_disassemble_asyncwith(self):
self.do_disassembly_test(_asyncwith, dis_asyncwith)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_disassemble_try_finally(self):
self.do_disassembly_test(_tryfinally, dis_tryfinally)
self.do_disassembly_test(_tryfinallyconst, dis_tryfinallyconst)
-1
View File
@@ -1624,7 +1624,6 @@ class TestBranchAndJumpEvents(CheckEvents):
('branch left', 'func', 44, 50),
('branch right', 'func', 28, 70)])
@unittest.expectedFailure # TODO: RUSTPYTHON; - bytecode layout differs from CPython
def test_except_star(self):
class Foo:
+1277 -4001
View File
File diff suppressed because it is too large. Load diff
+7 -3
View File
@@ -38,16 +38,20 @@ impl fmt::Display for CodegenError {
#[derive(Debug)]
#[non_exhaustive]
pub enum InternalError {
StackOverflow,
StackUnderflow,
InconsistentStackDepth,
InvalidStackEffect,
MalformedControlFlowGraph,
MissingSymbol(String),
}
impl Display for InternalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::StackOverflow => write!(f, "stack overflow"),
Self::StackUnderflow => write!(f, "stack underflow"),
Self::StackUnderflow => write!(f, "Invalid CFG, stack underflow"),
Self::InconsistentStackDepth => write!(f, "Invalid CFG, inconsistent stackdepth"),
Self::InvalidStackEffect => write!(f, "Invalid stack effect"),
Self::MalformedControlFlowGraph => write!(f, "malformed control flow graph."),
Self::MissingSymbol(s) => write!(
f,
"The symbol '{s}' must be present in the symbol table, even when it is undefined in python."
+6768 -11704
View File
File diff suppressed because it is too large. Load diff
+18
View File
@@ -60,6 +60,9 @@ pub struct SymbolTable {
/// Whether this scope contains yield/yield from (is a generator function)
pub is_generator: bool,
/// Whether this scope contains await or async comprehension machinery.
pub is_coroutine: bool,
/// Whether this comprehension scope should be inlined (PEP 709)
/// True for list/set/dict comprehensions in non-generator expressions
pub comp_inlined: bool,
@@ -102,6 +105,7 @@ impl SymbolTable {
needs_classdict: false,
can_see_class_scope: false,
is_generator: false,
is_coroutine: false,
comp_inlined: false,
annotation_block: None,
skip_enclosing_function_scope: false,
@@ -1532,6 +1536,9 @@ impl SymbolTableBuilder {
},
has_type_params, // skip_defaults: already scanned above
)?;
if *is_async {
self.tables.last_mut().unwrap().is_coroutine = true;
}
self.scan_statements(body)?;
self.leave_scope();
if type_params.is_some() {
@@ -2026,6 +2033,7 @@ impl SymbolTableBuilder {
range: _,
}) => {
self.scan_expression(value, context)?;
self.tables.last_mut().unwrap().is_coroutine = true;
}
Expr::Yield(ExprYield {
value,
@@ -2353,6 +2361,9 @@ impl SymbolTableBuilder {
);
// Generator expressions need the is_generator flag
self.tables.last_mut().unwrap().is_generator = is_generator;
if generators.iter().any(|generator| generator.is_async) {
self.tables.last_mut().unwrap().is_coroutine = true;
}
// PEP 709: Mark non-generator comprehensions for inlining.
// CPython's symtable marks all non-generator comprehensions for
@@ -2389,7 +2400,14 @@ impl SymbolTableBuilder {
}
self.scan_expression(elt1, ExpressionContext::Load)?;
// CPython symtable_handle_comprehension(): non-generator async
// comprehensions propagate ste_coroutine to the enclosing scope after
// the comprehension block is exited.
let propagate_coroutine = self.tables.last().unwrap().is_coroutine && !is_generator;
self.leave_scope();
if propagate_coroutine {
self.tables.last_mut().unwrap().is_coroutine = true;
}
Ok(())
}
+6 -1
View File
@@ -415,6 +415,10 @@ impl<T> IndexMut<oparg::VarNum> for [T] {
}
/// Per-slot kind flags for localsplus (co_localspluskinds).
pub const CO_FAST_ARG_POS: u8 = 0x02;
pub const CO_FAST_ARG_KW: u8 = 0x04;
pub const CO_FAST_ARG_VAR: u8 = 0x08;
pub const CO_FAST_ARG: u8 = CO_FAST_ARG_POS | CO_FAST_ARG_KW | CO_FAST_ARG_VAR;
pub const CO_FAST_HIDDEN: u8 = 0x10;
pub const CO_FAST_LOCAL: u8 = 0x20;
pub const CO_FAST_CELL: u8 = 0x40;
@@ -443,7 +447,8 @@ pub struct CodeObject<C: Constant = ConstantData> {
pub varnames: Box<[C::Name]>,
pub cellvars: Box<[C::Name]>,
pub freevars: Box<[C::Name]>,
/// Per-slot kind flags: CO_FAST_LOCAL, CO_FAST_CELL, CO_FAST_FREE, CO_FAST_HIDDEN.
/// Per-slot kind flags: CO_FAST_ARG_*, CO_FAST_LOCAL, CO_FAST_CELL,
/// CO_FAST_FREE, CO_FAST_HIDDEN.
/// Length = nlocalsplus (nlocals + ncells + nfrees).
pub localspluskinds: Box<[u8]>,
/// Line number table (CPython 3.11+ format)
@@ -63,11 +63,6 @@ macro_rules! define_opcodes {
$(Self::$op_name => $op_id,)*
}
}
/// Stack effect of [`Self::stack_effect_info`].
#[must_use]
$opcode_vis fn stack_effect(&self, oparg: u32) -> i32 {
self.stack_effect_info(oparg).effect()
}
}
impl From<$opcode_name> for $instr_name {
@@ -156,6 +151,46 @@ macro_rules! define_opcodes {
self.as_opcode().is_scope_exit()
}
#[must_use]
$instr_vis const fn is_terminator(&self) -> bool {
self.as_opcode().is_terminator()
}
#[must_use]
$instr_vis const fn is_no_fallthrough(&self) -> bool {
self.as_opcode().is_no_fallthrough()
}
#[must_use]
$instr_vis const fn has_target(&self) -> bool {
self.as_opcode().has_target()
}
#[must_use]
$instr_vis const fn has_jump(&self) -> bool {
self.as_opcode().has_jump()
}
#[must_use]
$instr_vis const fn has_arg(&self) -> bool {
self.as_opcode().has_arg()
}
#[must_use]
$instr_vis const fn has_const(&self) -> bool {
self.as_opcode().has_const()
}
#[must_use]
$instr_vis const fn has_eval_break(&self) -> bool {
self.as_opcode().has_eval_break()
}
#[must_use]
$instr_vis const fn is_assembler(&self) -> bool {
self.as_opcode().is_assembler()
}
#[must_use]
$instr_vis const fn cache_entries(&self) -> usize{
self.as_opcode().cache_entries()
@@ -673,16 +708,49 @@ impl Opcode {
)
}
/// CPython's `IS_ASSEMBLER_OPCODE`.
#[must_use]
pub const fn is_assembler(&self) -> bool {
matches!(
self,
Self::JumpForward | Self::JumpBackward | Self::JumpBackwardNoInterrupt
)
}
#[must_use]
pub const fn is_scope_exit(&self) -> bool {
matches!(self, Self::ReturnValue | Self::RaiseVarargs | Self::Reraise)
}
/// CPython's `IS_TERMINATOR_OPCODE`.
#[must_use]
pub const fn is_terminator(&self) -> bool {
self.has_jump() || self.is_scope_exit()
}
/// CPython's `IS_SCOPE_EXIT_OPCODE || IS_UNCONDITIONAL_JUMP_OPCODE`.
#[must_use]
pub const fn is_no_fallthrough(&self) -> bool {
self.is_scope_exit() || self.is_unconditional_jump()
}
/// CPython's `HAS_TARGET`.
#[must_use]
pub const fn has_target(&self) -> bool {
self.has_jump() || self.is_block_push()
}
#[must_use]
pub const fn is_block_push(&self) -> bool {
false
}
/// Stack effect of [`Self::stack_effect_info`].
#[must_use]
pub fn stack_effect(&self, oparg: u32) -> i32 {
self.stack_effect_info(oparg).effect()
}
/// Stack effect when the instruction takes its branch (jump=true).
///
/// CPython equivalent: `stack_effect(opcode, oparg, jump=True)`.
@@ -714,6 +782,39 @@ impl PseudoOpcode {
matches!(self, Self::Jump | Self::JumpNoInterrupt)
}
#[must_use]
pub const fn is_assembler(&self) -> bool {
false
}
/// CPython's `IS_TERMINATOR_OPCODE`.
#[must_use]
pub const fn is_terminator(&self) -> bool {
self.has_jump()
}
/// CPython's `IS_SCOPE_EXIT_OPCODE || IS_UNCONDITIONAL_JUMP_OPCODE`.
#[must_use]
pub const fn is_no_fallthrough(&self) -> bool {
self.is_unconditional_jump()
}
/// CPython's `HAS_TARGET`.
#[must_use]
pub const fn has_target(&self) -> bool {
self.has_jump() || self.is_block_push()
}
/// flowgraph.c get_stack_effects block-push non-jump case.
#[must_use]
pub fn stack_effect(&self, oparg: u32) -> i32 {
if self.is_block_push() {
0
} else {
self.stack_effect_info(oparg).effect()
}
}
/// Handler entry effect for SETUP_* pseudo ops.
///
/// Fallthrough effect is 0 (NOPs), but when the branch is taken the
@@ -778,6 +879,46 @@ impl AnyInstruction {
pub const fn is_scope_exit(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub const fn is_terminator(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub const fn is_no_fallthrough(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub const fn has_target(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub const fn has_jump(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub const fn has_arg(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub const fn has_const(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub const fn has_eval_break(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub const fn is_assembler(&self) -> bool
);
either_real_pseudo!(
#[must_use]
pub fn stack_effect(&self, oparg: u32) -> i32
@@ -1179,3 +1320,115 @@ impl<T: OpArgType> fmt::Debug for Arg<T> {
// breaks the VM:/
const _: () = assert!(core::mem::size_of::<Instruction>() == 1);
const _: () = assert!(core::mem::size_of::<PseudoInstruction>() == 2);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eval_break_flags_match_cpython_jump_metadata() {
assert!(Opcode::JumpBackward.has_eval_break());
assert!(!Opcode::JumpBackwardNoInterrupt.has_eval_break());
assert!(!Opcode::JumpForward.has_eval_break());
assert!(PseudoOpcode::Jump.has_eval_break());
assert!(!PseudoOpcode::JumpIfFalse.has_eval_break());
assert!(!PseudoOpcode::JumpIfTrue.has_eval_break());
assert!(!PseudoOpcode::JumpNoInterrupt.has_eval_break());
assert!(AnyInstruction::from(PseudoOpcode::Jump).has_eval_break());
}
#[test]
fn terminator_flags_match_cpython_opcode_utils() {
assert!(Opcode::JumpForward.is_terminator());
assert!(Opcode::PopJumpIfFalse.is_terminator());
assert!(Opcode::ForIter.is_terminator());
assert!(Opcode::ReturnValue.is_terminator());
assert!(!Opcode::Nop.is_terminator());
assert!(PseudoOpcode::JumpIfTrue.is_terminator());
assert!(PseudoOpcode::JumpNoInterrupt.is_terminator());
assert!(!PseudoOpcode::SetupFinally.is_terminator());
assert!(!PseudoOpcode::SetupWith.is_terminator());
assert!(!PseudoOpcode::SetupCleanup.is_terminator());
assert!(!PseudoOpcode::PopBlock.is_terminator());
assert!(AnyInstruction::from(PseudoOpcode::JumpIfFalse).is_terminator());
}
#[test]
fn assembler_flags_match_cpython_opcode_utils() {
assert!(Opcode::JumpForward.is_assembler());
assert!(Opcode::JumpBackward.is_assembler());
assert!(Opcode::JumpBackwardNoInterrupt.is_assembler());
assert!(!Opcode::PopJumpIfFalse.is_assembler());
assert!(!Opcode::Nop.is_assembler());
assert!(!PseudoOpcode::Jump.is_assembler());
assert!(!PseudoOpcode::JumpNoInterrupt.is_assembler());
assert!(!AnyInstruction::from(PseudoOpcode::Jump).is_assembler());
}
#[test]
fn target_flags_match_cpython_opcode_utils() {
assert!(Opcode::JumpForward.has_target());
assert!(Opcode::ForIter.has_target());
assert!(!Opcode::ReturnValue.has_target());
assert!(!Opcode::Nop.has_target());
assert!(PseudoOpcode::Jump.has_target());
assert!(PseudoOpcode::SetupFinally.has_target());
assert!(PseudoOpcode::SetupWith.has_target());
assert!(PseudoOpcode::SetupCleanup.has_target());
assert!(!PseudoOpcode::PopBlock.has_target());
assert!(AnyInstruction::from(PseudoOpcode::SetupFinally).has_target());
}
#[test]
fn arg_flags_match_cpython_opcode_metadata() {
assert!(Opcode::LoadConst.has_arg());
assert!(Opcode::YieldValue.has_arg());
assert!(!Opcode::Nop.has_arg());
assert!(!Opcode::ReturnValue.has_arg());
assert!(PseudoOpcode::Jump.has_arg());
assert!(PseudoOpcode::JumpIfFalse.has_arg());
assert!(PseudoOpcode::JumpIfTrue.has_arg());
assert!(PseudoOpcode::JumpNoInterrupt.has_arg());
assert!(PseudoOpcode::LoadClosure.has_arg());
assert!(PseudoOpcode::StoreFastMaybeNull.has_arg());
assert!(!PseudoOpcode::AnnotationsPlaceholder.has_arg());
assert!(!PseudoOpcode::PopBlock.has_arg());
}
#[test]
fn const_flags_match_cpython_opcode_metadata() {
assert!(Opcode::LoadConst.has_const());
assert!(Opcode::LoadConstImmortal.has_const());
assert!(Opcode::LoadConstMortal.has_const());
assert!(!Opcode::LoadSmallInt.has_const());
assert!(!Opcode::Nop.has_const());
assert!(!PseudoOpcode::LoadClosure.has_const());
assert!(!AnyInstruction::from(PseudoOpcode::Jump).has_const());
}
#[test]
fn no_fallthrough_flags_match_cpython_basicblock_nofallthrough() {
assert!(Opcode::JumpForward.is_no_fallthrough());
assert!(Opcode::ReturnValue.is_no_fallthrough());
assert!(!Opcode::PopJumpIfFalse.is_no_fallthrough());
assert!(!Opcode::ForIter.is_no_fallthrough());
assert!(!Opcode::Nop.is_no_fallthrough());
assert!(PseudoOpcode::Jump.is_no_fallthrough());
assert!(PseudoOpcode::JumpNoInterrupt.is_no_fallthrough());
assert!(!PseudoOpcode::JumpIfFalse.is_no_fallthrough());
assert!(!PseudoOpcode::SetupFinally.is_no_fallthrough());
assert!(!PseudoOpcode::SetupWith.is_no_fallthrough());
assert!(AnyInstruction::from(PseudoOpcode::Jump).is_no_fallthrough());
}
}
+40 -16
View File
@@ -282,6 +282,36 @@ impl super::Opcode {
)
}
/// Does this opcode have 'HAS_EVAL_BREAK_FLAG' set.
#[must_use]
pub const fn has_eval_break(self) -> bool {
matches!(
self,
Self::CallFunctionEx
| Self::Call
| Self::JumpBackward
| Self::Resume
| Self::CallBuiltinClass
| Self::CallBuiltinFast
| Self::CallBuiltinFastWithKeywords
| Self::CallBuiltinO
| Self::CallKwNonPy
| Self::CallMethodDescriptorFast
| Self::CallMethodDescriptorFastWithKeywords
| Self::CallMethodDescriptorNoargs
| Self::CallMethodDescriptorO
| Self::CallNonPyGeneral
| Self::CallStr1
| Self::CallTuple1
| Self::JumpBackwardJit
| Self::JumpBackwardNoJit
| Self::InstrumentedResume
| Self::InstrumentedCall
| Self::InstrumentedCallFunctionEx
| Self::InstrumentedJumpBackward
)
}
/// Does this opcode have 'HAS_FREE_FLAG' set.
#[must_use]
pub const fn has_free(self) -> bool {
@@ -441,10 +471,7 @@ impl super::Opcode {
Self::UnaryInvert => (1, 1),
Self::UnaryNegative => (1, 1),
Self::UnaryNot => (1, 1),
Self::WithExceptStart => (
7, // TODO: Differs from CPython `6`
6, // TODO: Differs from CPython `5`
),
Self::WithExceptStart => (6, 5),
Self::BinaryOp => (1, 2),
Self::BuildInterpolation => (1, 2 + (oparg & 1)),
Self::BuildList => (1, oparg),
@@ -730,6 +757,12 @@ impl super::PseudoOpcode {
false
}
/// Does this opcode have 'HAS_EVAL_BREAK_FLAG' set.
#[must_use]
pub const fn has_eval_break(self) -> bool {
matches!(self, Self::Jump)
}
/// Does this opcode have 'HAS_FREE_FLAG' set.
#[must_use]
pub const fn has_free(self) -> bool {
@@ -772,18 +805,9 @@ impl super::PseudoOpcode {
Self::JumpNoInterrupt => (0, 0),
Self::LoadClosure => (1, 0),
Self::PopBlock => (0, 0),
Self::SetupCleanup => (
0, // TODO: Differs from CPython `2`
0,
),
Self::SetupFinally => (
0, // TODO: Differs from CPython `1`
0,
),
Self::SetupWith => (
0, // TODO: Differs from CPython `1`
0,
),
Self::SetupCleanup => (2, 0),
Self::SetupFinally => (1, 0),
Self::SetupWith => (1, 0),
Self::StoreFastMaybeNull => (0, 1),
};
+57 -9
View File
@@ -575,6 +575,13 @@ pub trait MarshalBag: Copy {
}
fn constant_bag(self) -> Self::ConstantBag;
fn constant_ref_from_value(
&self,
_value: &Self::Value,
) -> Option<<Self::ConstantBag as ConstantBag>::Constant> {
None
}
}
impl<Bag: ConstantBag> MarshalBag for Bag {
@@ -669,6 +676,13 @@ impl<Bag: ConstantBag> MarshalBag for Bag {
fn constant_bag(self) -> Self::ConstantBag {
self
}
fn constant_ref_from_value(
&self,
value: &Self::Value,
) -> Option<<Self::ConstantBag as ConstantBag>::Constant> {
Some(value.clone())
}
}
pub const MAX_MARSHAL_STACK_DEPTH: usize = 2000;
@@ -727,16 +741,18 @@ fn deserialize_value_after_header<R: Read, Bag: MarshalBag>(
};
let typ = Type::try_from(type_code)?;
// Code-objects keep their own inner ref table because Bag::Value (the
// outer marshal value) and the constant-bag's Constant type are not
// in general the same. When the outer header carried FLAG_REF, the
// code object occupies slot 0 of the single global ref space, so we
// mirror that by reserving slot 0 of the inner table.
// CPython's r_object() uses one global ref table: TYPE_CODE reserves its
// slot before reading code fields, and those fields may use later TYPE_REF
// indexes. Keep the same indexes even when Bag::Value and Constant differ.
let value = if matches!(typ, Type::Code) {
let mut inner_refs: Vec<Option<<Bag::ConstantBag as ConstantBag>::Constant>> = Vec::new();
if flag {
inner_refs.push(None);
}
let mut inner_refs: Vec<Option<<Bag::ConstantBag as ConstantBag>::Constant>> = refs
.iter()
.map(|value| {
value
.as_ref()
.and_then(|value| bag.constant_ref_from_value(value))
})
.collect();
let code = deserialize_code_inner(rdr, bag.constant_bag(), depth - 1, &mut inner_refs)?;
bag.make_code(code)
} else {
@@ -1501,6 +1517,15 @@ mod tests {
}
}
fn decode_tuple(hex: &str) -> Vec<ConstantData> {
let bytes = hex_to_bytes(hex);
let value = deserialize_value(&mut &bytes[..], BasicBag).expect("decode failed");
match value {
ConstantData::Tuple { elements } => elements,
other => panic!("expected Tuple, got {other:?}"),
}
}
/// CPython 3.14 marshal output for: `compile("x = 1", "<t>", "exec")`.
/// Exercises FLAG_REF on the code object and TYPE_REF for qualname
/// pointing back at the obj_name slot.
@@ -1562,4 +1587,27 @@ mod tests {
));
assert!(matches!(consts[2], ConstantData::None));
}
/// CPython 3.14 marshal output for:
/// `(compile("x = 1", "<t>", "exec"),)`.
/// The outer tuple occupies ref slot 0 and the code object occupies
/// slot 1, so code-object fields must preserve that global ref offset.
#[test]
fn cpython_314_code_inside_tuple_preserves_ref_indexes() {
let hex = "a901630000000000000000000000000100000000000000f30a00000080005e017400\
520123002902e9010000004e2901da0178a900f300000000da033c743eda083c6d6f\
64756c653e720700000001000000730a000000f003010101d8040582017205000000";
let tuple = decode_tuple(hex);
assert_eq!(tuple.len(), 1);
let code = match &tuple[0] {
ConstantData::Code { code } => code,
other => panic!("expected nested Code, got {other:?}"),
};
assert_eq!(code.obj_name.as_str(), "<module>");
assert_eq!(code.qualname.as_str(), "<module>");
assert_eq!(code.source_path.as_str(), "<t>");
assert_eq!(code.names.len(), 1);
assert_eq!(code.names[0].as_str(), "x");
assert_eq!(code.constants.len(), 2);
}
}
@@ -1,5 +1,6 @@
---
source: crates/stdlib/src/_opcode.rs
assertion_line: 300
expression: "dis(r#\"\nasync def test():\n for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')):\n with self.subTest(type=type(stop_exc)):\n try:\n async with egg():\n raise stop_exc\n except Exception as ex:\n self.assertIs(ex, stop_exc)\n else:\n self.fail(f'{stop_exc} was suppressed')\n\"#)"
---
0 RESUME 0
@@ -146,29 +147,29 @@ Disassembly of <code object test at 0xdeadbeef file "<?>", line 1>:
L34: PUSH_EXC_INFO
WITH_EXCEPT_START
TO_BOOL
POP_JUMP_IF_TRUE 2 (to L37)
L35: NOT_TAKEN
L36: RERAISE 2
L37: POP_TOP
L38: POP_EXCEPT
POP_JUMP_IF_TRUE 2 (to L35)
NOT_TAKEN
RERAISE 2
L35: POP_TOP
L36: POP_EXCEPT
POP_TOP
POP_TOP
POP_TOP
JUMP_BACKWARD 205 (to L2)
-- L39: COPY 3
-- L37: COPY 3
POP_EXCEPT
RERAISE 1
L40: CALL_INTRINSIC_1 3 (INTRINSIC_STOPITERATION_ERROR)
L38: CALL_INTRINSIC_1 3 (INTRINSIC_STOPITERATION_ERROR)
RERAISE 1
ExceptionTable:
L1 to L3 -> L40 [0] lasti
L1 to L3 -> L38 [0] lasti
L3 to L4 -> L34 [3] lasti
L5 to L7 -> L27 [3]
L7 to L8 -> L12 [7]
L8 to L10 -> L27 [3]
L10 to L11 -> L14 [5] lasti
L11 to L12 -> L40 [0] lasti
L11 to L12 -> L38 [0] lasti
L12 to L13 -> L27 [3]
L14 to L16 -> L24 [7] lasti
L16 to L17 -> L18 [10]
@@ -181,7 +182,6 @@ ExceptionTable:
L29 to L30 -> L34 [3] lasti
L30 to L32 -> L32 [4] lasti
L32 to L33 -> L34 [3] lasti
L33 to L34 -> L40 [0] lasti
L34 to L35 -> L39 [5] lasti
L36 to L38 -> L39 [5] lasti
L38 to L40 -> L40 [0] lasti
L33 to L34 -> L38 [0] lasti
L34 to L36 -> L37 [5] lasti
L36 to L38 -> L38 [0] lasti
+25
View File
@@ -177,6 +177,7 @@ mod file_run {
#[cfg(test)]
mod tests {
use crate::object::AsObject;
use rustpython_vm::Interpreter;
fn interpreter() -> Interpreter {
@@ -205,4 +206,28 @@ mod tests {
assert_eq!(value, 5);
})
}
#[test]
fn block_expr_return_function_def() {
interpreter().enter(|vm| {
let scope = vm.new_scope_with_builtins();
let value =
vm.unwrap_pyresult(vm.run_block_expr(scope.clone(), "def f():\n return 7"));
vm.unwrap_pyresult(scope.globals.set_item("returned", value, vm));
let value = vm.unwrap_pyresult(vm.run_block_expr(scope, "returned is f"));
assert!(value.is(&vm.ctx.true_value));
})
}
#[test]
fn block_expr_return_class_def() {
interpreter().enter(|vm| {
let scope = vm.new_scope_with_builtins();
let value =
vm.unwrap_pyresult(vm.run_block_expr(scope.clone(), "class C:\n value = 11"));
vm.unwrap_pyresult(scope.globals.set_item("returned", value, vm));
let value = vm.unwrap_pyresult(vm.run_block_expr(scope, "returned is C"));
assert!(value.is(&vm.ctx.true_value));
})
}
}
-11
View File
@@ -1,11 +0,0 @@
[WithExceptStart]
stack_effect = { pushed = "7", popped = "6" }
[SetupCleanup]
stack_effect = { pushed = "0" }
[SetupFinally]
stack_effect = { pushed = "0" }
[SetupWith]
stack_effect = { pushed = "0" }
@@ -66,6 +66,12 @@ class OpcodeGen:
def fn_has_local(self) -> str:
return self.gen_fn_has_attr("has_local", "uses_locals", "HAS_LOCAL_FLAG")
@property
def fn_has_eval_break(self) -> str:
return self.gen_fn_has_attr(
"has_eval_break", "eval_breaker", "HAS_EVAL_BREAK_FLAG"
)
@property
def fn_is_instrumented(self) -> str:
arms = "|".join(