From 821e29e92cee07d466f2cd022f7feae651dbc21d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 11 Feb 2026 11:04:55 +0900 Subject: [PATCH] Add _ConstEvaluator, evaluate_* getters, format validation - Implement _ConstEvaluator type in _typing module with STRING format support via typing_type_repr (port of _Py_typing_type_repr) - Add evaluate_bound, evaluate_constraints, evaluate_default pygetset properties to TypeVar, ParamSpec, TypeVarTuple - Emit format validation in evaluator scopes (compile_type_param_bound_or_default, TypeAlias value scopes) so evaluators raise NotImplementedError for unsupported formats - Add non-default-after-default SyntaxError in scan_type_params - Fix ParamSpec default_value to use Mutex for proper caching - Fix TypeVar constructor: evaluate_constraints set to None instead of constraints tuple for eager-constructed TypeVars - Pass format=1 (FORMAT_VALUE) to all lazy evaluator calls - Remove 6 expectedFailure markers from test_type_params --- Lib/test/test_type_params.py | 5 -- crates/codegen/src/compile.rs | 14 ++++ crates/codegen/src/symboltable.rs | 31 +++++++-- crates/vm/src/stdlib/typevar.rs | 103 ++++++++++++++++++++------- crates/vm/src/stdlib/typing.rs | 112 +++++++++++++++++++++++++++--- 5 files changed, 219 insertions(+), 46 deletions(-) diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index 07b4957ad..410c3ef21 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -1374,13 +1374,11 @@ class DefaultsTest(unittest.TestCase): Ts, = ns["Alias"].__type_params__ self.assertEqual(Ts.__default__, next(iter(ns["default"]))) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised def test_nondefault_after_default(self): check_syntax_error(self, "def func[T=int, U](): pass", "non-default type parameter 'U' follows default type parameter") check_syntax_error(self, "class C[T=int, U]: pass", "non-default type parameter 'U' follows default type parameter") check_syntax_error(self, "type A[T=int, U] = int", "non-default type parameter 'U' follows default type parameter") - @unittest.expectedFailure # TODO: RUSTPYTHON; + defined def test_lazy_evaluation(self): ns = run_code(""" type Alias[T = Undefined, *U = Undefined, **V = Undefined] = int @@ -1431,7 +1429,6 @@ class DefaultsTest(unittest.TestCase): class TestEvaluateFunctions(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'TypeAliasType' object has no attribute 'evaluate_value' def test_general(self): type Alias = int Alias2 = TypeAliasType("Alias2", int) @@ -1459,7 +1456,6 @@ class TestEvaluateFunctions(unittest.TestCase): self.assertIs(annotationlib.call_evaluate_function(case, annotationlib.Format.FORWARDREF), int) self.assertEqual(annotationlib.call_evaluate_function(case, annotationlib.Format.STRING), 'int') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_constraints(self): def f[T: (int, str)](): pass T, = f.__type_params__ @@ -1471,7 +1467,6 @@ class TestEvaluateFunctions(unittest.TestCase): self.assertEqual(annotationlib.call_evaluate_function(case.evaluate_constraints, annotationlib.Format.FORWARDREF), (int, str)) self.assertEqual(annotationlib.call_evaluate_function(case.evaluate_constraints, annotationlib.Format.STRING), '(int, str)') - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'TypeVar' object has no attribute 'evaluate_bound' def test_const_evaluator(self): T = TypeVar("T", bound=int) self.assertEqual(repr(T.evaluate_bound), ">") diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 826848ff2..7924956cd 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -2461,6 +2461,10 @@ impl Compiler { 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.emit_format_validation()?; self.compile_expression(value)?; emit!(self, Instruction::ReturnValue); let value_code = self.exit_scope(); @@ -2494,6 +2498,10 @@ impl Compiler { 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.emit_format_validation()?; let prev_ctx = self.ctx; self.ctx = CompileContext { @@ -2635,6 +2643,12 @@ impl Compiler { // Enter scope with the type parameter name self.enter_scope(name, 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.emit_format_validation()?; + // TypeParams scope is function-like let prev_ctx = self.ctx; self.ctx = CompileContext { diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index f324f48d5..dae72e259 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -1485,6 +1485,8 @@ impl SymbolTableBuilder { CompilerScope::TypeParams, self.line_index_start(value.range()), ); + // Evaluator takes a format parameter + 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; @@ -1990,6 +1992,8 @@ impl SymbolTableBuilder { 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); + // Evaluator takes a format parameter + self.register_name("format", SymbolUsage::Parameter, TextRange::default())?; if in_class { if let Some(table) = self.tables.last_mut() { @@ -2015,11 +2019,15 @@ impl SymbolTableBuilder { fn scan_type_params(&mut self, type_params: &ast::TypeParams) -> SymbolTableResult { // Check for duplicate type parameter names let mut seen_names: IndexSet<&str> = IndexSet::default(); + // Check for non-default type parameter after default type parameter + let mut default_seen = false; for type_param in &type_params.type_params { - let (name, range) = match type_param { - ast::TypeParam::TypeVar(tv) => (tv.name.as_str(), tv.range), - ast::TypeParam::ParamSpec(ps) => (ps.name.as_str(), ps.range), - ast::TypeParam::TypeVarTuple(tvt) => (tvt.name.as_str(), tvt.range), + let (name, range, has_default) = match type_param { + ast::TypeParam::TypeVar(tv) => (tv.name.as_str(), tv.range, tv.default.is_some()), + ast::TypeParam::ParamSpec(ps) => (ps.name.as_str(), ps.range, ps.default.is_some()), + ast::TypeParam::TypeVarTuple(tvt) => { + (tvt.name.as_str(), tvt.range, tvt.default.is_some()) + } }; if !seen_names.insert(name) { return Err(SymbolTableError { @@ -2031,6 +2039,21 @@ impl SymbolTableBuilder { ), }); } + if has_default { + default_seen = true; + } else if default_seen { + return Err(SymbolTableError { + error: format!( + "non-default type parameter '{}' follows default 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) diff --git a/crates/vm/src/stdlib/typevar.rs b/crates/vm/src/stdlib/typevar.rs index ac8aeac36..855174d6a 100644 --- a/crates/vm/src/stdlib/typevar.rs +++ b/crates/vm/src/stdlib/typevar.rs @@ -10,7 +10,7 @@ pub(crate) mod typevar { common::lock::PyMutex, function::{FuncArgs, PyComparisonValue}, protocol::PyNumberMethods, - stdlib::typing::call_typing_func_object, + stdlib::typing::{call_typing_func_object, decl::constevaluator_alloc}, types::{AsNumber, Comparable, Constructor, Iterable, PyComparisonOp, Representable}, }; @@ -98,7 +98,7 @@ pub(crate) mod typevar { return Ok(constraints.clone()); } let r = if !vm.is_none(&self.evaluate_constraints) { - *constraints = self.evaluate_constraints.call((), vm)?; + *constraints = self.evaluate_constraints.call((1i32,), vm)?; constraints.clone() } else { vm.ctx.empty_tuple.clone().into() @@ -113,7 +113,7 @@ pub(crate) mod typevar { return Ok(bound.clone()); } let r = if !vm.is_none(&self.evaluate_bound) { - *bound = self.evaluate_bound.call((), vm)?; + *bound = self.evaluate_bound.call((1i32,), vm)?; bound.clone() } else { vm.ctx.none() @@ -139,20 +139,55 @@ pub(crate) mod typevar { #[pygetset] fn __default__(&self, vm: &VirtualMachine) -> PyResult { let mut default_value = self.default_value.lock(); - // Check if default_value is NoDefault (not just None) if !default_value.is(&vm.ctx.typing_no_default) { return Ok(default_value.clone()); } let evaluate_default = self.evaluate_default.lock(); if !vm.is_none(&evaluate_default) { - *default_value = evaluate_default.call((), vm)?; + *default_value = evaluate_default.call((1i32,), vm)?; Ok(default_value.clone()) } else { - // Return NoDefault singleton Ok(vm.ctx.typing_no_default.clone().into()) } } + #[pygetset] + fn evaluate_bound(&self, vm: &VirtualMachine) -> PyResult { + if !vm.is_none(&self.evaluate_bound) { + return Ok(self.evaluate_bound.clone()); + } + let bound = self.bound.lock(); + if !vm.is_none(&bound) { + return Ok(constevaluator_alloc(bound.clone(), vm)); + } + Ok(vm.ctx.none()) + } + + #[pygetset] + fn evaluate_constraints(&self, vm: &VirtualMachine) -> PyResult { + if !vm.is_none(&self.evaluate_constraints) { + return Ok(self.evaluate_constraints.clone()); + } + let constraints = self.constraints.lock(); + if !vm.is_none(&constraints) { + return Ok(constevaluator_alloc(constraints.clone(), vm)); + } + Ok(vm.ctx.none()) + } + + #[pygetset] + fn evaluate_default(&self, vm: &VirtualMachine) -> PyResult { + let evaluate_default = self.evaluate_default.lock(); + if !vm.is_none(&evaluate_default) { + return Ok(evaluate_default.clone()); + } + let default_value = self.default_value.lock(); + if !default_value.is(&vm.ctx.typing_no_default) { + return Ok(constevaluator_alloc(default_value.clone(), vm)); + } + Ok(vm.ctx.none()) + } + #[pymethod] fn __typing_subst__( zelf: crate::PyRef, @@ -333,7 +368,7 @@ pub(crate) mod typevar { return Err(vm.new_type_error("Constraints cannot be used with bound")); } let constraints_tuple = vm.ctx.new_tuple(constraints); - (constraints_tuple.clone().into(), constraints_tuple.into()) + (constraints_tuple.into(), vm.ctx.none()) } else { (vm.ctx.none(), vm.ctx.none()) }; @@ -403,7 +438,7 @@ pub(crate) mod typevar { pub struct ParamSpec { name: PyObjectRef, bound: Option, - default_value: PyObjectRef, + default_value: parking_lot::Mutex, evaluate_default: PyMutex, covariant: bool, contravariant: bool, @@ -465,23 +500,30 @@ pub(crate) mod typevar { #[pygetset] fn __default__(&self, vm: &VirtualMachine) -> PyResult { - // Check if default_value is NoDefault (not just None) - if !self.default_value.is(&vm.ctx.typing_no_default) { - return Ok(self.default_value.clone()); + let mut default_value = self.default_value.lock(); + if !default_value.is(&vm.ctx.typing_no_default) { + return Ok(default_value.clone()); } - // handle evaluate_default let evaluate_default = self.evaluate_default.lock(); if !vm.is_none(&evaluate_default) { - let default_value = evaluate_default.call((), vm)?; - return Ok(default_value); + *default_value = evaluate_default.call((1i32,), vm)?; + Ok(default_value.clone()) + } else { + Ok(vm.ctx.typing_no_default.clone().into()) } - // Return NoDefault singleton - Ok(vm.ctx.typing_no_default.clone().into()) } #[pygetset] - fn evaluate_default(&self, _vm: &VirtualMachine) -> PyObjectRef { - self.evaluate_default.lock().clone() + fn evaluate_default(&self, vm: &VirtualMachine) -> PyResult { + let evaluate_default = self.evaluate_default.lock(); + if !vm.is_none(&evaluate_default) { + return Ok(evaluate_default.clone()); + } + let default_value = self.default_value.lock(); + if !default_value.is(&vm.ctx.typing_no_default) { + return Ok(constevaluator_alloc(default_value.clone(), vm)); + } + Ok(vm.ctx.none()) } #[pymethod] @@ -494,8 +536,7 @@ pub(crate) mod typevar { if !vm.is_none(&self.evaluate_default.lock()) { return true; } - // Check if default_value is not NoDefault - !self.default_value.is(&vm.ctx.typing_no_default) + !self.default_value.lock().is(&vm.ctx.typing_no_default) } #[pymethod] @@ -596,7 +637,7 @@ pub(crate) mod typevar { let paramspec = Self { name, bound, - default_value, + default_value: parking_lot::Mutex::new(default_value), evaluate_default: PyMutex::new(vm.ctx.none()), covariant, contravariant, @@ -627,7 +668,7 @@ pub(crate) mod typevar { Self { name, bound: None, - default_value: vm.ctx.typing_no_default.clone().into(), + default_value: parking_lot::Mutex::new(vm.ctx.typing_no_default.clone().into()), evaluate_default: PyMutex::new(vm.ctx.none()), covariant: false, contravariant: false, @@ -655,27 +696,37 @@ pub(crate) mod typevar { #[pygetset] fn __default__(&self, vm: &VirtualMachine) -> PyResult { let mut default_value = self.default_value.lock(); - // Check if default_value is NoDefault (not just None) if !default_value.is(&vm.ctx.typing_no_default) { return Ok(default_value.clone()); } let evaluate_default = self.evaluate_default.lock(); if !vm.is_none(&evaluate_default) { - *default_value = evaluate_default.call((), vm)?; + *default_value = evaluate_default.call((1i32,), vm)?; Ok(default_value.clone()) } else { - // Return NoDefault singleton Ok(vm.ctx.typing_no_default.clone().into()) } } + #[pygetset] + fn evaluate_default(&self, vm: &VirtualMachine) -> PyResult { + let evaluate_default = self.evaluate_default.lock(); + if !vm.is_none(&evaluate_default) { + return Ok(evaluate_default.clone()); + } + let default_value = self.default_value.lock(); + if !default_value.is(&vm.ctx.typing_no_default) { + return Ok(constevaluator_alloc(default_value.clone(), vm)); + } + Ok(vm.ctx.none()) + } + #[pymethod] fn has_default(&self, vm: &VirtualMachine) -> bool { if !vm.is_none(&self.evaluate_default.lock()) { return true; } let default_value = self.default_value.lock(); - // Check if default_value is not NoDefault !default_value.is(&vm.ctx.typing_no_default) } diff --git a/crates/vm/src/stdlib/typing.rs b/crates/vm/src/stdlib/typing.rs index 640353ce5..4adff623f 100644 --- a/crates/vm/src/stdlib/typing.rs +++ b/crates/vm/src/stdlib/typing.rs @@ -34,7 +34,7 @@ pub(crate) mod decl { builtins::{PyGenericAlias, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef, type_}, function::FuncArgs, protocol::{PyMappingMethods, PyNumberMethods}, - types::{AsMapping, AsNumber, Constructor, Iterable, Representable}, + types::{AsMapping, AsNumber, Callable, Constructor, Iterable, Representable}, }; #[pyfunction] @@ -84,6 +84,103 @@ pub(crate) mod decl { } } + #[pyattr] + #[pyclass(name = "_ConstEvaluator", module = "_typing")] + #[derive(Debug, PyPayload)] + pub(crate) struct ConstEvaluator { + value: PyObjectRef, + } + + #[pyclass(with(Constructor, Callable, Representable), flags(IMMUTABLETYPE))] + impl ConstEvaluator {} + + impl Constructor for ConstEvaluator { + type Args = FuncArgs; + + fn slot_new(_cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("cannot create '_typing._ConstEvaluator' instances".to_owned())) + } + + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { + unreachable!("ConstEvaluator cannot be instantiated from Python") + } + } + + /// annotationlib.Format.STRING = 4 + const ANNOTATE_FORMAT_STRING: i32 = 4; + + impl Callable for ConstEvaluator { + type Args = FuncArgs; + + fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let (format,): (i32,) = args.bind(vm)?; + let value = &zelf.value; + if format == ANNOTATE_FORMAT_STRING { + return typing_type_repr_value(value, vm); + } + Ok(value.clone()) + } + } + + /// String representation of a type for annotation purposes. + /// Equivalent of _Py_typing_type_repr. + fn typing_type_repr(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + // Ellipsis + if obj.is(&vm.ctx.ellipsis) { + return Ok("...".to_owned()); + } + // NoneType -> "None" + if obj.is(&vm.ctx.types.none_type.as_object()) { + return Ok("None".to_owned()); + } + // Generic aliases (has __origin__ and __args__) -> repr + let has_origin = obj.get_attr("__origin__", vm).is_ok(); + let has_args = obj.get_attr("__args__", vm).is_ok(); + if has_origin && has_args { + return Ok(obj.repr(vm)?.to_string()); + } + // Has __qualname__ and __module__ + if let Ok(qualname) = obj.get_attr("__qualname__", vm) { + if let Ok(module) = obj.get_attr("__module__", vm) { + if !vm.is_none(&module) { + if let Some(module_str) = module.downcast_ref::() { + if module_str.as_str() == "builtins" { + return Ok(qualname.str(vm)?.to_string()); + } + return Ok(format!("{}.{}", module_str.as_str(), qualname.str(vm)?)); + } + } + } + } + // Fallback to repr + Ok(obj.repr(vm)?.to_string()) + } + + /// Format a value as a string for ANNOTATE_FORMAT_STRING. + /// Handles tuples specially by wrapping in parentheses. + fn typing_type_repr_value(value: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + if let Ok(tuple) = value.try_to_ref::(vm) { + let mut parts = Vec::with_capacity(tuple.len()); + for item in tuple.iter() { + parts.push(typing_type_repr(item, vm)?); + } + Ok(vm.ctx.new_str(format!("({})", parts.join(", "))).into()) + } else { + Ok(vm.ctx.new_str(typing_type_repr(value, vm)?).into()) + } + } + + impl Representable for ConstEvaluator { + fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let value_repr = zelf.value.repr(vm)?; + Ok(format!("", value_repr)) + } + } + + pub(crate) fn constevaluator_alloc(value: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef { + ConstEvaluator { value }.into_ref(&vm.ctx).into() + } + #[pyattr] #[pyclass(name, module = "typing")] #[derive(Debug, PyPayload)] @@ -140,7 +237,8 @@ pub(crate) mod decl { if let Some(value) = cached { return Ok(value); } - let value = self.compute_value.call((), vm)?; + // Call evaluator with format=1 (FORMAT_VALUE) + let value = self.compute_value.call((1i32,), vm)?; *self.cached_value.lock() = Some(value.clone()); Ok(value) } @@ -193,20 +291,12 @@ pub(crate) mod decl { vm.ctx.none() } - /// Returns the evaluator for the alias value. #[pygetset] fn evaluate_value(&self, vm: &VirtualMachine) -> PyResult { if self.is_lazy { - // Lazy path: return the compute function directly return Ok(self.compute_value.clone()); } - // Eager path: wrap value in a ConstEvaluator - let value = self.compute_value.clone(); - Ok(vm - .new_function("_ConstEvaluator", move |_args: FuncArgs| -> PyResult { - Ok(value.clone()) - }) - .into()) + Ok(constevaluator_alloc(self.compute_value.clone(), vm)) } /// Check type_params ordering: non-default params must precede default params.