diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 118ace15eaf..bdce48e3447 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1158,7 +1158,6 @@ def foo(x): 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] diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 2a3dc9e71db..e990771e249 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -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 = 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(" 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!("") - } else { - format!("") - }; - 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!(""); - 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!(""); - 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!(""); - 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()?; diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index f19453bc59a..19f5a0ca151 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -65,6 +65,11 @@ pub struct SymbolTable { /// Annotations are compiled as a separate `__annotate__` function pub annotation_block: Option>, + /// 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, bool)> = Vec::new(); let mut annotation_free: Option> = 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 { + fn found_in_outer_scope( + &mut self, + name: &str, + st_typ: CompilerScope, + skip_enclosing_function_scope: bool, + ) -> Option { 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!(""), 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!(""), - "a TypeVar constraint", - ) + let scope_info = if binding.is_tuple_expr() { + "a TypeVar constraint" } else { - (format!(""), "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!(""); 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!(""); 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!(""); self.scan_type_param_bound_or_default( default_value, - &scope_name, + name, "a TypeVarTuple default", )?; } diff --git a/crates/vm/src/stdlib/_symtable.rs b/crates/vm/src/stdlib/_symtable.rs index 299e006bdce..f1d571a8318 100644 --- a/crates/vm/src/stdlib/_symtable.rs +++ b/crates/vm/src/stdlib/_symtable.rs @@ -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 } }