compiler parity for typealias (#7645)

This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-04-21 23:59:20 +09:00
1 parent f8862e4eed
commit b929a50647
4 files changed
+205 -141

No files matched your search

-1
View File
@@ -1158,7 +1158,6 @@ class TestSpecifics(unittest.TestCase):
self.assertIn('LOAD_ATTR', instructions)
self.assertIn('CALL', instructions)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'LOAD_SMALL_INT' not found in ['RESUME', 'LOAD_CONST', 'RETURN_VALUE']
def test_folding_type_param(self):
get_code_fn_cls = lambda x: x.co_consts[0].co_consts[2]
get_code_type_alias = lambda x: x.co_consts[0].co_consts[3]
+130 -104
View File
@@ -1249,12 +1249,35 @@ impl Compiler {
// Build freevars using dictbytype (FREE scope, offset by cellvars size)
let mut freevar_cache = IndexSet::default();
let annotation_free_names: IndexSet<String> = ste
.annotation_block
.as_ref()
.map(|annotation| {
annotation
.symbols
.iter()
.filter(|(_, s)| {
s.scope == SymbolScope::Free || s.flags.contains(SymbolFlags::FREE_CLASS)
})
.map(|(name, _)| name.clone())
.collect()
})
.unwrap_or_default();
let mut free_names: Vec<_> = ste
.symbols
.iter()
.filter(|(_, s)| {
s.scope == SymbolScope::Free || s.flags.contains(SymbolFlags::FREE_CLASS)
})
.filter(|(name, symbol)| {
if !matches!(
scope_type,
CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda
) {
return true;
}
!(annotation_free_names.contains(*name) && symbol.flags.is_empty())
})
.map(|(name, _)| name.clone())
.collect();
free_names.sort();
@@ -1866,9 +1889,18 @@ impl Compiler {
let mut parent_idx = stack_size - 2;
let mut parent = &self.code_stack[parent_idx];
// If parent is ast::TypeParams scope, look at grandparent
// Check if parent is a type params scope by name pattern
if parent.metadata.name.starts_with("<generic parameters of ") {
let parent_scope = self
.symbol_table_stack
.get(parent_idx)
.map(|table| table.typ);
// CPython skips both generic-parameter scopes and annotation scopes
// when building qualnames for the contained function/class code object.
if matches!(
parent_scope,
Some(CompilerScope::TypeParams | CompilerScope::Annotation)
) || parent.metadata.name.starts_with("<generic parameters of ")
{
if stack_size == 2 {
// If we're immediately within the module, qualname is just the name
return current_obj_name;
@@ -2294,7 +2326,13 @@ impl Compiler {
NameOp::Name
}
}
SymbolScope::GlobalExplicit => NameOp::Global,
SymbolScope::GlobalExplicit => {
if can_see_class_scope {
NameOp::DictOrGlobals
} else {
NameOp::Global
}
}
SymbolScope::Unknown => {
if module_global_from_nested_scope {
NameOp::Global
@@ -2797,24 +2835,14 @@ impl Compiler {
value,
..
}) => {
// let name_string = name.to_string();
let Some(name) = name.as_name_expr() else {
// FIXME: is error here?
return Err(self.error(CodegenErrorType::SyntaxError(
"type alias expect name".to_owned(),
)));
};
let name_string = name.id.to_string();
// For PEP 695 syntax, we need to compile type_params first
// so that they're available when compiling the value expression
// Push name first
self.emit_load_const(ConstantData::Str {
value: name_string.clone().into(),
});
if let Some(type_params) = type_params {
// Outer scope for TypeParams
self.push_symbol_table()?;
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get().to_u32();
@@ -2830,34 +2858,18 @@ impl Compiler {
in_async_scope: false,
};
// Compile type params inside the scope
self.emit_load_const(ConstantData::Str {
value: name_string.clone().into(),
});
self.compile_type_params(type_params)?;
// Stack: [type_params_tuple]
// Inner closure for lazy value evaluation
self.push_symbol_table()?;
let inner_key = self.symbol_table_stack.len() - 1;
self.enter_scope("TypeAlias", CompilerScope::TypeParams, inner_key, lineno)?;
// Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
self.current_code_info()
.metadata
.varnames
.insert("format".to_owned());
self.emit_format_validation()?;
self.compile_expression(value)?;
emit!(self, Instruction::ReturnValue);
let value_code = self.exit_scope();
self.make_closure(value_code, bytecode::MakeFunctionFlags::new())?;
// Stack: [type_params_tuple, value_closure]
// Swap so unpack_sequence reverse gives correct order
emit!(self, Instruction::Swap { i: 2 });
// Stack: [value_closure, type_params_tuple]
// Build tuple and return from TypeParams scope
emit!(self, Instruction::BuildTuple { count: 2 });
self.compile_typealias_value_closure(&name_string, value)?;
emit!(self, Instruction::BuildTuple { count: 3 });
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeAlias
}
);
emit!(self, Instruction::ReturnValue);
let code = self.exit_scope();
@@ -2865,54 +2877,21 @@ impl Compiler {
self.make_closure(code, bytecode::MakeFunctionFlags::new())?;
emit!(self, Instruction::PushNull);
emit!(self, Instruction::Call { argc: 0 });
// Unpack: (value_closure, type_params_tuple)
// UnpackSequence reverses → stack: [name, type_params_tuple, value_closure]
emit!(self, Instruction::UnpackSequence { count: 2 });
} else {
// Push None for type_params
self.emit_load_const(ConstantData::Str {
value: name_string.clone().into(),
});
self.emit_load_const(ConstantData::None);
// Stack: [name, None]
// Create a closure for lazy evaluation of the value
self.push_symbol_table()?;
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get().to_u32();
self.enter_scope("TypeAlias", CompilerScope::TypeParams, key, lineno)?;
// Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
self.current_code_info()
.metadata
.varnames
.insert("format".to_owned());
self.emit_format_validation()?;
let prev_ctx = self.ctx;
self.ctx = CompileContext {
loop_data: None,
in_class: prev_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
self.compile_expression(value)?;
emit!(self, Instruction::ReturnValue);
let code = self.exit_scope();
self.ctx = prev_ctx;
self.make_closure(code, bytecode::MakeFunctionFlags::new())?;
// Stack: [name, None, closure]
self.compile_typealias_value_closure(&name_string, value)?;
emit!(self, Instruction::BuildTuple { count: 3 });
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeAlias
}
);
}
// Build tuple of 3 elements and call intrinsic
emit!(self, Instruction::BuildTuple { count: 3 });
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeAlias
}
);
self.store_name(&name_string)?;
}
ast::Stmt::IpyEscapeCommand(_) => todo!(),
@@ -3015,6 +2994,10 @@ impl Compiler {
name: &str,
allow_starred: bool,
) -> CompileResult<()> {
self.emit_load_const(ConstantData::Tuple {
elements: vec![ConstantData::Integer { value: 1.into() }],
});
// Push the next symbol table onto the stack
self.push_symbol_table()?;
@@ -3023,11 +3006,8 @@ impl Compiler {
let lineno = self.get_source_line_number().get().to_u32();
// Enter scope with the type parameter name
self.enter_scope(name, CompilerScope::TypeParams, key, lineno)?;
self.enter_scope(name, CompilerScope::Annotation, key, lineno)?;
// Evaluator takes a positional-only format parameter
self.current_code_info().metadata.argcount = 1;
self.current_code_info().metadata.posonlyargcount = 1;
self.current_code_info()
.metadata
.varnames
@@ -3061,8 +3041,50 @@ impl Compiler {
let code = self.exit_scope();
self.ctx = prev_ctx;
// Create closure for lazy evaluation
self.make_closure(code, bytecode::MakeFunctionFlags::new())?;
self.make_closure(
code,
bytecode::MakeFunctionFlags::from([bytecode::MakeFunctionFlag::Defaults]),
)?;
Ok(())
}
fn compile_typealias_value_closure(
&mut self,
alias_name: &str,
value: &ast::Expr,
) -> CompileResult<()> {
self.emit_load_const(ConstantData::Tuple {
elements: vec![ConstantData::Integer { value: 1.into() }],
});
self.push_symbol_table()?;
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get().to_u32();
self.enter_scope(alias_name, CompilerScope::Annotation, key, lineno)?;
self.current_code_info()
.metadata
.varnames
.insert(".format".to_owned());
self.emit_format_validation()?;
let prev_ctx = self.ctx;
self.ctx = CompileContext {
loop_data: None,
in_class: prev_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
self.compile_expression(value)?;
emit!(self, Instruction::ReturnValue);
let code = self.exit_scope();
self.ctx = prev_ctx;
self.make_closure(
code,
bytecode::MakeFunctionFlags::from([bytecode::MakeFunctionFlag::Defaults]),
)?;
Ok(())
}
@@ -3084,12 +3106,7 @@ impl Compiler {
});
if let Some(expr) = &bound {
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)?;
self.compile_type_param_bound_or_default(expr, name.as_str(), false)?;
let intrinsic = if expr.is_tuple_expr() {
bytecode::IntrinsicFunction2::TypeVarWithConstraint
@@ -3107,8 +3124,11 @@ impl Compiler {
}
if let Some(default_expr) = default {
let scope_name = format!("<TypeVar default of {name}>");
self.compile_type_param_bound_or_default(default_expr, &scope_name, false)?;
self.compile_type_param_bound_or_default(
default_expr,
name.as_str(),
false,
)?;
emit!(
self,
Instruction::CallIntrinsic2 {
@@ -3132,8 +3152,11 @@ impl Compiler {
);
if let Some(default_expr) = default {
let scope_name = format!("<ParamSpec default of {name}>");
self.compile_type_param_bound_or_default(default_expr, &scope_name, false)?;
self.compile_type_param_bound_or_default(
default_expr,
name.as_str(),
false,
)?;
emit!(
self,
Instruction::CallIntrinsic2 {
@@ -3160,8 +3183,11 @@ impl Compiler {
if let Some(default_expr) = 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)?;
self.compile_type_param_bound_or_default(
default_expr,
name.as_str(),
true,
)?;
emit!(
self,
Instruction::CallIntrinsic2 {
@@ -4560,7 +4586,7 @@ impl Compiler {
self.current_code_info()
.metadata
.varnames
.insert("format".to_owned());
.insert(".format".to_owned());
// Emit format validation: if format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError
self.emit_format_validation()?;
+70 -34
View File
@@ -65,6 +65,11 @@ pub struct SymbolTable {
/// Annotations are compiled as a separate `__annotate__` function
pub annotation_block: Option<Box<SymbolTable>>,
/// True only for deferred function/class/module annotation scopes that
/// should resolve outer names as if they were siblings of the owning
/// function body, matching CPython's PEP 649 lookup rules.
pub skip_enclosing_function_scope: bool,
/// PEP 649: Whether this scope has conditional annotations
/// (annotations inside if/for/while/etc. blocks or at module level)
pub has_conditional_annotations: bool,
@@ -95,6 +100,7 @@ impl SymbolTable {
is_generator: false,
comp_inlined: false,
annotation_block: None,
skip_enclosing_function_scope: false,
has_conditional_annotations: false,
future_annotations: false,
mangled_names: None,
@@ -458,7 +464,7 @@ use stack::StackStack;
#[derive(Default)]
#[repr(transparent)]
struct SymbolTableAnalyzer {
tables: StackStack<(SymbolMap, CompilerScope)>,
tables: StackStack<(SymbolMap, CompilerScope, bool)>,
}
impl SymbolTableAnalyzer {
@@ -500,7 +506,11 @@ impl SymbolTableAnalyzer {
let mut child_frees: Vec<(IndexSet<String>, bool)> = Vec::new();
let mut annotation_free: Option<IndexSet<String>> = None;
let mut info = (symbols, symbol_table.typ);
let mut info = (
symbols,
symbol_table.typ,
symbol_table.skip_enclosing_function_scope,
);
self.tables.with_append(&mut info, |list| {
let inner_scope = unsafe { &mut *(list as *mut _ as *mut Self) };
for sub_table in sub_tables.iter_mut() {
@@ -577,7 +587,13 @@ impl SymbolTableAnalyzer {
// Analyze symbols in current scope
for symbol in symbol_table.symbols.values_mut() {
self.analyze_symbol(symbol, symbol_table.typ, sub_tables, class_entry)?;
self.analyze_symbol(
symbol,
symbol_table.typ,
symbol_table.skip_enclosing_function_scope,
sub_tables,
class_entry,
)?;
// Collect free variables from this scope
if symbol.scope == SymbolScope::Free || symbol.flags.contains(SymbolFlags::FREE_CLASS) {
@@ -618,6 +634,7 @@ impl SymbolTableAnalyzer {
&mut self,
symbol: &mut Symbol,
st_typ: CompilerScope,
skip_enclosing_function_scope: bool,
sub_tables: &[SymbolTable],
class_entry: Option<&SymbolMap>,
) -> SymbolTableResult {
@@ -638,8 +655,11 @@ impl SymbolTableAnalyzer {
let scope_depth = self.tables.as_ref().len();
// check if the name is already defined in any outer scope
if scope_depth < 2
|| self.found_in_outer_scope(&symbol.name, st_typ)
!= Some(SymbolScope::Free)
|| self.found_in_outer_scope(
&symbol.name,
st_typ,
skip_enclosing_function_scope,
) != Some(SymbolScope::Free)
{
return Err(SymbolTableError {
error: format!("no binding for nonlocal '{}' found", symbol.name),
@@ -649,7 +669,7 @@ impl SymbolTableAnalyzer {
}
// Check if the nonlocal binding refers to a type parameter
if symbol.flags.contains(SymbolFlags::NONLOCAL) {
for (symbols, _typ) in self.tables.iter().rev() {
for (symbols, _typ, _skip) in self.tables.iter().rev() {
if let Some(sym) = symbols.get(&symbol.name) {
if sym.flags.contains(SymbolFlags::TYPE_PARAM) {
return Err(SymbolTableError {
@@ -699,7 +719,11 @@ impl SymbolTableAnalyzer {
self.found_in_inner_scope(sub_tables, &symbol.name, st_typ)
.unwrap_or(SymbolScope::Local)
}
} else if let Some(scope) = self.found_in_outer_scope(&symbol.name, st_typ) {
} else if let Some(scope) = self.found_in_outer_scope(
&symbol.name,
st_typ,
skip_enclosing_function_scope,
) {
// If found in enclosing scope (function/TypeParams), use that
scope
} else if let Some(class_symbols) = class_entry
@@ -724,19 +748,26 @@ impl SymbolTableAnalyzer {
Ok(())
}
fn found_in_outer_scope(&mut self, name: &str, st_typ: CompilerScope) -> Option<SymbolScope> {
fn found_in_outer_scope(
&mut self,
name: &str,
st_typ: CompilerScope,
skip_enclosing_function_scope: bool,
) -> Option<SymbolScope> {
let mut decl_depth = None;
for (i, (symbols, typ)) in self.tables.iter().rev().enumerate() {
for (i, (symbols, typ, _skip)) in self.tables.iter().rev().enumerate() {
if matches!(typ, CompilerScope::Module)
|| matches!(typ, CompilerScope::Class if name != "__class__" && name != "__classdict__" && name != "__conditional_annotations__")
{
continue;
}
// PEP 649: Annotation scope is conceptually a sibling of the function,
// not a child. Skip the immediate parent function scope when looking
// for outer variables from annotation scope.
// Real PEP 649 annotation blocks resolve names as siblings of the
// owning function body. Other annotation-like scopes such as type
// aliases and TypeVar bound/default evaluators keep normal lexical
// lookup and therefore leave this path disabled.
if st_typ == CompilerScope::Annotation
&& skip_enclosing_function_scope
&& i == 0
&& matches!(
typ,
@@ -785,7 +816,7 @@ impl SymbolTableAnalyzer {
let is_class_implicit =
name == "__classdict__" || name == "__conditional_annotations__";
for (table, typ) in self.tables.iter_mut().rev().take(decl_depth) {
for (table, typ, _skip) in self.tables.iter_mut().rev().take(decl_depth) {
if let CompilerScope::Class = typ {
if let Some(free_class) = table.get_mut(name) {
free_class.flags.insert(SymbolFlags::FREE_CLASS)
@@ -1126,6 +1157,7 @@ impl SymbolTableBuilder {
);
// Annotation scope in class can see class scope
annotation_table.can_see_class_scope = can_see_class_scope;
annotation_table.skip_enclosing_function_scope = true;
// Add 'format' parameter
annotation_table.varnames.push("format".to_owned());
current.annotation_block = Some(Box::new(annotation_table));
@@ -1734,6 +1766,17 @@ impl SymbolTableBuilder {
type_params,
..
}) => {
let Some(name_expr) = name.as_name_expr() else {
return Err(SymbolTableError {
error: "type alias expect name".to_owned(),
location: Some(
self.source_file
.to_source_code()
.source_location(name.range().start(), PositionEncoding::Utf8),
),
});
};
let alias_name = name_expr.id.to_string();
let was_in_type_alias = self.in_type_alias;
self.in_type_alias = true;
// Check before entering any sub-scopes
@@ -1744,7 +1787,7 @@ impl SymbolTableBuilder {
let is_generic = type_params.is_some();
if let Some(type_params) = type_params {
self.enter_type_param_block(
"TypeAlias",
&format!("<generic parameters of {alias_name}>"),
self.line_index_start(type_params.range),
false,
)?;
@@ -1752,12 +1795,12 @@ impl SymbolTableBuilder {
}
// Value scope for lazy evaluation
self.enter_scope(
"TypeAlias",
CompilerScope::TypeParams,
&alias_name,
CompilerScope::Annotation,
self.line_index_start(value.range()),
);
// Evaluator takes a format parameter
self.register_name("format", SymbolUsage::Parameter, TextRange::default())?;
self.register_name(".format", SymbolUsage::Parameter, TextRange::default())?;
if in_class {
if let Some(table) = self.tables.last_mut() {
table.can_see_class_scope = true;
@@ -2272,13 +2315,12 @@ impl SymbolTableBuilder {
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
// Bounds/defaults are compiled as annotation scopes in CPython.
let in_class = self.tables.last().is_some_and(|t| t.can_see_class_scope);
let line_number = self.line_index_start(expr.range());
self.enter_scope(scope_name, CompilerScope::TypeParams, line_number);
self.enter_scope(scope_name, CompilerScope::Annotation, line_number);
// Evaluator takes a format parameter
self.register_name("format", SymbolUsage::Parameter, TextRange::default())?;
self.register_name(".format", SymbolUsage::Parameter, TextRange::default())?;
if in_class {
if let Some(table) = self.tables.last_mut() {
@@ -2358,23 +2400,19 @@ impl SymbolTableBuilder {
// Process bound in a separate scope
if let Some(binding) = bound {
let (scope_name, scope_info) = if binding.is_tuple_expr() {
(
format!("<TypeVar constraint of {name}>"),
"a TypeVar constraint",
)
let scope_info = if binding.is_tuple_expr() {
"a TypeVar constraint"
} else {
(format!("<TypeVar bound of {name}>"), "a TypeVar bound")
"a TypeVar bound"
};
self.scan_type_param_bound_or_default(binding, &scope_name, scope_info)?;
self.scan_type_param_bound_or_default(binding, name.as_str(), 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,
name.as_str(),
"a TypeVar default",
)?;
}
@@ -2389,10 +2427,9 @@ 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,
name,
"a ParamSpec default",
)?;
}
@@ -2407,10 +2444,9 @@ 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,
name,
"a TypeVarTuple default",
)?;
}
+5 -2
View File
@@ -155,11 +155,14 @@ mod _symtable {
#[pygetset(name = "type")]
fn typ(&self) -> i32 {
match self.symtable.typ {
CompilerScope::Function => TYPE_FUNCTION,
CompilerScope::Function
| CompilerScope::AsyncFunction
| CompilerScope::Lambda
| CompilerScope::Comprehension => TYPE_FUNCTION,
CompilerScope::Class => TYPE_CLASS,
CompilerScope::Module => TYPE_MODULE,
CompilerScope::Annotation => TYPE_ANNOTATION,
CompilerScope::TypeParams => TYPE_TYPE_PARAMETERS,
_ => -1, // TODO: missing types from the C implementation
}
}