diff --git a/Lib/test/test_named_expressions.py b/Lib/test/test_named_expressions.py index 03f5384b632..68e4a7704f4 100644 --- a/Lib/test/test_named_expressions.py +++ b/Lib/test/test_named_expressions.py @@ -165,8 +165,6 @@ def test_named_expression_invalid_rebinding_list_comprehension_iteration_variabl with self.assertRaisesRegex(SyntaxError, msg): exec(code, {}, {}) - # TODO: RUSTPYTHON - @unittest.expectedFailure # wrong error message def test_named_expression_invalid_rebinding_list_comprehension_inner_loop(self): cases = [ ("Inner reuse", 'j', "[i for i in range(5) if (j := 0) for j in range(5)]"), @@ -223,8 +221,6 @@ def test_named_expression_invalid_rebinding_set_comprehension_iteration_variable with self.assertRaisesRegex(SyntaxError, msg): exec(code, {}, {}) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_named_expression_invalid_rebinding_set_comprehension_inner_loop(self): cases = [ ("Inner reuse", 'j', "{i for i in range(5) if (j := 0) for j in range(5)}"), diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index 07766373d5a..95f9c0cd400 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -585,7 +585,11 @@ impl SymbolTableAnalyzer { self.analyze_symbol_comprehension(symbol, parent_offset + 1)?; } CompilerScope::TypeParams => { - todo!("analyze symbol comprehension for type params"); + // Named expression in comprehension cannot be used in type params + return Err(SymbolTableError { + error: "assignment expression within a comprehension cannot be used within the definition of a generic".to_string(), + location: None, + }); } } Ok(()) @@ -617,6 +621,14 @@ struct SymbolTableBuilder { current_varnames: Vec, // Track if we're inside an iterable definition expression (for nested comprehensions) in_iter_def_exp: bool, + // Track if we're inside an annotation (yield/await/named expr not allowed) + in_annotation: bool, + // Track if we're inside a type alias (yield/await/named expr not allowed) + in_type_alias: bool, + // Track if we're scanning an inner loop iteration target (not the first generator) + in_comp_inner_loop_target: bool, + // Scope info for error messages (e.g., "a TypeVar bound") + scope_info: Option<&'static str>, } /// Enum to indicate in what mode an expression @@ -641,6 +653,10 @@ impl SymbolTableBuilder { source_file, current_varnames: Vec::new(), in_iter_def_exp: false, + in_annotation: false, + in_type_alias: false, + in_comp_inner_loop_target: false, + scope_info: None, }; this.enter_scope("top", CompilerScope::Module, 0); this @@ -751,7 +767,11 @@ impl SymbolTableBuilder { if self.future_annotations { Ok(()) } else { - self.scan_expression(annotation, ExpressionContext::Load) + let was_in_annotation = self.in_annotation; + self.in_annotation = true; + let result = self.scan_expression(annotation, ExpressionContext::Load); + self.in_annotation = was_in_annotation; + result } } @@ -1020,6 +1040,8 @@ impl SymbolTableBuilder { type_params, .. }) => { + let was_in_type_alias = self.in_type_alias; + self.in_type_alias = true; if let Some(type_params) = type_params { self.enter_type_param_block( "TypeAlias", @@ -1031,6 +1053,7 @@ impl SymbolTableBuilder { } else { self.scan_expression(value, ExpressionContext::Load)?; } + self.in_type_alias = was_in_type_alias; self.scan_expression(name, ExpressionContext::Store)?; } Stmt::IpyEscapeCommand(_) => todo!(), @@ -1067,24 +1090,40 @@ impl SymbolTableBuilder { ) -> SymbolTableResult { use ruff_python_ast::*; - // Check for expressions not allowed in type parameters scope - if let Some(table) = self.tables.last() - && table.typ == CompilerScope::TypeParams - && let Some(keyword) = match expression { - Expr::Yield(_) | Expr::YieldFrom(_) => Some("yield"), - Expr::Await(_) => Some("await"), - Expr::Named(_) => Some("named"), - _ => None, + // Check for expressions not allowed in certain contexts + // (type parameters, annotations, type aliases, TypeVar bounds/defaults) + if let Some(keyword) = match expression { + Expr::Yield(_) | Expr::YieldFrom(_) => Some("yield"), + Expr::Await(_) => Some("await"), + Expr::Named(_) => Some("named"), + _ => None, + } { + // Determine the context name for the error message + // scope_info takes precedence (e.g., "a TypeVar bound") + let context_name = if let Some(scope_info) = self.scope_info { + Some(scope_info) + } else if let Some(table) = self.tables.last() + && table.typ == CompilerScope::TypeParams + { + Some("a type parameter") + } else if self.in_annotation { + Some("an annotation") + } else if self.in_type_alias { + Some("a type alias") + } else { + None + }; + + if let Some(context_name) = context_name { + return Err(SymbolTableError { + error: format!("{keyword} expression cannot be used within {context_name}"), + location: Some( + self.source_file + .to_source_code() + .source_location(expression.range().start(), PositionEncoding::Utf8), + ), + }); } - { - return Err(SymbolTableError { - error: format!("{keyword} expression cannot be used within a type parameter"), - location: Some( - self.source_file - .to_source_code() - .source_location(expression.range().start(), PositionEncoding::Utf8), - ), - }); } match expression { @@ -1430,7 +1469,13 @@ impl SymbolTableBuilder { let mut is_first_generator = true; for generator in generators { + // Set flag for INNER_LOOP_CONFLICT check (only for inner loops, not the first) + if !is_first_generator { + self.in_comp_inner_loop_target = true; + } self.scan_expression(&generator.target, ExpressionContext::Iter)?; + self.in_comp_inner_loop_target = false; + if is_first_generator { is_first_generator = false; } else { @@ -1453,25 +1498,55 @@ impl SymbolTableBuilder { /// Scan type parameter bound or default in a separate scope // = symtable_visit_type_param_bound_or_default - fn scan_type_param_bound_or_default(&mut self, expr: &Expr, name: &str) -> SymbolTableResult { + fn scan_type_param_bound_or_default( + &mut self, + expr: &Expr, + scope_name: &str, + scope_info: &'static str, + ) -> SymbolTableResult { // Enter a new TypeParams scope for the bound/default expression // This allows the expression to access outer scope symbols let line_number = self.line_index_start(expr.range()); - self.enter_scope(name, CompilerScope::TypeParams, line_number); + self.enter_scope(scope_name, CompilerScope::TypeParams, line_number); // Note: In CPython, can_see_class_scope is preserved in the new scope // In RustPython, this is handled through the scope hierarchy + // Set scope_info for better error messages + let old_scope_info = self.scope_info; + self.scope_info = Some(scope_info); + // Scan the expression in this new scope let result = self.scan_expression(expr, ExpressionContext::Load); - // Exit the scope + // Restore scope_info and exit the scope + self.scope_info = old_scope_info; self.leave_scope(); result } fn scan_type_params(&mut self, type_params: &TypeParams) -> SymbolTableResult { + // Check for duplicate type parameter names + let mut seen_names: std::collections::HashSet<&str> = std::collections::HashSet::new(); + for type_param in &type_params.type_params { + let (name, range) = match type_param { + TypeParam::TypeVar(tv) => (tv.name.as_str(), tv.range), + TypeParam::ParamSpec(ps) => (ps.name.as_str(), ps.range), + TypeParam::TypeVarTuple(tvt) => (tvt.name.as_str(), tvt.range), + }; + if !seen_names.insert(name) { + return Err(SymbolTableError { + error: format!("duplicate type parameter '{}'", name), + location: Some( + self.source_file + .to_source_code() + .source_location(range.start(), PositionEncoding::Utf8), + ), + }); + } + } + // Register .type_params as a type parameter (automatically becomes cell variable) self.register_name(".type_params", SymbolUsage::TypeParam, type_params.range)?; @@ -1489,18 +1564,25 @@ impl SymbolTableBuilder { // Process bound in a separate scope if let Some(binding) = bound { - let scope_name = if binding.is_tuple_expr() { - format!("") + let (scope_name, scope_info) = if binding.is_tuple_expr() { + ( + format!(""), + "a TypeVar constraint", + ) } else { - format!("") + (format!(""), "a TypeVar bound") }; - self.scan_type_param_bound_or_default(binding, &scope_name)?; + self.scan_type_param_bound_or_default(binding, &scope_name, scope_info)?; } // Process default in a separate scope if let Some(default_value) = default { let scope_name = format!(""); - self.scan_type_param_bound_or_default(default_value, &scope_name)?; + self.scan_type_param_bound_or_default( + default_value, + &scope_name, + "a TypeVar default", + )?; } } TypeParam::ParamSpec(TypeParamParamSpec { @@ -1514,7 +1596,11 @@ impl SymbolTableBuilder { // Process default in a separate scope if let Some(default_value) = default { let scope_name = format!(""); - self.scan_type_param_bound_or_default(default_value, &scope_name)?; + self.scan_type_param_bound_or_default( + default_value, + &scope_name, + "a ParamSpec default", + )?; } } TypeParam::TypeVarTuple(TypeParamTypeVarTuple { @@ -1528,7 +1614,11 @@ impl SymbolTableBuilder { // Process default in a separate scope if let Some(default_value) = default { let scope_name = format!(""); - self.scan_type_param_bound_or_default(default_value, &scope_name)?; + self.scan_type_param_bound_or_default( + default_value, + &scope_name, + "a TypeVarTuple default", + )?; } } } @@ -1667,6 +1757,23 @@ impl SymbolTableBuilder { // Some checks for the symbol that present on this scope level: let symbol = if let Some(symbol) = table.symbols.get_mut(name.as_ref()) { let flags = &symbol.flags; + + // INNER_LOOP_CONFLICT: comprehension inner loop cannot rebind + // a variable that was used as a named expression target + // Example: [i for i in range(5) if (j := 0) for j in range(5)] + // Here 'j' is used in named expr first, then as inner loop iter target + if self.in_comp_inner_loop_target + && flags.contains(SymbolFlags::ASSIGNED_IN_COMPREHENSION) + { + return Err(SymbolTableError { + error: format!( + "comprehension inner loop cannot rebind assignment expression target '{}'", + name + ), + location, + }); + } + // Role already set.. match role { SymbolUsage::Global if !symbol.is_global() => {