Skip to content

compile_type_param_bound_or_default #6005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 20, 2025
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
90 changes: 67 additions & 23 deletions compiler/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1652,6 +1652,49 @@ impl Compiler<'_> {
}
}

/// Compile type parameter bound or default in a separate scope and return closure
fn compile_type_param_bound_or_default(
&mut self,
expr: &Expr,
name: &str,
allow_starred: bool,
) -> CompileResult<()> {
// Push the next symbol table onto the stack
self.push_symbol_table();

// Get the current symbol table
let key = self.symbol_table_stack.len() - 1;
let lineno = expr.range().start().to_u32();

// Enter scope with the type parameter name
self.enter_scope(name, CompilerScope::TypeParams, key, lineno)?;

// Compile the expression
if allow_starred && matches!(expr, Expr::Starred(_)) {
if let Expr::Starred(starred) = expr {
self.compile_expression(&starred.value)?;
emit!(self, Instruction::UnpackSequence { size: 1 });
}
} else {
self.compile_expression(expr)?;
}

// Return value
emit!(self, Instruction::ReturnValue);

// Exit scope and create closure
let code = self.exit_scope();
// Note: exit_scope already calls pop_symbol_table, so we don't need to call it again

// Create type params function with closure
self.make_closure(code, bytecode::MakeFunctionFlags::empty())?;

// Call the function immediately
emit!(self, Instruction::CallFunctionPositional { nargs: 0 });

Ok(())
}

/// Store each type parameter so it is accessible to the current scope, and leave a tuple of
/// all the type parameters on the stack.
fn compile_type_params(&mut self, type_params: &TypeParams) -> CompileResult<()> {
Expand All @@ -1664,21 +1707,25 @@ impl Compiler<'_> {
default,
..
}) => {
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});

