mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
67e3e4bebb
commit
5433a3d08c
6 files changed
+127
-39
No files matched your search
Vendored
+2
@@ -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
|
||||
|
||||
Vendored
-1
@@ -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]:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+23
-14
@@ -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
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user