Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fix classcell
  • Loading branch information
youknowone committed Jan 11, 2026
commit f6888795359811cdbc0ac84add42d86ea9ffeec8
8 changes: 4 additions & 4 deletions crates/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ enum SuperCallType<'a> {
self_arg: &'a Expr,
},
/// super() - implicit 0-argument form (uses __class__ cell)
/// TODO: Enable after proper __class__ cell handling
/// TODO: Enable after fixing __class__ cell handling in nested classes
#[allow(dead_code)]
ZeroArg,
}
Expand Down Expand Up @@ -743,9 +743,9 @@ impl Compiler {
}
0 => {
// 0-arg: super() - need __class__ cell and first parameter
// TODO: Enable 0-arg super() optimization after proper __class__ cell handling
// For now, skip optimization to avoid issues with nested class definitions
// and other complex scenarios where __class__ might not be properly available
// TODO: 0-arg super() optimization is disabled due to __class__ cell issues
// The __class__ cell handling in nested class definitions needs more work.
// For now, fall back to regular super() call.
None
}
_ => None, // 1 or 3+ args - not optimizable
Expand Down
68 changes: 47 additions & 21 deletions crates/codegen/src/symboltable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use ruff_python_ast::{
};
use ruff_text_size::{Ranged, TextRange};
use rustpython_compiler_core::{PositionEncoding, SourceFile, SourceLocation};
use std::collections::HashSet;

/// Captures all symbols in the current scope, and has a list of sub-scopes in this scope.
#[derive(Clone)]
Expand Down Expand Up @@ -244,30 +245,29 @@ impl core::fmt::Debug for SymbolTable {
*/
fn analyze_symbol_table(symbol_table: &mut SymbolTable) -> SymbolTableResult {
let mut analyzer = SymbolTableAnalyzer::default();
analyzer.analyze_symbol_table(symbol_table)
// Discard the newfree set at the top level - it's only needed for propagation
let _newfree = analyzer.analyze_symbol_table(symbol_table)?;
Ok(())
}

/* Drop __class__ and __classdict__ from free variables in class scope
and set the appropriate flags. Equivalent to CPython's drop_class_free().
See: https://github.com/python/cpython/blob/main/Python/symtable.c#L884

This function removes __class__ and __classdict__ from the
`newfree` set (which contains free variables collected from all child scopes)
and sets the corresponding flags on the class's symbol table entry.
*/
fn drop_class_free(symbol_table: &mut SymbolTable) {
// Check if __class__ is used as a free variable
if let Some(class_symbol) = symbol_table.symbols.get("__class__")
&& class_symbol.scope == SymbolScope::Free
{
fn drop_class_free(symbol_table: &mut SymbolTable, newfree: &mut HashSet<String>) {
// Check if __class__ is in the free variables collected from children
// If found, it means a child scope (method) references __class__
if newfree.remove("__class__") {
symbol_table.needs_class_closure = true;
// Note: In CPython, the symbol is removed from the free set,
// but in RustPython we handle this differently during code generation
}

// Check if __classdict__ is used as a free variable
if let Some(classdict_symbol) = symbol_table.symbols.get("__classdict__")
&& classdict_symbol.scope == SymbolScope::Free
{
// Check if __classdict__ is in the free variables collected from children
if newfree.remove("__classdict__") {
symbol_table.needs_classdict = true;
// Note: In CPython, the symbol is removed from the free set,
// but in RustPython we handle this differently during code generation
}
}

Expand Down Expand Up @@ -337,16 +337,26 @@ struct SymbolTableAnalyzer {
}

impl SymbolTableAnalyzer {
fn analyze_symbol_table(&mut self, symbol_table: &mut SymbolTable) -> SymbolTableResult {
/// Analyze a symbol table and return the set of free variables.
/// See symtable.c analyze_block().
fn analyze_symbol_table(
&mut self,
symbol_table: &mut SymbolTable,
) -> SymbolTableResult<HashSet<String>> {
let symbols = core::mem::take(&mut symbol_table.symbols);
let sub_tables = &mut *symbol_table.sub_tables;

// Collect free variables from all child scopes
let mut newfree = HashSet::new();

let mut info = (symbols, symbol_table.typ);
self.tables.with_append(&mut info, |list| {
let inner_scope = unsafe { &mut *(list as *mut _ as *mut Self) };
// Analyze sub scopes:
// Analyze sub scopes and collect their free variables
for sub_table in sub_tables.iter_mut() {
inner_scope.analyze_symbol_table(sub_table)?;
let child_free = inner_scope.analyze_symbol_table(sub_table)?;
// Propagate child's free variables to this scope
newfree.extend(child_free);
}
Ok(())
})?;
Expand Down Expand Up @@ -384,17 +394,25 @@ impl SymbolTableAnalyzer {
}
}

// Analyze symbols:
// Analyze symbols in current scope
for symbol in symbol_table.symbols.values_mut() {
self.analyze_symbol(symbol, symbol_table.typ, sub_tables)?;

// Collect free variables from this scope
// These will be propagated to the parent scope
if symbol.scope == SymbolScope::Free || symbol.flags.contains(SymbolFlags::FREE_CLASS) {
newfree.insert(symbol.name.clone());
}
}

// Handle class-specific implicit cells (like CPython)
// Handle class-specific implicit cells
// This removes __class__ and __classdict__ from newfree if present
// and sets the corresponding flags on the symbol table
if symbol_table.typ == CompilerScope::Class {
drop_class_free(symbol_table);
drop_class_free(symbol_table, &mut newfree);
}

Ok(())
Ok(newfree)
}

fn analyze_symbol(
Expand Down Expand Up @@ -475,6 +493,14 @@ impl SymbolTableAnalyzer {
{
continue;
}

// __class__ is implicitly declared in class scope
// This handles the case where super() is called in a nested class method
if name == "__class__" && matches!(typ, CompilerScope::Class) {
decl_depth = Some(i);
break;
}

if let Some(sym) = symbols.get(name) {
match sym.scope {
SymbolScope::GlobalExplicit => return Some(SymbolScope::GlobalExplicit),
Expand Down
11 changes: 8 additions & 3 deletions crates/vm/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2532,16 +2532,21 @@ impl ExecutingFrame<'_> {
}

fn load_super_attr(&mut self, vm: &VirtualMachine, oparg: u32) -> FrameResult {
let (name_idx, load_method, _has_class) = bytecode::decode_load_super_attr_arg(oparg);
let (name_idx, load_method, has_class) = bytecode::decode_load_super_attr_arg(oparg);
let attr_name = self.code.names[name_idx as usize];

// Pop [super, class, self] from stack
let self_obj = self.pop_value();
let class = self.pop_value();
let global_super = self.pop_value();

// Create super(class, self) object
let super_obj = global_super.call((class.clone(), self_obj.clone()), vm)?;
// Create super object - pass args based on has_class flag
// When super is shadowed, has_class=false means call with 0 args
let super_obj = if has_class {
global_super.call((class.clone(), self_obj.clone()), vm)?
} else {
global_super.call((), vm)?
};

if load_method {
// Method load: push [method, self_or_null]
Expand Down