Bytecode parity (#7507)

* Bytecode parity phase 3

Compiler changes:
- Emit TO_BOOL in and/or short-circuit evaluation (COPY+TO_BOOL+JUMP)
- Add module-level __conditional_annotations__ cell (PEP 649)
- Only set conditional annotations for AnnAssign, not function params
- Skip __classdict__ cell when future annotations are active
- Convert list literals to tuples in for-loop iterables
- Fix cell variable ordering: parameters first, then alphabetical
- Fix RESUME DEPTH1 flag for yield-from/await
- Don't propagate __classdict__/__conditional_annotations__ freevar
  through regular functions — only annotation/type-param scopes
- Inline string compilation path

* Skip test_thread_safety in _test_multiprocessing

SIGSEGV in _finalizer_registry dict access under aggressive GC
and thread switching. Root cause is dict thread-safety in VM.

* Skip list→tuple optimization for async for; propagate future_annotations to nested scopes
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-03-27 12:42:29 +09:00
1 parent 67e3e4bebb
commit 5433a3d08c
6 files changed
+127 -39

No files matched your search

+2
View File
@@ -4813,6 +4813,8 @@ class _TestFinalize(BaseTestCase):
result = [obj for obj in iter(conn.recv, 'STOP')]
self.assertEqual(result, ['a', 'b', 'd10', 'd03', 'd02', 'd01', 'e'])
# TODO: RUSTPYTHON; SIGSEGV due to dict thread-safety issue under aggressive GC
@unittest.skip("TODO: RUSTPYTHON")
@support.requires_resource('cpu')
def test_thread_safety(self):
# bpo-24484: _run_finalizers() should be thread-safe
-1
View File
@@ -645,7 +645,6 @@ class TestTranforms(BytecodeTestCase):
self.assertEqual(count_instr_recursively(containtest, 'BUILD_LIST'), 0)
self.check_lnotab(containtest)
@unittest.expectedFailure # TODO: RUSTPYTHON; no BUILD_LIST to BUILD_TUPLE optimization
def test_iterate_literal_list(self):
def forloop():
for x in [a, b]:
+50 -10
View File
@@ -1074,12 +1074,12 @@ impl Compiler {
.filter(|(_, s)| {
s.scope == SymbolScope::Cell || s.flags.contains(SymbolFlags::COMP_CELL)
})
.map(|(name, _)| name.clone())
.map(|(name, sym)| (name.clone(), sym.flags))
.collect();
let mut param_cells = Vec::new();
let mut nonparam_cells = Vec::new();
for name in cell_symbols {
if varname_cache.contains(&name) {
for (name, flags) in cell_symbols {
if flags.contains(SymbolFlags::PARAMETER) {
param_cells.push(name);
} else {
nonparam_cells.push(name);
@@ -1110,8 +1110,9 @@ impl Compiler {
}
// Handle implicit __conditional_annotations__ cell if needed
// Only for class scope - module scope uses NAME operations, not DEREF
if ste.has_conditional_annotations && scope_type == CompilerScope::Class {
if ste.has_conditional_annotations
&& matches!(scope_type, CompilerScope::Class | CompilerScope::Module)
{
cellvar_cache.insert("__conditional_annotations__".to_string());
}
@@ -1794,8 +1795,27 @@ impl Compiler {
let size_before = self.code_stack.len();
// Set future_annotations from symbol table (detected during symbol table scan)
self.future_annotations = symbol_table.future_annotations;
// Module-level __conditional_annotations__ cell
let has_module_cond_ann = symbol_table.has_conditional_annotations;
if has_module_cond_ann {
self.current_code_info()
.metadata
.cellvars
.insert("__conditional_annotations__".to_string());
}
self.symbol_table_stack.push(symbol_table);
// Emit MAKE_CELL for module-level cells (before RESUME)
if has_module_cond_ann {
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 });
}
}
self.emit_resume_for_scope(CompilerScope::Module, 1);
let (doc, statements) = split_doc(&body.body, &self.opts);
@@ -5437,7 +5457,25 @@ impl Compiler {
let mut end_async_for_target = BlockIdx::NULL;
// The thing iterated:
self.compile_expression(iter)?;
// Optimize: `for x in [a, b, c]` → use tuple instead of list
// (list creation is wasteful for iteration)
// Skip optimization if any element is starred (e.g., `[a, *b, c]`)
if !is_async
&& let ast::Expr::List(ast::ExprList { elts, .. }) = iter
&& !elts.iter().any(|e| matches!(e, ast::Expr::Starred(_)))
{
for elt in elts {
self.compile_expression(elt)?;
}
emit!(
self,
Instruction::BuildTuple {
count: u32::try_from(elts.len()).expect("too many elements"),
}
);
} else {
self.compile_expression(iter)?;
}
if is_async {
if self.ctx.func != FunctionContext::AsyncFunction {
@@ -7033,6 +7071,7 @@ impl Compiler {
/// For `And`, emits `PopJumpIfFalse`; for `Or`, emits `PopJumpIfTrue`.
fn emit_short_circuit_test(&mut self, op: &ast::BoolOp, target: BlockIdx) {
emit!(self, Instruction::Copy { i: 1 });
emit!(self, Instruction::ToBool);
match op {
ast::BoolOp::And => {
emit!(self, Instruction::PopJumpIfFalse { delta: target });
@@ -8554,11 +8593,11 @@ impl Compiler {
// fn block_done()
/// Convert a string literal AST node to Wtf8Buf, handling surrogates correctly.
/// Convert a string literal AST node to Wtf8Buf, handling surrogate literals 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
// Might have a surrogate literal; reparse from source to preserve them.
string
.value
.iter()
@@ -8601,8 +8640,9 @@ impl Compiler {
}
},
ast::Expr::StringLiteral(s) => {
let value = self.compile_string_value(s);
constants.push(ConstantData::Str { value });
constants.push(ConstantData::Str {
value: self.compile_string_value(s),
});
}
ast::Expr::BytesLiteral(b) => {
constants.push(ConstantData::Bytes {
+13 -5
View File
@@ -2172,11 +2172,19 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) {
blocks[bi].instructions[i].except_handler = handler_info;
// Track YIELD_VALUE except stack depth
if matches!(
blocks[bi].instructions[i].instr.real(),
Some(Instruction::YieldValue { .. })
) {
last_yield_except_depth = stack.len() as i32;
// Only count for direct yield (arg=0), not yield-from/await (arg=1)
// The yield-from's internal SETUP_FINALLY is not an external except depth
if let Some(Instruction::YieldValue { .. }) =
blocks[bi].instructions[i].instr.real()
{
let yield_arg = u32::from(blocks[bi].instructions[i].arg);
if yield_arg == 0 {
// Direct yield: count actual except depth
last_yield_except_depth = stack.len() as i32;
} else {
// yield-from/await: subtract 1 for the internal SETUP_FINALLY
last_yield_except_depth = (stack.len() as i32) - 1;
}
}
// Set RESUME DEPTH1 flag based on last yield's except depth
@@ -1,5 +1,6 @@
---
source: crates/codegen/src/compile.rs
assertion_line: 9688
expression: "compile_exec(\"\\\nx = Test() and False or False\n\")"
---
1 0 RESUME (0)
@@ -9,18 +10,26 @@ expression: "compile_exec(\"\\\nx = Test() and False or False\n\")"
4 CACHE
5 CACHE
6 CACHE
>> 7 COPY (1)
8 POP_JUMP_IF_FALSE (7)
7 COPY (1)
8 TO_BOOL
9 CACHE
10 NOT_TAKEN
11 POP_TOP
12 LOAD_CONST (False)
13 COPY (1)
14 POP_JUMP_IF_TRUE (3)
15 CACHE
16 NOT_TAKEN
17 POP_TOP
18 LOAD_CONST (False)
19 STORE_NAME (1, x)
20 LOAD_CONST (None)
21 RETURN_VALUE
10 CACHE
>> 11 CACHE
12 POP_JUMP_IF_FALSE (11)
13 CACHE
14 NOT_TAKEN
15 POP_TOP
16 LOAD_CONST (False)
17 COPY (1)
18 TO_BOOL
19 CACHE
20 CACHE
21 CACHE
22 POP_JUMP_IF_TRUE (3)
23 CACHE
24 NOT_TAKEN
25 POP_TOP
26 LOAD_CONST (False)
27 STORE_NAME (1, x)
28 LOAD_CONST (None)
29 RETURN_VALUE
+39 -9
View File
@@ -297,7 +297,8 @@ fn drop_class_free(symbol_table: &mut SymbolTable, newfree: &mut IndexSet<String
}
// Classes with function definitions need __classdict__ for PEP 649
if !symbol_table.needs_classdict {
// (but not when `from __future__ import annotations` is active)
if !symbol_table.needs_classdict && !symbol_table.future_annotations {
let has_functions = symbol_table.sub_tables.iter().any(|t| {
matches!(
t.typ,
@@ -571,6 +572,9 @@ impl SymbolTableAnalyzer {
newfree.extend(child_free);
}
if let Some(ann_free) = annotation_free {
// Propagate annotation-scope free names to this scope so
// implicit class-scope cells (__classdict__/__conditional_annotations__)
// can be materialized by drop_class_free when needed.
newfree.extend(ann_free);
}
@@ -758,6 +762,12 @@ impl SymbolTableAnalyzer {
if let Some(decl_depth) = decl_depth {
// decl_depth is the number of tables between the current one and
// the one that declared the cell var
// For implicit class scope variables (__classdict__, __conditional_annotations__),
// only propagate free to annotation/type-param scopes, not regular functions.
// Regular method functions don't need these in their freevars.
let is_class_implicit =
name == "__classdict__" || name == "__conditional_annotations__";
for (table, typ) in self.tables.iter_mut().rev().take(decl_depth) {
if let CompilerScope::Class = typ {
if let Some(free_class) = table.get_mut(name) {
@@ -768,10 +778,19 @@ impl SymbolTableAnalyzer {
symbol.scope = SymbolScope::Free;
table.insert(name.to_owned(), symbol);
}
} else if is_class_implicit
&& matches!(
typ,
CompilerScope::Function
| CompilerScope::AsyncFunction
| CompilerScope::Lambda
)
{
// Skip: don't add __classdict__/__conditional_annotations__
// as free vars in regular functions — only annotation/type scopes need them
} else if !table.contains_key(name) {
let mut symbol = Symbol::new(name);
symbol.scope = SymbolScope::Free;
// symbol.is_referenced = true;
table.insert(name.to_owned(), symbol);
}
}
@@ -1016,6 +1035,7 @@ impl SymbolTableBuilder {
.and_then(|t| t.mangled_names.clone())
.filter(|_| typ != CompilerScope::Class);
let mut table = SymbolTable::new(name.to_owned(), typ, line_number, is_nested);
table.future_annotations = self.future_annotations;
table.mangled_names = inherited_mangled_names;
self.tables.push(table);
// Save parent's varnames and start fresh for the new scope
@@ -1226,20 +1246,30 @@ impl SymbolTableBuilder {
}
fn scan_annotation(&mut self, annotation: &ast::Expr) -> SymbolTableResult {
self.scan_annotation_inner(annotation, false)
}
/// Scan an annotation from an AnnAssign statement (can be conditional)
fn scan_ann_assign_annotation(&mut self, annotation: &ast::Expr) -> SymbolTableResult {
self.scan_annotation_inner(annotation, true)
}
fn scan_annotation_inner(
&mut self,
annotation: &ast::Expr,
is_ann_assign: bool,
) -> SymbolTableResult {
let current_scope = self.tables.last().map(|t| t.typ);
// PEP 649: Check if this is a conditional annotation
// Module-level: always conditional (module may be partially executed)
// Class-level: conditional only when inside if/for/while/etc.
if !self.future_annotations {
// PEP 649: Only AnnAssign annotations can be conditional.
// Function parameter/return annotations are never conditional.
if is_ann_assign && !self.future_annotations {
let is_conditional = matches!(current_scope, Some(CompilerScope::Module))
|| (matches!(current_scope, Some(CompilerScope::Class))
&& self.in_conditional_block);
if is_conditional && !self.tables.last().unwrap().has_conditional_annotations {
self.tables.last_mut().unwrap().has_conditional_annotations = true;
// Register __conditional_annotations__ as both Assigned and Used so that
// it becomes a Cell variable in class scope (children reference it as Free)
self.register_name(
"__conditional_annotations__",
SymbolUsage::Assigned,
@@ -1571,7 +1601,7 @@ impl SymbolTableBuilder {
// sub_tables that cause mismatch in the annotation scope's sub_table index.
let is_simple_name = *simple && matches!(&**target, Expr::Name(_));
if is_simple_name {
self.scan_annotation(annotation)?;
self.scan_ann_assign_annotation(annotation)?;
} else {
// Still validate annotation for forbidden expressions
// (yield, await, named) even for non-simple targets.