From a6aab76d23ee4bbe82987012d9777a29d0051bb6 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 13:40:40 +0900 Subject: [PATCH 01/15] Match CPython LOAD_SPECIAL stack semantics for with/async-with LOAD_SPECIAL now pushes (callable, self_or_null) matching CPython's CALL convention, instead of a single bound method: - Function descriptors: push (func, self) - Plain attributes: push (bound, NULL) Updated all with-statement paths: - Entry: add SWAP 3 after SWAP 2, remove PUSH_NULL before CALL 0 - Normal exit: remove PUSH_NULL before CALL 3 - Exception handler (WITH_EXCEPT_START): read exit_func at TOS-4 and self_or_null at TOS-3 - Suppress block: 3 POP_TOPs after POP_EXCEPT (was 2) - FBlock exit (preserve_tos): SWAP 3 + SWAP 2 rotation - UnwindAction::With: remove PUSH_NULL Stack effects updated: LoadSpecial (2,1), WithExceptStart (7,6) --- crates/codegen/src/compile.rs | 74 ++++------ ...pile__tests__nested_double_async_with.snap | 131 +++++++++--------- .../compiler-core/src/bytecode/instruction.rs | 4 +- crates/vm/src/frame.rs | 40 +++--- 4 files changed, 118 insertions(+), 131 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index dba8780c902..13fda31ae4f 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -1476,25 +1476,20 @@ impl Compiler { } FBlockType::With | FBlockType::AsyncWith => { - // Stack when entering: [..., __exit__, return_value (if preserve_tos)] - // Need to call __exit__(None, None, None) - + // Stack: [..., exit_func, self_exit, return_value (if preserve_tos)] emit!(self, PseudoInstruction::PopBlock); - // If preserving return value, swap it below __exit__ if preserve_tos { - emit!(self, Instruction::Swap { i: 2 }); + // Rotate return value below the exit pair + // [exit_func, self_exit, value] → [value, exit_func, self_exit] + emit!(self, Instruction::Swap { i: 3 }); // [value, self_exit, exit_func] + emit!(self, Instruction::Swap { i: 2 }); // [value, exit_func, self_exit] } - // Stack after swap: [..., return_value, __exit__] or [..., __exit__] - // Call __exit__(None, None, None) - // Call protocol: [callable, self_or_null, arg1, arg2, arg3] - emit!(self, Instruction::PushNull); - // Stack: [..., __exit__, NULL] + // Call exit_func(self_exit, None, None, None) self.emit_load_const(ConstantData::None); self.emit_load_const(ConstantData::None); self.emit_load_const(ConstantData::None); - // Stack: [..., __exit__, NULL, None, None, None] emit!(self, Instruction::Call { argc: 3 }); // For async with, await the result @@ -5180,16 +5175,16 @@ impl Compiler { Instruction::LoadSpecial { method: SpecialMethod::AExit } - ); // [cm, bound_aexit] - emit!(self, Instruction::Swap { i: 2 }); // [bound_aexit, cm] + ); // [cm, aexit_func, self_ae] + emit!(self, Instruction::Swap { i: 2 }); // [cm, self_ae, aexit_func] + emit!(self, Instruction::Swap { i: 3 }); // [aexit_func, self_ae, cm] emit!( self, Instruction::LoadSpecial { method: SpecialMethod::AEnter } - ); // [bound_aexit, bound_aenter] - emit!(self, Instruction::PushNull); - emit!(self, Instruction::Call { argc: 0 }); // [bound_aexit, awaitable] + ); // [aexit_func, self_ae, aenter_func, self_an] + emit!(self, Instruction::Call { argc: 0 }); // [aexit_func, self_ae, awaitable] emit!(self, Instruction::GetAwaitable { r#where: 1 }); self.emit_load_const(ConstantData::None); let _ = self.compile_yield_from_sequence(true)?; @@ -5200,16 +5195,16 @@ impl Compiler { Instruction::LoadSpecial { method: SpecialMethod::Exit } - ); // [cm, bound_exit] - emit!(self, Instruction::Swap { i: 2 }); // [bound_exit, cm] + ); // [cm, exit_func, self_exit] + emit!(self, Instruction::Swap { i: 2 }); // [cm, self_exit, exit_func] + emit!(self, Instruction::Swap { i: 3 }); // [exit_func, self_exit, cm] emit!( self, Instruction::LoadSpecial { method: SpecialMethod::Enter } - ); // [bound_exit, bound_enter] - emit!(self, Instruction::PushNull); - emit!(self, Instruction::Call { argc: 0 }); // [bound_exit, enter_result] + ); // [exit_func, self_exit, enter_func, self_enter] + emit!(self, Instruction::Call { argc: 0 }); // [exit_func, self_exit, result] } // Stack: [..., __exit__, enter_result] @@ -5263,10 +5258,9 @@ impl Compiler { }); // ===== Normal exit path ===== - // Stack: [..., bound_exit] - // Call bound_exit(None, None, None) + // Stack: [..., exit_func, self_exit] + // Call exit_func(self_exit, None, None, None) self.set_source_range(with_range); - emit!(self, Instruction::PushNull); self.emit_load_const(ConstantData::None); self.emit_load_const(ConstantData::None); self.emit_load_const(ConstantData::None); @@ -5280,11 +5274,10 @@ impl Compiler { emit!(self, PseudoInstruction::Jump { delta: after_block }); // ===== Exception handler path ===== - // Stack at entry (after unwind): [..., __exit__, lasti, exc] - // PUSH_EXC_INFO -> [..., __exit__, lasti, prev_exc, exc] + // Stack at entry: [..., exit_func, self_exit, lasti, exc] + // PUSH_EXC_INFO -> [..., exit_func, self_exit, lasti, prev_exc, exc] self.switch_to_block(exc_handler_block); - // Create blocks for exception handling let cleanup_block = self.new_block(); let suppress_block = self.new_block(); @@ -5296,12 +5289,10 @@ impl Compiler { ); self.push_fblock(FBlockType::ExceptionHandler, exc_handler_block, after_block)?; - // PUSH_EXC_INFO: [exc] -> [prev_exc, exc] emit!(self, Instruction::PushExcInfo); - // WITH_EXCEPT_START: call __exit__(type, value, tb) - // Stack: [..., __exit__, lasti, prev_exc, exc] - // __exit__ is at TOS-3, call with exception info + // WITH_EXCEPT_START: call exit_func(self_exit, type, value, tb) + // Stack: [..., exit_func, self_exit, lasti, prev_exc, exc] emit!(self, Instruction::WithExceptStart); if is_async { @@ -5310,7 +5301,6 @@ impl Compiler { let _ = self.compile_yield_from_sequence(true)?; } - // TO_BOOL + POP_JUMP_IF_TRUE: check if exception is suppressed emit!(self, Instruction::ToBool); emit!( self, @@ -5319,25 +5309,19 @@ impl Compiler { } ); - // Pop the nested fblock BEFORE RERAISE so that RERAISE's exception - // handler points to the outer handler (try-except), not cleanup_block. - // This is critical: when RERAISE propagates the exception, the exception - // table should route it to the outer try-except, not back to cleanup. emit!(self, PseudoInstruction::PopBlock); self.pop_fblock(FBlockType::ExceptionHandler); - // Not suppressed: RERAISE 2 emit!(self, Instruction::Reraise { depth: 2 }); // ===== Suppress block ===== - // Exception was suppressed, clean up stack - // Stack: [..., __exit__, lasti, prev_exc, exc, True] - // Need to pop: True, exc, prev_exc, __exit__ + // Stack: [..., exit_func, self_exit, lasti, prev_exc, exc, True] self.switch_to_block(suppress_block); - emit!(self, Instruction::PopTop); // pop True (TO_BOOL result) - emit!(self, Instruction::PopExcept); // pop exc and restore prev_exc - emit!(self, Instruction::PopTop); // pop __exit__ + emit!(self, Instruction::PopTop); // pop True + emit!(self, Instruction::PopExcept); // pop exc, restore prev_exc emit!(self, Instruction::PopTop); // pop lasti + emit!(self, Instruction::PopTop); // pop self_exit + emit!(self, Instruction::PopTop); // pop exit_func emit!(self, PseudoInstruction::Jump { delta: after_block }); // ===== Cleanup block (for nested exception during __exit__) ===== @@ -8740,10 +8724,8 @@ impl Compiler { for action in unwind_actions { match action { UnwindAction::With { is_async } => { - // codegen_unwind_fblock(WITH/ASYNC_WITH) + // Stack: [..., exit_func, self_exit] emit!(self, PseudoInstruction::PopBlock); - // compiler_call_exit_with_nones - emit!(self, Instruction::PushNull); self.emit_load_const(ConstantData::None); self.emit_load_const(ConstantData::None); self.emit_load_const(ConstantData::None); diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap index 54d00d5ddb1..dae3014339b 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap @@ -1,6 +1,6 @@ --- source: crates/codegen/src/compile.rs -assertion_line: 9496 +assertion_line: 9591 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) @@ -23,8 +23,8 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 15 CACHE 16 CACHE 17 CACHE - >> 18 LOAD_CONST ("ham") - 19 CALL (1) + 18 LOAD_CONST ("ham") + >> 19 CALL (1) 20 CACHE 21 CACHE 22 CACHE @@ -67,8 +67,8 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 58 COPY (1) 59 LOAD_SPECIAL (__exit__) 60 SWAP (2) - 61 LOAD_SPECIAL (__enter__) - 62 PUSH_NULL + 61 SWAP (3) + 62 LOAD_SPECIAL (__enter__) 63 CALL (0) 64 CACHE 65 CACHE @@ -89,8 +89,8 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 78 COPY (1) 79 LOAD_SPECIAL (__aexit__) 80 SWAP (2) - 81 LOAD_SPECIAL (__aenter__) - 82 PUSH_NULL + 81 SWAP (3) + 82 LOAD_SPECIAL (__aenter__) 83 CALL (0) 84 CACHE 85 CACHE @@ -138,31 +138,31 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 124 POP_EXCEPT 125 POP_TOP 126 POP_TOP - 127 JUMP_FORWARD (3) - 128 COPY (3) - 129 POP_EXCEPT - 130 RERAISE (1) - 131 JUMP_FORWARD (46) - 132 PUSH_EXC_INFO + 127 POP_TOP + 128 JUMP_FORWARD (3) + 129 COPY (3) + 130 POP_EXCEPT + 131 RERAISE (1) + 132 JUMP_FORWARD (46) + 133 PUSH_EXC_INFO - 7 133 LOAD_GLOBAL (12, Exception) - 134 CACHE + 7 134 LOAD_GLOBAL (12, Exception) 135 CACHE 136 CACHE 137 CACHE - 138 CHECK_EXC_MATCH - 139 POP_JUMP_IF_FALSE (33) - 140 CACHE - 141 NOT_TAKEN - 142 STORE_FAST (1, ex) + 138 CACHE + 139 CHECK_EXC_MATCH + 140 POP_JUMP_IF_FALSE (33) + 141 CACHE + 142 NOT_TAKEN + 143 STORE_FAST (1, ex) - 8 143 LOAD_GLOBAL (4, self) - 144 CACHE + 8 144 LOAD_GLOBAL (4, self) 145 CACHE 146 CACHE 147 CACHE - 148 LOAD_ATTR (15, assertIs, method=true) - 149 CACHE + 148 CACHE + 149 LOAD_ATTR (15, assertIs, method=true) 150 CACHE 151 CACHE 152 CACHE @@ -171,34 +171,34 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 155 CACHE 156 CACHE 157 CACHE - 158 LOAD_FAST_LOAD_FAST (ex, stop_exc) - 159 CALL (2) - 160 CACHE + 158 CACHE + 159 LOAD_FAST_LOAD_FAST (ex, stop_exc) + 160 CALL (2) 161 CACHE 162 CACHE - 163 POP_TOP - 164 JUMP_FORWARD (4) - 165 LOAD_CONST (None) - 166 STORE_FAST (1, ex) - 167 DELETE_FAST (1, ex) - 168 RERAISE (1) - 169 POP_EXCEPT - 170 LOAD_CONST (None) - 171 STORE_FAST (1, ex) - 172 DELETE_FAST (1, ex) - 173 JUMP_FORWARD (28) - 174 RERAISE (0) - 175 COPY (3) - 176 POP_EXCEPT - 177 RERAISE (1) + 163 CACHE + 164 POP_TOP + 165 JUMP_FORWARD (4) + 166 LOAD_CONST (None) + 167 STORE_FAST (1, ex) + 168 DELETE_FAST (1, ex) + 169 RERAISE (1) + 170 POP_EXCEPT + 171 LOAD_CONST (None) + 172 STORE_FAST (1, ex) + 173 DELETE_FAST (1, ex) + 174 JUMP_FORWARD (28) + 175 RERAISE (0) + 176 COPY (3) + 177 POP_EXCEPT + 178 RERAISE (1) - 10 178 LOAD_GLOBAL (4, self) - 179 CACHE + 10 179 LOAD_GLOBAL (4, self) 180 CACHE 181 CACHE 182 CACHE - 183 LOAD_ATTR (17, fail, method=true) - 184 CACHE + 183 CACHE + 184 LOAD_ATTR (17, fail, method=true) 185 CACHE 186 CACHE 187 CACHE @@ -207,27 +207,27 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 190 CACHE 191 CACHE 192 CACHE - 193 LOAD_FAST_BORROW (0, stop_exc) - 194 FORMAT_SIMPLE - 195 LOAD_CONST (" was suppressed") - 196 BUILD_STRING (2) - 197 CALL (1) - 198 CACHE + 193 CACHE + 194 LOAD_FAST_BORROW (0, stop_exc) + 195 FORMAT_SIMPLE + 196 LOAD_CONST (" was suppressed") + 197 BUILD_STRING (2) + 198 CALL (1) 199 CACHE 200 CACHE - 201 POP_TOP - 202 NOP + 201 CACHE + 202 POP_TOP + 203 NOP - 3 203 PUSH_NULL - 204 LOAD_CONST (None) + 3 204 LOAD_CONST (None) 205 LOAD_CONST (None) 206 LOAD_CONST (None) 207 CALL (3) - >> 208 CACHE - 209 CACHE + 208 CACHE + >> 209 CACHE 210 CACHE 211 POP_TOP - 212 JUMP_FORWARD (18) + 212 JUMP_FORWARD (19) 213 PUSH_EXC_INFO 214 WITH_EXCEPT_START 215 TO_BOOL @@ -242,12 +242,13 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 224 POP_EXCEPT 225 POP_TOP 226 POP_TOP - 227 JUMP_FORWARD (3) - 228 COPY (3) - 229 POP_EXCEPT - 230 RERAISE (1) - 231 JUMP_BACKWARD (208) - 232 CACHE + 227 POP_TOP + 228 JUMP_FORWARD (3) + 229 COPY (3) + 230 POP_EXCEPT + 231 RERAISE (1) + 232 JUMP_BACKWARD (209) + 233 CACHE 2 MAKE_FUNCTION 3 STORE_NAME (0, test) diff --git a/crates/compiler-core/src/bytecode/instruction.rs b/crates/compiler-core/src/bytecode/instruction.rs index 6544c675c22..86ed1da7436 100644 --- a/crates/compiler-core/src/bytecode/instruction.rs +++ b/crates/compiler-core/src/bytecode/instruction.rs @@ -1020,7 +1020,7 @@ impl InstructionMetadata for Instruction { Self::LoadLocals => (1, 0), Self::LoadName { .. } => (1, 0), Self::LoadSmallInt { .. } => (1, 0), - Self::LoadSpecial { .. } => (1, 1), + Self::LoadSpecial { .. } => (2, 1), Self::LoadSuperAttr { .. } => (1 + (oparg & 1), 3), Self::LoadSuperAttrAttr => (1, 3), Self::LoadSuperAttrMethod => (2, 3), @@ -1085,7 +1085,7 @@ impl InstructionMetadata for Instruction { Self::UnpackSequenceList => (oparg, 1), Self::UnpackSequenceTuple => (oparg, 1), Self::UnpackSequenceTwoTuple => (2, 1), - Self::WithExceptStart => (6, 5), + Self::WithExceptStart => (7, 6), Self::YieldValue { .. } => (1, 1), }; diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 9338c74d8f8..8dcd626da3b 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -2946,22 +2946,23 @@ impl ExecutingFrame<'_> { Ok(None) } Instruction::LoadSpecial { method } => { - // Stack effect: 0 (replaces TOS with bound method) - // Input: [..., obj] - // Output: [..., bound_method] + // Pops obj, pushes (callable, self_or_null) for CALL convention. + // Push order: callable first (deeper), self_or_null on top. use crate::vm::PyMethod; let obj = self.pop_value(); let oparg = method.get(arg); let method_name = get_special_method_name(oparg, vm); - let bound = match vm.get_special_method(&obj, method_name)? { + match vm.get_special_method(&obj, method_name)? { Some(PyMethod::Function { target, func }) => { - crate::builtins::PyBoundMethod::new(target, func) - .into_ref(&vm.ctx) - .into() + self.push_value(func); // callable (deeper) + self.push_value(target); // self (TOS) + } + Some(PyMethod::Attribute(bound)) => { + self.push_value(bound); // callable (deeper) + self.push_null(); // NULL (TOS) } - Some(PyMethod::Attribute(bound)) => bound, None => { return Err(vm.new_type_error(get_special_method_error_msg( oparg, @@ -2970,7 +2971,6 @@ impl ExecutingFrame<'_> { ))); } }; - self.push_value(bound); Ok(None) } Instruction::MakeFunction => self.execute_make_function(vm), @@ -3522,24 +3522,28 @@ impl ExecutingFrame<'_> { self.unpack_sequence(expected, vm) } Instruction::WithExceptStart => { - // Stack: [..., __exit__, lasti, prev_exc, exc] - // Call __exit__(type, value, tb) and push result - // __exit__ is at TOS-3 (below lasti, prev_exc, and exc) + // Stack: [..., exit_func, self_or_null, lasti, prev_exc, exc] + // exit_func at TOS-4, self_or_null at TOS-3 let exc = vm.current_exception(); let stack_len = self.localsplus.stack_len(); - let exit = expect_unchecked( - self.localsplus.stack_index(stack_len - 4).clone(), - "WithExceptStart: __exit__ is NULL", + let exit_func = expect_unchecked( + self.localsplus.stack_index(stack_len - 5).clone(), + "WithExceptStart: exit_func is NULL", ); + let self_or_null = self.localsplus.stack_index(stack_len - 4).clone(); - let args = if let Some(ref exc) = exc { + let (tp, val, tb) = if let Some(ref exc) = exc { vm.split_exception(exc.clone()) } else { (vm.ctx.none(), vm.ctx.none(), vm.ctx.none()) }; - let exit_res = exit.call(args, vm)?; - // Push result on top of stack + + let exit_res = if let Some(self_exit) = self_or_null { + exit_func.call((self_exit.to_pyobj(), tp, val, tb), vm)? + } else { + exit_func.call((tp, val, tb), vm)? + }; self.push_value(exit_res); Ok(None) From d015f02c93f292432badaeef59961b65fd6a01d5 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 14:16:36 +0900 Subject: [PATCH 02/15] Normalize LOAD_FAST_CHECK and JUMP_BACKWARD_NO_INTERRUPT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add LOAD_FAST_CHECK → LOAD_FAST and JUMP_BACKWARD_NO_INTERRUPT → JUMP_BACKWARD to opname normalization in dis_dump.py. These are optimization variants with identical semantics. --- scripts/dis_dump.py | 248 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 scripts/dis_dump.py diff --git a/scripts/dis_dump.py b/scripts/dis_dump.py new file mode 100644 index 00000000000..e13a1af05d1 --- /dev/null +++ b/scripts/dis_dump.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +"""Dump normalized bytecode for Python source files as JSON. + +Designed to produce comparable output across different Python implementations. +Normalizes away implementation-specific details (byte offsets, memory addresses) +while preserving semantic instruction content. + +Usage: + python dis_dump.py Lib/ + python dis_dump.py path/to/file.py +""" + +import dis +import json +import os +import re +import sys +import types + +# Non-semantic filler instructions to skip +SKIP_OPS = frozenset({"CACHE", "PRECALL"}) + +# Opname normalization: map variant instructions to their base form. +# These variants differ only in optimization hints, not semantics. +_OPNAME_NORMALIZE = { + "LOAD_FAST_BORROW": "LOAD_FAST", + "LOAD_FAST_BORROW_LOAD_FAST_BORROW": "LOAD_FAST_LOAD_FAST", + "LOAD_FAST_CHECK": "LOAD_FAST", + "JUMP_BACKWARD_NO_INTERRUPT": "JUMP_BACKWARD", +} + +# Jump instruction names (fallback when hasjrel/hasjabs is incomplete) +_JUMP_OPNAMES = frozenset({ + "JUMP", "JUMP_FORWARD", "JUMP_BACKWARD", "JUMP_BACKWARD_NO_INTERRUPT", + "POP_JUMP_IF_TRUE", "POP_JUMP_IF_FALSE", + "POP_JUMP_IF_NONE", "POP_JUMP_IF_NOT_NONE", + "JUMP_IF_TRUE_OR_POP", "JUMP_IF_FALSE_OR_POP", + "FOR_ITER", "SEND", +}) + +_JUMP_OPCODES = None + + +def _jump_opcodes(): + global _JUMP_OPCODES + if _JUMP_OPCODES is None: + _JUMP_OPCODES = set() + if hasattr(dis, "hasjrel"): + _JUMP_OPCODES.update(dis.hasjrel) + if hasattr(dis, "hasjabs"): + _JUMP_OPCODES.update(dis.hasjabs) + return _JUMP_OPCODES + + +def _is_jump(inst): + """Check if an instruction is a jump (by opcode set or name).""" + return inst.opcode in _jump_opcodes() or inst.opname in _JUMP_OPNAMES + + +def _normalize_argrepr(argrepr): + """Strip runtime-specific details from arg repr.""" + if argrepr.startswith(" (CPython 3.14) + # (RustPython) + name = argrepr[len("= 0: + name = name[:idx] + return "" % name.rstrip(">").strip() + # Normalize COMPARE_OP: strip bool(...) wrapper from CPython 3.14 + # e.g. "bool(==)" -> "==", "bool(<)" -> "<" + m = re.match(r"^bool\((.+)\)$", argrepr) + if m: + return m.group(1) + # Remove memory addresses from other reprs + argrepr = re.sub(r" at 0x[0-9a-fA-F]+", "", argrepr) + # Remove LOAD_ATTR/LOAD_SUPER_ATTR suffixes: " + NULL|self", " + NULL" + argrepr = re.sub(r" \+ NULL\|self$", "", argrepr) + argrepr = re.sub(r" \+ NULL$", "", argrepr) + return argrepr + + +_IS_RUSTPYTHON = hasattr(sys, "implementation") and sys.implementation.name == "rustpython" + +# RustPython's ComparisonOperator enum values → operator strings +_RP_CMP_OPS = {0: "<", 1: "<", 2: ">", 3: "!=", 4: "==", 5: "<=", 6: ">="} + + +def _resolve_arg_fallback(code, opname, arg): + """Resolve a raw argument to its human-readable form. + + Used when the dis module doesn't populate argrepr (e.g., on RustPython). + """ + if not isinstance(arg, int): + return arg + try: + if "FAST" in opname: + if 0 <= arg < len(code.co_varnames): + return code.co_varnames[arg] + elif opname == "LOAD_CONST": + if 0 <= arg < len(code.co_consts): + return _normalize_argrepr(repr(code.co_consts[arg])) + elif opname in ("LOAD_DEREF", "STORE_DEREF", "DELETE_DEREF", + "LOAD_CLOSURE", "MAKE_CELL", "COPY_FREE_VARS"): + # These use fastlocal index: nlocals + cell/free offset + nlocals = len(code.co_varnames) + cell_and_free = code.co_cellvars + code.co_freevars + cell_idx = arg - nlocals + if 0 <= cell_idx < len(cell_and_free): + return cell_and_free[cell_idx] + elif 0 <= arg < len(cell_and_free): + # Fallback: direct index into cell_and_free + return cell_and_free[arg] + elif opname in ("LOAD_NAME", "STORE_NAME", "DELETE_NAME", + "LOAD_GLOBAL", "STORE_GLOBAL", "DELETE_GLOBAL", + "LOAD_ATTR", "STORE_ATTR", "DELETE_ATTR", + "IMPORT_NAME", "IMPORT_FROM"): + if 0 <= arg < len(code.co_names): + return code.co_names[arg] + elif opname == "LOAD_SUPER_ATTR": + name_idx = arg >> 2 + if 0 <= name_idx < len(code.co_names): + return code.co_names[name_idx] + except Exception: + pass + return arg + + +def _extract_instructions(code): + """Extract normalized instruction list from a code object. + + - Filters out CACHE/PRECALL instructions + - Converts jump targets from byte offsets to instruction indices + - Resolves argument names via fallback when argrepr is missing + - Normalizes argument representations + """ + try: + raw = list(dis.get_instructions(code)) + except Exception as e: + return [["ERROR", str(e)]] + + # Build filtered list and offset-to-index mapping + filtered = [] + offset_to_idx = {} + for inst in raw: + if inst.opname in SKIP_OPS: + continue + offset_to_idx[inst.offset] = len(filtered) + filtered.append(inst) + + # Map offsets that land on CACHE slots to the next real instruction + for inst in raw: + if inst.offset not in offset_to_idx: + for fi, finst in enumerate(filtered): + if finst.offset >= inst.offset: + offset_to_idx[inst.offset] = fi + break + + + result = [] + for inst in filtered: + opname = _OPNAME_NORMALIZE.get(inst.opname, inst.opname) + if _is_jump(inst) and isinstance(inst.argval, int): + target_idx = offset_to_idx.get(inst.argval) + # If argval wasn't resolved (RustPython), compute target offset + if target_idx is None and inst.arg is not None: + if "FORWARD" in inst.opname: + target_off = inst.offset + 2 + inst.arg * 2 + target_idx = offset_to_idx.get(target_off) + elif "BACKWARD" in inst.opname: + # Try several cache sizes (0-3) for backward jumps + for cache in range(4): + target_off = inst.offset + 2 + cache * 2 - inst.arg * 2 + if target_off >= 0 and target_off in offset_to_idx: + target_idx = offset_to_idx[target_off] + break + if target_idx is None: + target_idx = inst.argval + result.append([opname, "->%d" % target_idx]) + elif inst.opname == "COMPARE_OP": + # Normalize COMPARE_OP across interpreters (different encodings) + if _IS_RUSTPYTHON: + cmp_str = _RP_CMP_OPS.get(inst.arg, inst.argrepr) + else: + cmp_str = _normalize_argrepr(inst.argrepr) if inst.argrepr else str(inst.arg) + result.append([opname, cmp_str]) + elif inst.arg is not None and inst.argrepr: + result.append([opname, _normalize_argrepr(inst.argrepr)]) + elif inst.arg is not None: + resolved = _resolve_arg_fallback(code, inst.opname, inst.arg) + result.append([opname, resolved]) + else: + result.append([opname]) + + return result + + +def _dump_code(code): + """Recursively dump a code object and its nested code objects.""" + name = getattr(code, "co_qualname", None) or code.co_name + children = [_dump_code(c) for c in code.co_consts if isinstance(c, types.CodeType)] + r = {"name": name, "insts": _extract_instructions(code)} + if children: + r["children"] = children + return r + + +def process_file(path): + """Compile a single file and return its bytecode dump.""" + try: + with open(path, "rb") as f: + source = f.read() + code = compile(source, path, "exec") + return {"status": "ok", "code": _dump_code(code)} + except SyntaxError as e: + return {"status": "error", "error": "%s (line %s)" % (e.msg, e.lineno)} + except Exception as e: + return {"status": "error", "error": str(e)} + + +def main(): + if len(sys.argv) < 2: + sys.stderr.write("Usage: %s [...]\n" % sys.argv[0]) + sys.exit(1) + + results = {} + for target in sys.argv[1:]: + if os.path.isdir(target): + for root, dirs, files in os.walk(target): + dirs[:] = sorted( + d for d in dirs if d != "__pycache__" and not d.startswith(".") + ) + for fname in sorted(files): + if fname.endswith(".py"): + fpath = os.path.join(root, fname) + relpath = os.path.relpath(fpath, target) + results[relpath] = process_file(fpath) + elif target.endswith(".py"): + results[os.path.basename(target)] = process_file(target) + + json.dump(results, sys.stdout, ensure_ascii=False, separators=(",", ":")) + + +if __name__ == "__main__": + main() From 37d1e037071430f475707b70e025ec8c6e21b42a Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 14:41:48 +0900 Subject: [PATCH 03/15] Add EXTENDED_ARG to SKIP_OPS, normalize LOAD_FAST_CHECK and JUMP_BACKWARD_NO_INTERRUPT --- crates/codegen/src/ir.rs | 1 + ...thon_codegen__compile__tests__if_ands.snap | 22 +- ...hon_codegen__compile__tests__if_mixed.snap | 14 +- ...ython_codegen__compile__tests__if_ors.snap | 10 +- scripts/dis_dump.py | 248 ------------------ 5 files changed, 21 insertions(+), 274 deletions(-) delete mode 100644 scripts/dis_dump.py diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 0b7eae105e2..43541dd89e2 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -1987,6 +1987,7 @@ fn duplicate_end_returns(blocks: &mut [Block]) { // Check if the last block ends with LOAD_CONST + RETURN_VALUE (the implicit return) let last_insts = &blocks[last_block.idx()].instructions; // Only apply when the last block is EXACTLY a return-None epilogue + // AND the return instructions have no explicit line number (lineno <= 0) let is_return_block = last_insts.len() == 2 && matches!( last_insts[0].instr, diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap index f043fa790f5..1c3e554a24c 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap @@ -1,23 +1,21 @@ --- source: crates/codegen/src/compile.rs -assertion_line: 9458 +assertion_line: 9553 expression: "compile_exec(\"\\\nif True and False and False:\n pass\n\")" --- 1 0 RESUME (0) - 1 LOAD_CONST (True) - 2 POP_JUMP_IF_FALSE (11) - >> 3 CACHE + >> 1 LOAD_CONST (True) + 2 POP_JUMP_IF_FALSE (9) + 3 CACHE 4 NOT_TAKEN - 5 LOAD_CONST (False) - 6 POP_JUMP_IF_FALSE (7) - >> 7 CACHE + >> 5 LOAD_CONST (False) + 6 POP_JUMP_IF_FALSE (5) + 7 CACHE 8 NOT_TAKEN - 9 LOAD_CONST (False) - 10 POP_JUMP_IF_FALSE (3) - >> 11 CACHE + >> 9 LOAD_CONST (False) + 10 POP_JUMP_IF_FALSE (1) + 11 CACHE 12 NOT_TAKEN 2 13 LOAD_CONST (None) 14 RETURN_VALUE - 15 LOAD_CONST (None) - 16 RETURN_VALUE diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap index 076ae82fecd..a8525a4b0c1 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap @@ -1,27 +1,25 @@ --- source: crates/codegen/src/compile.rs -assertion_line: 9468 +assertion_line: 9563 expression: "compile_exec(\"\\\nif (True and False) or (False and True):\n pass\n\")" --- 1 0 RESUME (0) - 1 LOAD_CONST (True) + >> 1 LOAD_CONST (True) 2 POP_JUMP_IF_FALSE (5) - >> 3 CACHE + 3 CACHE 4 NOT_TAKEN >> 5 LOAD_CONST (False) 6 POP_JUMP_IF_TRUE (9) - >> 7 CACHE + 7 CACHE 8 NOT_TAKEN >> 9 LOAD_CONST (False) - 10 POP_JUMP_IF_FALSE (7) + 10 POP_JUMP_IF_FALSE (5) 11 CACHE 12 NOT_TAKEN 13 LOAD_CONST (True) - 14 POP_JUMP_IF_FALSE (3) + 14 POP_JUMP_IF_FALSE (1) 15 CACHE 16 NOT_TAKEN 2 17 LOAD_CONST (None) 18 RETURN_VALUE - 19 LOAD_CONST (None) - 20 RETURN_VALUE diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap index a7beb7766b2..3c42435ee72 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap @@ -1,23 +1,21 @@ --- source: crates/codegen/src/compile.rs -assertion_line: 9448 +assertion_line: 9543 expression: "compile_exec(\"\\\nif True or False or False:\n pass\n\")" --- 1 0 RESUME (0) - 1 LOAD_CONST (True) + >> 1 LOAD_CONST (True) 2 POP_JUMP_IF_TRUE (9) - >> 3 CACHE + 3 CACHE 4 NOT_TAKEN >> 5 LOAD_CONST (False) 6 POP_JUMP_IF_TRUE (5) 7 CACHE 8 NOT_TAKEN >> 9 LOAD_CONST (False) - 10 POP_JUMP_IF_FALSE (3) + 10 POP_JUMP_IF_FALSE (1) 11 CACHE 12 NOT_TAKEN 2 13 LOAD_CONST (None) 14 RETURN_VALUE - 15 LOAD_CONST (None) - 16 RETURN_VALUE diff --git a/scripts/dis_dump.py b/scripts/dis_dump.py deleted file mode 100644 index e13a1af05d1..00000000000 --- a/scripts/dis_dump.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python3 -"""Dump normalized bytecode for Python source files as JSON. - -Designed to produce comparable output across different Python implementations. -Normalizes away implementation-specific details (byte offsets, memory addresses) -while preserving semantic instruction content. - -Usage: - python dis_dump.py Lib/ - python dis_dump.py path/to/file.py -""" - -import dis -import json -import os -import re -import sys -import types - -# Non-semantic filler instructions to skip -SKIP_OPS = frozenset({"CACHE", "PRECALL"}) - -# Opname normalization: map variant instructions to their base form. -# These variants differ only in optimization hints, not semantics. -_OPNAME_NORMALIZE = { - "LOAD_FAST_BORROW": "LOAD_FAST", - "LOAD_FAST_BORROW_LOAD_FAST_BORROW": "LOAD_FAST_LOAD_FAST", - "LOAD_FAST_CHECK": "LOAD_FAST", - "JUMP_BACKWARD_NO_INTERRUPT": "JUMP_BACKWARD", -} - -# Jump instruction names (fallback when hasjrel/hasjabs is incomplete) -_JUMP_OPNAMES = frozenset({ - "JUMP", "JUMP_FORWARD", "JUMP_BACKWARD", "JUMP_BACKWARD_NO_INTERRUPT", - "POP_JUMP_IF_TRUE", "POP_JUMP_IF_FALSE", - "POP_JUMP_IF_NONE", "POP_JUMP_IF_NOT_NONE", - "JUMP_IF_TRUE_OR_POP", "JUMP_IF_FALSE_OR_POP", - "FOR_ITER", "SEND", -}) - -_JUMP_OPCODES = None - - -def _jump_opcodes(): - global _JUMP_OPCODES - if _JUMP_OPCODES is None: - _JUMP_OPCODES = set() - if hasattr(dis, "hasjrel"): - _JUMP_OPCODES.update(dis.hasjrel) - if hasattr(dis, "hasjabs"): - _JUMP_OPCODES.update(dis.hasjabs) - return _JUMP_OPCODES - - -def _is_jump(inst): - """Check if an instruction is a jump (by opcode set or name).""" - return inst.opcode in _jump_opcodes() or inst.opname in _JUMP_OPNAMES - - -def _normalize_argrepr(argrepr): - """Strip runtime-specific details from arg repr.""" - if argrepr.startswith(" (CPython 3.14) - # (RustPython) - name = argrepr[len("= 0: - name = name[:idx] - return "" % name.rstrip(">").strip() - # Normalize COMPARE_OP: strip bool(...) wrapper from CPython 3.14 - # e.g. "bool(==)" -> "==", "bool(<)" -> "<" - m = re.match(r"^bool\((.+)\)$", argrepr) - if m: - return m.group(1) - # Remove memory addresses from other reprs - argrepr = re.sub(r" at 0x[0-9a-fA-F]+", "", argrepr) - # Remove LOAD_ATTR/LOAD_SUPER_ATTR suffixes: " + NULL|self", " + NULL" - argrepr = re.sub(r" \+ NULL\|self$", "", argrepr) - argrepr = re.sub(r" \+ NULL$", "", argrepr) - return argrepr - - -_IS_RUSTPYTHON = hasattr(sys, "implementation") and sys.implementation.name == "rustpython" - -# RustPython's ComparisonOperator enum values → operator strings -_RP_CMP_OPS = {0: "<", 1: "<", 2: ">", 3: "!=", 4: "==", 5: "<=", 6: ">="} - - -def _resolve_arg_fallback(code, opname, arg): - """Resolve a raw argument to its human-readable form. - - Used when the dis module doesn't populate argrepr (e.g., on RustPython). - """ - if not isinstance(arg, int): - return arg - try: - if "FAST" in opname: - if 0 <= arg < len(code.co_varnames): - return code.co_varnames[arg] - elif opname == "LOAD_CONST": - if 0 <= arg < len(code.co_consts): - return _normalize_argrepr(repr(code.co_consts[arg])) - elif opname in ("LOAD_DEREF", "STORE_DEREF", "DELETE_DEREF", - "LOAD_CLOSURE", "MAKE_CELL", "COPY_FREE_VARS"): - # These use fastlocal index: nlocals + cell/free offset - nlocals = len(code.co_varnames) - cell_and_free = code.co_cellvars + code.co_freevars - cell_idx = arg - nlocals - if 0 <= cell_idx < len(cell_and_free): - return cell_and_free[cell_idx] - elif 0 <= arg < len(cell_and_free): - # Fallback: direct index into cell_and_free - return cell_and_free[arg] - elif opname in ("LOAD_NAME", "STORE_NAME", "DELETE_NAME", - "LOAD_GLOBAL", "STORE_GLOBAL", "DELETE_GLOBAL", - "LOAD_ATTR", "STORE_ATTR", "DELETE_ATTR", - "IMPORT_NAME", "IMPORT_FROM"): - if 0 <= arg < len(code.co_names): - return code.co_names[arg] - elif opname == "LOAD_SUPER_ATTR": - name_idx = arg >> 2 - if 0 <= name_idx < len(code.co_names): - return code.co_names[name_idx] - except Exception: - pass - return arg - - -def _extract_instructions(code): - """Extract normalized instruction list from a code object. - - - Filters out CACHE/PRECALL instructions - - Converts jump targets from byte offsets to instruction indices - - Resolves argument names via fallback when argrepr is missing - - Normalizes argument representations - """ - try: - raw = list(dis.get_instructions(code)) - except Exception as e: - return [["ERROR", str(e)]] - - # Build filtered list and offset-to-index mapping - filtered = [] - offset_to_idx = {} - for inst in raw: - if inst.opname in SKIP_OPS: - continue - offset_to_idx[inst.offset] = len(filtered) - filtered.append(inst) - - # Map offsets that land on CACHE slots to the next real instruction - for inst in raw: - if inst.offset not in offset_to_idx: - for fi, finst in enumerate(filtered): - if finst.offset >= inst.offset: - offset_to_idx[inst.offset] = fi - break - - - result = [] - for inst in filtered: - opname = _OPNAME_NORMALIZE.get(inst.opname, inst.opname) - if _is_jump(inst) and isinstance(inst.argval, int): - target_idx = offset_to_idx.get(inst.argval) - # If argval wasn't resolved (RustPython), compute target offset - if target_idx is None and inst.arg is not None: - if "FORWARD" in inst.opname: - target_off = inst.offset + 2 + inst.arg * 2 - target_idx = offset_to_idx.get(target_off) - elif "BACKWARD" in inst.opname: - # Try several cache sizes (0-3) for backward jumps - for cache in range(4): - target_off = inst.offset + 2 + cache * 2 - inst.arg * 2 - if target_off >= 0 and target_off in offset_to_idx: - target_idx = offset_to_idx[target_off] - break - if target_idx is None: - target_idx = inst.argval - result.append([opname, "->%d" % target_idx]) - elif inst.opname == "COMPARE_OP": - # Normalize COMPARE_OP across interpreters (different encodings) - if _IS_RUSTPYTHON: - cmp_str = _RP_CMP_OPS.get(inst.arg, inst.argrepr) - else: - cmp_str = _normalize_argrepr(inst.argrepr) if inst.argrepr else str(inst.arg) - result.append([opname, cmp_str]) - elif inst.arg is not None and inst.argrepr: - result.append([opname, _normalize_argrepr(inst.argrepr)]) - elif inst.arg is not None: - resolved = _resolve_arg_fallback(code, inst.opname, inst.arg) - result.append([opname, resolved]) - else: - result.append([opname]) - - return result - - -def _dump_code(code): - """Recursively dump a code object and its nested code objects.""" - name = getattr(code, "co_qualname", None) or code.co_name - children = [_dump_code(c) for c in code.co_consts if isinstance(c, types.CodeType)] - r = {"name": name, "insts": _extract_instructions(code)} - if children: - r["children"] = children - return r - - -def process_file(path): - """Compile a single file and return its bytecode dump.""" - try: - with open(path, "rb") as f: - source = f.read() - code = compile(source, path, "exec") - return {"status": "ok", "code": _dump_code(code)} - except SyntaxError as e: - return {"status": "error", "error": "%s (line %s)" % (e.msg, e.lineno)} - except Exception as e: - return {"status": "error", "error": str(e)} - - -def main(): - if len(sys.argv) < 2: - sys.stderr.write("Usage: %s [...]\n" % sys.argv[0]) - sys.exit(1) - - results = {} - for target in sys.argv[1:]: - if os.path.isdir(target): - for root, dirs, files in os.walk(target): - dirs[:] = sorted( - d for d in dirs if d != "__pycache__" and not d.startswith(".") - ) - for fname in sorted(files): - if fname.endswith(".py"): - fpath = os.path.join(root, fname) - relpath = os.path.relpath(fpath, target) - results[relpath] = process_file(fpath) - elif target.endswith(".py"): - results[os.path.basename(target)] = process_file(target) - - json.dump(results, sys.stdout, ensure_ascii=False, separators=(",", ":")) - - -if __name__ == "__main__": - main() From b559a6ee7f8fd5fab32739acaa10faeeb9c12a21 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 16:24:40 +0900 Subject: [PATCH 04/15] Remove duplicate return-None when block already has return Skip duplicate_end_returns for blocks that already end with LOAD_CONST + RETURN_VALUE. Run DCE + unreachable elimination after duplication to remove the now-unreachable original return block. --- crates/codegen/src/ir.rs | 20 +++++++++++++++---- ...thon_codegen__compile__tests__if_ands.snap | 20 ++++++++++--------- ...hon_codegen__compile__tests__if_mixed.snap | 12 ++++++----- ...ython_codegen__compile__tests__if_ors.snap | 8 +++++--- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 43541dd89e2..5c20bcc6368 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -214,6 +214,8 @@ impl CodeInfo { self.dce(); // re-run within-block DCE after normalize_jumps creates new instructions self.eliminate_unreachable_blocks(); duplicate_end_returns(&mut self.blocks); + self.dce(); // truncate after terminal in blocks that got return duplicated + self.eliminate_unreachable_blocks(); // remove now-unreachable last block self.optimize_load_global_push_null(); let max_stackdepth = self.max_stackdepth()?; @@ -2011,12 +2013,22 @@ fn duplicate_end_returns(blocks: &mut [Block]) { let block = &blocks[current.idx()]; if current != last_block && block.next == last_block && !block.cold && !block.except_handler { - let has_fallthrough = block - .instructions - .last() + let last_ins = block.instructions.last(); + let has_fallthrough = last_ins .map(|ins| !ins.instr.is_scope_exit() && !ins.instr.is_unconditional_jump()) .unwrap_or(true); - if has_fallthrough { + // Don't duplicate if block already ends with the same return pattern + let already_has_return = block.instructions.len() >= 2 && { + let n = block.instructions.len(); + matches!( + block.instructions[n - 2].instr, + AnyInstruction::Real(Instruction::LoadConst { .. }) + ) && matches!( + block.instructions[n - 1].instr, + AnyInstruction::Real(Instruction::ReturnValue) + ) + }; + if has_fallthrough && !already_has_return { blocks_to_fix.push(current); } } diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap index 1c3e554a24c..4783c0f2d5e 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap @@ -4,18 +4,20 @@ assertion_line: 9553 expression: "compile_exec(\"\\\nif True and False and False:\n pass\n\")" --- 1 0 RESUME (0) - >> 1 LOAD_CONST (True) - 2 POP_JUMP_IF_FALSE (9) - 3 CACHE + 1 LOAD_CONST (True) + 2 POP_JUMP_IF_FALSE (11) + >> 3 CACHE 4 NOT_TAKEN - >> 5 LOAD_CONST (False) - 6 POP_JUMP_IF_FALSE (5) - 7 CACHE + 5 LOAD_CONST (False) + 6 POP_JUMP_IF_FALSE (7) + >> 7 CACHE 8 NOT_TAKEN - >> 9 LOAD_CONST (False) - 10 POP_JUMP_IF_FALSE (1) - 11 CACHE + 9 LOAD_CONST (False) + 10 POP_JUMP_IF_FALSE (3) + >> 11 CACHE 12 NOT_TAKEN 2 13 LOAD_CONST (None) 14 RETURN_VALUE + 15 LOAD_CONST (None) + 16 RETURN_VALUE diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap index a8525a4b0c1..043bf380af3 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap @@ -4,22 +4,24 @@ assertion_line: 9563 expression: "compile_exec(\"\\\nif (True and False) or (False and True):\n pass\n\")" --- 1 0 RESUME (0) - >> 1 LOAD_CONST (True) + 1 LOAD_CONST (True) 2 POP_JUMP_IF_FALSE (5) - 3 CACHE + >> 3 CACHE 4 NOT_TAKEN >> 5 LOAD_CONST (False) 6 POP_JUMP_IF_TRUE (9) - 7 CACHE + >> 7 CACHE 8 NOT_TAKEN >> 9 LOAD_CONST (False) - 10 POP_JUMP_IF_FALSE (5) + 10 POP_JUMP_IF_FALSE (7) 11 CACHE 12 NOT_TAKEN 13 LOAD_CONST (True) - 14 POP_JUMP_IF_FALSE (1) + 14 POP_JUMP_IF_FALSE (3) 15 CACHE 16 NOT_TAKEN 2 17 LOAD_CONST (None) 18 RETURN_VALUE + 19 LOAD_CONST (None) + 20 RETURN_VALUE diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap index 3c42435ee72..bf4960582c1 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap @@ -4,18 +4,20 @@ assertion_line: 9543 expression: "compile_exec(\"\\\nif True or False or False:\n pass\n\")" --- 1 0 RESUME (0) - >> 1 LOAD_CONST (True) + 1 LOAD_CONST (True) 2 POP_JUMP_IF_TRUE (9) - 3 CACHE + >> 3 CACHE 4 NOT_TAKEN >> 5 LOAD_CONST (False) 6 POP_JUMP_IF_TRUE (5) 7 CACHE 8 NOT_TAKEN >> 9 LOAD_CONST (False) - 10 POP_JUMP_IF_FALSE (1) + 10 POP_JUMP_IF_FALSE (3) 11 CACHE 12 NOT_TAKEN 2 13 LOAD_CONST (None) 14 RETURN_VALUE + 15 LOAD_CONST (None) + 16 RETURN_VALUE From 6b200d95f39c5b0a6770cf4471d104f29d4f5ac7 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 16:45:44 +0900 Subject: [PATCH 05/15] Improve __static_attributes__ collection accuracy - Support tuple/list unpacking targets: (self.x, self.y) = val - Skip @staticmethod and @classmethod decorated methods - Use scan_target_for_attrs helper for recursive target scanning --- crates/codegen/src/compile.rs | 64 +++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 13fda31ae4f..8f88a521d78 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -4509,22 +4509,55 @@ impl Compiler { fn collect_static_attributes(body: &[ast::Stmt], attrs: Option<&mut IndexSet>) { let Some(attrs) = attrs else { return }; for stmt in body { - // Only scan def/async def at class body level - let (params, func_body) = match stmt { - ast::Stmt::FunctionDef(f) => (&f.parameters, &f.body), + let f = match stmt { + ast::Stmt::FunctionDef(f) => f, _ => continue, }; - // Get first parameter name (usually "self" or "cls") - let first_param = params + // Skip @staticmethod and @classmethod decorated functions + let dominated_by_special = f.decorator_list.iter().any(|d| { + matches!(&d.expression, ast::Expr::Name(n) + if n.id.as_str() == "staticmethod" || n.id.as_str() == "classmethod") + }); + if dominated_by_special { + continue; + } + let first_param = f + .parameters .args .first() - .or(params.posonlyargs.first()) + .or(f.parameters.posonlyargs.first()) .map(|p| &p.parameter.name); let Some(self_name) = first_param else { continue; }; - // Scan function body for self.xxx = ... (STORE_ATTR on first param) - Self::scan_store_attrs(func_body, self_name.as_str(), attrs); + Self::scan_store_attrs(&f.body, self_name.as_str(), attrs); + } + } + + /// Extract self.attr patterns from an assignment target expression. + fn scan_target_for_attrs(target: &ast::Expr, name: &str, attrs: &mut IndexSet) { + match target { + ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => { + if let ast::Expr::Name(n) = value.as_ref() { + if n.id.as_str() == name { + attrs.insert(attr.to_string()); + } + } + } + ast::Expr::Tuple(t) => { + for elt in &t.elts { + Self::scan_target_for_attrs(elt, name, attrs); + } + } + ast::Expr::List(l) => { + for elt in &l.elts { + Self::scan_target_for_attrs(elt, name, attrs); + } + } + ast::Expr::Starred(s) => { + Self::scan_target_for_attrs(&s.value, name, attrs); + } + _ => {} } } @@ -4534,22 +4567,11 @@ impl Compiler { match stmt { ast::Stmt::Assign(a) => { for target in &a.targets { - if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = target - && let ast::Expr::Name(n) = value.as_ref() - && n.id.as_str() == name - { - attrs.insert(attr.to_string()); - } + Self::scan_target_for_attrs(target, name, attrs); } } ast::Stmt::AnnAssign(a) => { - if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = - a.target.as_ref() - && let ast::Expr::Name(n) = value.as_ref() - && n.id.as_str() == name - { - attrs.insert(attr.to_string()); - } + Self::scan_target_for_attrs(&a.target, name, attrs); } ast::Stmt::AugAssign(a) => { if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = From 122f76a176855530459a79c0ad6e5e00ab260ecd Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 12:54:39 +0900 Subject: [PATCH 06/15] Use method mode for function-local import attribute calls Function-local imports (scope is Local+IMPORTED) should use method mode LOAD_ATTR like regular names, not plain mode. Only module/class scope imports use plain LOAD_ATTR + PUSH_NULL. --- crates/codegen/src/compile.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 8f88a521d78..c4c69169d49 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -694,13 +694,23 @@ impl Compiler { /// Check if a name is imported in current scope or any enclosing scope. fn is_name_imported(&self, name: &str) -> bool { - if let Some(sym) = self.current_symbol_table().symbols.get(name) { + let current = self.current_symbol_table(); + if let Some(sym) = current.symbols.get(name) { if sym.flags.contains(SymbolFlags::IMPORTED) { - return true; - } else if sym.scope == SymbolScope::Local { + // Module/class scope imports use plain LOAD_ATTR + // Function-local imports use method mode (scope is Local) + return !matches!( + current.typ, + CompilerScope::Function + | CompilerScope::AsyncFunction + | CompilerScope::Lambda + ); + } + if sym.scope == SymbolScope::Local { return false; } } + // Check enclosing scopes for module-level imports accessed as globals self.symbol_table_stack.iter().rev().skip(1).any(|table| { table .symbols From d1641dd9c7d4f3134ae55ca39c540d3f135ba50a Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 13:16:52 +0900 Subject: [PATCH 07/15] Optimize constant iterable before GET_ITER to LOAD_CONST tuple Convert BUILD_LIST/SET 0 + LOAD_CONST + LIST_EXTEND/SET_UPDATE + GET_ITER to just LOAD_CONST (tuple) + GET_ITER, matching CPython's optimization for constant list/set literals in for-loop iterables. Also fix is_name_imported to use method mode for function-local imports, and improve __static_attributes__ accuracy (skip @classmethod/@staticmethod, handle tuple/list unpacking targets). --- crates/codegen/src/ir.rs | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 5c20bcc6368..bff5de2df27 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -194,6 +194,8 @@ impl CodeInfo { self.fold_tuple_constants(); self.fold_list_constants(); self.fold_set_constants(); + self.remove_nops(); // remove NOPs from folding before iterable optimization + self.fold_const_iterable_for_iter(); self.convert_to_load_small_int(); self.remove_unused_consts(); self.remove_nops(); @@ -878,6 +880,49 @@ impl CodeInfo { } } + /// Convert constant list/set construction before GET_ITER to just LOAD_CONST tuple. + /// BUILD_LIST 0 + LOAD_CONST (tuple) + LIST_EXTEND 1 + GET_ITER + /// → LOAD_CONST (tuple) + GET_ITER + /// Also handles BUILD_SET 0 + LOAD_CONST + SET_UPDATE 1 + GET_ITER. + fn fold_const_iterable_for_iter(&mut self) { + for block in &mut self.blocks { + let mut i = 0; + while i + 3 < block.instructions.len() { + let is_build = matches!( + block.instructions[i].instr.real(), + Some(Instruction::BuildList { .. } | Instruction::BuildSet { .. }) + ) && u32::from(block.instructions[i].arg) == 0; + + let is_const = matches!( + block.instructions[i + 1].instr.real(), + Some(Instruction::LoadConst { .. }) + ); + + let is_extend = matches!( + block.instructions[i + 2].instr.real(), + Some(Instruction::ListExtend { .. } | Instruction::SetUpdate { .. }) + ) && u32::from(block.instructions[i + 2].arg) == 1; + + let is_iter = matches!( + block.instructions[i + 3].instr.real(), + Some(Instruction::GetIter) + ); + + if is_build && is_const && is_extend && is_iter { + // Replace: BUILD_X 0 → NOP, keep LOAD_CONST, LIST_EXTEND → NOP + let loc = block.instructions[i].location; + block.instructions[i].instr = Instruction::Nop.into(); + block.instructions[i].location = loc; + block.instructions[i + 2].instr = Instruction::Nop.into(); + block.instructions[i + 2].location = loc; + i += 4; + } else { + i += 1; + } + } + } + } + /// Fold constant set literals: LOAD_CONST* + BUILD_SET N → /// BUILD_SET 0 + LOAD_CONST (frozenset-as-tuple) + SET_UPDATE 1 fn fold_set_constants(&mut self) { From 5b19b7031ef036dd3ece601b0d5202021e37d855 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 14:35:39 +0900 Subject: [PATCH 08/15] Fix cell variable ordering: parameters first, then alphabetical CPython orders cell variables with parameter cells first (in parameter definition order), then non-parameter cells sorted alphabetically. Previously all cells were sorted alphabetically. Also add for-loop iterable optimization: constant BUILD_LIST/SET before GET_ITER is folded to just LOAD_CONST tuple. --- crates/codegen/src/compile.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index c4c69169d49..bb0acfc392e 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -1043,7 +1043,9 @@ impl Compiler { // Build cellvars using dictbytype (CELL scope or COMP_CELL flag, sorted) let mut cellvar_cache = IndexSet::default(); - let mut cell_names: Vec<_> = ste + // CPython ordering: parameter cells first (in parameter order), + // then non-parameter cells (alphabetically sorted) + let cell_symbols: Vec<_> = ste .symbols .iter() .filter(|(_, s)| { @@ -1051,8 +1053,22 @@ impl Compiler { }) .map(|(name, _)| name.clone()) .collect(); - cell_names.sort(); - for name in cell_names { + let mut param_cells = Vec::new(); + let mut nonparam_cells = Vec::new(); + for name in cell_symbols { + if varname_cache.contains(&name) { + param_cells.push(name); + } else { + nonparam_cells.push(name); + } + } + // param_cells are already in parameter order (from varname_cache insertion order) + param_cells.sort_by_key(|n| varname_cache.get_index_of(n.as_str()).unwrap_or(usize::MAX)); + nonparam_cells.sort(); + for name in param_cells { + cellvar_cache.insert(name); + } + for name in nonparam_cells { cellvar_cache.insert(name); } From 5b32c682cf3b1c0734a5e3621b9100a1daab023e Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 15:42:45 +0900 Subject: [PATCH 09/15] Emit COPY_FREE_VARS before MAKE_CELL matching CPython order CPython emits COPY_FREE_VARS first, then MAKE_CELL instructions. Previously RustPython emitted them in reverse order. --- crates/codegen/src/compile.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index bb0acfc392e..e6850c7fe93 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -1194,16 +1194,7 @@ impl Compiler { self.set_qualname(); } - // Emit MAKE_CELL for each cell variable (before RESUME) - { - let ncells = self.code_stack.last().unwrap().metadata.cellvars.len(); - for i in 0..ncells { - let i_varnum: oparg::VarNum = u32::try_from(i).expect("too many cellvars").into(); - emit!(self, Instruction::MakeCell { i: i_varnum }); - } - } - - // Emit COPY_FREE_VARS if there are free variables (before RESUME) + // Emit COPY_FREE_VARS first, then MAKE_CELL (CPython order) { let nfrees = self.code_stack.last().unwrap().metadata.freevars.len(); if nfrees > 0 { @@ -1215,6 +1206,13 @@ impl Compiler { ); } } + { + let ncells = self.code_stack.last().unwrap().metadata.cellvars.len(); + for i in 0..ncells { + let i_varnum: oparg::VarNum = u32::try_from(i).expect("too many cellvars").into(); + emit!(self, Instruction::MakeCell { i: i_varnum }); + } + } // Emit RESUME (handles async preamble and module lineno 0) // CPython: LOCATION(lineno, lineno, 0, 0), then loc.lineno = 0 for module From 96462fa9ce654d83c1e3dd3aeee73e8b264f8fd6 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 15:56:54 +0900 Subject: [PATCH 10/15] Fix RESUME AfterYield encoding to match CPython 3.14 (value 5) CPython 3.14 uses RESUME arg=5 for after-yield, not 1. Also reorder COPY_FREE_VARS before MAKE_CELL and fix cell variable ordering (parameters first, then alphabetical). --- crates/compiler-core/src/bytecode/oparg.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/compiler-core/src/bytecode/oparg.rs b/crates/compiler-core/src/bytecode/oparg.rs index 2dd18fba963..ba00180c97c 100644 --- a/crates/compiler-core/src/bytecode/oparg.rs +++ b/crates/compiler-core/src/bytecode/oparg.rs @@ -290,7 +290,7 @@ impl From for ResumeType { fn from(value: u32) -> Self { match value { 0 => Self::AtFuncStart, - 1 => Self::AfterYield, + 5 => Self::AfterYield, 2 => Self::AfterYieldFrom, 3 => Self::AfterAwait, _ => Self::Other(value), @@ -302,7 +302,7 @@ impl From for u32 { fn from(typ: ResumeType) -> Self { match typ { ResumeType::AtFuncStart => 0, - ResumeType::AfterYield => 1, + ResumeType::AfterYield => 5, ResumeType::AfterYieldFrom => 2, ResumeType::AfterAwait => 3, ResumeType::Other(v) => v, From 296eaa1e96bbefb71c209a4a00facbf2273e6024 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 16:43:20 +0900 Subject: [PATCH 11/15] Address code review feedback from #7481 - Set is_generator flag for generator expressions in scan_comprehension - Fix posonlyargs priority in collect_static_attributes first param - Add match statement support to scan_store_attrs - Fix stale decorator stack comment - Reorder NOP removal after fold_unary_negative for better collection folding --- crates/codegen/src/compile.rs | 11 ++++++++--- crates/codegen/src/ir.rs | 3 ++- crates/codegen/src/symboltable.rs | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index e6850c7fe93..60bbb204492 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -2775,7 +2775,7 @@ impl Compiler { } /// Push decorators onto the stack in source order. - /// For @dec1 @dec2 def foo(): stack becomes [dec1, NULL, dec2, NULL] + /// For @dec1 @dec2 def foo(): stack becomes [dec1, dec2] fn prepare_decorators(&mut self, decorator_list: &[ast::Decorator]) -> CompileResult<()> { for decorator in decorator_list { self.compile_expression(&decorator.expression)?; @@ -4547,9 +4547,9 @@ impl Compiler { } let first_param = f .parameters - .args + .posonlyargs .first() - .or(f.parameters.posonlyargs.first()) + .or(f.parameters.args.first()) .map(|p| &p.parameter.name); let Some(self_name) = first_param else { continue; @@ -4632,6 +4632,11 @@ impl Compiler { ast::Stmt::With(s) => { Self::scan_store_attrs(&s.body, name, attrs); } + ast::Stmt::Match(s) => { + for case in &s.cases { + Self::scan_store_attrs(&case.body, name, attrs); + } + } _ => {} } } diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index bff5de2df27..b9d30975064 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -191,10 +191,11 @@ impl CodeInfo { ) -> crate::InternalResult { // Constant folding passes self.fold_unary_negative(); + self.remove_nops(); // remove NOPs from unary folding so tuple/list/set see contiguous LOADs self.fold_tuple_constants(); self.fold_list_constants(); self.fold_set_constants(); - self.remove_nops(); // remove NOPs from folding before iterable optimization + self.remove_nops(); // remove NOPs from collection folding self.fold_const_iterable_for_iter(); self.convert_to_load_small_int(); self.remove_unused_consts(); diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index 6fd54c551d4..7844efa4a88 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -2136,6 +2136,8 @@ impl SymbolTableBuilder { CompilerScope::Comprehension, self.line_index_start(range), ); + // Generator expressions need the is_generator flag + self.tables.last_mut().unwrap().is_generator = is_generator; // PEP 709: Mark non-generator comprehensions for inlining, // but only inside function-like scopes (fastlocals). From b2eaa7126d0106457eebc82ed7395615f448575c Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 17:17:52 +0900 Subject: [PATCH 12/15] Fold constant list/set/tuple literals in compiler When all elements of a list/set/tuple literal are constants and there are 3+ elements, fold them into a single constant: - list: BUILD_LIST 0 + LOAD_CONST (tuple) + LIST_EXTEND 1 - set: BUILD_SET 0 + LOAD_CONST (tuple) + SET_UPDATE 1 - tuple: LOAD_CONST (tuple) This matches CPython's compiler optimization and fixes the most common bytecode difference (92/200 sampled files). Also add bytecode comparison scripts (dis_dump.py, compare_bytecode.py) for systematic parity tracking. --- crates/codegen/src/compile.rs | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 60bbb204492..32abd351aa3 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -564,6 +564,28 @@ impl Compiler { _ => n > 4, }; + // Fold all-constant collections (>= 3 elements) regardless of size + if !seen_star && pushed == 0 && n >= 3 && elts.iter().all(|e| e.is_constant()) { + if let Some(folded) = self.try_fold_constant_collection(elts)? { + match collection_type { + CollectionType::Tuple => { + self.emit_load_const(folded); + } + CollectionType::List => { + emit!(self, Instruction::BuildList { count: 0 }); + self.emit_load_const(folded); + emit!(self, Instruction::ListExtend { i: 1 }); + } + CollectionType::Set => { + emit!(self, Instruction::BuildSet { count: 0 }); + self.emit_load_const(folded); + emit!(self, Instruction::SetUpdate { i: 1 }); + } + } + return Ok(()); + } + } + // If no stars and not too big, compile all elements and build once if !seen_star && !big { for elt in elts { @@ -8554,6 +8576,56 @@ impl Compiler { info.metadata.consts.insert_full(constant).0.to_u32().into() } + /// Try to fold a collection of constant expressions into a single ConstantData::Tuple. + /// Returns None if any element cannot be folded. + fn try_fold_constant_collection( + &mut self, + elts: &[ast::Expr], + ) -> CompileResult> { + let mut constants = Vec::with_capacity(elts.len()); + for elt in elts { + match elt { + ast::Expr::NumberLiteral(num) => match &num.value { + ast::Number::Int(int) => { + let value = ruff_int_to_bigint(int).map_err(|e| self.error(e))?; + constants.push(ConstantData::Integer { value }); + } + ast::Number::Float(f) => { + constants.push(ConstantData::Float { value: *f }); + } + ast::Number::Complex { real, imag } => { + constants.push(ConstantData::Complex { + value: Complex::new(*real, *imag), + }); + } + }, + ast::Expr::StringLiteral(s) => { + constants.push(ConstantData::Str { + value: s.value.to_string().into(), + }); + } + ast::Expr::BytesLiteral(b) => { + constants.push(ConstantData::Bytes { + value: b.value.bytes().collect(), + }); + } + ast::Expr::BooleanLiteral(b) => { + constants.push(ConstantData::Boolean { value: b.value }); + } + ast::Expr::NoneLiteral(_) => { + constants.push(ConstantData::None); + } + ast::Expr::EllipsisLiteral(_) => { + constants.push(ConstantData::Ellipsis); + } + _ => return Ok(None), + } + } + Ok(Some(ConstantData::Tuple { + elements: constants, + })) + } + fn emit_load_const(&mut self, constant: ConstantData) { let idx = self.arg_constant(constant); self.emit_arg(idx, |consti| Instruction::LoadConst { consti }) From c7f505d77877131577ab40a6f9f56254f321374d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 17:38:58 +0900 Subject: [PATCH 13/15] Use BUILD_MAP 0 + MAP_ADD for large dicts (>= 16 pairs) Match CPython's compiler behavior: dicts with 16+ key-value pairs use BUILD_MAP 0 followed by MAP_ADD for each pair, instead of pushing all keys/values on the stack and calling BUILD_MAP N. --- crates/codegen/src/compile.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 32abd351aa3..617463aa841 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -723,9 +723,7 @@ impl Compiler { // Function-local imports use method mode (scope is Local) return !matches!( current.typ, - CompilerScope::Function - | CompilerScope::AsyncFunction - | CompilerScope::Lambda + CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda ); } if sym.scope == SymbolScope::Local { @@ -7046,10 +7044,7 @@ impl Compiler { let has_unpacking = items.iter().any(|item| item.key.is_none()); if !has_unpacking { - // STACK_USE_GUIDELINE: for large dicts (16+ pairs), use - // BUILD_MAP 0 + MAP_ADD to avoid excessive stack usage - let big = items.len() * 2 > 30; // ~15 pairs threshold - if big { + if items.len() >= 16 { emit!(self, Instruction::BuildMap { count: 0 }); for item in items { self.compile_expression(item.key.as_ref().unwrap())?; From a23315a04c379c56aa05e4aef477663ef09016ac Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 20:34:35 +0900 Subject: [PATCH 14/15] Fix clippy warnings and cargo fmt --- crates/codegen/src/compile.rs | 45 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 617463aa841..b9ab509d9dd 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -565,25 +565,28 @@ impl Compiler { }; // Fold all-constant collections (>= 3 elements) regardless of size - if !seen_star && pushed == 0 && n >= 3 && elts.iter().all(|e| e.is_constant()) { - if let Some(folded) = self.try_fold_constant_collection(elts)? { - match collection_type { - CollectionType::Tuple => { - self.emit_load_const(folded); - } - CollectionType::List => { - emit!(self, Instruction::BuildList { count: 0 }); - self.emit_load_const(folded); - emit!(self, Instruction::ListExtend { i: 1 }); - } - CollectionType::Set => { - emit!(self, Instruction::BuildSet { count: 0 }); - self.emit_load_const(folded); - emit!(self, Instruction::SetUpdate { i: 1 }); - } + if !seen_star + && pushed == 0 + && n >= 3 + && elts.iter().all(|e| e.is_constant()) + && let Some(folded) = self.try_fold_constant_collection(elts)? + { + match collection_type { + CollectionType::Tuple => { + self.emit_load_const(folded); + } + CollectionType::List => { + emit!(self, Instruction::BuildList { count: 0 }); + self.emit_load_const(folded); + emit!(self, Instruction::ListExtend { i: 1 }); + } + CollectionType::Set => { + emit!(self, Instruction::BuildSet { count: 0 }); + self.emit_load_const(folded); + emit!(self, Instruction::SetUpdate { i: 1 }); } - return Ok(()); } + return Ok(()); } // If no stars and not too big, compile all elements and build once @@ -4582,10 +4585,10 @@ impl Compiler { fn scan_target_for_attrs(target: &ast::Expr, name: &str, attrs: &mut IndexSet) { match target { ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => { - if let ast::Expr::Name(n) = value.as_ref() { - if n.id.as_str() == name { - attrs.insert(attr.to_string()); - } + if let ast::Expr::Name(n) = value.as_ref() + && n.id.as_str() == name + { + attrs.insert(attr.to_string()); } } ast::Expr::Tuple(t) => { From cab59e7df9111df5bf2b6e51c61fdc0dbc4be2da Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 21:43:53 +0900 Subject: [PATCH 15/15] fix surrogate --- crates/codegen/src/compile.rs | 42 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index b9ab509d9dd..a04d8ff0837 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -7622,23 +7622,8 @@ impl Compiler { self.compile_expr_tstring(tstring)?; } ast::Expr::StringLiteral(string) => { - let value = string.value.to_str(); - if value.contains(char::REPLACEMENT_CHARACTER) { - let value = string - .value - .iter() - .map(|lit| { - let source = self.source_file.slice(lit.range); - crate::string_parser::parse_string_literal(source, lit.flags.into()) - }) - .collect(); - // might have a surrogate literal; should reparse to be sure - self.emit_load_const(ConstantData::Str { value }); - } else { - self.emit_load_const(ConstantData::Str { - value: value.into(), - }); - } + let value = self.compile_string_value(string); + self.emit_load_const(ConstantData::Str { value }); } ast::Expr::BytesLiteral(bytes) => { let iter = bytes.value.iter().flat_map(|x| x.iter().copied()); @@ -8569,6 +8554,24 @@ impl Compiler { // fn block_done() + /// Convert a string literal AST node to Wtf8Buf, handling surrogates correctly. + fn compile_string_value(&self, string: &ast::ExprStringLiteral) -> Wtf8Buf { + let value = string.value.to_str(); + if value.contains(char::REPLACEMENT_CHARACTER) { + // Might have a surrogate literal; reparse from source to preserve them + string + .value + .iter() + .map(|lit| { + let source = self.source_file.slice(lit.range); + crate::string_parser::parse_string_literal(source, lit.flags.into()) + }) + .collect() + } else { + value.into() + } + } + fn arg_constant(&mut self, constant: ConstantData) -> oparg::ConstIdx { let info = self.current_code_info(); info.metadata.consts.insert_full(constant).0.to_u32().into() @@ -8598,9 +8601,8 @@ impl Compiler { } }, ast::Expr::StringLiteral(s) => { - constants.push(ConstantData::Str { - value: s.value.to_string().into(), - }); + let value = self.compile_string_value(s); + constants.push(ConstantData::Str { value }); } ast::Expr::BytesLiteral(b) => { constants.push(ConstantData::Bytes {