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
3 changes: 3 additions & 0 deletions Lib/_opcode_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@
'JUMP_IF_NOT_EXC_MATCH': 131,
'SET_EXC_INFO': 134,
'SUBSCRIPT': 135,
'LOAD_SUPER_METHOD': 136,
'LOAD_ZERO_SUPER_ATTR': 137,
'LOAD_ZERO_SUPER_METHOD': 138,
'RESUME': 149,
'JUMP': 252,
'LOAD_CLOSURE': 253,
Expand Down
221 changes: 209 additions & 12 deletions crates/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ pub enum FBlockDatum {
ExceptionName(String),
}

/// Type of super() call optimization detected by can_optimize_super_call()
#[derive(Debug, Clone)]
enum SuperCallType<'a> {
/// super(class, self) - explicit 2-argument form
TwoArg {
class_arg: &'a Expr,
self_arg: &'a Expr,
},
/// super() - implicit 0-argument form (uses __class__ cell)
ZeroArg,
}

#[derive(Debug, Clone)]
pub struct FBlockInfo {
pub fb_type: FBlockType,
Expand Down Expand Up @@ -661,6 +673,153 @@ impl Compiler {
self.symbol_table_stack.pop().expect("compiler bug")
}

/// Check if a super() call can be optimized
/// Returns Some(SuperCallType) if optimization is possible, None otherwise
fn can_optimize_super_call<'a>(
&self,
value: &'a Expr,
attr: &str,
) -> Option<SuperCallType<'a>> {
use ruff_python_ast::*;

// 1. value must be a Call expression
let Expr::Call(ExprCall {
func, arguments, ..
}) = value
else {
return None;
};

// 2. func must be Name("super")
let Expr::Name(ExprName { id, .. }) = func.as_ref() else {
return None;
};
if id.as_str() != "super" {
return None;
}

// 3. attr must not be "__class__"
if attr == "__class__" {
return None;
}

// 4. No keyword arguments
if !arguments.keywords.is_empty() {
return None;
}

// 5. Must be inside a function (not at module level or class body)
if !self.ctx.in_func() {
return None;
}

// 6. "super" must be GlobalImplicit (not redefined locally or at module level)
let table = self.current_symbol_table();
if let Some(symbol) = table.lookup("super")
&& symbol.scope != SymbolScope::GlobalImplicit
{
return None;
}
// Also check top-level scope to detect module-level shadowing.
// Only block if super is actually *bound* at module level (not just used).
if let Some(top_table) = self.symbol_table_stack.first()
&& let Some(sym) = top_table.lookup("super")
&& sym.scope != SymbolScope::GlobalImplicit
{
return None;
}

// 7. Check argument pattern
let args = &arguments.args;

// No starred expressions allowed
if args.iter().any(|arg| matches!(arg, Expr::Starred(_))) {
return None;
}

match args.len() {
2 => {
// 2-arg: super(class, self)
Some(SuperCallType::TwoArg {
class_arg: &args[0],
self_arg: &args[1],
})
}
0 => {
// 0-arg: super() - need __class__ cell and first parameter
// Enclosing function should have at least one positional argument
let info = self.code_stack.last()?;
if info.metadata.argcount == 0 && info.metadata.posonlyargcount == 0 {
return None;
}

// Check if __class__ is available as a cell/free variable
// The scope must be Free (from enclosing class) or have FREE_CLASS flag
if let Some(symbol) = table.lookup("__class__") {
if symbol.scope != SymbolScope::Free
&& !symbol.flags.contains(SymbolFlags::FREE_CLASS)
{
return None;
}
} else {
// __class__ not in symbol table, optimization not possible
return None;
}

Some(SuperCallType::ZeroArg)
}
_ => None, // 1 or 3+ args - not optimizable
}
}
Comment on lines +676 to +773
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:

# Search for load_args_for_super to verify Cell handling
rg -n "load_args_for_super" crates/codegen/src/compile.rs -A 30 -B 2

Repository: RustPython/RustPython

Length of output: 5185


🏁 Script executed:

# Search for SymbolScope and SymbolFlags definitions
rg -n "enum SymbolScope|SymbolScope::" crates/codegen/src -A 15 | head -100

Repository: RustPython/RustPython

Length of output: 7084


🏁 Script executed:

# Check how __class__ is handled throughout the codebase
rg -n "__class__" crates/codegen/src/compile.rs -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 2935


🏁 Script executed:

# Look for Cell scope usage in symbol handling
rg -n "SymbolScope::Cell" crates/codegen/src

Repository: RustPython/RustPython

Length of output: 1106


Super-call eligibility checks: align __class__ scope handling with load_args_for_super().

