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
This commit is contained in:
Jeong, YunWon
2026-02-11 11:04:55 +09:00
parent a41d4c5029
commit d1e81225bc
5 changed files with 219 additions and 46 deletions

View File

@@ -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), "<constevaluator <class 'int'>>")

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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<Self>,
@@ -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<PyObjectRef>,
default_value: PyObjectRef,
default_value: parking_lot::Mutex<PyObjectRef>,
evaluate_default: PyMutex<PyObjectRef>,
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)
}

View File

@@ -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<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
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<Self>, 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<String> {
// 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::<crate::builtins::PyStr>() {
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::<PyTuple>(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<Self>, vm: &VirtualMachine) -> PyResult<String> {
let value_repr = zelf.value.repr(vm)?;
Ok(format!("<constevaluator {}>", 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.