Skip to content

Commit 7a2f43c

Browse files
committed
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.
1 parent f49af3f commit 7a2f43c

File tree

2 files changed

+135
-2
lines changed

2 files changed

+135
-2
lines changed

Lib/test/_test_multiprocessing.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5442,8 +5442,6 @@ def run_in_child(cls, start_method):
54425442
flags = (tuple(sys.flags), grandchild_flags)
54435443
print(json.dumps(flags))
54445444

5445-
# TODO: RUSTPYTHON - SyntaxError in subprocess after fork
5446-
@unittest.expectedFailure
54475445
def test_flags(self):
54485446
import json
54495447
# start child process using unusual flags

crates/codegen/src/compile.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2366,6 +2366,14 @@ impl Compiler {
23662366
);
23672367

23682368
self.switch_to_block(after_block);
2369+
} else {
2370+
// Optimized-out asserts still need to consume any nested
2371+
// scope symbol tables they contain so later nested scopes
2372+
// stay aligned with AST traversal order.
2373+
self.consume_skipped_nested_scopes_in_expr(test)?;
2374+
if let Some(expr) = msg {
2375+
self.consume_skipped_nested_scopes_in_expr(expr)?;
2376+
}
23692377
}
23702378
}
23712379
ast::Stmt::Break(_) => {
@@ -7605,6 +7613,92 @@ impl Compiler {
76057613
})
76067614
}
76077615

7616+
fn consume_next_sub_table(&mut self) -> CompileResult<()> {
7617+
{
7618+
let _ = self.push_symbol_table()?;
7619+
}
7620+
let _ = self.pop_symbol_table();
7621+
Ok(())
7622+
}
7623+
7624+
fn consume_skipped_nested_scopes_in_expr(
7625+
&mut self,
7626+
expression: &ast::Expr,
7627+
) -> CompileResult<()> {
7628+
use ast::visitor::Visitor;
7629+
7630+
struct SkippedScopeVisitor<'a> {
7631+
compiler: &'a mut Compiler,
7632+
error: Option<CodegenError>,
7633+
}
7634+
7635+
impl SkippedScopeVisitor<'_> {
7636+
fn consume_scope(&mut self) {
7637+
if self.error.is_none() {
7638+
self.error = self.compiler.consume_next_sub_table().err();
7639+
}
7640+
}
7641+
}
7642+
7643+
impl ast::visitor::Visitor<'_> for SkippedScopeVisitor<'_> {
7644+
fn visit_expr(&mut self, expr: &ast::Expr) {
7645+
if self.error.is_some() {
7646+
return;
7647+
}
7648+
7649+
match expr {
7650+
ast::Expr::Lambda(ast::ExprLambda { parameters, .. }) => {
7651+
// Defaults are scanned before enter_scope in the
7652+
// symbol table builder, so their nested scopes
7653+
// precede the lambda scope in sub_tables.
7654+
if let Some(params) = parameters.as_deref() {
7655+
for default in params
7656+
.posonlyargs
7657+
.iter()
7658+
.chain(&params.args)
7659+
.chain(&params.kwonlyargs)
7660+
.filter_map(|p| p.default.as_deref())
7661+
{
7662+
self.visit_expr(default);
7663+
}
7664+
}
7665+
self.consume_scope();
7666+
}
7667+
ast::Expr::ListComp(ast::ExprListComp { generators, .. })
7668+
| ast::Expr::SetComp(ast::ExprSetComp { generators, .. })
7669+
| ast::Expr::Generator(ast::ExprGenerator { generators, .. }) => {
7670+
// leave_scope runs before the first iterator is
7671+
// scanned, so the comprehension scope comes first
7672+
// in sub_tables, then any nested scopes from the
7673+
// first iterator.
7674+
self.consume_scope();
7675+
if let Some(first) = generators.first() {
7676+
self.visit_expr(&first.iter);
7677+
}
7678+
}
7679+
ast::Expr::DictComp(ast::ExprDictComp { generators, .. }) => {
7680+
self.consume_scope();
7681+
if let Some(first) = generators.first() {
7682+
self.visit_expr(&first.iter);
7683+
}
7684+
}
7685+
_ => ast::visitor::walk_expr(self, expr),
7686+
}
7687+
}
7688+
}
7689+
7690+
let mut visitor = SkippedScopeVisitor {
7691+
compiler: self,
7692+
error: None,
7693+
};
7694+
visitor.visit_expr(expression);
7695+
if let Some(err) = visitor.error {
7696+
Err(err)
7697+
} else {
7698+
Ok(())
7699+
}
7700+
}
7701+
76087702
fn compile_comprehension(
76097703
&mut self,
76107704
name: &str,
@@ -9184,4 +9278,45 @@ async def test():
91849278
"
91859279
));
91869280
}
9281+
9282+
#[test]
9283+
fn test_optimized_assert_preserves_nested_scope_order() {
9284+
compile_exec_optimized(
9285+
"\
9286+
class S:
9287+
def f(self, sequence):
9288+
_formats = [self._types_mapping[type(item)] for item in sequence]
9289+
_list_len = len(_formats)
9290+
assert sum(len(fmt) <= 8 for fmt in _formats) == _list_len
9291+
_recreation_codes = [self._extract_recreation_code(item) for item in sequence]
9292+
",
9293+
);
9294+
}
9295+
9296+
#[test]
9297+
fn test_optimized_assert_with_nested_scope_in_first_iter() {
9298+
// First iterator of a comprehension is evaluated in the enclosing
9299+
// scope, so nested scopes inside it (the generator here) must also
9300+
// be consumed when the assert is optimized away.
9301+
compile_exec_optimized(
9302+
"\
9303+
def f(items):
9304+
assert [x for x in (y for y in items)]
9305+
return [x for x in items]
9306+
",
9307+
);
9308+
}
9309+
9310+
#[test]
9311+
fn test_optimized_assert_with_lambda_defaults() {
9312+
// Lambda default values are evaluated in the enclosing scope,
9313+
// so nested scopes inside defaults must be consumed.
9314+
compile_exec_optimized(
9315+
"\
9316+
def f(items):
9317+
assert (lambda x=[i for i in items]: x)()
9318+
return [x for x in items]
9319+
",
9320+
);
9321+
}
91879322
}

0 commit comments

Comments
 (0)