if let Some(expr) = &bound {
self.compile_expression(expr)?;
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
emit!(
self,
Instruction::CallIntrinsic2 {
func: bytecode::IntrinsicFunction2::TypeVarWithBound
}
);
let scope_name = if expr.is_tuple_expr() {
format!("<TypeVar constraint of {name}>")
} else {
format!("<TypeVar bound of {name}>")
};
self.compile_type_param_bound_or_default(expr, &scope_name, false)?;

let intrinsic = if expr.is_tuple_expr() {
bytecode::IntrinsicFunction2::TypeVarWithConstraint
} else {
bytecode::IntrinsicFunction2::TypeVarWithBound
};
emit!(self, Instruction::CallIntrinsic2 { func: intrinsic });
} else {
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
emit!(
self,
Instruction::CallIntrinsic1 {
Expand All @@ -1689,9 +1736,8 @@ impl Compiler<'_> {

// Handle default value if present (PEP 695)
if let Some(default_expr) = default {
// Compile the default expression
self.compile_expression(default_expr)?;

let scope_name = format!("<TypeVar default of {name}>");
self.compile_type_param_bound_or_default(default_expr, &scope_name, false)?;
emit!(
self,
Instruction::CallIntrinsic2 {
Expand All @@ -1716,9 +1762,8 @@ impl Compiler<'_> {

// Handle default value if present (PEP 695)
if let Some(default_expr) = default {
// Compile the default expression
self.compile_expression(default_expr)?;

let scope_name = format!("<ParamSpec default of {name}>");
self.compile_type_param_bound_or_default(default_expr, &scope_name, false)?;
emit!(
self,
Instruction::CallIntrinsic2 {
Expand All @@ -1743,10 +1788,9 @@ impl Compiler<'_> {

// Handle default value if present (PEP 695)
if let Some(default_expr) = default {
// Compile the default expression
self.compile_expression(default_expr)?;

// Handle starred expression (*default)
// TypeVarTuple allows starred expressions
let scope_name = format!("<TypeVarTuple default of {name}>");
self.compile_type_param_bound_or_default(default_expr, &scope_name, true)?;
emit!(
self,
Instruction::CallIntrinsic2 {
Expand Down
101 changes: 85 additions & 16 deletions compiler/codegen/src/symboltable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub struct SymbolTable {

/// Whether this class scope needs an implicit __classdict__ cell
pub needs_classdict: bool,

/// Whether this type param scope can see the parent class scope
pub can_see_class_scope: bool,
}

impl SymbolTable {
Expand All @@ -68,6 +71,7 @@ impl SymbolTable {
varnames: Vec::new(),
needs_class_closure: false,
needs_classdict: false,
can_see_class_scope: false,
}
}

Expand Down Expand Up @@ -665,6 +669,31 @@ impl SymbolTableBuilder<'_> {
self.current_varnames.clear();
}

fn enter_type_param_block(&mut self, name: &str, line_number: u32) -> SymbolTableResult {
// Check if we're in a class scope
let in_class = self
.tables
.last()
.is_some_and(|t| t.typ == CompilerScope::Class);

self.enter_scope(name, CompilerScope::TypeParams, line_number);

// If we're in a class, mark that this type param scope can see the class scope
if let Some(table) = self.tables.last_mut() {
table.can_see_class_scope = in_class;

// Add __classdict__ as a USE symbol in type param scope if in class
if in_class {
self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?;
}
}

// Register .type_params as a SET symbol (it will be converted to cell variable later)
self.register_name(".type_params", SymbolUsage::Assigned, TextRange::default())?;

Ok(())
}

/// Pop symbol table and add to sub table of parent table.
fn leave_scope(&mut self) {
let mut table = self.tables.pop().unwrap();
Expand Down Expand Up @@ -746,12 +775,10 @@ impl SymbolTableBuilder<'_> {
self.scan_annotation(expression)?;
}
if let Some(type_params) = type_params {
self.enter_scope(
self.enter_type_param_block(
&format!("<generic parameters of {}>", name.as_str()),
CompilerScope::TypeParams,
// FIXME: line no
self.line_index_start(*range),
);
self.line_index_start(type_params.range),
)?;
self.scan_type_params(type_params)?;
}
self.enter_scope_with_parameters(
Expand All @@ -774,11 +801,10 @@ impl SymbolTableBuilder<'_> {
range,
}) => {
if let Some(type_params) = type_params {
self.enter_scope(
self.enter_type_param_block(
&format!("<generic parameters of {}>", name.as_str()),
CompilerScope::TypeParams,
self.line_index_start(type_params.range),
);
)?;
self.scan_type_params(type_params)?;
}
self.enter_scope(
Expand Down Expand Up @@ -965,12 +991,10 @@ impl SymbolTableBuilder<'_> {
..
}) => {
if let Some(type_params) = type_params {
self.enter_scope(
// &name.to_string(),
self.enter_type_param_block(
"TypeAlias",
CompilerScope::TypeParams,
self.line_index_start(type_params.range),
);
)?;
self.scan_type_params(type_params)?;
self.scan_expression(value, ExpressionContext::Load)?;
self.leave_scope();
Expand Down Expand Up @@ -1344,6 +1368,26 @@ impl SymbolTableBuilder<'_> {
Ok(())
}

/// 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 {
// 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);

// Note: In CPython, can_see_class_scope is preserved in the new scope
// In RustPython, this is handled through the scope hierarchy

// Scan the expression in this new scope
let result = self.scan_expression(expr, ExpressionContext::Load);

// Exit the scope
self.leave_scope();

result
}

fn scan_type_params(&mut self, type_params: &TypeParams) -> SymbolTableResult {
// Register .type_params as a type parameter (automatically becomes cell variable)
self.register_name(".type_params", SymbolUsage::TypeParam, type_params.range)?;
Expand All @@ -1355,26 +1399,51 @@ impl SymbolTableBuilder<'_> {
name,
bound,
range: type_var_range,
..
default,
}) => {
self.register_name(name.as_str(), SymbolUsage::TypeParam, *type_var_range)?;

// Process bound in a separate scope
if let Some(binding) = bound {
self.scan_expression(binding, ExpressionContext::Load)?;
let scope_name = if binding.is_tuple_expr() {
format!("<TypeVar constraint of {name}>")
} else {
format!("<TypeVar bound of {name}>")
};
self.scan_type_param_bound_or_default(binding, &scope_name)?;
}

// 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)?;
}
}
TypeParam::ParamSpec(TypeParamParamSpec {
name,
range: param_spec_range,
..
default,
}) => {
self.register_name(name, SymbolUsage::TypeParam, *param_spec_range)?;

// 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)?;
}
}
TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
name,
range: type_var_tuple_range,
..
default,
}) => {
self.register_name(name, SymbolUsage::TypeParam, *type_var_tuple_range)?;

// 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)?;
}
}
}
}
Expand Down
Loading