set_f_lineno, set_f_lasti, PyAtomic refactor

- Implement set_f_lineno with stack analysis and deferred unwinding
- Add Frame::set_lasti() for trace callback line jumps
- Implement co_branches() on code objects
- Clear _cache_format in opcode.py (no inline caches)
- Fix getattro slot inheritance: preserve native slot from inherit_slots
- Fix BRANCH_RIGHT src_offset in InstrumentedPopJumpIf*
- Move lasti increment before line event for correct f_lineno
- Skip RESUME instruction from generating line events
- Defer stack pops via pending_stack_pops/pending_unwind_from_stack
  to avoid deadlock with state mutex
- Fix ForIter exhaust target in mark_stacks to skip END_FOR
- Prevent exception handler paths from overwriting normal-flow stacks
- Replace #[cfg(feature = "threading")] duplication with PyAtomic<T>
  from rustpython_common::atomic (Radium-based unified API)
- Remove expectedFailure from 31 now-passing jump tests
This commit is contained in:
Jeong, YunWon
2026-02-24 08:59:39 +09:00
parent fa1e75a809
commit 20a93c54c4
17 changed files with 945 additions and 451 deletions

View File

@@ -33,10 +33,10 @@ CLASSDEREF
classdict
cmpop
codedepth
constevaluator
CODEUNIT
CONIN
CONOUT
constevaluator
CONVFUNC
convparam
copyslot
@@ -62,6 +62,7 @@ fieldlist
fileutils
finalbody
finalizers
firsttraceable
flowgraph
formatfloat
freevar

69
Lib/opcode.py vendored
View File

