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
4 changes: 0 additions & 4 deletions Lib/test/test_named_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]"),
Expand Down Expand Up @@ -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)}"),
Expand Down
165 changes: 136 additions & 29 deletions crates/codegen/src/symboltable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down Expand Up @@ -617,6 +621,14 @@ struct SymbolTableBuilder {
current_varnames: Vec<String>,
// 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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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",
Expand All @@ -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!(),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)?;

Expand All @@ -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!("<TypeVar constraint of {name}>")
let (scope_name, scope_info) = if binding.is_tuple_expr() {
(
format!("<TypeVar constraint of {name}>"),
"a TypeVar constraint",
)
} else {
format!("<TypeVar bound of {name}>")
(format!("<TypeVar bound of {name}>"), "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!("<TypeVar default of {name}>");
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 {
Expand All @@ -1514,7 +1596,11 @@ impl SymbolTableBuilder {
// Process default in a separate scope
if let Some(default_value) = default {
let scope_name = format!("<ParamSpec default of {name}>");
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 {
Expand All @@ -1528,7 +1614,11 @@ impl SymbolTableBuilder {
// Process default in a separate scope
if let Some(default_value) = default {
let scope_name = format!("<TypeVarTuple default of {name}>");
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",
)?;
}
}
}
Expand Down Expand Up @@ -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() => {
Expand Down
Loading