The 0-arg super() path rejects __class__ unless it's Free or has the FREE_CLASS flag (lines 759–760), but load_args_for_super() explicitly handles SymbolScope::Cell (line 795). This inconsistency may unnecessarily block valid optimizations when __class__ is scoped as Cell in the method.

Proposed fix
                 if let Some(symbol) = table.lookup("__class__") {
-                    if symbol.scope != SymbolScope::Free
-                        && !symbol.flags.contains(SymbolFlags::FREE_CLASS)
+                    if !matches!(symbol.scope, SymbolScope::Free | SymbolScope::Cell)
+                        && !symbol.flags.contains(SymbolFlags::FREE_CLASS)
                     {
                         return None;
                     }
🤖 Prompt for AI Agents
In @crates/codegen/src/compile.rs around lines 676 - 773,
can_optimize_super_call rejects 0-arg super() when __class__ is not
SymbolScope::Free or lacks FREE_CLASS, but load_args_for_super also accepts
SymbolScope::Cell; update can_optimize_super_call to treat SymbolScope::Cell as
acceptable (i.e., allow optimization when table.lookup("__class__") returns a
symbol whose scope is Free or Cell or whose flags contain FREE_CLASS) so
eligibility matches load_args_for_super; reference functions
can_optimize_super_call and load_args_for_super and the symbol/name "__class__"
and SymbolScope::Cell/FREE_CLASS.


/// Load arguments for super() optimization onto the stack
/// Stack result: [global_super, class, self]
fn load_args_for_super(&mut self, super_type: &SuperCallType<'_>) -> CompileResult<()> {
// 1. Load global super
self.compile_name("super", NameUsage::Load)?;

match super_type {
SuperCallType::TwoArg {
class_arg,
self_arg,
} => {
// 2-arg: load provided arguments
self.compile_expression(class_arg)?;
self.compile_expression(self_arg)?;
}
SuperCallType::ZeroArg => {
// 0-arg: load __class__ cell and first parameter
// Load __class__ from cell/free variable
let scope = self.get_ref_type("__class__").map_err(|e| self.error(e))?;
let idx = match scope {
SymbolScope::Cell => self.get_cell_var_index("__class__")?,
SymbolScope::Free => self.get_free_var_index("__class__")?,
_ => {
return Err(self.error(CodegenErrorType::SyntaxError(
"super(): __class__ cell not found".to_owned(),
)));
}
};
self.emit_arg(idx, Instruction::LoadDeref);

// Load first parameter (typically 'self').
// Safety: can_optimize_super_call() ensures argcount > 0, and
// parameters are always added to varnames first (see symboltable.rs).
let first_param = {
let info = self.code_stack.last().unwrap();
info.metadata.varnames.first().cloned()
};
let first_param = first_param.ok_or_else(|| {
self.error(CodegenErrorType::SyntaxError(
"super(): no arguments and no first parameter".to_owned(),
))
})?;
self.compile_name(&first_param, NameUsage::Load)?;
}
}
Ok(())
}

/// Check if this is an inlined comprehension context (PEP 709)
/// Currently disabled - always returns false to avoid stack issues
fn is_inlined_comprehension_context(&self, _comprehension_type: ComprehensionType) -> bool {
Expand Down Expand Up @@ -3357,12 +3516,14 @@ impl Compiler {
/// Determines if a variable should be CELL or FREE type
// = get_ref_type
fn get_ref_type(&self, name: &str) -> Result<SymbolScope, CodegenErrorType> {
let table = self.symbol_table_stack.last().unwrap();

// Special handling for __class__ and __classdict__ in class scope
if self.ctx.in_class && (name == "__class__" || name == "__classdict__") {
// This should only apply when we're actually IN a class body,
// not when we're in a method nested inside a class.
if table.typ == CompilerScope::Class && (name == "__class__" || name == "__classdict__") {
return Ok(SymbolScope::Cell);
}

let table = self.symbol_table_stack.last().unwrap();
match table.lookup(name) {
Some(symbol) => match symbol.scope {
SymbolScope::Cell => Ok(SymbolScope::Cell),
Expand Down Expand Up @@ -5732,9 +5893,28 @@ impl Compiler {
};
}
Expr::Attribute(ExprAttribute { value, attr, .. }) => {
self.compile_expression(value)?;
let idx = self.name(attr.as_str());
emit!(self, Instruction::LoadAttr { idx });
// Check for super() attribute access optimization
if let Some(super_type) = self.can_optimize_super_call(value, attr.as_str()) {
// super().attr or super(cls, self).attr optimization
// Stack: [global_super, class, self] → LOAD_SUPER_ATTR → [attr]
self.load_args_for_super(&super_type)?;
let idx = self.name(attr.as_str());
match super_type {
SuperCallType::TwoArg { .. } => {
// LoadSuperAttr (pseudo) - will be converted to real LoadSuperAttr
// with flags=0b10 (has_class=true, load_method=false) in ir.rs
emit!(self, Instruction::LoadSuperAttr { arg: idx });
}
SuperCallType::ZeroArg => {
emit!(self, Instruction::LoadZeroSuperAttr { idx });
}
}
} else {
// Normal attribute access
self.compile_expression(value)?;
let idx = self.name(attr.as_str());
emit!(self, Instruction::LoadAttr { idx });
}
}
Expr::Compare(ExprCompare {
left,
Expand Down Expand Up @@ -6159,12 +6339,29 @@ impl Compiler {
// Method call: obj → LOAD_ATTR_METHOD → [method, self_or_null] → args → CALL
// Regular call: func → PUSH_NULL → args → CALL
if let Expr::Attribute(ExprAttribute { value, attr, .. }) = &func {
// Method call: compile object, then LOAD_ATTR_METHOD
// LOAD_ATTR_METHOD pushes [method, self_or_null] on stack
self.compile_expression(value)?;
let idx = self.name(attr.as_str());
emit!(self, Instruction::LoadAttrMethod { idx });
self.compile_call_helper(0, args)?;
// Check for super() method call optimization
if let Some(super_type) = self.can_optimize_super_call(value, attr.as_str()) {
// super().method() or super(cls, self).method() optimization
// Stack: [global_super, class, self] → LOAD_SUPER_METHOD → [method, self]
self.load_args_for_super(&super_type)?;
let idx = self.name(attr.as_str());
match super_type {
SuperCallType::TwoArg { .. } => {
emit!(self, Instruction::LoadSuperMethod { idx });
}
SuperCallType::ZeroArg => {
emit!(self, Instruction::LoadZeroSuperMethod { idx });
}
}
self.compile_call_helper(0, args)?;
} else {
// Normal method call: compile object, then LOAD_ATTR_METHOD
// LOAD_ATTR_METHOD pushes [method, self_or_null] on stack
self.compile_expression(value)?;
let idx = self.name(attr.as_str());
emit!(self, Instruction::LoadAttrMethod { idx });
self.compile_call_helper(0, args)?;
}
} else {
// Regular call: push func, then NULL for self_or_null slot
// Stack layout: [func, NULL, args...] - same as method call [func, self, args...]
Expand Down
26 changes: 25 additions & 1 deletion crates/codegen/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rustpython_compiler_core::{
bytecode::{
Arg, CodeFlags, CodeObject, CodeUnit, CodeUnits, ConstantData, ExceptionTableEntry,
InstrDisplayContext, Instruction, Label, OpArg, PyCodeLocationInfoKind,
encode_exception_table, encode_load_attr_arg,
encode_exception_table, encode_load_attr_arg, encode_load_super_attr_arg,
},
varint::{write_signed_varint, write_varint},
};
Expand Down Expand Up @@ -212,6 +212,30 @@ impl CodeInfo {
Instruction::PopBlock => {
info.instr = Instruction::Nop;
}
// LOAD_SUPER_METHOD pseudo → LOAD_SUPER_ATTR (flags=0b11: method=1, class=1)
Instruction::LoadSuperMethod { idx } => {
let encoded = encode_load_super_attr_arg(idx.get(info.arg), true, true);
info.arg = OpArg(encoded);
info.instr = Instruction::LoadSuperAttr { arg: Arg::marker() };
}
// LOAD_ZERO_SUPER_ATTR pseudo → LOAD_SUPER_ATTR (flags=0b00: method=0, class=0)
Instruction::LoadZeroSuperAttr { idx } => {
let encoded = encode_load_super_attr_arg(idx.get(info.arg), false, false);
info.arg = OpArg(encoded);
info.instr = Instruction::LoadSuperAttr { arg: Arg::marker() };
}
// LOAD_ZERO_SUPER_METHOD pseudo → LOAD_SUPER_ATTR (flags=0b01: method=1, class=0)
Instruction::LoadZeroSuperMethod { idx } => {
let encoded = encode_load_super_attr_arg(idx.get(info.arg), true, false);
info.arg = OpArg(encoded);
info.instr = Instruction::LoadSuperAttr { arg: Arg::marker() };
}
// LOAD_SUPER_ATTR → encode with flags=0b10 (method=0, class=1)
Instruction::LoadSuperAttr { arg: idx } => {
let encoded = encode_load_super_attr_arg(idx.get(info.arg), false, true);
info.arg = OpArg(encoded);
info.instr = Instruction::LoadSuperAttr { arg: Arg::marker() };
}
_ => {}
}
}
Expand Down
Loading
Loading