diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index fc897d3b4c7..00fd7f0244e 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -31,6 +31,7 @@ CLASSDEREF classdict cmpop codedepth +constevaluator CODEUNIT CONVFUNC convparam diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 8b935dc7f12..76446f5c919 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -2107,7 +2107,6 @@ def test_issue17998(self): self.assertEqual(re.compile(pattern, re.S).findall(b'xyz'), [b'xyz'], msg=pattern) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_match_repr(self): for string in '[abracadabra]', S('[abracadabra]'): m = re.search(r'(.+)(.*?)\1', string) diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index 07b4957adec..269c325d642 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -165,7 +165,6 @@ def inner[X](): """ check_syntax_error(self, code) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised def test_nonlocal_disallowed_02(self): code = """ def outer2[T](): @@ -174,7 +173,6 @@ def inner1(): """ check_syntax_error(self, textwrap.dedent(code)) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised def test_nonlocal_disallowed_03(self): code = """ class Cls[T]: @@ -268,7 +266,6 @@ def func[A](): with self.assertRaisesRegex(NameError, "name 'A' is not defined"): run_code(code) - @unittest.expectedFailure # TODO: RUSTPYTHON; NameError: name 'x' is not defined def test_method_access_01(self): ns = run_code(""" class ClassA: @@ -320,7 +317,6 @@ def funcB[B](self): ... with self.assertRaisesRegex(NameError, "name 'B' is not defined"): run_code(code) - @unittest.expectedFailure # TODO: RUSTPYTHON; NameError: name 'x' is not defined def test_class_scope_interaction_01(self): ns = run_code(""" class C: @@ -486,7 +482,6 @@ class Inner[U](make_base([T for _ in (1,)]), make_base(T)): self.assertEqual(base1.__arg__, [ns["C"].__type_params__[0]]) self.assertEqual(base2.__arg__, "class") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ~T != 'class' def test_gen_exp_in_generic_method(self): code = """ class C[T]: @@ -1374,13 +1369,11 @@ def test_starred_typevartuple(self): 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 +1424,6 @@ def test_symtable_key_regression_name(self): 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 +1451,6 @@ def f[T: int = int, **P = int, *Ts = int](): pass 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 +1462,6 @@ def f[T: (int, str)](): pass 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/Lib/test/test_typing.py b/Lib/test/test_typing.py index a37d64946f8..788da8f41e4 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8437,7 +8437,6 @@ class Bar(NamedTuple): self.assertIsInstance(bar.attr, Vanilla) self.assertEqual(bar.attr.name, "attr") - @unittest.expectedFailure # TODO: RUSTPYTHON; + Error calling __set_name__ on 'Annoying' instance attr in 'NamedTupleClass' def test_setname_raises_the_same_as_on_other_classes(self): class CustomException(BaseException): pass @@ -9433,7 +9432,6 @@ def test_repr(self): self.assertEqual(repr(Match[str]), 'typing.Match[str]') self.assertEqual(repr(Match[bytes]), 'typing.Match[bytes]') - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "type 're\.Match' is not an acceptable base type" does not match "type '_sre.Match' is not an acceptable base type" def test_cannot_subclass(self): with self.assertRaisesRegex( TypeError, diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 826848ff271..7924956cd22 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 f324f48d507..4afd4cc75da 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -528,7 +528,6 @@ impl SymbolTableAnalyzer { if !self.tables.as_ref().is_empty() { let scope_depth = self.tables.as_ref().len(); // check if the name is already defined in any outer scope - // therefore if scope_depth < 2 || self.found_in_outer_scope(&symbol.name, st_typ) != Some(SymbolScope::Free) @@ -539,6 +538,25 @@ impl SymbolTableAnalyzer { location: None, }); } + // Check if the nonlocal binding refers to a type parameter + if symbol.flags.contains(SymbolFlags::NONLOCAL) { + for (symbols, _typ) in self.tables.iter().rev() { + if let Some(sym) = symbols.get(&symbol.name) { + if sym.flags.contains(SymbolFlags::TYPE_PARAM) { + return Err(SymbolTableError { + error: format!( + "nonlocal binding not allowed for type parameter '{}'", + symbol.name + ), + location: None, + }); + } + if sym.is_bound() { + break; + } + } + } + } } else { return Err(SymbolTableError { error: format!( @@ -933,7 +951,8 @@ impl SymbolTableBuilder { /// Creates or reuses the annotation block for the current scope fn enter_annotation_scope(&mut self, line_number: u32) { let current = self.tables.last_mut().unwrap(); - let can_see_class_scope = current.typ == CompilerScope::Class; + let can_see_class_scope = + current.typ == CompilerScope::Class || current.can_see_class_scope; let has_conditional = current.has_conditional_annotations; // Create annotation block if not exists @@ -1485,6 +1504,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 +2011,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 +2038,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 +2058,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/builtins/interpolation.rs b/crates/vm/src/builtins/interpolation.rs index d8127c151e3..afdce51be9b 100644 --- a/crates/vm/src/builtins/interpolation.rs +++ b/crates/vm/src/builtins/interpolation.rs @@ -1,4 +1,7 @@ -use super::{PyStr, PyStrRef, PyTupleRef, PyType, tuple::IntoPyTuple}; +use super::{ + PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef, genericalias::PyGenericAlias, + tuple::IntoPyTuple, +}; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, @@ -135,6 +138,11 @@ impl PyInterpolation { self.format_spec.clone() } + #[pyclassmethod] + fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + PyGenericAlias::from_args(cls, args, vm) + } + #[pymethod] fn __reduce__(zelf: PyRef, vm: &VirtualMachine) -> PyTupleRef { let cls = zelf.class().to_owned(); diff --git a/crates/vm/src/builtins/memory.rs b/crates/vm/src/builtins/memory.rs index 31afce48a6d..329a6be6737 100644 --- a/crates/vm/src/builtins/memory.rs +++ b/crates/vm/src/builtins/memory.rs @@ -552,8 +552,7 @@ impl Py { flags(SEQUENCE) )] impl PyMemoryView { - // TODO: Uncomment when Python adds __class_getitem__ to memoryview - // #[pyclassmethod] + #[pyclassmethod] fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { PyGenericAlias::from_args(cls, args, vm) } diff --git a/crates/vm/src/builtins/template.rs b/crates/vm/src/builtins/template.rs index 891de8c6c2f..0496504cbef 100644 --- a/crates/vm/src/builtins/template.rs +++ b/crates/vm/src/builtins/template.rs @@ -1,9 +1,9 @@ -use super::{PyStr, PyTupleRef, PyType}; -use crate::common::lock::LazyLock; +use super::{PyStr, PyTupleRef, PyType, PyTypeRef, genericalias::PyGenericAlias}; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, atomic_func, class::PyClassImpl, + common::lock::LazyLock, function::{FuncArgs, PyComparisonValue}, protocol::{PyIterReturn, PySequenceMethods}, types::{ @@ -178,6 +178,11 @@ impl PyTemplate { self.concat(&other, vm) } + #[pyclassmethod] + fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + PyGenericAlias::from_args(cls, args, vm) + } + #[pymethod] fn __reduce__(&self, vm: &VirtualMachine) -> PyResult { // Import string.templatelib._template_unpickle diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index e2852ee7dc5..bdbbc367e18 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -1629,7 +1629,7 @@ impl Constructor for PyType { // PEP 678: Add a note to the original exception instead of wrapping it // (Python 3.12+, gh-77757) let note = format!( - "Error calling __set_name__ on '{}' instance {} in '{}'", + "Error calling __set_name__ on '{}' instance '{}' in '{}'", obj.class().name(), name, typ.name() diff --git a/crates/vm/src/stdlib/sre.rs b/crates/vm/src/stdlib/sre.rs index 367cf245102..eb0cb05eb7d 100644 --- a/crates/vm/src/stdlib/sre.rs +++ b/crates/vm/src/stdlib/sre.rs @@ -190,7 +190,7 @@ mod _sre { } #[pyattr] - #[pyclass(name = "Pattern")] + #[pyclass(module = "re", name = "Pattern")] #[derive(Debug, PyPayload)] pub(crate) struct Pattern { pub pattern: PyObjectRef, @@ -597,7 +597,7 @@ mod _sre { } #[pyattr] - #[pyclass(name = "Match")] + #[pyclass(module = "re", name = "Match")] #[derive(Debug, PyPayload)] pub(crate) struct Match { string: PyObjectRef, diff --git a/crates/vm/src/stdlib/typevar.rs b/crates/vm/src/stdlib/typevar.rs index ac8aeac3636..9645f5a86ef 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::const_evaluator_alloc}, types::{AsNumber, Comparable, Constructor, Iterable, PyComparisonOp, Representable}, }; @@ -69,11 +69,11 @@ pub(crate) mod typevar { #[allow(dead_code)] pub struct TypeVar { name: PyObjectRef, // TODO PyStrRef? - bound: parking_lot::Mutex, + bound: PyMutex, evaluate_bound: PyObjectRef, - constraints: parking_lot::Mutex, + constraints: PyMutex, evaluate_constraints: PyObjectRef, - default_value: parking_lot::Mutex, + default_value: PyMutex, evaluate_default: PyMutex, covariant: bool, contravariant: bool, @@ -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(const_evaluator_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(const_evaluator_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(const_evaluator_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()) }; @@ -361,11 +396,11 @@ pub(crate) mod typevar { Ok(Self { name, - bound: parking_lot::Mutex::new(bound_obj), + bound: PyMutex::new(bound_obj), evaluate_bound, - constraints: parking_lot::Mutex::new(constraints_obj), + constraints: PyMutex::new(constraints_obj), evaluate_constraints, - default_value: parking_lot::Mutex::new(default_value), + default_value: PyMutex::new(default_value), evaluate_default: PyMutex::new(evaluate_default), covariant, contravariant, @@ -383,11 +418,11 @@ pub(crate) mod typevar { ) -> Self { Self { name, - bound: parking_lot::Mutex::new(vm.ctx.none()), + bound: PyMutex::new(vm.ctx.none()), evaluate_bound, - constraints: parking_lot::Mutex::new(vm.ctx.none()), + constraints: PyMutex::new(vm.ctx.none()), evaluate_constraints, - default_value: parking_lot::Mutex::new(vm.ctx.typing_no_default.clone().into()), + default_value: PyMutex::new(vm.ctx.typing_no_default.clone().into()), evaluate_default: PyMutex::new(vm.ctx.none()), covariant: false, contravariant: false, @@ -403,7 +438,7 @@ pub(crate) mod typevar { pub struct ParamSpec { name: PyObjectRef, bound: Option, - default_value: PyObjectRef, + default_value: PyMutex, 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(const_evaluator_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: PyMutex::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: PyMutex::new(vm.ctx.typing_no_default.clone().into()), evaluate_default: PyMutex::new(vm.ctx.none()), covariant: false, contravariant: false, @@ -642,7 +683,7 @@ pub(crate) mod typevar { #[allow(dead_code)] pub struct TypeVarTuple { name: PyObjectRef, - default_value: parking_lot::Mutex, + default_value: PyMutex, evaluate_default: PyMutex, } #[pyclass(flags(HAS_DICT), with(Constructor, Representable, Iterable))] @@ -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(const_evaluator_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) } @@ -762,7 +813,7 @@ pub(crate) mod typevar { let typevartuple = Self { name, - default_value: parking_lot::Mutex::new(default_value), + default_value: PyMutex::new(default_value), evaluate_default: PyMutex::new(evaluate_default), }; @@ -789,7 +840,7 @@ pub(crate) mod typevar { pub fn new(name: PyObjectRef, vm: &VirtualMachine) -> Self { Self { name, - default_value: parking_lot::Mutex::new(vm.ctx.typing_no_default.clone().into()), + default_value: PyMutex::new(vm.ctx.typing_no_default.clone().into()), evaluate_default: PyMutex::new(vm.ctx.none()), } } diff --git a/crates/vm/src/stdlib/typing.rs b/crates/vm/src/stdlib/typing.rs index 640353ce50c..9ae33cea7ee 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,101 @@ 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) + && let Ok(module) = obj.get_attr("__module__", vm) + && !vm.is_none(&module) + && 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 const_evaluator_alloc(value: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef { + ConstEvaluator { value }.into_ref(&vm.ctx).into() + } + #[pyattr] #[pyclass(name, module = "typing")] #[derive(Debug, PyPayload)] @@ -140,7 +235,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 +289,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(const_evaluator_alloc(self.compute_value.clone(), vm)) } /// Check type_params ordering: non-default params must precede default params.