mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
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:
@@ -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
69
Lib/opcode.py
vendored
@@ -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
5
Lib/test/dis_module.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
# A simple module for testing the dis module.
|
||||
|
||||
def f(): pass
|
||||
def g(): pass
|
||||
5
Lib/test/test_dis.py
vendored
5
Lib/test/test_dis.py
vendored
@@ -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", ()),
|
||||
|
||||
13
Lib/test/test_monitoring.py
vendored
13
Lib/test/test_monitoring.py
vendored
@@ -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:
|
||||
|
||||
3
Lib/test/test_peepholer.py
vendored
3
Lib/test/test_peepholer.py
vendored
@@ -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]:
|
||||
|
||||
66
Lib/test/test_sys_settrace.py
vendored
66
Lib/test/test_sys_settrace.py
vendored
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user