@@ -46,75 +46,6 @@ _nb_ops = _opcode.get_nb_ops()
hascompare = [opmap["COMPARE_OP"]]
_cache_format = {
"LOAD_GLOBAL": {
"counter": 1,
"index": 1,
"module_keys_version": 1,
"builtin_keys_version": 1,
},
"BINARY_OP": {
"counter": 1,
"descr": 4,
},
"UNPACK_SEQUENCE": {
"counter": 1,
},
"COMPARE_OP": {
"counter": 1,
},
"CONTAINS_OP": {
"counter": 1,
},
"FOR_ITER": {
"counter": 1,
},
"LOAD_SUPER_ATTR": {
"counter": 1,
},
"LOAD_ATTR": {
"counter": 1,
"version": 2,
"keys_version": 2,
"descr": 4,
},
"STORE_ATTR": {
"counter": 1,
"version": 2,
"index": 1,
},
"CALL": {
"counter": 1,
"func_version": 2,
},
"CALL_KW": {
"counter": 1,
"func_version": 2,
},
"STORE_SUBSCR": {
"counter": 1,
},
"SEND": {
"counter": 1,
},
"JUMP_BACKWARD": {
"counter": 1,
},
"TO_BOOL": {
"counter": 1,
"version": 2,
},
"POP_JUMP_IF_TRUE": {
"counter": 1,
},
"POP_JUMP_IF_FALSE": {
"counter": 1,
},
"POP_JUMP_IF_NONE": {
"counter": 1,
},
"POP_JUMP_IF_NOT_NONE": {
"counter": 1,
},
}
_inline_cache_entries = {

5
Lib/test/dis_module.py vendored Normal file
View File

@@ -0,0 +1,5 @@
# A simple module for testing the dis module.
def f(): pass
def g(): pass

View File

@@ -1147,7 +1147,6 @@ class DisTests(DisTestBase):
self.assertIn("CALL_INTRINSIC_2 1 (INTRINSIC_PREP_RERAISE_STAR)",
self.get_disassembly("try: pass\nexcept* Exception: x"))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_big_linenos(self):
def func(count):
namespace = {}
@@ -2119,7 +2118,6 @@ class InstructionTests(InstructionTestCase):
positions=None)
self.assertEqual(instruction.arg, instruction.oparg)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_show_caches_with_label(self):
def f(x, y, z):
if x:
@@ -2238,6 +2236,7 @@ class InstructionTests(InstructionTestCase):
def get_instructions(self, code):
return dis._get_instructions_bytes(code)
@unittest.expectedFailure # TODO: RUSTPYTHON; no inline caches
def test_start_offset(self):
# When no extended args are present,
# start_offset should be equal to offset
@@ -2290,6 +2289,7 @@ class InstructionTests(InstructionTestCase):
self.assertEqual(14, instructions[6].offset)
self.assertEqual(8, instructions[6].start_offset)
@unittest.expectedFailure # TODO: RUSTPYTHON; no inline caches
def test_cache_offset_and_end_offset(self):
code = bytes([
opcode.opmap["LOAD_GLOBAL"], 0x01,
@@ -2422,7 +2422,6 @@ class TestFinderMethods(unittest.TestCase):
self.assertEqual(len(res), 1)
self.assertEqual(res[0], expected)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test__find_store_names(self):
cases = [
("x+y", ()),

View File

@@ -1744,7 +1744,6 @@ class TestBranchAndJumpEvents(CheckEvents):
('branch right', 'func', 6, 8),
('branch right', 'func', 2, 10)])
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: attribute 'f_lineno' of 'frame' objects is not writable
def test_callback_set_frame_lineno(self):
def func(s: str) -> int:
if s.startswith("t"):
@@ -1802,7 +1801,6 @@ class TestBranchConsistency(MonitoringTestBase, unittest.TestCase):
for recorder in recorders:
sys.monitoring.register_callback(tool, recorder.event_type, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'code' object has no attribute 'co_branches'
def test_simple(self):
def func():
@@ -1823,7 +1821,6 @@ class TestBranchConsistency(MonitoringTestBase, unittest.TestCase):
self.check_branches(whilefunc)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'code' object has no attribute 'co_branches'
def test_except_star(self):
class Foo:
@@ -1842,7 +1839,6 @@ class TestBranchConsistency(MonitoringTestBase, unittest.TestCase):
self.check_branches(func)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'code' object has no attribute 'co_branches'
def test4(self):
def foo(n=0):
@@ -1853,7 +1849,6 @@ class TestBranchConsistency(MonitoringTestBase, unittest.TestCase):
self.check_branches(foo)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'code' object has no attribute 'co_branches'
def test_async_for(self):
async def gen():
@@ -1942,7 +1937,7 @@ class TestLoadSuperAttr(CheckEvents):
]
return d["f"], expected
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: False != True
@unittest.expectedFailure # TODO: RUSTPYTHON; line number differences in multi-line super() calls
def test_method_call(self):
nonopt_func, nonopt_expected = self._super_method_call(optimized=False)
opt_func, opt_expected = self._super_method_call(optimized=True)
@@ -1994,7 +1989,7 @@ class TestLoadSuperAttr(CheckEvents):
]
return d["f"], expected
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: False != True
@unittest.expectedFailure # TODO: RUSTPYTHON; line number differences in multi-line super() calls
def test_method_call_error(self):
nonopt_func, nonopt_expected = self._super_method_call_error(optimized=False)
opt_func, opt_expected = self._super_method_call_error(optimized=True)
@@ -2032,7 +2027,7 @@ class TestLoadSuperAttr(CheckEvents):
]
return d["f"], expected
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: False != True
@unittest.expectedFailure # TODO: RUSTPYTHON; line number differences in multi-line super() calls
def test_attr(self):
nonopt_func, nonopt_expected = self._super_attr(optimized=False)
opt_func, opt_expected = self._super_attr(optimized=True)
@@ -2040,7 +2035,7 @@ class TestLoadSuperAttr(CheckEvents):
self.check_events(nonopt_func, recorders=self.RECORDERS, expected=nonopt_expected)
self.check_events(opt_func, recorders=self.RECORDERS, expected=opt_expected)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; line number differences in multi-line super() calls
def test_vs_other_type_call(self):
code_template = textwrap.dedent("""
class C:

View File

@@ -546,6 +546,7 @@ class TestTranforms(BytecodeTestCase):
self.assertEqual(len(returns), 2)
self.check_lnotab(f)
@unittest.expectedFailure # TODO: RUSTPYTHON; absolute jump encoding
def test_elim_jump_to_uncond_jump(self):
# POP_JUMP_IF_FALSE to JUMP_FORWARD --> POP_JUMP_IF_FALSE to non-jump
def f():
@@ -640,12 +641,14 @@ class TestTranforms(BytecodeTestCase):
self.assertNotInBytecode(f, 'BINARY_OP')
self.check_lnotab(f)
@unittest.expectedFailure # TODO: RUSTPYTHON; no BUILD_LIST to BUILD_TUPLE optimization
def test_in_literal_list(self):
def containtest():
return x in [a, b]
self.assertEqual(count_instr_recursively(containtest, 'BUILD_LIST'), 0)
self.check_lnotab(containtest)
@unittest.expectedFailure # TODO: RUSTPYTHON; no BUILD_LIST to BUILD_TUPLE optimization
def test_iterate_literal_list(self):
def forloop():
for x in [a, b]:

View File

@@ -1393,23 +1393,17 @@ class JumpTestCase(unittest.TestCase):
## The first set of 'jump' tests are for things that are allowed:
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(1, 3, [3])
def test_jump_simple_forwards(output):
output.append(1)
output.append(2)
output.append(3)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(2, 1, [1, 1, 2])
def test_jump_simple_backwards(output):
output.append(1)
output.append(2)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(3, 5, [2, 5])
def test_jump_out_of_block_forwards(output):
for i in 1, 2:
@@ -1418,8 +1412,6 @@ class JumpTestCase(unittest.TestCase):
output.append(4)
output.append(5)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(6, 1, [1, 3, 5, 1, 3, 5, 6, 7])
def test_jump_out_of_block_backwards(output):
output.append(1)
@@ -1451,8 +1443,6 @@ class JumpTestCase(unittest.TestCase):
output.append(5)
output.append(6)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(1, 2, [3])
def test_jump_to_codeless_line(output):
output.append(1)
@@ -1465,8 +1455,6 @@ class JumpTestCase(unittest.TestCase):
output.append(2)
output.append(3)
# TODO: RUSTPYTHON
@unittest.expectedFailure
# Tests jumping within a finally block, and over one.
@jump_test(4, 9, [2, 9])
def test_jump_in_nested_finally(output):
@@ -1480,8 +1468,6 @@ class JumpTestCase(unittest.TestCase):
output.append(8)
output.append(9)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(6, 7, [2, 7], (ZeroDivisionError, ''))
def test_jump_in_nested_finally_2(output):
try:
@@ -1493,8 +1479,6 @@ class JumpTestCase(unittest.TestCase):
output.append(7)
output.append(8)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(6, 11, [2, 11], (ZeroDivisionError, ''))
def test_jump_in_nested_finally_3(output):
try:
@@ -1535,8 +1519,6 @@ class JumpTestCase(unittest.TestCase):
output.append(3)
output.append(4)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(2, 4, [4, 4])
def test_jump_forwards_into_while_block(output):
i = 1
@@ -1545,8 +1527,6 @@ class JumpTestCase(unittest.TestCase):
output.append(4)
i += 1
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(5, 3, [3, 3, 3, 5])
def test_jump_backwards_into_while_block(output):
i = 1
@@ -1555,8 +1535,6 @@ class JumpTestCase(unittest.TestCase):
i += 1
output.append(5)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(2, 3, [1, 3])
def test_jump_forwards_out_of_with_block(output):
with tracecontext(output, 1):
@@ -1571,8 +1549,6 @@ class JumpTestCase(unittest.TestCase):
output.append(2)
output.append(3)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(3, 1, [1, 2, 1, 2, 3, -2])
def test_jump_backwards_out_of_with_block(output):
output.append(1)
@@ -1587,8 +1563,6 @@ class JumpTestCase(unittest.TestCase):
async with asynctracecontext(output, 2):
output.append(3)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(2, 5, [5])
def test_jump_forwards_out_of_try_finally_block(output):
try:
@@ -1597,8 +1571,6 @@ class JumpTestCase(unittest.TestCase):
output.append(4)
output.append(5)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(3, 1, [1, 1, 3, 5])
def test_jump_backwards_out_of_try_finally_block(output):
output.append(1)
@@ -1607,8 +1579,6 @@ class JumpTestCase(unittest.TestCase):
finally:
output.append(5)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(2, 6, [6])
def test_jump_forwards_out_of_try_except_block(output):
try:
@@ -1618,8 +1588,6 @@ class JumpTestCase(unittest.TestCase):
raise
output.append(6)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(3, 1, [1, 1, 3])
def test_jump_backwards_out_of_try_except_block(output):
output.append(1)
@@ -1629,8 +1597,6 @@ class JumpTestCase(unittest.TestCase):
output.append(5)
raise
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(5, 7, [4, 7, 8])
def test_jump_between_except_blocks(output):
try:
@@ -1642,8 +1608,6 @@ class JumpTestCase(unittest.TestCase):
output.append(7)
output.append(8)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(5, 6, [4, 6, 7])
def test_jump_within_except_block(output):
try:
@@ -1654,8 +1618,6 @@ class JumpTestCase(unittest.TestCase):
output.append(6)
output.append(7)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(2, 4, [1, 4, 5, -4])
def test_jump_across_with(output):
output.append(1)
@@ -1674,8 +1636,6 @@ class JumpTestCase(unittest.TestCase):
async with asynctracecontext(output, 4):
output.append(5)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(4, 5, [1, 3, 5, 6])
def test_jump_out_of_with_block_within_for_block(output):
output.append(1)
@@ -1696,8 +1656,6 @@ class JumpTestCase(unittest.TestCase):
output.append(5)
output.append(6)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(4, 5, [1, 2, 3, 5, -2, 6])
def test_jump_out_of_with_block_within_with_block(output):
output.append(1)
@@ -1718,8 +1676,6 @@ class JumpTestCase(unittest.TestCase):
output.append(5)
output.append(6)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(5, 6, [2, 4, 6, 7])
def test_jump_out_of_with_block_within_finally_block(output):
try:
@@ -1742,8 +1698,6 @@ class JumpTestCase(unittest.TestCase):
output.append(6)
output.append(7)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(8, 11, [1, 3, 5, 11, 12])
def test_jump_out_of_complex_nested_blocks(output):
output.append(1)
@@ -1759,8 +1713,6 @@ class JumpTestCase(unittest.TestCase):
output.append(11)
output.append(12)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(3, 5, [1, 2, 5])
def test_jump_out_of_with_assignment(output):
output.append(1)
@@ -1779,8 +1731,6 @@ class JumpTestCase(unittest.TestCase):
output.append(4)
output.append(5)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(3, 6, [1, 6, 8, 9])
def test_jump_over_return_in_try_finally_block(output):
output.append(1)
@@ -1793,8 +1743,6 @@ class JumpTestCase(unittest.TestCase):
output.append(8)
output.append(9)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(5, 8, [1, 3, 8, 10, 11, 13])
def test_jump_over_break_in_try_finally_block(output):
output.append(1)
@@ -1811,8 +1759,6 @@ class JumpTestCase(unittest.TestCase):
break
output.append(13)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(1, 7, [7, 8])
def test_jump_over_for_block_before_else(output):
output.append(1)
@@ -2015,8 +1961,6 @@ class JumpTestCase(unittest.TestCase):
output.append(7)
output.append(8)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(1, 5, [5])
def test_jump_into_finally_block(output):
output.append(1)
@@ -2025,8 +1969,6 @@ class JumpTestCase(unittest.TestCase):
finally:
output.append(5)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(3, 6, [2, 6, 7])
def test_jump_into_finally_block_from_try_block(output):
try:
@@ -2037,8 +1979,6 @@ class JumpTestCase(unittest.TestCase):
output.append(6)
output.append(7)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(5, 1, [1, 3, 1, 3, 5])
def test_jump_out_of_finally_block(output):
output.append(1)
@@ -2117,8 +2057,6 @@ class JumpTestCase(unittest.TestCase):
output.append(6)
output.append(7)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@jump_test(3, 5, [1, 2, 5, -2])
def test_jump_between_with_blocks(output):
output.append(1)
@@ -2187,8 +2125,6 @@ class JumpTestCase(unittest.TestCase):
# triggered.
no_jump_without_trace_function()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_large_function(self):
d = {}
exec("""def f(output): # line 0
@@ -2203,8 +2139,6 @@ class JumpTestCase(unittest.TestCase):
f = d['f']
self.run_test(f, 2, 1007, [0])
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_jump_to_firstlineno(self):
# This tests that PDB can jump back to the first line in a
# file. See issue #1689458. It can only be triggered in a

View File

@@ -2050,6 +2050,7 @@ impl Compiler {
fn compile_statement(&mut self, statement: &ast::Stmt) -> CompileResult<()> {
trace!("Compiling {statement:?}");
let prev_source_range = self.current_source_range;
self.set_source_range(statement.range());
match &statement {
@@ -2433,7 +2434,14 @@ impl Compiler {
value,
simple,
..
}) => self.compile_annotated_assign(target, annotation, value.as_deref(), *simple)?,
}) => {
self.compile_annotated_assign(target, annotation, value.as_deref(), *simple)?;
// Bare annotations in function scope emit no code; restore
// source range so subsequent instructions keep the correct line.
if value.is_none() && self.ctx.in_func() {
self.set_source_range(prev_source_range);
}
}
ast::Stmt::Delete(ast::StmtDelete { targets, .. }) => {
for target in targets {
self.compile_delete(target)?;
@@ -3807,7 +3815,6 @@ impl Compiler {
let code = self.exit_scope();
self.ctx = prev_ctx;
// Restore source range so MAKE_FUNCTION is attributed to the `def` line
self.set_source_range(saved_range);
// Create function object with closure
@@ -5180,23 +5187,21 @@ impl Compiler {
// No PopBlock here - for async, POP_BLOCK is already in for_block
self.pop_fblock(FBlockType::ForLoop);
// End-of-loop instructions are on the `for` line, not the body's last line
let saved_range = self.current_source_range;
self.set_source_range(iter.range());
if is_async {
emit!(self, Instruction::EndAsyncFor);
} else {
// END_FOR + POP_ITER are on the `for` line, not the body's last line
let saved_range = self.current_source_range;
self.set_source_range(iter.range());
emit!(self, Instruction::EndFor);
emit!(self, Instruction::PopIter);
self.set_source_range(saved_range);
}
self.set_source_range(saved_range);
self.compile_statements(orelse)?;
self.switch_to_block(after_block);
// Restore source range to the `for` line so any implicit return
// (LOAD_CONST None, RETURN_VALUE) is attributed to the `for` line,
// not the loop body's last line.
// Implicit return after for-loop should be attributed to the `for` line
self.set_source_range(iter.range());
self.leave_conditional_block();
@@ -6233,6 +6238,8 @@ impl Compiler {
ops: &[ast::CmpOp],
comparators: &[ast::Expr],
) -> CompileResult<()> {
// Save the full Compare expression range for COMPARE_OP positions
let compare_range = self.current_source_range;
let (last_op, mid_ops) = ops.split_last().unwrap();
let (last_comparator, mid_comparators) = comparators.split_last().unwrap();
@@ -6241,6 +6248,7 @@ impl Compiler {
if mid_comparators.is_empty() {
self.compile_expression(last_comparator)?;
self.set_source_range(compare_range);
self.compile_addcompare(last_op);
return Ok(());
@@ -6253,6 +6261,7 @@ impl Compiler {
self.compile_expression(comparator)?;
// store rhs for the next comparison in chain
self.set_source_range(compare_range);
emit!(self, Instruction::Swap { index: 2 });
emit!(self, Instruction::Copy { index: 2 });
@@ -6265,6 +6274,7 @@ impl Compiler {
}
self.compile_expression(last_comparator)?;
self.set_source_range(compare_range);
self.compile_addcompare(last_op);
let end = self.new_block();

View File

@@ -257,14 +257,13 @@ impl CodeInfo {
.filter(|b| b.next != BlockIdx::NULL || !b.instructions.is_empty())
{
// Collect lines that have non-NOP instructions in this block
let non_nop_lines: std::collections::HashSet<_> = block
let non_nop_lines: IndexSet<_> = block
.instructions
.iter()
.filter(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop)))
.map(|ins| ins.location.line)
.collect();
let mut kept_nop_lines: std::collections::HashSet<OneIndexed> =
std::collections::HashSet::new();
let mut kept_nop_lines: IndexSet<OneIndexed> = IndexSet::default();
block.instructions.retain(|ins| {
if matches!(ins.instr.real(), Some(Instruction::Nop)) {
let line = ins.location.line;
@@ -519,10 +518,8 @@ impl CodeInfo {
let tuple_const = ConstantData::Tuple { elements };
let (const_idx, _) = self.metadata.consts.insert_full(tuple_const);
// Replace preceding LOAD instructions with NOP, using the
// BUILD_TUPLE location so remove_nops() treats them as
// same-line and removes them (multi-line tuple literals
// would otherwise leave line-introducing NOPs behind).
// Replace preceding LOAD instructions with NOP at the
// BUILD_TUPLE location so remove_nops() can eliminate them.
let folded_loc = block.instructions[i].location;
for j in start_idx..i {
block.instructions[j].instr = Instruction::Nop.into();
@@ -705,10 +702,8 @@ impl CodeInfo {
if matches!(ins.instr.real(), Some(Instruction::Nop)) {
let line = ins.location.line;
if prev_line == Some(line) {
// Same line as previous instruction — safe to remove
return false;
}
// This NOP introduces a new line — keep it
}
prev_line = Some(ins.location.line);
true

View File

@@ -3,8 +3,7 @@ source: crates/codegen/src/compile.rs
expression: "compile_exec(\"\\\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\")"
---
1 0 RESUME (0)
3 1 LOAD_CONST (<code object test at ??? file "source_path", line 1>): 1 0 RETURN_GENERATOR
1 LOAD_CONST (<code object test at ??? file "source_path", line 1>): 1 0 RETURN_GENERATOR
1 POP_TOP
2 RESUME (0)
@@ -18,7 +17,7 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter
10 CALL (1)
11 BUILD_TUPLE (2)
12 GET_ITER
>> 13 FOR_ITER (49)
>> 13 FOR_ITER (50)
14 STORE_FAST (0, stop_exc)
3 15 LOAD_GLOBAL (2, self)
@@ -37,132 +36,138 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter
28 CALL (0)
29 POP_TOP
5 30 LOAD_GLOBAL (5, egg)
31 PUSH_NULL
32 CALL (0)
33 COPY (1)
34 LOAD_SPECIAL (__aexit__)
35 SWAP (2)
36 LOAD_SPECIAL (__aenter__)
37 PUSH_NULL
38 CALL (0)
39 GET_AWAITABLE (1)
40 LOAD_CONST (None)
>> 41 SEND (45)
42 YIELD_VALUE (1)
43 RESUME (3)
44 JUMP_BACKWARD_NO_INTERRUPT(41)
>> 45 END_SEND
46 POP_TOP
4 30 NOP
6 47 LOAD_FAST (0, stop_exc)
48 RAISE_VARARGS (Raise)
5 31 LOAD_GLOBAL (5, egg)
32 PUSH_NULL
33 CALL (0)
34 COPY (1)
35 LOAD_SPECIAL (__aexit__)
36 SWAP (2)
37 LOAD_SPECIAL (__aenter__)
38 PUSH_NULL
39 CALL (0)
40 GET_AWAITABLE (1)
41 LOAD_CONST (None)
>> 42 SEND (46)
43 YIELD_VALUE (1)
44 RESUME (3)
45 JUMP_BACKWARD_NO_INTERRUPT(42)
>> 46 END_SEND
47 POP_TOP
3 >> 49 END_FOR
50 POP_ITER
51 LOAD_CONST (None)
52 RETURN_VALUE
6 48 LOAD_FAST (0, stop_exc)
49 RAISE_VARARGS (Raise)
5 53 CLEANUP_THROW
54 JUMP_BACKWARD_NO_INTERRUPT(45)
55 PUSH_NULL
56 LOAD_CONST (None)
57 LOAD_CONST (None)
2 >> 50 END_FOR
51 POP_ITER
52 LOAD_CONST (None)
53 RETURN_VALUE
5 54 CLEANUP_THROW
55 JUMP_BACKWARD_NO_INTERRUPT(46)
6 56 NOP
5 57 PUSH_NULL
58 LOAD_CONST (None)
59 CALL (3)
60 GET_AWAITABLE (2)
61 LOAD_CONST (None)
>> 62 SEND (67)
63 YIELD_VALUE (1)
64 RESUME (3)
65 JUMP_BACKWARD_NO_INTERRUPT(62)
66 CLEANUP_THROW
>> 67 END_SEND
68 POP_TOP
69 JUMP_FORWARD (92)
70 PUSH_EXC_INFO
71 WITH_EXCEPT_START
72 GET_AWAITABLE (2)
73 LOAD_CONST (None)
>> 74 SEND (79)
75 YIELD_VALUE (1)
76 RESUME (3)
77 JUMP_BACKWARD_NO_INTERRUPT(74)
78 CLEANUP_THROW
>> 79 END_SEND
80 TO_BOOL
81 POP_JUMP_IF_TRUE (84)
82 NOT_TAKEN
83 RERAISE (2)
>> 84 POP_TOP
85 POP_EXCEPT
86 POP_TOP
87 POP_TOP
88 JUMP_FORWARD (92)
89 COPY (3)
90 POP_EXCEPT
91 RERAISE (1)
>> 92 JUMP_FORWARD (119)
93 PUSH_EXC_INFO
59 LOAD_CONST (None)
60 LOAD_CONST (None)
61 CALL (3)
62 GET_AWAITABLE (2)
63 LOAD_CONST (None)
>> 64 SEND (69)
65 YIELD_VALUE (1)
66 RESUME (3)
67 JUMP_BACKWARD_NO_INTERRUPT(64)
68 CLEANUP_THROW
>> 69 END_SEND
70 POP_TOP
71 JUMP_FORWARD (94)
72 PUSH_EXC_INFO
73 WITH_EXCEPT_START
74 GET_AWAITABLE (2)
75 LOAD_CONST (None)
>> 76 SEND (81)
77 YIELD_VALUE (1)
78 RESUME (3)
79 JUMP_BACKWARD_NO_INTERRUPT(76)
80 CLEANUP_THROW
>> 81 END_SEND
82 TO_BOOL
83 POP_JUMP_IF_TRUE (86)
84 NOT_TAKEN
85 RERAISE (2)
>> 86 POP_TOP
87 POP_EXCEPT
88 POP_TOP
89 POP_TOP
90 JUMP_FORWARD (94)
91 COPY (3)
92 POP_EXCEPT
93 RERAISE (1)
>> 94 JUMP_FORWARD (121)
95 PUSH_EXC_INFO
7 94 LOAD_GLOBAL (6, Exception)
95 CHECK_EXC_MATCH
96 POP_JUMP_IF_FALSE (115)
97 NOT_TAKEN
98 STORE_FAST (1, ex)
7 96 LOAD_GLOBAL (6, Exception)
97 CHECK_EXC_MATCH
98 POP_JUMP_IF_FALSE (117)
99 NOT_TAKEN
100 STORE_FAST (1, ex)
8 99 LOAD_GLOBAL (2, self)
100 LOAD_ATTR (15, assertIs, method=true)
101 LOAD_FAST (1, ex)
102 LOAD_FAST (0, stop_exc)
103 CALL (2)
104 POP_TOP
105 JUMP_BACKWARD_NO_INTERRUPT(110)
106 LOAD_CONST (None)
107 STORE_FAST (1, ex)
108 DELETE_FAST (1, ex)
109 RAISE_VARARGS (ReraiseFromStack)
>> 110 POP_EXCEPT
111 LOAD_CONST (None)
112 STORE_FAST (1, ex)
113 DELETE_FAST (1, ex)
114 JUMP_BACKWARD_NO_INTERRUPT(127)
>> 115 RAISE_VARARGS (ReraiseFromStack)
116 COPY (3)
117 POP_EXCEPT
118 RAISE_VARARGS (ReraiseFromStack)
8 101 LOAD_GLOBAL (2, self)
102 LOAD_ATTR (15, assertIs, method=true)
103 LOAD_FAST (1, ex)
104 LOAD_FAST (0, stop_exc)
105 CALL (2)
106 POP_TOP
107 JUMP_BACKWARD_NO_INTERRUPT(112)
108 LOAD_CONST (None)
109 STORE_FAST (1, ex)
110 DELETE_FAST (1, ex)
111 RAISE_VARARGS (ReraiseFromStack)
>> 112 POP_EXCEPT
113 LOAD_CONST (None)
114 STORE_FAST (1, ex)
115 DELETE_FAST (1, ex)
116 JUMP_BACKWARD_NO_INTERRUPT(129)
>> 117 RAISE_VARARGS (ReraiseFromStack)
118 COPY (3)
119 POP_EXCEPT
120 RAISE_VARARGS (ReraiseFromStack)
10 >> 119 LOAD_GLOBAL (2, self)
120 LOAD_ATTR (17, fail, method=true)
121 LOAD_FAST_BORROW (0, stop_exc)
122 FORMAT_SIMPLE
123 LOAD_CONST (" was suppressed")
124 BUILD_STRING (2)
125 CALL (1)
126 POP_TOP
10 >> 121 LOAD_GLOBAL (2, self)
122 LOAD_ATTR (17, fail, method=true)
123 LOAD_FAST_BORROW (0, stop_exc)
124 FORMAT_SIMPLE
125 LOAD_CONST (" was suppressed")
126 BUILD_STRING (2)
127 CALL (1)
128 POP_TOP
>> 129 NOP
3 >> 127 PUSH_NULL
128 LOAD_CONST (None)
129 LOAD_CONST (None)
130 LOAD_CONST (None)
131 CALL (3)
132 POP_TOP
133 JUMP_FORWARD (148)
134 PUSH_EXC_INFO
135 WITH_EXCEPT_START
136 TO_BOOL
137 POP_JUMP_IF_TRUE (140)
138 NOT_TAKEN
139 RERAISE (2)
>> 140 POP_TOP
141 POP_EXCEPT
142 POP_TOP
143 POP_TOP
144 JUMP_FORWARD (148)
145 COPY (3)
146 POP_EXCEPT
147 RERAISE (1)
>> 148 JUMP_BACKWARD (13)
3 130 PUSH_NULL
131 LOAD_CONST (None)
132 LOAD_CONST (None)
133 LOAD_CONST (None)
134 CALL (3)
135 POP_TOP
136 JUMP_FORWARD (151)
137 PUSH_EXC_INFO
138 WITH_EXCEPT_START
139 TO_BOOL
140 POP_JUMP_IF_TRUE (143)
141 NOT_TAKEN
142 RERAISE (2)
>> 143 POP_TOP
144 POP_EXCEPT
145 POP_TOP
146 POP_TOP
147 JUMP_FORWARD (151)
148 COPY (3)
149 POP_EXCEPT
150 RERAISE (1)
>> 151 JUMP_BACKWARD (13)
2 MAKE_FUNCTION
3 STORE_NAME (0, test)

View File

@@ -8,7 +8,7 @@ use crate::{
};
use alloc::{borrow::ToOwned, boxed::Box, collections::BTreeSet, fmt, string::String, vec::Vec};
use bitflags::bitflags;
use core::{hash, mem, ops::Deref};
use core::{cell::UnsafeCell, hash, mem, ops::Deref};
use itertools::Itertools;
use malachite_bigint::BigInt;
use num_complex::Complex64;
@@ -116,9 +116,12 @@ pub fn decode_exception_table(table: &[u8]) -> Vec<ExceptionTableEntry> {
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: start + size,
end,
target,
depth: (depth_lasti >> 1) as u16,
push_lasti: (depth_lasti & 1) != 0,
@@ -357,8 +360,28 @@ impl TryFrom<&[u8]> for CodeUnit {
}
}
#[derive(Clone, Debug)]
pub struct CodeUnits(Box<[CodeUnit]>);
pub struct CodeUnits(UnsafeCell<Box<[CodeUnit]>>);
// SAFETY: All mutation of the inner buffer is serialized by `monitoring_data: PyMutex`
// in `PyCode`. The `UnsafeCell` is required because `replace_op` mutates through `&self`.
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 inner = unsafe { &*self.0.get() };
Self(UnsafeCell::new(inner.clone()))
}
}
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.0.get() };
f.debug_tuple("CodeUnits").field(inner).finish()
}
}
impl TryFrom<&[u8]> for CodeUnits {
type Error = MarshalError;
@@ -374,19 +397,19 @@ impl TryFrom<&[u8]> for CodeUnits {
impl<const N: usize> From<[CodeUnit; N]> for CodeUnits {
fn from(value: [CodeUnit; N]) -> Self {
Self(Box::from(value))
Self(UnsafeCell::new(Box::from(value)))
}
}
impl From<Vec<CodeUnit>> for CodeUnits {
fn from(value: Vec<CodeUnit>) -> Self {
Self(value.into_boxed_slice())
Self(UnsafeCell::new(value.into_boxed_slice()))
}
}
impl FromIterator<CodeUnit> for CodeUnits {
fn from_iter<T: IntoIterator<Item = CodeUnit>>(iter: T) -> Self {
Self(iter.into_iter().collect())
Self(UnsafeCell::new(iter.into_iter().collect()))
}
}
@@ -394,7 +417,10 @@ impl Deref for CodeUnits {
type Target = [CodeUnit];
fn deref(&self) -> &Self::Target {
&self.0
// 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.0.get() }
}
}
@@ -402,15 +428,17 @@ impl CodeUnits {
/// Replace the opcode at `index` in-place without changing the arg byte.
///
/// # Safety
/// Caller must ensure `index` is in bounds and `new_op` has the same
/// arg semantics as the original opcode.
/// - `index` must be in bounds.
/// - `new_op` must have the same arg semantics as the original opcode.
/// - The caller must ensure exclusive access to the instruction buffer
/// (no concurrent reads or writes to the same `CodeUnits`).
pub unsafe fn replace_op(&self, index: usize, new_op: Instruction) {
unsafe {
let ptr = self.0.as_ptr() as *mut CodeUnit;
let unit_ptr = ptr.add(index);
let units = &mut *self.0.get();
let unit_ptr = units.as_mut_ptr().add(index);
// Write only the opcode byte (first byte of CodeUnit due to #[repr(C)])
let op_ptr = unit_ptr as *mut u8;
core::ptr::write_volatile(op_ptr, new_op.into());
core::ptr::write(op_ptr, new_op.into());
}
}
}

View File

@@ -434,7 +434,7 @@ impl Instruction {
/// Panics if called on an already-instrumented opcode.
pub fn to_instrumented(self) -> Option<Self> {
debug_assert!(
self.to_base().is_none(),
!self.is_instrumented(),
"to_instrumented called on already-instrumented opcode {self:?}"
);
Some(match self {
@@ -462,13 +462,13 @@ impl Instruction {
}
/// Map an INSTRUMENTED_* opcode back to its base variant.
/// Returns `None` if this is not an instrumented opcode.
/// Returns `None` for non-instrumented opcodes, and also for
/// `InstrumentedLine` / `InstrumentedInstruction` which are event-layer
/// placeholders without a fixed base opcode (the real opcode is stored in
/// `CoMonitoringData`).
///
/// The returned base opcode uses `Arg::marker()` for typed fields —
/// only the opcode byte matters since `replace_op` preserves the arg byte.
///
/// # Panics (debug)
/// Panics if called on a base opcode that has an instrumented counterpart.
pub fn to_base(self) -> Option<Self> {
Some(match self {
Self::InstrumentedResume => Self::Resume { arg: Arg::marker() },

View File

@@ -5,7 +5,7 @@ use crate::common::lock::PyMutex;
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::PyStrInterned,
bytecode::{self, AsBag, BorrowedConstant, CodeFlags, Constant, ConstantBag},
bytecode::{self, AsBag, BorrowedConstant, CodeFlags, Constant, ConstantBag, Instruction},
class::{PyClassImpl, StaticType},
convert::{ToPyException, ToPyObject},
frozen,
@@ -913,6 +913,79 @@ impl PyCode {
vm.call_method(list.as_object(), "__iter__", ())
}
#[pymethod]
pub fn co_branches(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
let instructions = &self.code.instructions;
let mut branches = Vec::new();
let mut extended_arg: u32 = 0;
for (i, unit) in instructions.iter().enumerate() {
// De-instrument: use base opcode for instrumented variants
let op = unit.op.to_base().unwrap_or(unit.op);
let raw_arg = u32::from(u8::from(unit.arg));
if matches!(op, Instruction::ExtendedArg) {
extended_arg = (extended_arg | raw_arg) << 8;
continue;
}
let oparg = extended_arg | raw_arg;
extended_arg = 0;
let (src, left, right) = match op {
Instruction::ForIter { .. } => {
// left = fall-through (continue iteration)
// right = past END_FOR (iterator exhausted, skip cleanup)
let target = oparg as usize;
let right = if matches!(
instructions.get(target).map(|u| u.op),
Some(Instruction::EndFor) | Some(Instruction::InstrumentedEndFor)
) {
(target + 1) * 2
} else {
target * 2
};
(i * 2, (i + 1) * 2, right)
}
Instruction::PopJumpIfFalse { .. }
| Instruction::PopJumpIfTrue { .. }
| Instruction::PopJumpIfNone { .. }
| Instruction::PopJumpIfNotNone { .. } => {
// left = fall-through (skip NOT_TAKEN if present)
// right = jump target (condition met)
let next_op = instructions
.get(i + 1)
.map(|u| u.op.to_base().unwrap_or(u.op));
let fallthrough = if matches!(next_op, Some(Instruction::NotTaken)) {
(i + 2) * 2
} else {
(i + 1) * 2
};
(i * 2, fallthrough, oparg as usize * 2)
}
Instruction::EndAsyncFor => {
// src = END_SEND position (next_i - oparg)
let next_i = i + 1;
let Some(src_i) = next_i.checked_sub(oparg as usize) else {
continue;
};
(src_i * 2, (src_i + 2) * 2, next_i * 2)
}
_ => continue,
};
let tuple = vm.ctx.new_tuple(vec![
vm.ctx.new_int(src).into(),
vm.ctx.new_int(left).into(),
vm.ctx.new_int(right).into(),
]);
branches.push(tuple.into());
}
let list = vm.ctx.new_list(branches);
vm.call_method(list.as_object(), "__iter__", ())
}
#[pymethod]
pub fn replace(&self, args: ReplaceArgs, vm: &VirtualMachine) -> PyResult<Self> {
let ReplaceArgs {

View File

@@ -11,6 +11,417 @@ use crate::{
types::Representable,
};
use num_traits::Zero;
use rustpython_compiler_core::bytecode::{
self, Constant, Instruction, InstructionMetadata, StackEffect,
};
use stack_analysis::*;
/// Stack state analysis for safe line-number jumps.
///
/// Models the evaluation stack as a 64-bit integer, encoding the kind of each
/// stack entry in 3-bit blocks. Used by `set_f_lineno` to verify that a jump
/// is safe and to determine how many values need to be popped.
pub(crate) mod stack_analysis {
use super::*;
const BITS_PER_BLOCK: u32 = 3;
const MASK: i64 = (1 << BITS_PER_BLOCK) - 1; // 0b111
const MAX_STACK_ENTRIES: u32 = 63 / BITS_PER_BLOCK; // 21
const WILL_OVERFLOW: u64 = 1u64 << ((MAX_STACK_ENTRIES - 1) * BITS_PER_BLOCK);
pub const EMPTY_STACK: i64 = 0;
pub const UNINITIALIZED: i64 = -2;
pub const OVERFLOWED: i64 = -1;
/// Kind of a stack entry.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(i64)]
pub enum Kind {
Iterator = 1,
Except = 2,
Object = 3,
Null = 4,
Lasti = 5,
}
impl Kind {
fn from_i64(v: i64) -> Option<Self> {
match v {
1 => Some(Kind::Iterator),
2 => Some(Kind::Except),
3 => Some(Kind::Object),
4 => Some(Kind::Null),
5 => Some(Kind::Lasti),
_ => None,
}
}
}
pub fn push_value(stack: i64, kind: i64) -> i64 {
if (stack as u64) >= WILL_OVERFLOW {
OVERFLOWED
} else {
(stack << BITS_PER_BLOCK) | kind
}
}
pub fn pop_value(stack: i64) -> i64 {
stack >> BITS_PER_BLOCK
}
pub fn top_of_stack(stack: i64) -> i64 {
stack & MASK
}
fn peek(stack: i64, n: u32) -> i64 {
debug_assert!(n >= 1);
(stack >> (BITS_PER_BLOCK * (n - 1))) & MASK
}
fn stack_swap(stack: i64, n: u32) -> i64 {
debug_assert!(n >= 1);
let to_swap = peek(stack, n);
let top = top_of_stack(stack);
let shift = BITS_PER_BLOCK * (n - 1);
let replaced_low = (stack & !(MASK << shift)) | (top << shift);
(replaced_low & !MASK) | to_swap
}
fn pop_to_level(mut stack: i64, level: u32) -> i64 {
if level == 0 {
return EMPTY_STACK;
}
let max_item: i64 = (1 << BITS_PER_BLOCK) - 1;
let level_max_stack = max_item << ((level - 1) * BITS_PER_BLOCK);
while stack > level_max_stack {
stack = pop_value(stack);
}
stack
}
fn compatible_kind(from: i64, to: i64) -> bool {
if to == 0 {
return false;
}
if to == Kind::Object as i64 {
return from != Kind::Null as i64;
}
if to == Kind::Null as i64 {
return true;
}
from == to
}
pub fn compatible_stack(from_stack: i64, to_stack: i64) -> bool {
if from_stack < 0 || to_stack < 0 {
return false;
}
let mut from = from_stack;
let mut to = to_stack;
while from > to {
from = pop_value(from);
}
while from != 0 {
let from_top = top_of_stack(from);
let to_top = top_of_stack(to);
if !compatible_kind(from_top, to_top) {
return false;
}
from = pop_value(from);
to = pop_value(to);
}
to == 0
}
pub fn explain_incompatible_stack(to_stack: i64) -> &'static str {
debug_assert!(to_stack != 0);
if to_stack == OVERFLOWED {
return "stack is too deep to analyze";
}
if to_stack == UNINITIALIZED {
return "can't jump into an exception handler, or code may be unreachable";
}
match Kind::from_i64(top_of_stack(to_stack)) {
Some(Kind::Except) => "can't jump into an 'except' block as there's no exception",
Some(Kind::Lasti) => "can't jump into a re-raising block as there's no location",
Some(Kind::Iterator) => "can't jump into the body of a for loop",
_ => "incompatible stacks",
}
}
/// Analyze bytecode and compute the stack state at each instruction index.
pub fn mark_stacks<C: Constant>(code: &bytecode::CodeObject<C>) -> Vec<i64> {
let instructions = &*code.instructions;
let len = instructions.len();
let mut stacks = vec![UNINITIALIZED; len + 1];
stacks[0] = EMPTY_STACK;
let mut todo = true;
while todo {
todo = false;
let mut i = 0;
while i < len {
let mut next_stack = stacks[i];
let mut opcode = instructions[i].op;
let mut oparg: u32 = 0;
// Accumulate EXTENDED_ARG prefixes
while matches!(opcode, Instruction::ExtendedArg) {
oparg = (oparg << 8) | u32::from(u8::from(instructions[i].arg));
i += 1;
if i >= len {
break;
}
stacks[i] = next_stack;
opcode = instructions[i].op;
}
if i >= len {
break;
}
oparg = (oparg << 8) | u32::from(u8::from(instructions[i].arg));
// De-instrument: get the underlying real instruction
let opcode = opcode.to_base().unwrap_or(opcode);
let next_i = i + 1; // No inline caches in RustPython
if next_stack == UNINITIALIZED {
i = next_i;
continue;
}
match opcode {
Instruction::PopJumpIfFalse { .. }
| Instruction::PopJumpIfTrue { .. }
| Instruction::PopJumpIfNone { .. }
| Instruction::PopJumpIfNotNone { .. } => {
// Jump target is absolute instruction index
let j = oparg as usize;
next_stack = pop_value(next_stack);
let target_stack = next_stack;
if j < stacks.len() && stacks[j] == UNINITIALIZED {
stacks[j] = target_stack;
}
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::Send { .. } => {
// target is absolute
let j = oparg as usize;
if j < stacks.len() && stacks[j] == UNINITIALIZED {
stacks[j] = next_stack;
}
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::JumpForward { .. } => {
// target is absolute in RustPython
let j = oparg as usize;
if j < stacks.len() && stacks[j] == UNINITIALIZED {
stacks[j] = next_stack;
}
}
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. } => {
// target is absolute in RustPython
let j = oparg as usize;
if j < stacks.len() && stacks[j] == UNINITIALIZED {
stacks[j] = next_stack;
if j < i {
todo = true;
}
}
}
Instruction::GetIter | Instruction::GetAIter => {
next_stack = push_value(pop_value(next_stack), Kind::Iterator as i64);
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::ForIter { .. } => {
// Fall-through (iteration continues): pushes the next value
let body_stack = push_value(next_stack, Kind::Object as i64);
if next_i < stacks.len() {
stacks[next_i] = body_stack;
}
// Exhaustion path: execute_for_iter skips END_FOR and
// jumps directly to POP_ITER. The iterator stays on
// the stack and POP_ITER removes it.
let mut j = oparg as usize;
if j < instructions.len() {
let target_op =
instructions[j].op.to_base().unwrap_or(instructions[j].op);
if matches!(target_op, Instruction::EndFor) {
j += 1;
}
}
if j < stacks.len() && stacks[j] == UNINITIALIZED {
stacks[j] = next_stack;
}
}
Instruction::EndAsyncFor => {
next_stack = pop_value(pop_value(next_stack));
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::PushExcInfo => {
next_stack = push_value(next_stack, Kind::Except as i64);
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::PopExcept => {
next_stack = pop_value(next_stack);
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::ReturnValue => {
// End of block, no fall-through
}
Instruction::RaiseVarargs { .. } => {
// End of block, no fall-through
}
Instruction::Reraise { .. } => {
// End of block, no fall-through
}
Instruction::PushNull => {
next_stack = push_value(next_stack, Kind::Null as i64);
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::LoadGlobal(_) => {
// RustPython's LOAD_GLOBAL doesn't encode push_null in oparg
// (separate PUSH_NULL instructions are used instead)
next_stack = push_value(next_stack, Kind::Object as i64);
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::LoadAttr { .. } => {
// LoadAttr: pops object, pushes result
// If oparg & 1, it also pushes Null (method load)
let attr_oparg = oparg;
if attr_oparg & 1 != 0 {
next_stack = pop_value(next_stack);
next_stack = push_value(next_stack, Kind::Object as i64);
next_stack = push_value(next_stack, Kind::Null as i64);
}
// else: default stack_effect handles it
else {
let effect: StackEffect = opcode.stack_effect_info(oparg);
let popped = effect.popped() as i64;
let pushed = effect.pushed() as i64;
for _ in 0..popped {
next_stack = pop_value(next_stack);
}
for _ in 0..pushed {
next_stack = push_value(next_stack, Kind::Object as i64);
}
}
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::Swap { .. } => {
let n = oparg;
next_stack = stack_swap(next_stack, n);
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
Instruction::Copy { .. } => {
let n = oparg;
next_stack = push_value(next_stack, peek(next_stack, n));
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
_ => {
// Default: use stack_effect
let effect: StackEffect = opcode.stack_effect_info(oparg);
let popped = effect.popped() as i64;
let pushed = effect.pushed() as i64;
let mut ns = next_stack;
for _ in 0..popped {
ns = pop_value(ns);
}
for _ in 0..pushed {
ns = push_value(ns, Kind::Object as i64);
}
next_stack = ns;
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
}
}
i = next_i;
}
// Scan exception table
let exception_table = bytecode::decode_exception_table(&code.exceptiontable);
for entry in &exception_table {
let start_offset = entry.start as usize;
let handler = entry.target as usize;
let level = entry.depth as u32;
let has_lasti = entry.push_lasti;
if start_offset < stacks.len()
&& stacks[start_offset] != UNINITIALIZED
&& handler < stacks.len()
&& stacks[handler] == UNINITIALIZED
{
todo = true;
let mut target_stack = pop_to_level(stacks[start_offset], level);
if has_lasti {
target_stack = push_value(target_stack, Kind::Lasti as i64);
}
target_stack = push_value(target_stack, Kind::Except as i64);
stacks[handler] = target_stack;
}
}
}
stacks
}
/// Build a mapping from instruction index to line number.
/// Returns -1 for indices with no line start.
pub fn mark_lines<C: Constant>(code: &bytecode::CodeObject<C>) -> Vec<i32> {
let len = code.instructions.len();
let mut line_starts = vec![-1i32; len];
let mut last_line: i32 = -1;
for (i, (loc, _)) in code.locations.iter().enumerate() {
if i >= len {
break;
}
let line = loc.line.get() as i32;
if line != last_line && line > 0 {
line_starts[i] = line;
last_line = line;
}
}
line_starts
}
/// Find the first line number >= `line` that has code.
pub fn first_line_not_before(lines: &[i32], line: i32) -> i32 {
let mut result = i32::MAX;
for &l in lines {
if l >= line && l < result {
result = l;
}
}
if result == i32::MAX { -1 } else { result }
}
}
pub fn init(context: &Context) {
Frame::extend_class(context, context.types.frame_type);
@@ -71,6 +482,113 @@ impl Frame {
}
}
#[pygetset(setter)]
fn set_f_lineno(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
let l_new_lineno = match value {
PySetterValue::Assign(val) => {
let line_ref: PyIntRef = val
.downcast()
.map_err(|_| vm.new_value_error("lineno must be an integer".to_owned()))?;
line_ref
.try_to_primitive::<i32>(vm)
.map_err(|_| vm.new_value_error("lineno must be an integer".to_owned()))?
}
PySetterValue::Delete => {
return Err(vm.new_type_error("can't delete f_lineno attribute".to_owned()));
}
};
let first_line = self
.code
.first_line_number
.map(|n| n.get() as i32)
.unwrap_or(1);
if l_new_lineno < first_line {
return Err(vm.new_value_error(format!(
"line {l_new_lineno} comes before the current code block"
)));
}
let py_code: &PyCode = &self.code;
let code = &py_code.code;
let lines = mark_lines(code);
// Find the first line >= target that has actual code
let new_lineno = first_line_not_before(&lines, l_new_lineno);
if new_lineno < 0 {
return Err(vm.new_value_error(format!(
"line {l_new_lineno} comes after the current code block"
)));
}
let stacks = mark_stacks(code);
let len = self.code.instructions.len();
// lasti points past the current instruction (already incremented).
// stacks[lasti - 1] gives the stack state before executing the
// instruction that triggered this trace event, which is the current
// evaluation stack.
let current_lasti = self.lasti() as usize;
let start_idx = current_lasti.saturating_sub(1);
let start_stack = if start_idx < stacks.len() {
stacks[start_idx]
} else {
OVERFLOWED
};
let mut best_stack = OVERFLOWED;
let mut best_addr: i32 = -1;
let mut err: i32 = -1;
let mut msg = "cannot find bytecode for specified line";
for i in 0..len {
if lines[i] == new_lineno {
let target_stack = stacks[i];
if compatible_stack(start_stack, target_stack) {
err = 0;
if target_stack > best_stack {
best_stack = target_stack;
best_addr = i as i32;
}
} else if err < 0 {
if start_stack == OVERFLOWED {
msg = "stack to deep to analyze";
} else if start_stack == UNINITIALIZED {
msg = "can't jump from unreachable code";
} else {
msg = explain_incompatible_stack(target_stack);
err = 1;
}
}
}
}
if err != 0 {
return Err(vm.new_value_error(msg.to_owned()));
}
// Count how many entries to pop
let mut pop_count = 0usize;
{
let mut s = start_stack;
while s > best_stack {
pop_count += 1;
s = pop_value(s);
}
}
// Store the pending unwind for the execution loop to perform.
// We cannot pop stack entries here because the execution loop
// holds the state mutex, and trying to lock it again would deadlock.
self.set_pending_stack_pops(pop_count as u32);
self.set_pending_unwind_from_stack(start_stack);
// Set lasti to best_addr. The executor will read lasti and execute
// the instruction at that index next.
self.set_lasti(best_addr as u32);
Ok(())
}
#[pygetset]
fn f_trace(&self) -> PyObjectRef {
let boxed = self.trace.lock();

View File

@@ -7,6 +7,7 @@ use crate::{
PyInterpolation, PyList, PySet, PySlice, PyStr, PyStrInterned, PyTemplate, PyTraceback,
PyType, PyUtf8Str,
asyncgenerator::PyAsyncGenWrappedValue,
frame::stack_analysis,
function::{PyCell, PyCellRef, PyFunction},
tuple::{PyTuple, PyTupleRef},
},
@@ -19,7 +20,7 @@ use crate::{
object::{Traverse, TraverseFn},
protocol::{PyIter, PyIterReturn},
scope::Scope,
stdlib::{builtins, typing},
stdlib::{builtins, sys::monitoring, typing},
types::PyTypeFlags,
vm::{Context, PyMethod},
};
@@ -28,9 +29,10 @@ use bstr::ByteSlice;
use core::iter::zip;
use core::sync::atomic;
use core::sync::atomic::AtomicPtr;
use core::sync::atomic::Ordering::Relaxed;
use indexmap::IndexMap;
use itertools::Itertools;
use rustpython_common::atomic::{PyAtomic, Radium};
use rustpython_common::{
boxvec::BoxVec,
lock::PyMutex,
@@ -60,9 +62,6 @@ struct FrameState {
stack: BoxVec<Option<PyObjectRef>>,
/// Cell and free variable references (cellvars + freevars).
cells_frees: Box<[PyCellRef]>,
/// index of last instruction ran
#[cfg(feature = "threading")]
lasti: u32,
}
/// Tracks who owns a frame.
@@ -89,11 +88,6 @@ impl FrameOwner {
}
}
#[cfg(feature = "threading")]
type Lasti = atomic::AtomicU32;
#[cfg(not(feature = "threading"))]
type Lasti = core::cell::Cell<u32>;
#[pyclass(module = false, name = "frame", traverse = "manual")]
pub struct Frame {
pub code: PyRef<PyCode>,
@@ -104,10 +98,8 @@ pub struct Frame {
pub globals: PyDictRef,
pub builtins: PyObjectRef,
// on feature=threading, this is a duplicate of FrameState.lasti, but it's faster to do an
// atomic store than it is to do a fetch_add, for every instruction executed
/// index of last instruction ran
pub lasti: Lasti,
pub lasti: PyAtomic<u32>,
/// tracer function for this frame (usually is None)
pub trace: PyMutex<PyObjectRef>,
state: PyMutex<FrameState>,
@@ -129,6 +121,14 @@ pub struct Frame {
pub(crate) owner: atomic::AtomicI8,
/// Set when f_locals is accessed. Cleared after locals_to_fast() sync.
pub(crate) locals_dirty: atomic::AtomicBool,
/// Number of stack entries to pop after set_f_lineno returns to the
/// execution loop. set_f_lineno cannot pop directly because the
/// execution loop holds the state mutex.
pub(crate) pending_stack_pops: PyAtomic<u32>,
/// The encoded stack state that set_f_lineno wants to unwind *from*.
/// Used together with `pending_stack_pops` to identify Except entries
/// that need special exception-state handling.
pub(crate) pending_unwind_from_stack: PyAtomic<i64>,
}
impl PyPayload for Frame {
@@ -200,8 +200,6 @@ impl Frame {
let state = FrameState {
stack: BoxVec::new(code.max_stackdepth as usize),
cells_frees,
#[cfg(feature = "threading")]
lasti: 0,
};
Self {
@@ -211,7 +209,7 @@ impl Frame {
builtins,
code,
func_obj,
lasti: Lasti::new(0),
lasti: Radium::new(0),
state: PyMutex::new(state),
trace: PyMutex::new(vm.ctx.none()),
trace_lines: PyMutex::new(true),
@@ -221,6 +219,8 @@ impl Frame {
previous: AtomicPtr::new(core::ptr::null_mut()),
owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8),
locals_dirty: atomic::AtomicBool::new(false),
pending_stack_pops: Default::default(),
pending_unwind_from_stack: Default::default(),
}
}
@@ -283,14 +283,27 @@ impl Frame {
}
pub fn lasti(&self) -> u32 {
#[cfg(feature = "threading")]
{
self.lasti.load(atomic::Ordering::Relaxed)
}
#[cfg(not(feature = "threading"))]
{
self.lasti.get()
}
self.lasti.load(Relaxed)
}
pub fn set_lasti(&self, val: u32) {
self.lasti.store(val, Relaxed);
}
pub(crate) fn pending_stack_pops(&self) -> u32 {
self.pending_stack_pops.load(Relaxed)
}
pub(crate) fn set_pending_stack_pops(&self, val: u32) {
self.pending_stack_pops.store(val, Relaxed);
}
pub(crate) fn pending_unwind_from_stack(&self) -> i64 {
self.pending_unwind_from_stack.load(Relaxed)
}
pub(crate) fn set_pending_unwind_from_stack(&self, val: i64) {
self.pending_unwind_from_stack.store(val, Relaxed);
}
/// Sync locals dict back to fastlocals. Called before generator/coroutine resume
@@ -450,7 +463,7 @@ struct ExecutingFrame<'a> {
globals: &'a PyDictRef,
builtins: &'a PyObjectRef,
object: &'a Py<Frame>,
lasti: &'a Lasti,
lasti: &'a PyAtomic<u32>,
state: &'a mut FrameState,
/// Cached monitoring events mask. Reloaded at Resume instruction only,
monitoring_mask: u32,
@@ -471,29 +484,37 @@ impl fmt::Debug for ExecutingFrame<'_> {
impl ExecutingFrame<'_> {
#[inline(always)]
fn update_lasti(&mut self, f: impl FnOnce(&mut u32)) {
#[cfg(feature = "threading")]
{
f(&mut self.state.lasti);
self.lasti
.store(self.state.lasti, atomic::Ordering::Relaxed);
}
#[cfg(not(feature = "threading"))]
{
let mut lasti = self.lasti.get();
f(&mut lasti);
self.lasti.set(lasti);
}
let mut val = self.lasti.load(Relaxed);
f(&mut val);
self.lasti.store(val, Relaxed);
}
#[inline(always)]
const fn lasti(&self) -> u32 {
#[cfg(feature = "threading")]
{
self.state.lasti
}
#[cfg(not(feature = "threading"))]
{
self.lasti.get()
fn lasti(&self) -> u32 {
self.lasti.load(Relaxed)
}
/// Perform deferred stack unwinding after set_f_lineno.
///
/// set_f_lineno cannot pop the value stack directly because the execution
/// loop holds the state mutex. Instead it records the work in
/// `pending_stack_pops` / `pending_unwind_from_stack` and we execute it
/// here, inside the execution loop where we already own the state.
fn unwind_stack_for_lineno(&mut self, pop_count: usize, from_stack: i64, vm: &VirtualMachine) {
let mut cur_stack = from_stack;
for _ in 0..pop_count {
let val = self.pop_value_opt();
if stack_analysis::top_of_stack(cur_stack) == stack_analysis::Kind::Except as i64
&& let Some(exc_obj) = val
{
if vm.is_none(&exc_obj) {
vm.set_exception(None);
} else {
let exc = exc_obj.downcast::<PyBaseException>().ok();
vm.set_exception(exc);
}
}
cur_stack = stack_analysis::pop_value(cur_stack);
}
}
@@ -507,23 +528,46 @@ impl ExecutingFrame<'_> {
let mut arg_state = bytecode::OpArgState::default();
loop {
let idx = self.lasti() as usize;
// Advance lasti past the current instruction BEFORE firing the
// line event. This ensures that f_lineno (which reads
// locations[lasti - 1]) returns the line of the instruction
// being traced, not the previous one.
self.update_lasti(|i| *i += 1);
// Fire 'line' trace event when line number changes.
// Only fire if this frame has a per-frame trace function set
// (frames entered before sys.settrace() have trace=None).
// Skip RESUME it should not generate user-visible line events.
if vm.use_tracing.get()
&& !vm.is_none(&self.object.trace.lock())
&& !matches!(
instructions.get(idx).map(|u| u.op),
Some(Instruction::Resume { .. } | Instruction::InstrumentedResume)
)
&& let Some((loc, _)) = self.code.locations.get(idx)
&& loc.line.get() as u32 != self.prev_line
{
self.prev_line = loc.line.get() as u32;
vm.trace_event(crate::protocol::TraceEvent::Line, None)?;
// Trace callback may have changed lasti via set_f_lineno.
// Re-read and restart the loop from the new position.
if self.lasti() != (idx as u32 + 1) {
// set_f_lineno defers stack unwinding because we hold
// the state mutex. Perform it now.
let pops = self.object.pending_stack_pops();
if pops > 0 {
let from_stack = self.object.pending_unwind_from_stack();
self.unwind_stack_for_lineno(pops as usize, from_stack, vm);
self.object.set_pending_stack_pops(0);
}
arg_state.reset();
continue;
}
}
self.update_lasti(|i| *i += 1);
let bytecode::CodeUnit { op, arg } = instructions[idx];
let arg = arg_state.extend(arg);
let mut do_extend_arg = false;
// Track current line for settrace LINE event and monitoring.
if !matches!(
op,
Instruction::Resume { .. }
@@ -621,12 +665,8 @@ impl ExecutingFrame<'_> {
);
// Fire RAISE or RERAISE monitoring event.
// fire_reraise internally deduplicates: only the first
// re-raise after each EXCEPTION_HANDLED fires the event.
// If the callback raises (e.g. ValueError for illegal DISABLE),
// replace the original exception.
// If the callback raises, replace the original exception.
let exception = {
use crate::stdlib::sys::monitoring;
let mon_events = vm.state.monitoring_events.load();
if is_reraise {
if mon_events & monitoring::EVENT_RERAISE != 0 {
@@ -657,14 +697,12 @@ impl ExecutingFrame<'_> {
Err(exception) => {
// Fire PY_UNWIND: exception escapes this frame
let exception = if vm.state.monitoring_events.load()
& crate::stdlib::sys::monitoring::EVENT_PY_UNWIND
& monitoring::EVENT_PY_UNWIND
!= 0
{
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match crate::stdlib::sys::monitoring::fire_py_unwind(
vm, self.code, offset, &exc_obj,
) {
match monitoring::fire_py_unwind(vm, self.code, offset, &exc_obj) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
@@ -849,11 +887,9 @@ impl ExecutingFrame<'_> {
exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx)));
}
// Fire PY_THROW and RAISE events before raising the exception in the
// generator. do_monitor_exc in CPython replaces the active exception
// when a callback fails, so we mirror that here.
// Fire PY_THROW and RAISE events before raising the exception.
// If a monitoring callback fails, its exception replaces the original.
let exception = {
use crate::stdlib::sys::monitoring;
let mon_events = vm.state.monitoring_events.load();
let exception = if mon_events & monitoring::EVENT_PY_THROW != 0 {
let offset = idx as u32 * 2;
@@ -890,22 +926,17 @@ impl ExecutingFrame<'_> {
Ok(Some(result)) => Ok(result),
Err(exception) => {
// Fire PY_UNWIND: exception escapes the generator frame.
// do_monitor_exc replaces the exception on callback failure.
let exception = if vm.state.monitoring_events.load()
& crate::stdlib::sys::monitoring::EVENT_PY_UNWIND
!= 0
{
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match crate::stdlib::sys::monitoring::fire_py_unwind(
vm, self.code, offset, &exc_obj,
) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
} else {
exception
};
let exception =
if vm.state.monitoring_events.load() & monitoring::EVENT_PY_UNWIND != 0 {
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match monitoring::fire_py_unwind(vm, self.code, offset, &exc_obj) {
Ok(()) => exception,
Err(monitor_exc) => monitor_exc,
}
} else {
exception
};
Err(exception)
}
}
@@ -2137,7 +2168,7 @@ impl ExecutingFrame<'_> {
.load(atomic::Ordering::Acquire);
if code_ver != global_ver {
let events = vm.state.monitoring_events.load();
crate::stdlib::sys::monitoring::instrument_code(self.code, events);
monitoring::instrument_code(self.code, events);
self.code
.instrumentation_version
.store(global_ver, atomic::Ordering::Release);
@@ -2186,10 +2217,8 @@ impl ExecutingFrame<'_> {
vm.set_exception(Some(exc_ref.to_owned()));
}
// Complete stack operations
self.push_value(prev_exc);
self.push_value(exc);
Ok(None)
}
Instruction::CheckExcMatch => {
@@ -2496,11 +2525,7 @@ impl ExecutingFrame<'_> {
instruction.is_instrumented(),
"execute_instrumented called with non-instrumented opcode {instruction:?}"
);
// Refresh monitoring mask from global state on every instrumented opcode
// execution. This ensures frames already past RESUME pick up events that
// were enabled by a set_events() call while the frame was executing.
self.monitoring_mask = vm.state.monitoring_events.load();
use crate::stdlib::sys::monitoring;
match instruction {
Instruction::InstrumentedResume => {
// Version check: re-instrument if stale
@@ -2594,20 +2619,25 @@ impl ExecutingFrame<'_> {
}
Err(exc) => {
// Fire C_RAISE on failure
if let Some((global_super, arg0)) = call_args {
let _ = monitoring::fire_c_raise(
let exc = if let Some((global_super, arg0)) = call_args {
match monitoring::fire_c_raise(
vm,
self.code,
offset,
&global_super,
arg0,
);
}
) {
Ok(()) => exc,
Err(monitor_exc) => monitor_exc,
}
} else {
exc
};
Err(exc)
}
}
}
Instruction::InstrumentedJumpForward => {
Instruction::InstrumentedJumpForward | Instruction::InstrumentedJumpBackward => {
let src_offset = (self.lasti() - 1) * 2;
let target = bytecode::Label::from(u32::from(arg));
self.jump(target);
@@ -2616,15 +2646,6 @@ impl ExecutingFrame<'_> {
}
Ok(None)
}
Instruction::InstrumentedJumpBackward => {
let src_offset = (self.lasti() - 1) * 2;
let dest = bytecode::Label::from(u32::from(arg));
self.jump(dest);
if self.monitoring_mask & monitoring::EVENT_JUMP != 0 {
monitoring::fire_jump(vm, self.code, src_offset, dest.0 * 2)?;
}
Ok(None)
}
Instruction::InstrumentedForIter => {
let src_offset = (self.lasti() - 1) * 2;
let target = bytecode::Label::from(u32::from(arg));
@@ -2672,55 +2693,50 @@ impl ExecutingFrame<'_> {
Ok(None)
}
Instruction::InstrumentedPopJumpIfTrue => {
let src_offset = (self.lasti() - 1) * 2;
let target = bytecode::Label::from(u32::from(arg));
let obj = self.pop_value();
let value = obj.try_to_bool(vm)?;
if value {
self.jump(target);
// Branch taken → fire BRANCH_RIGHT
if self.monitoring_mask & monitoring::EVENT_BRANCH_RIGHT != 0 {
let src_offset = (self.lasti() - 1) * 2;
monitoring::fire_branch_right(vm, self.code, src_offset, target.0 * 2)?;
}
}
// Branch not taken → InstrumentedNotTaken fires BRANCH_LEFT
Ok(None)
}
Instruction::InstrumentedPopJumpIfFalse => {
let src_offset = (self.lasti() - 1) * 2;
let target = bytecode::Label::from(u32::from(arg));
let obj = self.pop_value();
let value = obj.try_to_bool(vm)?;
if !value {
self.jump(target);
// Branch taken → fire BRANCH_RIGHT
if self.monitoring_mask & monitoring::EVENT_BRANCH_RIGHT != 0 {
let src_offset = (self.lasti() - 1) * 2;
monitoring::fire_branch_right(vm, self.code, src_offset, target.0 * 2)?;
}
}
Ok(None)
}
Instruction::InstrumentedPopJumpIfNone => {
let src_offset = (self.lasti() - 1) * 2;
let value = self.pop_value();
let target = bytecode::Label::from(u32::from(arg));
if vm.is_none(&value) {
self.jump(target);
// Branch taken → fire BRANCH_RIGHT
if self.monitoring_mask & monitoring::EVENT_BRANCH_RIGHT != 0 {
let src_offset = (self.lasti() - 1) * 2;
monitoring::fire_branch_right(vm, self.code, src_offset, target.0 * 2)?;
}
}
Ok(None)
}
Instruction::InstrumentedPopJumpIfNotNone => {
let src_offset = (self.lasti() - 1) * 2;
let value = self.pop_value();
let target = bytecode::Label::from(u32::from(arg));
if !vm.is_none(&value) {
self.jump(target);
// Branch taken → fire BRANCH_RIGHT
if self.monitoring_mask & monitoring::EVENT_BRANCH_RIGHT != 0 {
let src_offset = (self.lasti() - 1) * 2;
monitoring::fire_branch_right(vm, self.code, src_offset, target.0 * 2)?;
}
}
@@ -3067,18 +3083,11 @@ impl ExecutingFrame<'_> {
// Fire EXCEPTION_HANDLED before setting up handler.
// If the callback raises, the handler is NOT set up and the
// new exception propagates instead.
if vm.state.monitoring_events.load()
& crate::stdlib::sys::monitoring::EVENT_EXCEPTION_HANDLED
!= 0
if vm.state.monitoring_events.load() & monitoring::EVENT_EXCEPTION_HANDLED != 0
{
let byte_offset = offset * 2;
let exc_obj: PyObjectRef = exception.clone().into();
crate::stdlib::sys::monitoring::fire_exception_handled(
vm,
self.code,
byte_offset,
&exc_obj,
)?;
monitoring::fire_exception_handled(vm, self.code, byte_offset, &exc_obj)?;
}
// 1. Pop stack to entry.depth
@@ -3288,9 +3297,7 @@ impl ExecutingFrame<'_> {
let self_or_null = self.pop_value_opt(); // Option<PyObjectRef>
let callable = self.pop_value();
// If self_or_null is Some (not NULL), prepend it to args
let final_args = if let Some(self_val) = self_or_null {
// Method call: prepend self to args
let mut all_args = vec![self_val];
all_args.extend(args.args);
FuncArgs {
@@ -3298,28 +3305,19 @@ impl ExecutingFrame<'_> {
kwargs: args.kwargs,
}
} else {
// Regular attribute call: self_or_null is NULL
args
};
match callable.call(final_args, vm) {
Ok(value) => {
self.push_value(value);
Ok(None)
}
Err(exc) => Err(exc),
}
let value = callable.call(final_args, vm)?;
self.push_value(value);
Ok(None)
}
/// Instrumented version of execute_call: fires CALL, C_RETURN, and C_RAISE events.
fn execute_call_instrumented(&mut self, args: FuncArgs, vm: &VirtualMachine) -> FrameResult {
use crate::stdlib::sys::monitoring;
// Stack: [callable, self_or_null, ...]
let self_or_null = self.pop_value_opt(); // Option<PyObjectRef>
let self_or_null = self.pop_value_opt();
let callable = self.pop_value();
// If self_or_null is Some (not NULL), prepend it to args
let final_args = if let Some(self_val) = self_or_null {
let mut all_args = vec![self_val];
all_args.extend(args.args);
@@ -3532,8 +3530,7 @@ impl ExecutingFrame<'_> {
Ok(true)
}
Ok(PyIterReturn::StopIteration(_)) => {
// Skip END_FOR if followed by POP_ITER (both base and instrumented).
// PopIter may be further wrapped by InstrumentedInstruction / InstrumentedLine.
// Skip END_FOR (base or instrumented) and jump to POP_ITER.
let target_idx = target.0 as usize;
let jump_target = if let Some(unit) = self.code.instructions.get(target_idx) {
if matches!(

View File

@@ -180,22 +180,17 @@ fn parse_single_event(event: i32, vm: &VirtualMachine) -> PyResult<usize> {
}
fn normalize_event_set(event_set: i32, local: bool, vm: &VirtualMachine) -> PyResult<u32> {
let kind = if local {
"local event set"
} else {
"event set"
};
if event_set < 0 {
let kind = if local {
"local event set"
} else {
"event set"
};
return Err(vm.new_value_error(format!("invalid {kind} 0x{event_set:x}")));
}
let mut event_set = event_set as u32;
if event_set >= (1 << EVENTS_COUNT) {
let kind = if local {
"local event set"
} else {
"event set"
};
return Err(vm.new_value_error(format!("invalid {kind} 0x{event_set:x}")));
}
@@ -897,13 +892,17 @@ pub fn fire_reraise(
return Ok(());
}
RERAISE_PENDING.with(|f| f.set(true));
fire(
let result = fire(
vm,
EVENT_RERAISE,
code,
offset,
&[vm.ctx.new_int(offset).into(), exception.clone()],
)
);
if result.is_err() {
RERAISE_PENDING.with(|f| f.set(false));
}
result
}
pub fn fire_exception_handled(
@@ -953,7 +952,6 @@ pub fn fire_py_throw(
)
}
#[allow(dead_code)]
pub fn fire_stop_iteration(
vm: &VirtualMachine,
code: &PyRef<PyCode>,

View File

@@ -812,6 +812,8 @@ impl PyType {
}) {
self.slots.getattro.store(Some(func));
} else {
// __getattribute__ is a Python method somewhere in MRO;
// use the wrapper to dispatch through it.
self.slots.getattro.store(Some(getattro_wrapper));
}
} else {