diff --git a/Lib/_opcode_metadata.py b/Lib/_opcode_metadata.py index 40cd95cca0c..560523bfab2 100644 --- a/Lib/_opcode_metadata.py +++ b/Lib/_opcode_metadata.py @@ -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, diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index a04f5ad0dfa..b7bc3f4d9c7 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -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, @@ -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> { + 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 + } + } + + /// 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 { @@ -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 { + 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), @@ -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, @@ -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...] diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 298566d7ad7..82b9921911c 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -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}, }; @@ -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() }; + } _ => {} } } diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index 47a6ccbbf30..22b5bf358a1 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -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)] @@ -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) { + // 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 } } @@ -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> { 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(()) })?; @@ -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( @@ -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), diff --git a/crates/compiler-core/src/bytecode.rs b/crates/compiler-core/src/bytecode.rs index 61707b7757e..5270b89f3ad 100644 --- a/crates/compiler-core/src/bytecode.rs +++ b/crates/compiler-core/src/bytecode.rs @@ -102,6 +102,21 @@ pub const fn decode_load_attr_arg(oparg: u32) -> (u32, bool) { (name_idx, is_method) } +/// Encode LOAD_SUPER_ATTR oparg: bit 0 = load_method, bit 1 = has_class, bits 2+ = name index. +#[inline] +pub const fn encode_load_super_attr_arg(name_idx: u32, load_method: bool, has_class: bool) -> u32 { + (name_idx << 2) | ((has_class as u32) << 1) | (load_method as u32) +} + +/// Decode LOAD_SUPER_ATTR oparg: returns (name_idx, load_method, has_class). +#[inline] +pub const fn decode_load_super_attr_arg(oparg: u32) -> (u32, bool, bool) { + let load_method = (oparg & 1) == 1; + let has_class = (oparg & 2) == 2; + let name_idx = oparg >> 2; + (name_idx, load_method, has_class) +} + /// Oparg values for [`Instruction::ConvertValue`]. /// /// ## See also diff --git a/crates/compiler-core/src/bytecode/instruction.rs b/crates/compiler-core/src/bytecode/instruction.rs index 24cccbb3ed2..c41da105a3d 100644 --- a/crates/compiler-core/src/bytecode/instruction.rs +++ b/crates/compiler-core/src/bytecode/instruction.rs @@ -6,6 +6,7 @@ use crate::{ Arg, BinaryOperator, BorrowedConstant, BuildSliceArgCount, ComparisonOperator, Constant, ConvertValueOparg, InstrDisplayContext, IntrinsicFunction1, IntrinsicFunction2, Invert, Label, MakeFunctionFlags, NameIdx, OpArg, RaiseKind, UnpackExArgs, decode_load_attr_arg, + decode_load_super_attr_arg, }, marshal::MarshalError, }; @@ -175,7 +176,7 @@ pub enum Instruction { LoadName(Arg) = 92, LoadSuperAttr { arg: Arg, - } = 93, // Placeholder + } = 93, MakeCell(Arg) = 94, // Placeholder MapAdd { i: Arg, @@ -243,6 +244,20 @@ pub enum Instruction { Resume { arg: Arg, } = 149, + // ===== LOAD_SUPER_* Pseudo Opcodes (136-138) ===== + // These are converted to LoadSuperAttr during bytecode finalization. + // "Zero" variants are for 0-arg super() calls (has_class=false). + // Non-"Zero" variants are for 2-arg super(cls, self) calls (has_class=true). + /// 2-arg super(cls, self).method() - has_class=true, load_method=true + LoadSuperMethod { + idx: Arg, + } = 136, // CPython uses pseudo-op 260 + LoadZeroSuperAttr { + idx: Arg, + } = 137, // CPython uses pseudo-op 261 + LoadZeroSuperMethod { + idx: Arg, + } = 138, // CPython uses pseudo-op 262 // ==================== RustPython-only instructions (119-135) ==================== // Ideally, we want to be fully aligned with CPython opcodes, but we still have some leftovers. // So we assign random IDs to these opcodes. @@ -338,6 +353,10 @@ impl TryFrom for Instruction { u8::from(Self::JumpIfNotExcMatch(Arg::marker())), u8::from(Self::SetExcInfo), u8::from(Self::Subscript), + // LOAD_SUPER_* pseudo opcodes (136-138) + u8::from(Self::LoadSuperMethod { idx: Arg::marker() }), + u8::from(Self::LoadZeroSuperAttr { idx: Arg::marker() }), + u8::from(Self::LoadZeroSuperMethod { idx: Arg::marker() }), ]; // Pseudo opcodes (252-255) @@ -588,6 +607,16 @@ impl Instruction { Self::UnaryNot => 0, Self::GetYieldFromIter => 0, Self::PushNull => 1, // Push NULL for call protocol + // LoadSuperAttr: pop [super, class, self], push [attr] or [method, self_or_null] + // stack_effect depends on load_method flag (bit 0 of oparg) + Self::LoadSuperAttr { arg: idx } => { + let (_, load_method, _) = decode_load_super_attr_arg(idx.get(arg)); + if load_method { -3 + 2 } else { -3 + 1 } + } + // Pseudo instructions (calculated before conversion) + Self::LoadSuperMethod { .. } => -3 + 2, // pop 3, push [method, self_or_null] + Self::LoadZeroSuperAttr { .. } => -3 + 1, // pop 3, push [attr] + Self::LoadZeroSuperMethod { .. } => -3 + 2, // pop 3, push [method, self_or_null] Self::Cache => 0, Self::BinarySlice => 0, Self::BinaryOpInplaceAddUnicode => 0, @@ -611,7 +640,6 @@ impl Instruction { Self::LoadFromDictOrGlobals(_) => 0, Self::SetUpdate { .. } => 0, Self::MakeCell(_) => 0, - Self::LoadSuperAttr { .. } => 0, Self::StoreFastStoreFast { .. } => 0, Self::PopJumpIfNone { .. } => 0, Self::PopJumpIfNotNone { .. } => 0, @@ -768,6 +796,19 @@ impl Instruction { Self::LoadFastAndClear(idx) => w!(LOAD_FAST_AND_CLEAR, varname = idx), Self::LoadGlobal(idx) => w!(LOAD_GLOBAL, name = idx), Self::LoadName(idx) => w!(LOAD_NAME, name = idx), + Self::LoadSuperAttr { arg: idx } => { + let encoded = idx.get(arg); + let (name_idx, load_method, has_class) = decode_load_super_attr_arg(encoded); + let attr_name = name(name_idx); + write!( + f, + "{:pad$}({}, {}, method={}, class={})", + "LOAD_SUPER_ATTR", encoded, attr_name, load_method, has_class + ) + } + Self::LoadSuperMethod { idx } => w!(LOAD_SUPER_METHOD, name = idx), + Self::LoadZeroSuperAttr { idx } => w!(LOAD_ZERO_SUPER_ATTR, name = idx), + Self::LoadZeroSuperMethod { idx } => w!(LOAD_ZERO_SUPER_METHOD, name = idx), Self::MakeFunction => w!(MAKE_FUNCTION), Self::MapAdd { i } => w!(MAP_ADD, i), Self::MatchClass(arg) => w!(MATCH_CLASS, arg), diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index f8e343b1915..5fb44d8a58b 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -1104,6 +1104,14 @@ impl ExecutingFrame<'_> { bytecode::Instruction::LoadAttrMethod { .. } => { unreachable!("LoadAttrMethod is converted to LoadAttr during compilation") } + bytecode::Instruction::LoadSuperAttr { arg: idx } => { + self.load_super_attr(vm, idx.get(arg)) + } + bytecode::Instruction::LoadSuperMethod { .. } + | bytecode::Instruction::LoadZeroSuperAttr { .. } + | bytecode::Instruction::LoadZeroSuperMethod { .. } => { + unreachable!("LOAD_SUPER_* pseudo instructions are converted during compilation") + } bytecode::Instruction::LoadBuildClass => { self.push_value(vm.builtins.get_attr(identifier!(vm, __build_class__), vm)?); Ok(None) @@ -2523,6 +2531,45 @@ impl ExecutingFrame<'_> { Ok(None) } + 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 attr_name = self.code.names[name_idx as usize]; + + // Stack layout (bottom to top): [super, class, self] + // Pop in LIFO order: self, class, super + let self_obj = self.pop_value(); + let class = self.pop_value(); + let global_super = self.pop_value(); + + // 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] + let method = PyMethod::get(super_obj, attr_name, vm)?; + match method { + PyMethod::Function { target: _, func } => { + self.push_value(func); + self.push_value(self_obj); + } + PyMethod::Attribute(val) => { + self.push_value(val); + self.push_null(); + } + } + } else { + // Regular attribute access + let obj = super_obj.get_attr(attr_name, vm)?; + self.push_value(obj); + } + Ok(None) + } + fn store_attr(&mut self, vm: &VirtualMachine, attr: bytecode::NameIdx) -> FrameResult { let attr_name = self.code.names[attr as usize]; let parent = self.pop_value();