Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"sysmodule",
"tracebacks",
"typealiases",
"typevartuples",
"unhashable",
"uninit",
"unraisable",
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_concurrent_futures/test_process_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_traceback(self):
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
Expand Down
4 changes: 0 additions & 4 deletions Lib/test/test_type_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ def func[A, B](a: dict[A, B]):
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]()):
Expand Down Expand Up @@ -866,7 +865,6 @@ class Inner[T]:
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:
Expand All @@ -880,7 +878,6 @@ class Derived[T](__Base, __kwarg=1):
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
Expand Down Expand Up @@ -911,7 +908,6 @@ class Y[T: __X](
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
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion crates/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
126 changes: 104 additions & 22 deletions crates/codegen/src/symboltable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -88,6 +93,7 @@ impl SymbolTable {
annotation_block: None,
has_conditional_annotations: false,
future_annotations: false,
mangled_names: None,
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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())?;

Expand Down Expand Up @@ -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)?;
}
Expand All @@ -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();
Expand All @@ -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(
Expand All @@ -1257,18 +1294,23 @@ 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)?;
self.register_name("__class__", SymbolUsage::Assigned, *range)?;
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 {
Expand All @@ -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)?;
}
Expand Down Expand Up @@ -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)?;
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -2233,23 +2279,32 @@ impl SymbolTableBuilder {
Ok(())
}

/// 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()
.chain(parameters.args.iter())
.chain(parameters.kwonlyargs.iter())
.filter_map(|arg| arg.default.as_ref())
{
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:
for default in parameters
.posonlyargs
.iter()
.chain(parameters.args.iter())
.chain(parameters.kwonlyargs.iter())
.filter_map(|arg| arg.default.as_ref())
{
self.scan_expression(default, ExpressionContext::Load)?; // not ExprContext?
// 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:
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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('_');
Comment on lines +2643 to +2644
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check the actual code at lines 2638-2661 in symboltable.rs
sed -n '2630,2670p' crates/codegen/src/symboltable.rs

Repository: RustPython/RustPython

Length of output: 1120


🏁 Script executed:

#!/bin/bash
# Search for mangle_name usage and test cases
rg -n "mangle_name|mangle" crates/codegen/src/symboltable.rs | head -30
# Check for test files related to name mangling
fd -e rs -e py | xargs grep -l "mangle" | head -10

Repository: RustPython/RustPython

Length of output: 1805


🏁 Script executed:

#!/bin/bash
# Search for test files in the codegen directory
fd -e rs crates/codegen/src/ | xargs grep -l "test.*mangle\|mangle.*test" 2>/dev/null
# Search for any tests that use double-underscore names (which trigger mangling)
rg -n "__.*__" crates/codegen/src/ | grep -i "test\|def\|class" | head -20
# Check if there are any Python test files related to name mangling
fd test crates/codegen/ -type f

Repository: RustPython/RustPython

Length of output: 2865


🏁 Script executed:

#!/bin/bash
# Search for Python test files that might test name mangling
find . -path ./target -prune -o -name "*.py" -type f -exec grep -l "^class _" {} \; | head -10

# Check if there are any tests for name mangling behavior
rg -i "name.*mangle\|mangle.*test" Lib/test/ | head -20

# Also check the Python standard library for all-underscore class names
rg "^class _[_]+[^_]" --type py | head -10

Repository: RustPython/RustPython

Length of output: 5821


🏁 Script executed:

#!/bin/bash
# Check the context around the all-underscore class definitions
grep -B2 -A10 "^class __:" Lib/test/pyclbr_input.py
echo "---"
grep -B2 -A10 "^class ___:" Lib/test/pyclbr_input.py
echo "---"
grep -B2 -A10 "^class ___(object):" Lib/test/test_copyreg.py

Repository: RustPython/RustPython

Length of output: 772


🏁 Script executed:

#!/bin/bash
# Create a simple test script to verify CPython's behavior
python3 << 'EOF'
# Test CPython's name mangling with all-underscore class names
class __:
    def __foo(self):
        return "foo"
    
class ___:
    def __bar(self):
        return "bar"

# Check what the actual method names are after mangling
import inspect

# For class __
cls1 = __
methods1 = [m for m in dir(cls1) if not m.startswith('_') or m.startswith('__')]
print("Class __ methods:", [m for m in dir(cls1) if 'foo' in m])

# For class ___
cls2 = ___
print("Class ___ methods:", [m for m in dir(cls2) if 'bar' in m])

# The key question: are the names mangled or not?
# If they're mangled, they'd have the class name prefix
# If not mangled, they'd stay as __foo and __bar
EOF

Repository: RustPython/RustPython

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Search for tests that use all-underscore class names with double-underscore methods
rg -n "class __|class ___" extra_tests/ Lib/test/ --type py

# Check if pyclbr_input.py is actually tested
rg -n "pyclbr_input" . --type py | head -10

# Check if there are specific tests for name mangling behavior
rg -n "mangle|__.*__" extra_tests/ --type py | grep -i "class\|def" | head -20

Repository: RustPython/RustPython

Length of output: 3837


Add empty-string check after stripping leading underscores from class name.

When class_name consists entirely of underscores (e.g., "__" or "___"), trim_start_matches('_') produces an empty string. CPython's mangling logic skips mangling in this case, but the current code proceeds to produce an incorrect mangled name.

For example, class __: with a method def __foo(self): should remain __foo (unmangled), not become ___foo.

Proposed fix
     // Strip leading underscores from class name
     let class_name = class_name.trim_start_matches('_');
+    if class_name.is_empty() {
+        return name.into();
+    }
     let mut ret = String::with_capacity(1 + class_name.len() + name.len());
🤖 Prompt for AI Agents
In `@crates/codegen/src/symboltable.rs` around lines 2638 - 2639, After trimming
leading underscores from class_name (let class_name =
class_name.trim_start_matches('_');), add a check for an empty result and skip
mangling if empty: if class_name.is_empty() { return original_name.to_string();
} so that classes made only of underscores (e.g., "__") do not trigger
private-name mangling; update the code path that builds the mangled identifier
to return the unmodified identifier when class_name is empty.

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)
}
12 changes: 5 additions & 7 deletions crates/vm/src/builtins/genericalias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading
Loading