From 7a2f43c0d2c990f2af197a433529250f3f5599bf Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 14 Mar 2026 03:55:08 +0900 Subject: [PATCH] Consume nested scope tables in optimized-out asserts When -O flag removes assert statements, any nested scopes (generators, comprehensions, lambdas) inside the assert expression still have symbol tables in the sub_tables list. Without consuming them, the next_sub_table index gets misaligned, causing later scopes to use wrong symbol tables. Walk the skipped assert expression with an AST visitor to find and consume nested scope symbol tables, keeping the index aligned with AST traversal order. --- Lib/test/_test_multiprocessing.py | 2 - crates/codegen/src/compile.rs | 135 ++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 35ce70fced2..965c0c0f493 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -5442,8 +5442,6 @@ def run_in_child(cls, start_method): flags = (tuple(sys.flags), grandchild_flags) print(json.dumps(flags)) - # TODO: RUSTPYTHON - SyntaxError in subprocess after fork - @unittest.expectedFailure def test_flags(self): import json # start child process using unusual flags diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 94c6642aac2..f7f5b944a0f 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -2366,6 +2366,14 @@ impl Compiler { ); self.switch_to_block(after_block); + } else { + // Optimized-out asserts still need to consume any nested + // scope symbol tables they contain so later nested scopes + // stay aligned with AST traversal order. + self.consume_skipped_nested_scopes_in_expr(test)?; + if let Some(expr) = msg { + self.consume_skipped_nested_scopes_in_expr(expr)?; + } } } ast::Stmt::Break(_) => { @@ -7605,6 +7613,92 @@ impl Compiler { }) } + fn consume_next_sub_table(&mut self) -> CompileResult<()> { + { + let _ = self.push_symbol_table()?; + } + let _ = self.pop_symbol_table(); + Ok(()) + } + + fn consume_skipped_nested_scopes_in_expr( + &mut self, + expression: &ast::Expr, + ) -> CompileResult<()> { + use ast::visitor::Visitor; + + struct SkippedScopeVisitor<'a> { + compiler: &'a mut Compiler, + error: Option, + } + + impl SkippedScopeVisitor<'_> { + fn consume_scope(&mut self) { + if self.error.is_none() { + self.error = self.compiler.consume_next_sub_table().err(); + } + } + } + + impl ast::visitor::Visitor<'_> for SkippedScopeVisitor<'_> { + fn visit_expr(&mut self, expr: &ast::Expr) { + if self.error.is_some() { + return; + } + + match expr { + ast::Expr::Lambda(ast::ExprLambda { parameters, .. }) => { + // Defaults are scanned before enter_scope in the + // symbol table builder, so their nested scopes + // precede the lambda scope in sub_tables. + if let Some(params) = parameters.as_deref() { + for default in params + .posonlyargs + .iter() + .chain(¶ms.args) + .chain(¶ms.kwonlyargs) + .filter_map(|p| p.default.as_deref()) + { + self.visit_expr(default); + } + } + self.consume_scope(); + } + ast::Expr::ListComp(ast::ExprListComp { generators, .. }) + | ast::Expr::SetComp(ast::ExprSetComp { generators, .. }) + | ast::Expr::Generator(ast::ExprGenerator { generators, .. }) => { + // leave_scope runs before the first iterator is + // scanned, so the comprehension scope comes first + // in sub_tables, then any nested scopes from the + // first iterator. + self.consume_scope(); + if let Some(first) = generators.first() { + self.visit_expr(&first.iter); + } + } + ast::Expr::DictComp(ast::ExprDictComp { generators, .. }) => { + self.consume_scope(); + if let Some(first) = generators.first() { + self.visit_expr(&first.iter); + } + } + _ => ast::visitor::walk_expr(self, expr), + } + } + } + + let mut visitor = SkippedScopeVisitor { + compiler: self, + error: None, + }; + visitor.visit_expr(expression); + if let Some(err) = visitor.error { + Err(err) + } else { + Ok(()) + } + } + fn compile_comprehension( &mut self, name: &str, @@ -9184,4 +9278,45 @@ async def test(): " )); } + + #[test] + fn test_optimized_assert_preserves_nested_scope_order() { + compile_exec_optimized( + "\ +class S: + def f(self, sequence): + _formats = [self._types_mapping[type(item)] for item in sequence] + _list_len = len(_formats) + assert sum(len(fmt) <= 8 for fmt in _formats) == _list_len + _recreation_codes = [self._extract_recreation_code(item) for item in sequence] +", + ); + } + + #[test] + fn test_optimized_assert_with_nested_scope_in_first_iter() { + // First iterator of a comprehension is evaluated in the enclosing + // scope, so nested scopes inside it (the generator here) must also + // be consumed when the assert is optimized away. + compile_exec_optimized( + "\ +def f(items): + assert [x for x in (y for y in items)] + return [x for x in items] +", + ); + } + + #[test] + fn test_optimized_assert_with_lambda_defaults() { + // Lambda default values are evaluated in the enclosing scope, + // so nested scopes inside defaults must be consumed. + compile_exec_optimized( + "\ +def f(items): + assert (lambda x=[i for i in items]: x)() + return [x for x in items] +", + ); + } }