X Tutup
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4813,6 +4813,8 @@ def test_finalize(self):
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
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_peepholer.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,6 @@ def containtest():
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]:
Expand Down
60 changes: 50 additions & 10 deletions crates/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Comment on lines +1113 to 1116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the module conditional-annotation preamble shared across module entry paths.

This now wires the root-module cell + MAKE_CELL preamble only through compile_program(). compile_program_single() still goes straight to RESUME, so the same annotated module will compile differently between exec and single, which leaves a bytecode-parity gap in exactly the area this PR is tightening.

As per coding guidelines, "When branches differ only in a value but share common logic, extract the differing value first, then call the common logic once to avoid duplicate code".

Also applies to: 1799-1817

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/codegen/src/compile.rs` around lines 1113 - 1116, The module
conditional-annotation preamble must be emitted identically for both entry
paths; modify compile_program_single() to share the same logic used by
compile_program() by checking ste.has_conditional_annotations (or computing the
conditional-annotations flag first) and then invoking the common preamble
emissions that insert the root-module cell into cellvar_cache (insert
"__conditional_annotations__") and emit the MAKE_CELL preamble instead of
falling through to RESUME; refactor so the differing entry value is determined
first and the shared logic that updates cellvar_cache and emits the MAKE_CELL
preamble is called from both compile_program() and compile_program_single(),
ensuring bytecode parity (also apply same refactor to the other occurrence
around the code referenced at ~1799-1817).

}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 13 additions & 5 deletions crates/codegen/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 39 additions & 9 deletions crates/codegen/src/symboltable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
Loading
X Tutup