Fix typing, typevar, genericalias, and symboltable (#7091)

- TypeVar/ParamSpec repr: use infer_variance flag
- ParamSpec: add type_check on bound argument
- ParamSpecArgs/Kwargs: use equality instead of identity
- NoDefault: change to IMMUTABLETYPE flag
- subscript_generic: wrap TypeVarTuple in Unpack
- symboltable: selective name mangling in type param scopes
- symboltable: fix double default scanning for non-generic fns
- Unmark 4 passing tests in test_type_params
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-02-14 09:15:57 +09:00
1 parent 12de55fc68
commit 8ea96fb540
10 files changed
+187 -106

No files matched your search

+1
View File
@@ -121,6 +121,7 @@
"sysmodule",
"tracebacks",
"typealiases",
"typevartuples",
"unhashable",
"uninit",
"unraisable",
+1 -1
View File
@@ -85,7 +85,7 @@ class ProcessPoolExecutorTest(ExecutorTest):
self.assertIn('raise RuntimeError(123) # some comment',
f1.getvalue())
@unittest.skipIf(sys.platform == 'linux', 'TODO: RUSTPYTHON flaky EOFError')
@unittest.skip('TODO: RUSTPYTHON flaky EOFError')
@hashlib_helper.requires_hashdigest('md5')
def test_ressources_gced_in_workers(self):
# Ensure that argument for a job are correctly gc-ed after the job
-4
View File
@@ -243,7 +243,6 @@ class TypeParamsAccessTest(unittest.TestCase):
A, B = func.__type_params__
self.assertEqual(func.__annotations__["a"], dict[A, B])
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: the symbol 'list' must be present in the symbol table
def test_function_access_02(self):
code = """
def func[A](a = list[A]()):
@@ -866,7 +865,6 @@ class TypeParamsManglingTest(unittest.TestCase):
self.assertEqual(Outer.Inner._Inner__x, "inner")
self.assertEqual(Outer._Outer__after, "after")
@unittest.expectedFailure # TODO: RUSTPYTHON; NameError: name '_Derived__Base' is not defined
def test_no_mangling_in_bases(self):
ns = run_code("""
class __Base:
@@ -880,7 +878,6 @@ class TypeParamsManglingTest(unittest.TestCase):
self.assertEqual(Derived.__bases__, (ns["__Base"], Generic))
self.assertEqual(Derived.kwargs, {"__kwarg": 1})
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: the symbol '_Y__X' must be present in the symbol table
def test_no_mangling_in_nested_scopes(self):
ns = run_code("""
from test.test_type_params import make_base
@@ -911,7 +908,6 @@ class TypeParamsManglingTest(unittest.TestCase):
base3 = Y.__bases__[3]
self.assertEqual(list(base3.__arg__), [ns["__X"]])
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: the symbol '_Foo__T' must be present in the symbol table
def test_type_params_are_mangled(self):
ns = run_code("""
from test.test_type_params import make_base
-1
View File
@@ -10914,7 +10914,6 @@ class TypeIterationTests(BaseTestCase):
Annotated[T, ''],
)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_cannot_iterate(self):
expected_error_regex = "object is not iterable"
for test_type in self._UNITERABLE_TYPES:
+2 -1
View File
@@ -1852,7 +1852,8 @@ impl Compiler {
.code_stack
.last()
.and_then(|info| info.private.as_deref());
symboltable::mangle_name(private, name)
let mangled_names = self.current_symbol_table().mangled_names.as_ref();
symboltable::maybe_mangle_name(private, mangled_names, name)
}
// = compiler_nameop
+105 -23
View File
@@ -68,6 +68,11 @@ pub struct SymbolTable {
/// Whether `from __future__ import annotations` is active
pub future_annotations: bool,
/// Names of type parameters that should still be mangled in type param scopes.
/// When Some, only names in this set are mangled; other names are left unmangled.
/// Set on type param blocks for generic classes; inherited by non-class child scopes.
pub mangled_names: Option<IndexSet<String>>,
}
impl SymbolTable {
@@ -88,6 +93,7 @@ impl SymbolTable {
annotation_block: None,
has_conditional_annotations: false,
future_annotations: false,
mangled_names: None,
}
}
@@ -905,14 +911,26 @@ impl SymbolTableBuilder {
)
})
.unwrap_or(false);
let table = SymbolTable::new(name.to_owned(), typ, line_number, is_nested);
// Inherit mangled_names from parent for non-class scopes
let inherited_mangled_names = self
.tables
.last()
.and_then(|t| t.mangled_names.clone())
.filter(|_| typ != CompilerScope::Class);
let mut table = SymbolTable::new(name.to_owned(), typ, line_number, is_nested);
table.mangled_names = inherited_mangled_names;
self.tables.push(table);
// Save parent's varnames and start fresh for the new scope
self.varnames_stack
.push(core::mem::take(&mut self.current_varnames));
}
fn enter_type_param_block(&mut self, name: &str, line_number: u32) -> SymbolTableResult {
fn enter_type_param_block(
&mut self,
name: &str,
line_number: u32,
for_class: bool,
) -> SymbolTableResult {
// Check if we're in a class scope
let in_class = self
.tables
@@ -921,16 +939,21 @@ impl SymbolTableBuilder {
self.enter_scope(name, CompilerScope::TypeParams, line_number);
// If we're in a class, mark that this type param scope can see the class scope
// Set properties on the newly created type param scope
if let Some(table) = self.tables.last_mut() {
table.can_see_class_scope = in_class;
// Add __classdict__ as a USE symbol in type param scope if in class
if in_class {
self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?;
// For generic classes, create mangled_names set so that only
// type parameter names get mangled (not bases or other expressions)
if for_class {
table.mangled_names = Some(IndexSet::default());
}
}
// Add __classdict__ as a USE symbol in type param scope if in class
if in_class {
self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?;
}
// Register .type_params as a SET symbol (it will be converted to cell variable later)
self.register_name(".type_params", SymbolUsage::Assigned, TextRange::default())?;
@@ -1202,12 +1225,20 @@ impl SymbolTableBuilder {
None
};
// For generic functions, scan defaults before entering type_param_block
// (defaults are evaluated in the enclosing scope, not the type param scope)
let has_type_params = type_params.is_some();
if has_type_params {
self.scan_parameter_defaults(parameters)?;
}
// For generic functions, enter type_param block FIRST so that
// annotation scopes are nested inside and can see type parameters.
if let Some(type_params) = type_params {
self.enter_type_param_block(
&format!("<generic parameters of {}>", name.as_str()),
self.line_index_start(type_params.range),
false,
)?;
self.scan_type_params(type_params)?;
}
@@ -1223,6 +1254,7 @@ impl SymbolTableBuilder {
self.line_index_start(*range),
has_return_annotation,
*is_async,
has_type_params, // skip_defaults: already scanned above
)?;
self.scan_statements(body)?;
self.leave_scope();
@@ -1244,11 +1276,16 @@ impl SymbolTableBuilder {
range,
node_index: _,
}) => {
// Save class_name for the entire ClassDef processing
let prev_class = self.class_name.take();
if let Some(type_params) = type_params {
self.enter_type_param_block(
&format!("<generic parameters of {}>", name.as_str()),
self.line_index_start(type_params.range),
true, // for_class: enable selective mangling
)?;
// Set class_name for mangling in type param scope
self.class_name = Some(name.to_string());
self.scan_type_params(type_params)?;
}
self.enter_scope(
@@ -1257,10 +1294,9 @@ impl SymbolTableBuilder {
self.line_index_start(*range),
);
// Reset in_conditional_block for new class scope
// (each scope has its own conditional context)
let saved_in_conditional = self.in_conditional_block;
self.in_conditional_block = false;
let prev_class = self.class_name.replace(name.to_string());
self.class_name = Some(name.to_string());
self.register_name("__module__", SymbolUsage::Assigned, *range)?;
self.register_name("__qualname__", SymbolUsage::Assigned, *range)?;
self.register_name("__doc__", SymbolUsage::Assigned, *range)?;
@@ -1268,7 +1304,13 @@ impl SymbolTableBuilder {
self.scan_statements(body)?;
self.leave_scope();
self.in_conditional_block = saved_in_conditional;
self.class_name = prev_class;
// For non-generic classes, restore class_name before base scanning.
// Bases are evaluated in the enclosing scope, not the class scope.
// For generic classes, bases are scanned within the type_param scope
// where class_name is already correctly set.
if type_params.is_none() {
self.class_name = prev_class.clone();
}
if let Some(arguments) = arguments {
self.scan_expressions(&arguments.args, ExpressionContext::Load)?;
for keyword in &arguments.keywords {
@@ -1278,6 +1320,8 @@ impl SymbolTableBuilder {
if type_params.is_some() {
self.leave_scope();
}
// Restore class_name after all ClassDef processing
self.class_name = prev_class;
self.scan_decorators(decorator_list, ExpressionContext::Load)?;
self.register_ident(name, SymbolUsage::Assigned)?;
}
@@ -1506,6 +1550,7 @@ impl SymbolTableBuilder {
self.enter_type_param_block(
"TypeAlias",
self.line_index_start(type_params.range),
false,
)?;
self.scan_type_params(type_params)?;
}
@@ -1833,6 +1878,7 @@ impl SymbolTableBuilder {
self.line_index_start(expression.range()),
false, // lambdas have no return annotation
false, // lambdas are never async
false, // don't skip defaults
)?;
} else {
self.enter_scope(
@@ -2233,15 +2279,8 @@ impl SymbolTableBuilder {
Ok(())
}
fn enter_scope_with_parameters(
&mut self,
name: &str,
parameters: &ast::Parameters,
line_number: u32,
has_return_annotation: bool,
is_async: bool,
) -> SymbolTableResult {
// Evaluate eventual default parameters:
/// Scan default parameter values (evaluated in the enclosing scope)
fn scan_parameter_defaults(&mut self, parameters: &ast::Parameters) -> SymbolTableResult {
for default in parameters
.posonlyargs
.iter()
@@ -2249,7 +2288,23 @@ impl SymbolTableBuilder {
.chain(parameters.kwonlyargs.iter())
.filter_map(|arg| arg.default.as_ref())
{
self.scan_expression(default, ExpressionContext::Load)?; // not ExprContext?
self.scan_expression(default, ExpressionContext::Load)?;
}
Ok(())
}
fn enter_scope_with_parameters(
&mut self,
name: &str,
parameters: &ast::Parameters,
line_number: u32,
has_return_annotation: bool,
is_async: bool,
skip_defaults: bool,
) -> SymbolTableResult {
// Evaluate eventual default parameters (unless already scanned before type_param_block):
if !skip_defaults {
self.scan_parameter_defaults(parameters)?;
}
// Annotations are scanned in outer scope:
@@ -2381,7 +2436,18 @@ impl SymbolTableBuilder {
let scope_depth = self.tables.len();
let table = self.tables.last_mut().unwrap();
let name = mangle_name(self.class_name.as_deref(), name);
// Add type param names to mangled_names set for selective mangling
if matches!(role, SymbolUsage::TypeParam)
&& let Some(ref mut set) = table.mangled_names
{
set.insert(name.to_owned());
}
let name = maybe_mangle_name(
self.class_name.as_deref(),
table.mangled_names.as_ref(),
name,
);
// Some checks for the symbol that present on this scope level:
let symbol = if let Some(symbol) = table.symbols.get_mut(name.as_ref()) {
let flags = &symbol.flags;
@@ -2574,11 +2640,27 @@ pub(crate) fn mangle_name<'a>(class_name: Option<&str>, name: &'a str) -> Cow<'a
if !name.starts_with("__") || name.ends_with("__") || name.contains('.') {
return name.into();
}
// strip leading underscore
let class_name = class_name.strip_prefix(|c| c == '_').unwrap_or(class_name);
// Strip leading underscores from class name
let class_name = class_name.trim_start_matches('_');
let mut ret = String::with_capacity(1 + class_name.len() + name.len());
ret.push('_');
ret.push_str(class_name);
ret.push_str(name);
ret.into()
}
/// Selective mangling for type parameter scopes around generic classes.
/// If `mangled_names` is Some, only mangle names that are in the set;
/// other names are left unmangled.
pub(crate) fn maybe_mangle_name<'a>(
class_name: Option<&str>,
mangled_names: Option<&IndexSet<String>>,
name: &'a str,
) -> Cow<'a, str> {
if let Some(set) = mangled_names
&& !set.contains(name)
{
return name.into();
}
mangle_name(class_name, name)
}
+5 -7
View File
@@ -720,24 +720,22 @@ impl crate::types::IterNext for PyGenericAliasIterator {
}
}
/// Creates a GenericAlias from type parameters, equivalent to CPython's _Py_subscript_generic
/// This is used for PEP 695 classes to create Generic[T] from type parameters
/// Creates a GenericAlias from type parameters, equivalent to _Py_subscript_generic.
/// This is used for PEP 695 classes to create Generic[T] from type parameters.
// _Py_subscript_generic
pub fn subscript_generic(type_params: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// Get typing module and _GenericAlias
let typing_module = vm.import("typing", 0)?;
let generic_type = typing_module.get_attr("Generic", vm)?;
// Call typing._GenericAlias(Generic, type_params)
let generic_alias_class = typing_module.get_attr("_GenericAlias", vm)?;
let args = if let Ok(tuple) = type_params.try_to_ref::<PyTuple>(vm) {
let params = if let Ok(tuple) = type_params.try_to_ref::<PyTuple>(vm) {
tuple.to_owned()
} else {
PyTuple::new_ref(vec![type_params], &vm.ctx)
};
// Create _GenericAlias instance
let args = crate::stdlib::typing::unpack_typevartuples(&params, vm)?;
generic_alias_class.call((generic_type, args.to_pyobject(vm)), vm)
}
+59 -66
View File
@@ -23,6 +23,25 @@ pub(crate) mod typevar {
call_typing_func_object(vm, "_type_check", (arg, message_str))
}
fn variance_repr(
name: &str,
infer_variance: bool,
covariant: bool,
contravariant: bool,
) -> String {
if infer_variance {
return name.to_string();
}
let prefix = if covariant {
'+'
} else if contravariant {
'-'
} else {
'~'
};
format!("{prefix}{name}")
}
/// Get the module of the caller frame, similar to CPython's caller() function.
/// Returns the module name or None if not found.
///
@@ -270,14 +289,12 @@ pub(crate) mod typevar {
#[inline(always)]
fn repr_str(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
let name = zelf.name.str(vm)?;
let repr = if zelf.covariant {
format!("+{name}")
} else if zelf.contravariant {
format!("-{name}")
} else {
format!("~{name}")
};
Ok(repr)
Ok(variance_repr(
name.as_str(),
zelf.infer_variance,
zelf.covariant,
zelf.contravariant,
))
}
}
@@ -601,7 +618,10 @@ pub(crate) mod typevar {
return Err(vm.new_type_error("ParamSpec() takes at most 1 positional argument"));
};
let bound = kwargs.swap_remove("bound");
let bound = kwargs
.swap_remove("bound")
.map(|b| type_check(b, "Bound must be a type.", vm))
.transpose()?;
let covariant = kwargs
.swap_remove("covariant")
.map(|v| v.try_to_bool(vm))
@@ -665,7 +685,12 @@ pub(crate) mod typevar {
#[inline(always)]
fn repr_str(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
let name = zelf.__name__().str(vm)?;
Ok(format!("~{name}"))
Ok(variance_repr(
name.as_str(),
zelf.infer_variance,
zelf.covariant,
zelf.contravariant,
))
}
}
@@ -902,35 +927,19 @@ pub(crate) mod typevar {
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult<PyComparisonValue> {
fn eq(
zelf: &crate::Py<ParamSpecArgs>,
other: PyObjectRef,
_vm: &VirtualMachine,
) -> PyResult<bool> {
// First check if other is also ParamSpecArgs
if let Ok(other_args) = other.downcast::<ParamSpecArgs>() {
// Check if they have the same origin
return Ok(zelf.__origin__.is(&other_args.__origin__));
op.eq_only(|| {
if other.class().is(zelf.class())
&& let Some(other_args) = other.downcast_ref::<ParamSpecArgs>()
{
let eq = zelf.__origin__.rich_compare_bool(
&other_args.__origin__,
PyComparisonOp::Eq,
vm,
)?;
return Ok(PyComparisonValue::Implemented(eq));
}
Ok(false)
}
match op {
PyComparisonOp::Eq => {
if let Ok(result) = eq(zelf, other.to_owned(), vm) {
Ok(result.into())
} else {
Ok(PyComparisonValue::NotImplemented)
}
}
PyComparisonOp::Ne => {
if let Ok(result) = eq(zelf, other.to_owned(), vm) {
Ok((!result).into())
} else {
Ok(PyComparisonValue::NotImplemented)
}
}
_ => Ok(PyComparisonValue::NotImplemented),
}
Ok(PyComparisonValue::NotImplemented)
})
}
}
@@ -981,35 +990,19 @@ pub(crate) mod typevar {
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult<PyComparisonValue> {
fn eq(
zelf: &crate::Py<ParamSpecKwargs>,
other: PyObjectRef,
_vm: &VirtualMachine,
) -> PyResult<bool> {
// First check if other is also ParamSpecKwargs
if let Ok(other_kwargs) = other.downcast::<ParamSpecKwargs>() {
// Check if they have the same origin
return Ok(zelf.__origin__.is(&other_kwargs.__origin__));
op.eq_only(|| {
if other.class().is(zelf.class())
&& let Some(other_kwargs) = other.downcast_ref::<ParamSpecKwargs>()
{
let eq = zelf.__origin__.rich_compare_bool(
&other_kwargs.__origin__,
PyComparisonOp::Eq,
vm,
)?;
return Ok(PyComparisonValue::Implemented(eq));
}
Ok(false)
}
match op {
PyComparisonOp::Eq => {
if let Ok(result) = eq(zelf, other.to_owned(), vm) {
Ok(result.into())
} else {
Ok(PyComparisonValue::NotImplemented)
}
}
PyComparisonOp::Ne => {
if let Ok(result) = eq(zelf, other.to_owned(), vm) {
Ok((!result).into())
} else {
Ok(PyComparisonValue::NotImplemented)
}
}
_ => Ok(PyComparisonValue::NotImplemented),
}
Ok(PyComparisonValue::NotImplemented)
})
}
}
+5 -2
View File
@@ -56,7 +56,7 @@ pub(crate) mod decl {
#[derive(Debug, PyPayload)]
pub struct NoDefault;
#[pyclass(with(Constructor, Representable), flags(BASETYPE))]
#[pyclass(with(Constructor, Representable), flags(IMMUTABLETYPE))]
impl NoDefault {
#[pymethod]
fn __reduce__(&self, _vm: &VirtualMachine) -> String {
@@ -467,7 +467,10 @@ pub(crate) mod decl {
}
/// Wrap TypeVarTuples in Unpack[], matching unpack_typevartuples()
fn unpack_typevartuples(type_params: &PyTupleRef, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
pub(crate) fn unpack_typevartuples(
type_params: &PyTupleRef,
vm: &VirtualMachine,
) -> PyResult<PyTupleRef> {
let has_tvt = type_params
.iter()
.any(|p| p.downcastable::<crate::stdlib::typevar::TypeVarTuple>());
+9 -1
View File
@@ -534,7 +534,15 @@ pub(crate) fn richcompare_wrapper(
}
fn iter_wrapper(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
vm.call_special_method(&zelf, identifier!(vm, __iter__), ())
// slot_tp_iter: if __iter__ is None, the type is explicitly not iterable
let cls = zelf.class();
let iter_attr = cls.get_attr(identifier!(vm, __iter__));
match iter_attr {
Some(attr) if vm.is_none(&attr) => {
Err(vm.new_type_error(format!("'{}' object is not iterable", cls.name())))
}
_ => vm.call_special_method(&zelf, identifier!(vm, __iter__), ()),
}
}
fn bool_wrapper(num: PyNumber<'_>, vm: &VirtualMachine) -> PyResult<bool> {