instruction improvements (#6829)

New Features

Direct small-integer loading (0–255) and locals-loading for faster execution
Async-generator wrapping and improved generator resume behavior
Performance

Faster integer loads and streamlined jump/loop handling for better runtime performance
Bug Fixes

More robust StopIteration handling and stricter init return checks
Corrected iterator cleanup for async and sync loops
Improvements

Aligns loop and jump semantics with CPython 3.14 patterns
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-01-22 17:25:56 +09:00
1 parent aed3a60e17
commit 40a43f3210
13 files changed
+433 -253

No files matched your search

+21 -19
View File
@@ -135,12 +135,12 @@ opmap = {
'BINARY_OP_EXTEND': 132,
'BINARY_OP_MULTIPLY_FLOAT': 133,
'BINARY_OP_MULTIPLY_INT': 134,
'BINARY_SUBSCR_DICT': 135,
'BINARY_SUBSCR_GETITEM': 136,
'BINARY_SUBSCR_LIST_INT': 137,
'BINARY_SUBSCR_LIST_SLICE': 138,
'BINARY_SUBSCR_STR_INT': 139,
'BINARY_SUBSCR_TUPLE_INT': 140,
'BINARY_OP_SUBSCR_DICT': 135,
'BINARY_OP_SUBSCR_GETITEM': 136,
'BINARY_OP_SUBSCR_LIST_INT': 137,
'BINARY_OP_SUBSCR_LIST_SLICE': 138,
'BINARY_OP_SUBSCR_STR_INT': 139,
'BINARY_OP_SUBSCR_TUPLE_INT': 140,
'BINARY_OP_SUBTRACT_FLOAT': 141,
'BINARY_OP_SUBTRACT_INT': 142,
'CALL_ALLOC_AND_ENTER_INIT': 143,
@@ -234,19 +234,21 @@ opmap = {
'INSTRUMENTED_JUMP_BACKWARD': 253,
'INSTRUMENTED_LINE': 254,
'ENTER_EXECUTOR': 255,
'JUMP': 256,
'JUMP_NO_INTERRUPT': 257,
'RESERVED_258': 258,
'LOAD_ATTR_METHOD': 259,
'LOAD_SUPER_METHOD': 260,
'LOAD_ZERO_SUPER_ATTR': 261,
'LOAD_ZERO_SUPER_METHOD': 262,
'POP_BLOCK': 263,
'SETUP_CLEANUP': 264,
'SETUP_FINALLY': 265,
'SETUP_WITH': 266,
'STORE_FAST_MAYBE_NULL': 267,
'LOAD_CLOSURE': 268,
'ANNOTATIONS_PLACEHOLDER': 256,
'JUMP': 257,
'JUMP_IF_FALSE': 258,
'JUMP_IF_TRUE': 259,
'JUMP_NO_INTERRUPT': 260,
'LOAD_CLOSURE': 261,
'POP_BLOCK': 262,
'SETUP_CLEANUP': 263,
'SETUP_FINALLY': 264,
'SETUP_WITH': 265,
'STORE_FAST_MAYBE_NULL': 266,
'LOAD_ATTR_METHOD': 267,
'LOAD_SUPER_METHOD': 268,
'LOAD_ZERO_SUPER_ATTR': 269,
'LOAD_ZERO_SUPER_METHOD': 270,
}
# CPython 3.13 compatible: opcodes < 44 have no argument
-1
View File
@@ -83,7 +83,6 @@ class StackEffectTests(unittest.TestCase):
self.assertRaises(ValueError, stack_effect, code)
self.assertRaises(ValueError, stack_effect, code, 0)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_stack_effect_jump(self):
FOR_ITER = dis.opmap['FOR_ITER']
self.assertEqual(stack_effect(FOR_ITER, 0), 1)
+4
View File
@@ -2185,24 +2185,28 @@ class BytecodeTests(InstructionTestCase, DisTestBase):
assert instr.positions == positions
class TestBytecodeTestCase(BytecodeTestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; RETURN_VALUE
def test_assert_not_in_with_op_not_in_bytecode(self):
code = compile("a = 1", "<string>", "exec")
self.assertInBytecode(code, "LOAD_CONST", 1)
self.assertNotInBytecode(code, "LOAD_NAME")
self.assertNotInBytecode(code, "LOAD_NAME", "a")
@unittest.expectedFailure # TODO: RUSTPYTHON; RETURN_VALUE
def test_assert_not_in_with_arg_not_in_bytecode(self):
code = compile("a = 1", "<string>", "exec")
self.assertInBytecode(code, "LOAD_CONST")
self.assertInBytecode(code, "LOAD_CONST", 1)
self.assertNotInBytecode(code, "LOAD_CONST", 2)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: AssertionError not raised
def test_assert_not_in_with_arg_in_bytecode(self):
code = compile("a = 1", "<string>", "exec")
with self.assertRaises(AssertionError):
self.assertNotInBytecode(code, "LOAD_CONST", 1)
class TestFinderMethods(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON
def test__find_imports(self):
cases = [
("import a.b.c", ('a.b.c', 0, None)),
+2 -2
View File
@@ -1385,7 +1385,7 @@ class PycRewritingTests(unittest.TestCase):
import sys
code_filename = sys._getframe().f_code.co_filename
module_filename = __file__
constant = 1
constant = 1000
def func():
pass
func_filename = func.__code__.co_filename
@@ -1455,7 +1455,7 @@ func_filename = func.__code__.co_filename
code = marshal.load(f)
constants = list(code.co_consts)
foreign_code = importlib.import_module.__code__
pos = constants.index(1)
pos = constants.index(1000)
constants[pos] = foreign_code
code = code.replace(co_consts=tuple(constants))
with open(self.compiled_name, "wb") as f:
+41 -13
View File
@@ -1136,6 +1136,13 @@ impl Compiler {
// Set the source range for the RESUME instruction
// For now, just use an empty range at the beginning
self.current_source_range = TextRange::default();
// For async functions/coroutines, emit RETURN_GENERATOR + POP_TOP before RESUME
if scope_type == CompilerScope::AsyncFunction {
emit!(self, Instruction::ReturnGenerator);
emit!(self, Instruction::PopTop);
}
emit!(
self,
Instruction::Resume {
@@ -1371,7 +1378,7 @@ impl Compiler {
if preserve_tos {
emit!(self, Instruction::Swap { index: 2 });
}
emit!(self, Instruction::PopTop);
emit!(self, Instruction::PopIter);
}
FBlockType::TryExcept => {
@@ -3623,6 +3630,7 @@ impl Compiler {
self.current_code_info().flags |= bytecode::CodeFlags::HAS_DOCSTRING;
}
// If no docstring, don't add None to co_consts
// Note: RETURN_GENERATOR + POP_TOP for async functions is emitted in enter_scope()
// Compile body statements
self.compile_statements(body)?;
@@ -4348,10 +4356,7 @@ impl Compiler {
// PEP 649: Initialize __classdict__ cell for class annotation scope
if self.current_symbol_table().needs_classdict {
let locals_name = self.name("locals");
emit!(self, Instruction::LoadName(locals_name));
emit!(self, Instruction::PushNull);
emit!(self, Instruction::Call { nargs: 0 });
emit!(self, Instruction::LoadLocals);
let classdict_idx = self.get_cell_var_index("__classdict__")?;
emit!(self, Instruction::StoreDeref(classdict_idx));
}
@@ -4978,8 +4983,10 @@ impl Compiler {
if is_async {
emit!(self, Instruction::EndAsyncFor);
} else {
// Pop the iterator after loop ends
emit!(self, Instruction::PopTop);
// END_FOR + POP_ITER pattern (CPython 3.14)
// FOR_ITER jumps to END_FOR, but VM skips it (+1) to reach POP_ITER
emit!(self, Instruction::EndFor);
emit!(self, Instruction::PopIter);
}
self.compile_statements(orelse)?;
@@ -6527,8 +6534,11 @@ impl Compiler {
}
);
// JUMP_NO_INTERRUPT send (regular JUMP in RustPython)
emit!(self, PseudoInstruction::Jump { target: send_block });
// JUMP_BACKWARD_NO_INTERRUPT send
emit!(
self,
PseudoInstruction::JumpNoInterrupt { target: send_block }
);
// fail: CLEANUP_THROW
// Stack when exception: [receiver, yielded_value, exc]
@@ -7424,7 +7434,9 @@ impl Compiler {
emit!(self, Instruction::EndAsyncFor);
emit!(self, Instruction::PopTop);
} else {
emit!(self, Instruction::PopTop);
// END_FOR + POP_ITER pattern (CPython 3.14)
emit!(self, Instruction::EndFor);
emit!(self, Instruction::PopIter);
}
}
@@ -7621,9 +7633,13 @@ impl Compiler {
self.switch_to_block(after_block);
if is_async {
emit!(self, Instruction::EndAsyncFor);
// Pop the iterator
emit!(self, Instruction::PopTop);
} else {
// END_FOR + POP_ITER pattern (CPython 3.14)
emit!(self, Instruction::EndFor);
emit!(self, Instruction::PopIter);
}
// Pop the iterator
emit!(self, Instruction::PopTop);
}
// Step 8: Clean up - restore saved locals
@@ -7741,6 +7757,18 @@ impl Compiler {
}
fn emit_load_const(&mut self, constant: ConstantData) {
// Use LOAD_SMALL_INT for integers in small int cache range (-5..=256)
// Still add to co_consts for compatibility (CPython does this too)
if let ConstantData::Integer { ref value } = constant
&& let Some(small_int) = value.to_i32()
&& (-5..=256).contains(&small_int)
{
// Add to co_consts even though we use LOAD_SMALL_INT
let _idx = self.arg_constant(constant);
// Store as u32 (two's complement for negative values)
self.emit_arg(small_int as u32, |idx| Instruction::LoadSmallInt { idx });
return;
}
let idx = self.arg_constant(constant);
self.emit_arg(idx, |idx| Instruction::LoadConst { idx })
}
@@ -7924,7 +7952,7 @@ impl Compiler {
// For break in a for loop, pop the iterator
if is_break && is_for_loop {
emit!(self, Instruction::PopTop);
emit!(self, Instruction::PopIter);
}
// Jump to target
+16 -7
View File
@@ -270,15 +270,16 @@ impl CodeInfo {
info.arg = OpArg(new_idx);
info.instr = Instruction::LoadFast(Arg::marker()).into();
}
PseudoInstruction::Jump { .. } => {
// PseudoInstruction::Jump instructions are handled later
PseudoInstruction::Jump { .. } | PseudoInstruction::JumpNoInterrupt { .. } => {
// Jump pseudo instructions are handled later
}
PseudoInstruction::JumpNoInterrupt { .. }
| PseudoInstruction::Reserved258
PseudoInstruction::AnnotationsPlaceholder
| PseudoInstruction::JumpIfFalse { .. }
| PseudoInstruction::JumpIfTrue { .. }
| PseudoInstruction::SetupCleanup
| PseudoInstruction::SetupFinally
| PseudoInstruction::SetupWith
| PseudoInstruction::StoreFastMaybeNull => {
| PseudoInstruction::StoreFastMaybeNull(_) => {
unimplemented!("Got a placeholder pseudo instruction ({instr:?})")
}
}
@@ -335,6 +336,14 @@ impl CodeInfo {
}
}
}
AnyInstruction::Pseudo(PseudoInstruction::JumpNoInterrupt { .. })
if target != BlockIdx::NULL =>
{
// JumpNoInterrupt is always backward (used in yield-from/await loops)
Instruction::JumpBackwardNoInterrupt {
target: Arg::marker(),
}
}
other => other.expect_real(),
};
@@ -468,7 +477,7 @@ impl CodeInfo {
let block = &self.blocks[block_idx];
for ins in &block.instructions {
let instr = &ins.instr;
let effect = instr.stack_effect(ins.arg, false);
let effect = instr.stack_effect(ins.arg);
if DEBUG {
let display_arg = if ins.target == BlockIdx::NULL {
ins.arg
@@ -493,7 +502,7 @@ impl CodeInfo {
}
// Process target blocks for branching instructions
if ins.target != BlockIdx::NULL {
let effect = instr.stack_effect(ins.arg, true);
// Both jump and non-jump paths have the same stack effect
let target_depth = depth.checked_add_signed(effect).ok_or({
if effect < 0 {
InternalError::StackUnderflow
@@ -1,159 +1,161 @@
---
source: crates/codegen/src/compile.rs
assertion_line: 8655
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\")"
---
3 0 LOAD_CONST (<code object test at ??? file "source_path", line 1>): 1 0 RESUME (0)
3 0 LOAD_CONST (<code object test at ??? file "source_path", line 1>): 1 0 RUSTPYTHON_PLACEHOLDER
1 POP_TOP
2 RESUME (0)
2 1 LOAD_GLOBAL (0, StopIteration)
2 PUSH_NULL
3 LOAD_CONST ("spam")
4 CALL (1)
5 LOAD_GLOBAL (1, StopAsyncIteration)
6 PUSH_NULL
7 LOAD_CONST ("ham")
8 CALL (1)
9 BUILD_TUPLE (2)
10 GET_ITER
>> 11 FOR_ITER (139)
12 STORE_FAST (0, stop_exc)
2 3 LOAD_GLOBAL (0, StopIteration)
4 PUSH_NULL
5 LOAD_CONST ("spam")
6 CALL (1)
7 LOAD_GLOBAL (1, StopAsyncIteration)
8 PUSH_NULL
9 LOAD_CONST ("ham")
10 CALL (1)
11 BUILD_TUPLE (2)
12 GET_ITER
>> 13 FOR_ITER (141)
14 STORE_FAST (0, stop_exc)
3 13 LOAD_GLOBAL (2, self)
14 LOAD_ATTR (7, subTest, method=true)
15 LOAD_GLOBAL (4, type)
16 PUSH_NULL
17 LOAD_FAST (0, stop_exc)
18 CALL (1)
19 LOAD_CONST (("type"))
20 CALL_KW (1)
21 COPY (1)
22 LOAD_SPECIAL (__exit__)
23 SWAP (2)
24 LOAD_SPECIAL (__enter__)
25 PUSH_NULL
26 CALL (0)
27 POP_TOP
3 15 LOAD_GLOBAL (2, self)
16 LOAD_ATTR (7, subTest, method=true)
17 LOAD_GLOBAL (4, type)
18 PUSH_NULL
19 LOAD_FAST (0, stop_exc)
20 CALL (1)
21 LOAD_CONST (("type"))
22 CALL_KW (1)
23 COPY (1)
24 LOAD_SPECIAL (__exit__)
25 SWAP (2)
26 LOAD_SPECIAL (__enter__)
27 PUSH_NULL
28 CALL (0)
29 POP_TOP
5 28 LOAD_GLOBAL (5, egg)
29 PUSH_NULL
30 CALL (0)
31 COPY (1)
32 LOAD_SPECIAL (__aexit__)
33 SWAP (2)
34 LOAD_SPECIAL (__aenter__)
35 PUSH_NULL
36 CALL (0)
37 GET_AWAITABLE
38 LOAD_CONST (None)
>> 39 SEND (44)
40 YIELD_VALUE (1)
41 RESUME (3)
42 JUMP_BACKWARD (39)
43 CLEANUP_THROW
>> 44 END_SEND
45 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
40 LOAD_CONST (None)
>> 41 SEND (46)
42 YIELD_VALUE (1)
43 RESUME (3)
44 JUMP_BACKWARD_NO_INTERRUPT(41)
45 CLEANUP_THROW
>> 46 END_SEND
47 POP_TOP
6 46 LOAD_FAST (0, stop_exc)
47 RAISE_VARARGS (Raise)
6 48 LOAD_FAST (0, stop_exc)
49 RAISE_VARARGS (Raise)
5 48 PUSH_NULL
49 LOAD_CONST (None)
50 LOAD_CONST (None)
5 50 PUSH_NULL
51 LOAD_CONST (None)
52 CALL (3)
53 GET_AWAITABLE
54 LOAD_CONST (None)
>> 55 SEND (60)
56 YIELD_VALUE (1)
57 RESUME (3)
58 JUMP_BACKWARD (55)
59 CLEANUP_THROW
>> 60 END_SEND
61 POP_TOP
62 JUMP_FORWARD (84)
63 PUSH_EXC_INFO
64 WITH_EXCEPT_START
65 GET_AWAITABLE
66 LOAD_CONST (None)
>> 67 SEND (72)
68 YIELD_VALUE (1)
69 RESUME (3)
70 JUMP_BACKWARD (67)
71 CLEANUP_THROW
>> 72 END_SEND
73 TO_BOOL
74 POP_JUMP_IF_TRUE (76)
75 RERAISE (2)
>> 76 POP_TOP
77 POP_EXCEPT
78 POP_TOP
79 POP_TOP
80 JUMP_FORWARD (84)
81 COPY (3)
82 POP_EXCEPT
83 RERAISE (1)
>> 84 JUMP_FORWARD (110)
85 PUSH_EXC_INFO
52 LOAD_CONST (None)
53 LOAD_CONST (None)
54 CALL (3)
55 GET_AWAITABLE
56 LOAD_CONST (None)
>> 57 SEND (62)
58 YIELD_VALUE (1)
59 RESUME (3)
60 JUMP_BACKWARD_NO_INTERRUPT(57)
61 CLEANUP_THROW
>> 62 END_SEND
63 POP_TOP
64 JUMP_FORWARD (86)
65 PUSH_EXC_INFO
66 WITH_EXCEPT_START
67 GET_AWAITABLE
68 LOAD_CONST (None)
>> 69 SEND (74)
70 YIELD_VALUE (1)
71 RESUME (3)
72 JUMP_BACKWARD_NO_INTERRUPT(69)
73 CLEANUP_THROW
>> 74 END_SEND
75 TO_BOOL
76 POP_JUMP_IF_TRUE (78)
77 RERAISE (2)
>> 78 POP_TOP
79 POP_EXCEPT
80 POP_TOP
81 POP_TOP
82 JUMP_FORWARD (86)
83 COPY (3)
84 POP_EXCEPT
85 RERAISE (1)
>> 86 JUMP_FORWARD (112)
87 PUSH_EXC_INFO
7 86 LOAD_GLOBAL (6, Exception)
87 CHECK_EXC_MATCH
88 POP_JUMP_IF_FALSE (106)
89 STORE_FAST (1, ex)
7 88 LOAD_GLOBAL (6, Exception)
89 CHECK_EXC_MATCH
90 POP_JUMP_IF_FALSE (108)
91 STORE_FAST (1, ex)
8 90 LOAD_GLOBAL (2, self)
91 LOAD_ATTR (15, assertIs, method=true)
92 LOAD_FAST (1, ex)
93 LOAD_FAST (0, stop_exc)
94 CALL (2)
95 POP_TOP
96 JUMP_FORWARD (101)
97 LOAD_CONST (None)
98 STORE_FAST (1, ex)
99 DELETE_FAST (1, ex)
100 RAISE_VARARGS (ReraiseFromStack)
>> 101 POP_EXCEPT
102 LOAD_CONST (None)
103 STORE_FAST (1, ex)
104 DELETE_FAST (1, ex)
105 JUMP_FORWARD (118)
>> 106 RAISE_VARARGS (ReraiseFromStack)
107 COPY (3)
108 POP_EXCEPT
109 RAISE_VARARGS (ReraiseFromStack)
8 92 LOAD_GLOBAL (2, self)
93 LOAD_ATTR (15, assertIs, method=true)
94 LOAD_FAST (1, ex)
95 LOAD_FAST (0, stop_exc)
96 CALL (2)
97 POP_TOP
98 JUMP_FORWARD (103)
99 LOAD_CONST (None)
100 STORE_FAST (1, ex)
101 DELETE_FAST (1, ex)
102 RAISE_VARARGS (ReraiseFromStack)
>> 103 POP_EXCEPT
104 LOAD_CONST (None)
105 STORE_FAST (1, ex)
106 DELETE_FAST (1, ex)
107 JUMP_FORWARD (120)
>> 108 RAISE_VARARGS (ReraiseFromStack)
109 COPY (3)
110 POP_EXCEPT
111 RAISE_VARARGS (ReraiseFromStack)
10 >> 110 LOAD_GLOBAL (2, self)
111 LOAD_ATTR (17, fail, method=true)
112 LOAD_FAST (0, stop_exc)
113 FORMAT_SIMPLE
114 LOAD_CONST (" was suppressed")
115 BUILD_STRING (2)
116 CALL (1)
117 POP_TOP
10 >> 112 LOAD_GLOBAL (2, self)
113 LOAD_ATTR (17, fail, method=true)
114 LOAD_FAST (0, stop_exc)
115 FORMAT_SIMPLE
116 LOAD_CONST (" was suppressed")
117 BUILD_STRING (2)
118 CALL (1)
119 POP_TOP
3 >> 118 PUSH_NULL
119 LOAD_CONST (None)
120 LOAD_CONST (None)
3 >> 120 PUSH_NULL
121 LOAD_CONST (None)
122 CALL (3)
123 POP_TOP
124 JUMP_FORWARD (138)
125 PUSH_EXC_INFO
126 WITH_EXCEPT_START
127 TO_BOOL
128 POP_JUMP_IF_TRUE (130)
129 RERAISE (2)
>> 130 POP_TOP
131 POP_EXCEPT
132 POP_TOP
133 POP_TOP
134 JUMP_FORWARD (138)
135 COPY (3)
136 POP_EXCEPT
137 RERAISE (1)
>> 138 JUMP_BACKWARD (11)
>> 139 POP_TOP
140 LOAD_CONST (None)
141 RETURN_VALUE
122 LOAD_CONST (None)
123 LOAD_CONST (None)
124 CALL (3)
125 POP_TOP
126 JUMP_FORWARD (140)
127 PUSH_EXC_INFO
128 WITH_EXCEPT_START
129 TO_BOOL
130 POP_JUMP_IF_TRUE (132)
131 RERAISE (2)
>> 132 POP_TOP
133 POP_EXCEPT
134 POP_TOP
135 POP_TOP
136 JUMP_FORWARD (140)
137 COPY (3)
138 POP_EXCEPT
139 RERAISE (1)
>> 140 JUMP_BACKWARD (13)
>> 141 END_FOR
142 POP_ITER
143 LOAD_CONST (None)
144 RETURN_VALUE
1 MAKE_FUNCTION
2 STORE_NAME (0, test)
+21 -3
View File
@@ -845,7 +845,13 @@ impl SymbolTableBuilder {
let is_nested = self
.tables
.last()
.map(|table| table.is_nested || table.typ == CompilerScope::Function)
.map(|table| {
table.is_nested
|| matches!(
table.typ,
CompilerScope::Function | CompilerScope::AsyncFunction
)
})
.unwrap_or(false);
let table = SymbolTable::new(name.to_owned(), typ, line_number, is_nested);
self.tables.push(table);
@@ -1103,6 +1109,7 @@ impl SymbolTableBuilder {
type_params,
returns,
range,
is_async,
..
}) => {
self.scan_decorators(decorator_list, ExpressionContext::Load)?;
@@ -1142,6 +1149,7 @@ impl SymbolTableBuilder {
parameters,
self.line_index_start(*range),
has_return_annotation,
*is_async,
)?;
self.scan_statements(body)?;
self.leave_scope();
@@ -1709,7 +1717,10 @@ impl SymbolTableBuilder {
// Interesting stuff about the __class__ variable:
// https://docs.python.org/3/reference/datamodel.html?highlight=__class__#creating-the-class-object
if context == ExpressionContext::Load
&& self.tables.last().unwrap().typ == CompilerScope::Function
&& matches!(
self.tables.last().unwrap().typ,
CompilerScope::Function | CompilerScope::AsyncFunction
)
&& id == "super"
{
self.register_name("__class__", SymbolUsage::Used, *range)?;
@@ -1727,6 +1738,7 @@ impl SymbolTableBuilder {
parameters,
self.line_index_start(expression.range()),
false, // lambdas have no return annotation
false, // lambdas are never async
)?;
} else {
self.enter_scope(
@@ -2093,6 +2105,7 @@ impl SymbolTableBuilder {
parameters: &ast::Parameters,
line_number: u32,
has_return_annotation: bool,
is_async: bool,
) -> SymbolTableResult {
// Evaluate eventual default parameters:
for default in parameters
@@ -2157,7 +2170,12 @@ impl SymbolTableBuilder {
None
};
self.enter_scope(name, CompilerScope::Function, line_number);
let scope_type = if is_async {
CompilerScope::AsyncFunction
} else {
CompilerScope::Function
};
self.enter_scope(name, scope_type, line_number);
// Move annotation_block to function scope only if we have one
if let Some(block) = annotation_block {
@@ -31,7 +31,7 @@ pub enum Instruction {
CheckExcMatch = 6,
CleanupThrow = 7,
DeleteSubscr = 8,
EndFor = 9, // Placeholder
EndFor = 9,
EndSend = 10,
ExitInitCheck = 11, // Placeholder
FormatSimple = 12,
@@ -52,11 +52,11 @@ pub enum Instruction {
Nop = 27,
NotTaken = 28, // Placeholder
PopExcept = 29,
PopIter = 30, // Placeholder
PopIter = 30,
PopTop = 31,
PushExcInfo = 32,
PushNull = 33,
ReturnGenerator = 34, // Placeholder
ReturnGenerator = 34,
ReturnValue = 35,
SetupAnnotations = 36,
StoreSlice = 37, // Placeholder
@@ -190,7 +190,7 @@ pub enum Instruction {
LoadName(Arg<NameIdx>) = 93,
LoadSmallInt {
idx: Arg<u32>,
} = 94, // Placeholder
} = 94,
LoadSpecial {
method: Arg<SpecialMethod>,
} = 95,
@@ -269,12 +269,12 @@ pub enum Instruction {
BinaryOpExtend = 132, // Placeholder
BinaryOpMultiplyFloat = 133, // Placeholder
BinaryOpMultiplyInt = 134, // Placeholder
BinarySubscrDict = 135, // Placeholder
BinarySubscrGetitem = 136, // Placeholder
BinarySubscrListInt = 137, // Placeholder
BinarySubscrListSlice = 138, // Placeholder
BinarySubscrStrInt = 139, // Placeholder
BinarySubscrTupleInt = 140, // Placeholder
BinaryOpSubscrDict = 135, // Placeholder
BinaryOpSubscrGetitem = 136, // Placeholder
BinaryOpSubscrListInt = 137, // Placeholder
BinaryOpSubscrListSlice = 138, // Placeholder
BinaryOpSubscrStrInt = 139, // Placeholder
BinaryOpSubscrTupleInt = 140, // Placeholder
BinaryOpSubtractFloat = 141, // Placeholder
BinaryOpSubtractInt = 142, // Placeholder
CallAllocAndEnterInit = 143, // Placeholder
@@ -444,7 +444,7 @@ impl InstructionMetadata for Instruction {
)
}
fn stack_effect(&self, arg: OpArg, jump: bool) -> i32 {
fn stack_effect(&self, arg: OpArg) -> i32 {
match self {
Self::Nop => 0,
Self::NotTaken => 0,
@@ -506,11 +506,7 @@ impl InstructionMetadata for Instruction {
Self::ConvertValue { .. } => 0,
Self::FormatSimple => 0,
Self::FormatWithSpec => -1,
Self::ForIter { .. } => {
// jump=False: push next value (+1)
// jump=True: iterator stays on stack, no change (0)
if jump { 0 } else { 1 }
}
Self::ForIter { .. } => 1, // push next value
Self::IsOp(_) => -1,
Self::ContainsOp(_) => -1,
Self::ReturnValue => -1,
@@ -571,7 +567,7 @@ impl InstructionMetadata for Instruction {
let UnpackExArgs { before, after } = args.get(arg);
-1 + before as i32 + 1 + after as i32
}
Self::PopExcept => 0,
Self::PopExcept => -1,
Self::PopIter => -1,
Self::GetAwaitable => 0,
Self::GetAIter => 0,
@@ -595,14 +591,14 @@ impl InstructionMetadata for Instruction {
}
// Pseudo instructions (calculated before conversion)
Self::Cache => 0,
Self::BinarySlice => 0,
Self::BinarySlice => -2, // (container, start, stop -- res)
Self::BinaryOpInplaceAddUnicode => 0,
Self::EndFor => 0,
Self::ExitInitCheck => 0,
Self::EndFor => -1, // pop next value at end of loop iteration
Self::ExitInitCheck => -1, // (should_be_none -- )
Self::InterpreterExit => 0,
Self::LoadLocals => 0,
Self::ReturnGenerator => 0,
Self::StoreSlice => 0,
Self::LoadLocals => 1, // ( -- locals)
Self::ReturnGenerator => 1, // pushes None for POP_TOP to consume
Self::StoreSlice => -4, // (v, container, start, stop -- )
Self::CopyFreeVars { .. } => 0,
Self::EnterExecutor => 0,
Self::JumpBackwardNoInterrupt { .. } => 0,
@@ -614,8 +610,8 @@ impl InstructionMetadata for Instruction {
Self::LoadFromDictOrGlobals(_) => 0,
Self::MakeCell(_) => 0,
Self::StoreFastStoreFast { .. } => 0,
Self::PopJumpIfNone { .. } => 0,
Self::PopJumpIfNotNone { .. } => 0,
Self::PopJumpIfNone { .. } => -1, // (value -- )
Self::PopJumpIfNotNone { .. } => -1, // (value -- )
Self::BinaryOpAddFloat => 0,
Self::BinaryOpAddInt => 0,
Self::BinaryOpAddUnicode => 0,
@@ -624,12 +620,12 @@ impl InstructionMetadata for Instruction {
Self::BinaryOpMultiplyInt => 0,
Self::BinaryOpSubtractFloat => 0,
Self::BinaryOpSubtractInt => 0,
Self::BinarySubscrDict => 0,
Self::BinarySubscrGetitem => 0,
Self::BinarySubscrListInt => 0,
Self::BinarySubscrListSlice => 0,
Self::BinarySubscrStrInt => 0,
Self::BinarySubscrTupleInt => 0,
Self::BinaryOpSubscrDict => 0,
Self::BinaryOpSubscrGetitem => 0,
Self::BinaryOpSubscrListInt => 0,
Self::BinaryOpSubscrListSlice => 0,
Self::BinaryOpSubscrStrInt => 0,
Self::BinaryOpSubscrTupleInt => 0,
Self::CallAllocAndEnterInit => 0,
Self::CallBoundMethodExactArgs => 0,
Self::CallBoundMethodGeneral => 0,
@@ -848,6 +844,7 @@ impl InstructionMetadata for Instruction {
Self::LoadBuildClass => w!(LOAD_BUILD_CLASS),
Self::LoadFromDictOrDeref(i) => w!(LOAD_FROM_DICT_OR_DEREF, cell_name = i),
Self::LoadConst { idx } => fmt_const("LOAD_CONST", arg, f, idx),
Self::LoadSmallInt { idx } => w!(LOAD_SMALL_INT, idx),
Self::LoadDeref(idx) => w!(LOAD_DEREF, cell_name = idx),
Self::LoadFast(idx) => w!(LOAD_FAST, varname = idx),
Self::LoadFastAndClear(idx) => w!(LOAD_FAST_AND_CLEAR, varname = idx),
@@ -875,6 +872,8 @@ impl InstructionMetadata for Instruction {
Self::PopJumpIfFalse { target } => w!(POP_JUMP_IF_FALSE, target),
Self::PopJumpIfTrue { target } => w!(POP_JUMP_IF_TRUE, target),
Self::PopTop => w!(POP_TOP),
Self::EndFor => w!(END_FOR),
Self::PopIter => w!(POP_ITER),
Self::PushExcInfo => w!(PUSH_EXC_INFO),
Self::PushNull => w!(PUSH_NULL),
Self::RaiseVarargs { kind } => w!(RAISE_VARARGS, ?kind),
@@ -917,37 +916,48 @@ impl InstructionMetadata for Instruction {
}
/// Instructions used by the compiler. They are not executed by the VM.
///
/// CPython 3.14.2 aligned (256-266), RustPython-specific variants start at 267.
#[derive(Clone, Copy, Debug)]
#[repr(u16)]
pub enum PseudoInstruction {
// CPython 3.14.2 pseudo instructions (256-266)
AnnotationsPlaceholder = 256,
Jump {
target: Arg<Label>,
} = 256,
} = 257,
JumpIfFalse {
target: Arg<Label>,
} = 258,
JumpIfTrue {
target: Arg<Label>,
} = 259,
JumpNoInterrupt {
target: Arg<Label>,
} = 257, // Placeholder
Reserved258 = 258,
} = 260,
LoadClosure(Arg<NameIdx>) = 261,
PopBlock = 262,
SetupCleanup = 263,
SetupFinally = 264,
SetupWith = 265,
StoreFastMaybeNull(Arg<NameIdx>) = 266,
// RustPython-specific pseudo instructions (267+)
LoadAttrMethod {
idx: Arg<NameIdx>,
} = 259,
} = 267,
// "Zero" variants are for 0-arg super() calls (has_class=false).
// Non-"Zero" variants are for 2-arg super(cls, self) calls (has_class=true).
/// 2-arg super(cls, self).method() - has_class=true, load_method=true
LoadSuperMethod {
idx: Arg<NameIdx>,
} = 260,
} = 268,
LoadZeroSuperAttr {
idx: Arg<NameIdx>,
} = 261,
} = 269,
LoadZeroSuperMethod {
idx: Arg<NameIdx>,
} = 262,
PopBlock = 263,
SetupCleanup = 264, // Placeholder
SetupFinally = 265, // Placeholder
SetupWith = 266, // Placeholder
StoreFastMaybeNull = 267, // Placeholder
LoadClosure(Arg<NameIdx>) = 268,
} = 270,
}
const _: () = assert!(mem::size_of::<PseudoInstruction>() == 2);
@@ -965,10 +975,8 @@ impl TryFrom<u16> for PseudoInstruction {
#[inline]
fn try_from(value: u16) -> Result<Self, MarshalError> {
let start = u16::from(Self::Jump {
target: Arg::marker(),
});
let end = u16::from(Self::LoadClosure(Arg::marker()));
let start = u16::from(Self::AnnotationsPlaceholder);
let end = u16::from(Self::LoadZeroSuperMethod { idx: Arg::marker() });
if (start..=end).contains(&value) {
Ok(unsafe { mem::transmute::<u16, Self>(value) })
@@ -981,30 +989,35 @@ impl TryFrom<u16> for PseudoInstruction {
impl InstructionMetadata for PseudoInstruction {
fn label_arg(&self) -> Option<Arg<Label>> {
match self {
Self::Jump { target: l } => Some(*l),
Self::Jump { target: l }
| Self::JumpIfFalse { target: l }
| Self::JumpIfTrue { target: l }
| Self::JumpNoInterrupt { target: l } => Some(*l),
_ => None,
}
}
fn unconditional_branch(&self) -> bool {
matches!(self, Self::Jump { .. },)
matches!(self, Self::Jump { .. } | Self::JumpNoInterrupt { .. })
}
fn stack_effect(&self, _arg: OpArg, _jump: bool) -> i32 {
fn stack_effect(&self, _arg: OpArg) -> i32 {
match self {
Self::AnnotationsPlaceholder => 0,
Self::Jump { .. } => 0,
Self::JumpIfFalse { .. } => 0, // peek, don't pop: COPY + TO_BOOL + POP_JUMP_IF_FALSE
Self::JumpIfTrue { .. } => 0, // peek, don't pop: COPY + TO_BOOL + POP_JUMP_IF_TRUE
Self::JumpNoInterrupt { .. } => 0,
Self::LoadAttrMethod { .. } => 1, // pop obj, push method + self_or_null
Self::LoadSuperMethod { .. } => -3 + 2, // pop 3, push [method, self_or_null]
Self::LoadZeroSuperAttr { .. } => -3 + 1, // pop 3, push [attr]
Self::LoadZeroSuperMethod { .. } => -3 + 2, // pop 3, push [method, self_or_null]
Self::LoadClosure(_) => 1,
Self::PopBlock => 0,
Self::SetupCleanup => 0,
Self::SetupFinally => 0,
Self::SetupWith => 0,
Self::StoreFastMaybeNull => 0,
Self::Reserved258 => 0,
Self::LoadClosure(_) => 1,
Self::StoreFastMaybeNull(_) => -1,
Self::LoadAttrMethod { .. } => 1, // pop obj, push method + self_or_null
Self::LoadSuperMethod { .. } => -3 + 2, // pop 3, push [method, self_or_null]
Self::LoadZeroSuperAttr { .. } => -3 + 1, // pop 3, push [attr]
Self::LoadZeroSuperMethod { .. } => -3 + 2, // pop 3, push [method, self_or_null]
}
}
@@ -1074,7 +1087,7 @@ impl InstructionMetadata for AnyInstruction {
inst_either!(fn unconditional_branch(&self) -> bool);
inst_either!(fn stack_effect(&self, arg: OpArg, jump: bool) -> i32);
inst_either!(fn stack_effect(&self, arg: OpArg) -> i32);
inst_either!(fn fmt_dis(
&self,
@@ -1148,9 +1161,9 @@ pub trait InstructionMetadata {
/// use rustpython_compiler_core::bytecode::{Arg, Instruction, Label, InstructionMetadata};
/// let (target, jump_arg) = Arg::new(Label(0xF));
/// let jump_instruction = Instruction::JumpForward { target };
/// assert_eq!(jump_instruction.stack_effect(jump_arg, true), 0);
/// assert_eq!(jump_instruction.stack_effect(jump_arg), 0);
/// ```
fn stack_effect(&self, arg: OpArg, jump: bool) -> i32;
fn stack_effect(&self, arg: OpArg) -> i32;
#[allow(clippy::too_many_arguments)]
fn fmt_dis(
+3 -2
View File
@@ -290,8 +290,9 @@ op_arg_enum!(
Print = 1,
/// Import * operation
ImportStar = 2,
// StopIterationError = 3,
// AsyncGenWrap = 4,
/// Convert StopIteration to RuntimeError in async context
StopIterationError = 3,
AsyncGenWrap = 4,
UnaryPositive = 5,
/// Convert list to tuple
ListToTuple = 6,
+6
View File
@@ -572,6 +572,12 @@ impl<'a, 'b> FunctionCompiler<'a, 'b> {
self.stack.push(val);
Ok(())
}
Instruction::LoadSmallInt { idx } => {
let small_int = idx.get(arg) as i64;
let val = self.builder.ins().iconst(types::I64, small_int);
self.stack.push(JitValue::Int(val));
Ok(())
}
Instruction::LoadFast(idx) => {
let local = self.variables[idx.get(arg) as usize]
.as_ref()
+2 -1
View File
@@ -194,7 +194,8 @@ mod _opcode {
let opcode = Opcode::try_from_pyint(args.opcode, vm)?;
Ok(opcode.stack_effect(oparg.into(), jump))
let _ = jump; // Python API accepts jump but it's not used
Ok(opcode.stack_effect(oparg.into()))
}
#[pyfunction]
+100 -3
View File
@@ -1134,6 +1134,12 @@ impl ExecutingFrame<'_> {
self.push_value(vm.builtins.get_attr(identifier!(vm, __build_class__), vm)?);
Ok(None)
}
Instruction::LoadLocals => {
// Push the locals dict onto the stack
let locals = self.locals.clone().into_object();
self.push_value(locals);
Ok(None)
}
Instruction::LoadFromDictOrDeref(i) => {
let i = i.get(arg) as usize;
let name = if i < self.code.cellvars.len() {
@@ -1177,6 +1183,12 @@ impl ExecutingFrame<'_> {
self.push_value(self.code.constants[idx.get(arg) as usize].clone().into());
Ok(None)
}
Instruction::LoadSmallInt { idx } => {
// Push small integer (-5..=256) directly without constant table lookup
let value = vm.ctx.new_int(idx.get(arg) as i32);
self.push_value(value.into());
Ok(None)
}
Instruction::LoadDeref(i) => {
let idx = i.get(arg) as usize;
let x = self.cells_frees[idx]
@@ -1487,6 +1499,15 @@ impl ExecutingFrame<'_> {
Ok(None)
}
Instruction::Nop => Ok(None),
Instruction::ReturnGenerator => {
// In RustPython, generators/coroutines are created in function.rs
// before the frame starts executing. The RETURN_GENERATOR instruction
// pushes None so that the following POP_TOP has something to consume.
// This matches CPython's semantics where the sent value (None for first call)
// is on the stack when the generator resumes.
self.push_value(vm.ctx.none());
Ok(None)
}
Instruction::PopExcept => {
// Pop prev_exc from value stack and restore it
let prev_exc = self.pop_value();
@@ -1507,11 +1528,35 @@ impl ExecutingFrame<'_> {
}
Instruction::PopJumpIfFalse { target } => self.pop_jump_if(vm, target.get(arg), false),
Instruction::PopJumpIfTrue { target } => self.pop_jump_if(vm, target.get(arg), true),
Instruction::PopJumpIfNone { target } => {
let value = self.pop_value();
if vm.is_none(&value) {
self.jump(target.get(arg));
}
Ok(None)
}
Instruction::PopJumpIfNotNone { target } => {
let value = self.pop_value();
if !vm.is_none(&value) {
self.jump(target.get(arg));
}
Ok(None)
}
Instruction::PopTop => {
// Pop value from stack and ignore.
self.pop_value();
Ok(None)
}
Instruction::EndFor => {
// Pop the next value from stack (cleanup after loop body)
self.pop_value();
Ok(None)
}
Instruction::PopIter => {
// Pop the iterator from stack (end of for loop)
self.pop_value();
Ok(None)
}
Instruction::PushNull => {
// Push NULL for self_or_null slot in call protocol
self.push_null();
@@ -1765,6 +1810,17 @@ impl ExecutingFrame<'_> {
self.push_value(value);
Ok(None)
}
Instruction::ExitInitCheck => {
// Check that __init__ returned None
let should_be_none = self.pop_value();
if !vm.is_none(&should_be_none) {
return Err(vm.new_type_error(format!(
"__init__() should return None, not '{}'",
should_be_none.class().name()
)));
}
Ok(None)
}
Instruction::CleanupThrow => {
// CLEANUP_THROW: (sub_iter, last_sent_val, exc) -> (None, value) OR re-raise
// If StopIteration: pop all 3, extract value, push (None, value)
@@ -2285,9 +2341,26 @@ impl ExecutingFrame<'_> {
Ok(None)
}
Ok(PyIterReturn::StopIteration(_)) => {
// CPython 3.14: Do NOT pop iterator here
// POP_ITER instruction will handle cleanup after the loop
self.jump(target);
// Check if target instruction is END_FOR (CPython 3.14 pattern)
// If so, skip it and jump to target + 1 instruction (POP_ITER)
let target_idx = target.0 as usize;
let jump_target = if let Some(unit) = self.code.instructions.get(target_idx) {
if matches!(unit.op, bytecode::Instruction::EndFor)
&& matches!(
self.code.instructions.get(target_idx + 1).map(|u| &u.op),
Some(bytecode::Instruction::PopIter)
)
{
// Skip END_FOR, jump to POP_ITER
bytecode::Label(target.0 + 1)
} else {
// Legacy pattern: jump directly to target (POP_TOP/POP_ITER)
target
}
} else {
target
};
self.jump(jump_target);
Ok(None)
}
Err(next_error) => {
@@ -2684,6 +2757,30 @@ impl ExecutingFrame<'_> {
.map_err(|_| vm.new_type_error("LIST_TO_TUPLE expects a list"))?;
Ok(vm.ctx.new_tuple(list.borrow_vec().to_vec()).into())
}
bytecode::IntrinsicFunction1::StopIterationError => {
// Convert StopIteration to RuntimeError
// Used to ensure async generators don't raise StopIteration directly
// _PyGen_FetchStopIterationValue
// Use fast_isinstance to handle subclasses of StopIteration
if arg.fast_isinstance(vm.ctx.exceptions.stop_iteration) {
Err(vm.new_runtime_error("coroutine raised StopIteration"))
} else {
// If not StopIteration, just re-raise the original exception
Err(arg.downcast().unwrap_or_else(|obj| {
vm.new_runtime_error(format!(
"unexpected exception type: {:?}",
obj.class()
))
}))
}
}
bytecode::IntrinsicFunction1::AsyncGenWrap => {
// Wrap value for async generator
// Creates an AsyncGenWrappedValue
Ok(crate::builtins::asyncgenerator::PyAsyncGenWrappedValue(arg)
.into_ref(&vm.ctx)
.into())
}
}
}