Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Jan 8, 2026

Summary by CodeRabbit

  • Refactor

    • Centralized all function/method call emission into a single unified call flow.
    • VM stack and execution converted to NULL-aware slots with explicit NULL insertions in call/unwind paths.
    • JIT updated to represent NULL and follow the new NULL-aware call protocol.
    • Method resolution refined to correctly handle classmethod descriptors.
  • New Features

    • Bytecode/disassembly updated to expose a new call/attribute-call protocol and a PushNull instruction.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Consolidate call/method emission and introduce NULL-aware self_or_null semantics: replace legacy CallMethod/CallFunction* and LoadMethod with Call/CallKw/CallFunctionEx, LoadAttrMethod, and PushNull; centralize call emission via compile_call_helper; adapt bytecode, codegen, JIT, VM/frame stack (Option slots), and method resolution.

Changes

Cohort / File(s) Summary
Bytecode definitions
crates/compiler-core/src/bytecode.rs
Add Call { nargs }, CallKw { nargs }, CallFunctionEx { has_kwargs }, LoadAttrMethod { idx }; remove LoadMethod and CallMethod* variants; adjust opcode mapping, stack effects, disassembly text, and reserve freed opcode slots.
Compiler codegen
crates/codegen/src/compile.rs
Introduce compile_call_helper; replace distributed call-emission paths with centralized helper; rename LoadMethodLoadAttrMethod; insert PushNull where call protocol requires (decorators, class/type-params, with/finally/unwind flows).
VM frame & execution
crates/vm/src/frame.rs
Change FrameState.stack to BoxVec<Option<PyObjectRef>>; add push_null/push_value_opt/pop_value_opt/expect_unchecked; implement LoadAttrMethod/PushNull semantics; unify method vs function invocation by handling optional self.
JIT support
crates/jit/src/instructions.rs
Add JitValue::Null; add Instruction::PushNull; rename CallFunctionPositionalCall { nargs }; call handling now pops/validates self_or_null (Null treated as not-representable for JIT).
Stdlib opcode utilities
crates/stdlib/src/opcode.rs
Replace LoadMethod checks with LoadAttrMethod in opcode predicates (e.g., has_name).
Method resolution
crates/vm/src/vm/method.rs
For PyMethodDescriptor with CLASS flag, call descr_get to convert instance→class (handle classmethod descriptors) instead of always treating method descriptors as bound methods.
Public API / manifests
.../Cargo.toml, bytecode/public maps
Update public instruction variants and textual mappings; expose new variants, reserve removed slots, and update LAST_INSTRUCTION/wiring and manifest lines.

Sequence Diagram(s)

sequenceDiagram
    participant Compiler as Compiler (codegen)
    participant Bytecode as Bytecode (instructions)
    participant VM as VM (frame/exec)
    participant Callable as Callable/Method

    Compiler->>Bytecode: emit PushNull, LoadAttrMethod(attr), Call/CallKw/CallFunctionEx
    Note over VM: Stack before call → [callable, NULL|self, args..., (kw_names?)]
    Bytecode->>VM: execute LoadAttrMethod -> push method and self_or_null
    VM->>VM: execute Call* -> pop callable, self_or_null, args
    alt self_or_null is NULL
        VM->>Callable: invoke callable(args)
    else self_or_null is Some(self)
        VM->>Callable: invoke callable(self, args)
    end
    Callable-->>VM: return value
    VM->>VM: push return value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ShaharNaveh
  • arihant2math

Poem

🐰 I tuck a NULL beneath my paw so light,
I hop through bytecode to make bound calls right.
PushNull hums softly, LoadAttrMethod leads the way,
Self slips in before args and we merrily play.
Hooray — the VM hops forward, bright as day!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely summarizes the main change: replacing CallMethod/LoadMethod opcodes with plain Call variants, which aligns with the core refactoring across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@youknowone youknowone force-pushed the call branch 2 times, most recently from 46de323 to 5a5c7be Compare January 8, 2026 21:34
@youknowone youknowone marked this pull request as ready for review January 9, 2026 00:29
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codegen/src/compile.rs (1)

3712-3763: Stack layout mismatch: CallWithSelfKw expects tuple of keyword names on top, but code pushes individual name/value pairs without accounting for the extra items.

The instruction expects stack [callable, self_or_null, args..., kwarg_names] where kwarg_names is a single tuple. Currently, the code pushes one string (name) + one value for each keyword (2 items per keyword), but only increments nargs by keywords.len(). This creates a discrepancy: the VM will try to pop nargs items expecting aligned positional arguments and keyword values, but the keyword names aren't consolidated into a tuple.

Follow compile_keywords' pattern or the proposed fix: build a tuple of keyword names to place on top of the stack as a single item, ensuring the stack accounting matches CallWithSelfKw's contract.

🧹 Nitpick comments (3)
crates/jit/src/instructions.rs (1)

480-505: Potential panic in argument processing.

Line 486 uses arg.into_value().unwrap() which will panic if the argument is JitValue::None, JitValue::Null, JitValue::Tuple, or JitValue::FuncRef. This mirrors the existing pattern in CallFunctionPositional (line 466), so it's consistent with the codebase, but both locations have this limitation.

Consider returning JitCompileError::NotSupported instead of panicking when an argument cannot be converted to a value:

♻️ Suggested improvement
             let mut args = Vec::new();
             for _ in 0..nargs {
                 let arg = self.stack.pop().ok_or(JitCompileError::BadBytecode)?;
-                args.push(arg.into_value().unwrap());
+                args.push(arg.into_value().ok_or(JitCompileError::NotSupported)?);
             }
crates/vm/src/frame.rs (2)

2180-2202: LGTM: execute_call_with_self correctly implements the self_or_null protocol.

The implementation properly handles both cases:

  • When self_or_null is Some(self_val) (method call): prepends self to arguments
  • When self_or_null is None (NULL, regular call): uses arguments as-is

The logic is clear and correct.

♻️ Optional optimization: pre-allocate capacity for method calls
         let final_args = if let Some(self_val) = self_or_null {
             // Method call: prepend self to args
-            let mut all_args = vec![self_val];
+            let mut all_args = Vec::with_capacity(1 + args.args.len());
+            all_args.push(self_val);
             all_args.extend(args.args);

This avoids a potential reallocation when extending the vector.


2398-2401: The stack manipulation is correct but somewhat complex.

The code swaps the top two stack elements and ensures attr_value is not NULL:

  1. Pop func (Option) from top
  2. Replace current top with func, getting the old top as attr_value
  3. Expect attr_value is not NULL

The net effect is: [..., attr_value, func] → [..., func] with attr_value extracted.

While correct, this could be clearer with a comment explaining the swap operation.

💡 Optional: Add explanatory comment
+        // Swap top two elements: pop func, replace TOS with func, extract attr_value
         let func = self.pop_value_opt();
         let attr_value = self
             .replace_top(func)
             .expect("replace_top encountered NULL");
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3425b4 and d409baf.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_xml_etree.py is excluded by !Lib/**
  • crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/vm/method.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/vm/method.rs
  • crates/stdlib/src/opcode.rs
  • crates/jit/src/instructions.rs
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/vm/src/frame.rs
🔇 Additional comments (28)
crates/stdlib/src/opcode.rs (1)

76-76: LGTM!

The addition of Instruction::LoadAttrMethod { .. } to has_name is correct since this instruction uses co_names to look up the attribute name, consistent with LoadAttr and other name-using instructions.

crates/vm/src/vm/method.rs (1)

41-49: LGTM - Classmethod descriptor handling.

The logic correctly differentiates classmethods from regular methods: classmethods need descr_get to properly bind the class (not the instance), while regular method descriptors can use the fast path with is_method = true.

crates/jit/src/instructions.rs (2)

506-509: LGTM!

The PushNull instruction correctly pushes JitValue::Null onto the stack to support the self-or-null calling convention.


30-30: LGTM - JitValue::Null variant additions.

The Null variant is consistently integrated across all relevant methods:

  • to_jit_type() correctly returns None
  • into_value() correctly returns None
  • boolean_val() correctly returns NotSupported

Also applies to: 49-49, 56-56, 143-145

crates/compiler-core/src/bytecode.rs (4)

1084-1101: LGTM - New self-aware call instructions.

The new instructions (CallWithSelf, LoadAttrMethod, CallWithSelfKw, CallWithSelfEx) are well-documented with clear stack layout comments. The design aligns with the self-or-null calling convention for method dispatch optimization.


1946-1948: Stack effect for LoadAttrMethod is correct.

LoadAttrMethod pops the object and pushes both the method and self_or_null, resulting in a net stack effect of +1.


1982-1987: Stack effects for CallWithSelf instructions are correct.*

The calculations properly account for:

  • CallWithSelf: pops nargs + self_or_null + callable, pushes result
  • CallWithSelfKw: additionally pops kwargs_names tuple
  • CallWithSelfEx: pops args_tuple + optional kwargs_dict + self_or_null + callable

1046-1049: Reserved instruction slots maintain opcode numbering.

The reserved slots (Reserved125, Reserved126, Reserved127, Reserved135) correctly preserve opcode numbering for future use or compatibility.

Also applies to: 1072-1073

crates/codegen/src/compile.rs (10)

1026-1054: Good: with/async with cleanup now matches self_or_null stack shape.

Pushing NULL before calling __exit__ makes the call path consistent with CallWithSelf { nargs: 3 }.


1876-1887: Good: assert now calls AssertionError(...) with an explicit NULL receiver slot.

This aligns LoadGlobal(AssertionError) + PushNull + CallWithSelf { nargs: 0/1 } with the new call convention.


2096-2110: Decorator application stack contract looks correct (and preserves reverse-application order).

prepare_decorators() pushing [decorator, NULL] and apply_decorators() doing CallWithSelf { nargs: 1 } should consume [decorator, NULL, func] repeatedly, applying decorators inside-out as expected.


2147-2152: Good: type-param bound/default closure is invoked via PushNull + CallWithSelf.

Keeps the “immediate invoke” path consistent with the new calling convention.


3775-3789: Good: non-generic class creation path now consistently uses PushNull + compile_call_with_self.

Nice reuse of the shared call dispatcher.


3934-3943: Good: with normal-exit __exit__(None, None, None) call updated for self_or_null.


6113-6144: Call-site refactor looks coherent; please confirm LoadAttrMethod stack contract matches CallWithSelf*.

The split is clean:

  • method call: value then LoadAttrMethod then CallWithSelf*
  • non-method call: func then PushNull then CallWithSelf*

Main thing to verify is that LoadAttrMethod always leaves the stack in the [callable, self_or_null] shape expected by CallWithSelf* (especially in edge cases like descriptors / fallback-to-attribute).


6429-6450: Good: comprehension invocation now supplies the NULL receiver slot before passing the iterator arg.


6858-6874: Good: break/continue unwinding through with now calls __exit__ via CallWithSelf with an explicit NULL.


3324-3347: CallFunctionPositional is valid and correctly used; no blocker here.

CallFunctionPositional remains an active, non-deprecated opcode. The stack rearrangement via Swap to produce [closure, arg1, ..., argN] is correct for this instruction, which expects no self_or_null slot. The comment "internal call where self_or_null doesn't apply" accurately describes this design choice: the closure is called as a direct function, not a method, so the self_or_null slot is intentionally avoided for the >0 args case. The 0-arg case using PushNull + CallWithSelf { nargs: 0 } is also valid and will execute correctly. Both paths will compile and run without stack-shape issues.

crates/vm/src/frame.rs (10)

56-57: LGTM: Stack now supports NULL slots for the new calling convention.

The change from BoxVec<PyObjectRef> to BoxVec<Option<PyObjectRef>> enables NULL entries on the stack, which is essential for the self_or_null method call pattern. The comment clearly documents this purpose.

Note: This adds a small memory overhead (one discriminant byte per stack slot), but it's necessary for the new calling semantics.


721-735: LGTM: New CallWithSelf instructions implement the self_or_null calling convention.*

The three instruction variants (CallWithSelf, CallWithSelfKw, CallWithSelfEx) correctly delegate to execute_call_with_self and mirror the structure of the existing Call* instructions. The stack layout comments clearly document the expected state: [callable, self_or_null, arg1, ..., argN].


792-794: LGTM: CopyItem correctly handles Option-wrapped stack slots.

The use of push_value_opt preserves NULL slots during copy operations, which is consistent with the new stack model.


1450-1454: LGTM: PushNull instruction is simple and well-documented.

The instruction correctly pushes NULL onto the stack for the self_or_null calling convention.


1809-1813: LGTM: New reserved instruction opcodes.

The new reserved opcodes (125, 126, 127, 135) are appropriately handled with unreachable panics, consistent with other placeholder instructions.


2570-2589: LGTM: load_attr_method correctly sets up the stack for the self_or_null calling convention.

The implementation properly distinguishes between method descriptors and regular attributes:

  • Method descriptor (PyMethod::Function): Pushes [func, self] - the self argument will be prepended in execute_call_with_self
  • Regular attribute (PyMethod::Attribute): Pushes [attr, NULL] - NULL indicates no self argument

This correctly implements the calling convention where the stack layout is [callable, self_or_null, ...] before CallWithSelf* instructions.


2774-2801: pop_multiple uses the same unreachable_unchecked pattern as pop_value.

The implementation is consistent with pop_value() - it uses unreachable_unchecked() when encountering NULL slots. The same safety considerations apply here as discussed in the pop_value() review comment.

The comment on line 2774 clearly documents that this panics on NULL, which is correct for its use cases (instructions like BuildString, BuildTuple, etc. that expect non-NULL arguments).


2804-2828: LGTM: Stack accessor methods correctly handle NULL slots.

The methods properly handle the Option-wrapped stack:

  • replace_top: Swaps with Option<PyObjectRef>, enabling NULL swaps
  • top_value and nth_value: Return &PyObject and panic with clear messages on NULL

The panics in top_value and nth_value are more debuggable than unreachable_unchecked, which is good for troubleshooting compiler bugs.


2842-2855: LGTM: Debug formatting correctly displays NULL slots.

The updated Debug implementation properly handles the new Option-wrapped stack, displaying NULL slots as "> NULL", which aids in debugging the self_or_null calling convention.


2639-2647: Consider the debuggability implications of unreachable_unchecked() in pop_value().

The pop_value() method assumes the bytecode compiler never generates code that pops from a NULL stack slot. When this assumption is violated (e.g., due to a compiler bug), using unreachable_unchecked() converts what would be a detectable panic into undefined behavior.

Trade-offs:

  • ✅ Performance: Eliminates runtime check in a frequently-called path
  • ❌ Debuggability: Compiler bugs become silent UB instead of panics

NULL values are legitimately used in the stack (e.g., push_null() for missing self in method calls), so this optimization assumes strict separation between slots where NULL is valid vs. where it must be non-NULL. If the bytecode compiler incorrectly generates a pop_value() call on a NULL slot, the result is undefined behavior rather than a catchable error.

Alternatives to consider:

  • Use panic!() for better runtime detection of compiler bugs
  • Add a #[cfg(debug_assertions)] branch that panics while keeping the optimization in release builds
  • Add validation tests in the bytecode compiler to prevent NULL pops in non-NULL contexts

@youknowone youknowone changed the title implementing call the correct way Replace CallFunction/LoadMethod opcodes with CallWithSelf Jan 9, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/stdlib/src/opcode.rs (1)

48-52: has_arg() has an off-by-one error and excludes opcode 44.
With HAVE_ARGUMENT = 44, the condition should be opcode >= Self::HAVE_ARGUMENT to match CPython semantics, where opcodes >= 44 require arguments. The current opcode > Self::HAVE_ARGUMENT incorrectly treats opcode 44 as having no argument.

Proposed fix
-        pub fn has_arg(opcode: i32) -> bool {
-            Self::is_valid(opcode) && opcode > Self::HAVE_ARGUMENT
-        }
+        pub fn has_arg(opcode: i32) -> bool {
+            Self::is_valid(opcode) && opcode >= Self::HAVE_ARGUMENT
+        }
crates/jit/src/instructions.rs (1)

460-485: Fix JIT CALL instruction: unwrap() panic and reversed argument order

Two critical bugs in the CALL handler:

  1. Panic on non-Value arguments: into_value().unwrap() panics if the argument is None, Null, Tuple, or FuncRef instead of returning a proper error. Use ok_or(JitCompileError::NotSupported)? instead.

  2. Arguments passed in reversed order: Popping from the stack in a loop builds arguments as [argN, ..., arg1] instead of [arg1, ..., argN]. Call the Cranelift IR instruction with correct argument ordering by adding args.reverse().

The proposed fix addresses both issues:

  • Replace .unwrap() with proper error handling
  • Use Vec::with_capacity() for efficiency
  • Add args.reverse() to restore correct order
crates/codegen/src/compile.rs (1)

3717-3785: Critical: generic class call path panics on **kwargs and rejects starred bases (*bases).

  • panic!("keyword argument name must be set") will crash the compiler on valid class C(**kw): ....
  • Compiling bases via self.compile_expression(arg)?; will error on Expr::Starred (since Expr::Starred is rejected outside annotation context), so valid class C(*bases): ... breaks for generic classes only.

This likely needs to share the non-generic compile_call_inner() machinery (or equivalent) so generic and non-generic classdefs accept the same Python syntax surface.

🧹 Nitpick comments (5)
crates/compiler-core/src/bytecode.rs (2)

1899-1977: Stack-effect math looks correct; consider making bool→i32 conversions more readable.
Call, CallKw, and CallFunctionEx net effects match the documented stack layouts. For readability, you could replace (has_kwargs.get(arg) as i32) with i32::from(has_kwargs.get(arg)).


2141-2252: Disassembly output for RESERVED opcodes is inconsistent (minor).
Some reserved variants go through w!(RESERVED) while others print a raw "RESERVED" without padding. Consider routing all reserved variants through the same formatting path for stable disassembly output.

Proposed tweak (unify reserved formatting)
-            Reserved125 | Reserved126 | Reserved127 | Reserved135 => {
-                write!(f, "RESERVED")
-            }
+            Reserved125 | Reserved126 | Reserved127 | Reserved135 => w!(RESERVED),
crates/vm/src/frame.rs (2)

2371-2399: SET_FUNCTION_ATTRIBUTE: NULL-safety is a bit indirect; consider failing fast on NULL func.
Right now, a NULL func would be placed back on stack and later trigger top_value() fatal. Consider unwrapping func immediately (compiler bug) to keep the failure localized.

Possible tightening
-        let func = self.pop_value_opt();
+        let func = Some(self.pop_value());
         let attr_value = self
             .replace_top(func)
             .expect("replace_top encountered NULL");

1097-1101: Use the target value returned by descriptor resolution instead of discarding it.

In load_attr_method, the PyMethod::Function pattern destructures target but discards it with _, then pushes parent instead. This pattern should use the target value that descriptor resolution explicitly returned, which is both more semantically clear and follows Rust idioms of not discarding values unnecessarily.

Suggested fix
             PyMethod::Function { target: _, func } => {
-                // Method descriptor found: push [method, self]
+                // Descriptor resolved: push [callable, self_or_null] where self_or_null is `target`
                 self.push_value(func);
-                self.push_value(parent); // self
+                self.push_value(target);
             }
crates/codegen/src/compile.rs (1)

3315-3352: Type-params scope call: stack shuffling is plausible but fragile—consider encapsulating.
This Reverse/Swap/Reverse sequence is easy to regress; a small helper (e.g., “emit_call_with_null_and_n_args(n)”) would make invariants clearer.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d409baf and 3d61ff1.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_xml_etree.py is excluded by !Lib/**
  • crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/vm/method.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/vm/src/vm/method.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/codegen/src/compile.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
🔇 Additional comments (21)
crates/compiler-core/src/bytecode.rs (1)

802-821: Good: opcode-level self_or_null calling convention is explicit and consistent with the new LOAD_ATTR_METHOD.
The stack layout comments for Call, CallKw, CallFunctionEx, and LoadAttrMethod line up with the new protocol and make downstream implementations easier to reason about.

Also applies to: 1046-1092

crates/stdlib/src/opcode.rs (1)

64-83: Correct update: has_name() now includes LoadAttrMethod.
This keeps the stdlib opcode module aligned with the bytecode refactor away from LoadMethod.

crates/jit/src/instructions.rs (2)

24-59: Nice: explicit JitValue::Null keeps JIT stack model aligned with VM self_or_null semantics.
The “Null is non-materializable” handling in to_jit_type, into_value, and boolean_val is consistent.

Also applies to: 129-147


486-489: PushNull support in the JIT is the right primitive for this calling convention.

crates/vm/src/frame.rs (7)

52-60: Good: Option-wrapped stack + push_null/pop_value_opt cleanly model NULL-aware bytecode.
This is a coherent mechanical refactor and the helper APIs make the new invariant obvious.

Also applies to: 2590-2627


708-722: Call protocol wiring looks right: args collected first, then self_or_null, then callable.
execute_call() prepending self when present matches the new stack contract for LOAD_ATTR_METHOD + CALL*.

Also applies to: 1437-1441, 2162-2184


677-686: Verify BUILD_STRING’s Wtf8Buf collection compiles + is intended.
let s: Wtf8Buf = parts.iter().map(|pyobj| pyobj.downcast_ref::<PyStr>().unwrap()).collect(); depends on a FromIterator<&PyStr> (or compatible) impl. If it doesn’t exist, you’ll need to explicitly concatenate .as_wtf8()/.as_str() into a buffer.


1900-1957: Good: exception unwinding now discards NULL slots safely.
Popping to entry.depth without assuming Some(_) is exactly what the NULL-aware stack needs.


2812-2838: Debug output improvement is solid (NULL slots are visible).
This will help a lot when tracking stack-shape issues during the opcode transition.


2753-2774: Verify compiler invariants ensure stack slots are never NULL when using pop_value(). The code deliberately pushes NULL (None) via push_null(), then relies on expect_unchecked() to extract values. In debug builds this panics safely, but in release builds unwrap_unchecked() silently causes UB if a NULL slot is popped. This requires ironclad invariants: every call site using pop_value() must guarantee the popped slot contains a value; pop_value_opt() should be used when NULL is possible. If the compiler ever incorrectly allows a NULL slot to be popped via pop_value(), the result is silent undefined behavior in release builds.


1-20: Run cargo fmt, cargo clippy, and targeted tests to verify this opcode-wide change.

This review cannot be fully verified in the current environment. The import statements shown appear properly formatted and follow Rust conventions. However, verification requires:

  • cargo fmt --all to ensure code formatting compliance
  • cargo clippy --all-targets --all-features -- -D warnings to check for lints
  • Targeted tests for PushNull/LoadAttrMethod/Call opcode sequence integration, including @classmethod and JIT recursive call scenarios

The implementation of related opcodes and descriptors exists in the codebase, but complete validation depends on toolchain execution.

crates/codegen/src/compile.rs (10)

1871-1888: Assert raising: PushNull insertion matches new call convention.


2097-2111: Decorator application: [decorator, NULL, arg] stack discipline is coherent.
The “push decorator then PushNull” scheme + calling in source order correctly applies decorators from the innermost outward via stack order.


2113-2155: Type-param bound/default closure call: PushNull before Call { nargs: 0 } matches convention.


3786-3800: Non-generic class call: good reuse of compile_call_inner() + compile_call_with_self(), but currently inherits the Ex-opcode bug (see below).


3945-3960: with normal-exit __exit__ call: PushNull placement is correct for [__exit__, NULL, None, None, None].


6440-6463: Comprehension call: PushNull before Call { nargs: 1 } aligns with unified call convention.


6867-6885: Break/continue with-unwind: call shape updated, but confirm block-pop semantics vs return-unwind.
This path now does PushNull + Call { nargs: 3 }, but (unlike return unwinding) doesn’t emit PopBlock. Please confirm the VM/exception-table model doesn’t require it here.


1-80: Housekeeping: run rustfmt + clippy for this PR branch.
As per coding guidelines, please ensure cargo fmt and cargo clippy are clean after these opcode/stack changes.


6124-6156: CallFunctionEx already correctly handles the self_or_null stack layout—no change needed.

The CallFunctionEx opcode's documentation in both bytecode.rs and frame.rs explicitly shows it expects and handles the self_or_null slot: Stack: [func, self_or_null, callargs, kwargs?]. The stack effect calculation confirms it consumes all four components (callable, self_or_null, args_tuple, optional kwargs dict). The code at line 6153 is correct as-is; compile_call ensures self_or_null is always on the stack before calling compile_call_with_self, and CallFunctionEx is the appropriate opcode for the Ex case. There is no CallWithSelfEx opcode to propose—CallFunctionEx is already the self-aware variant.

Likely an incorrect or invalid review comment.


1027-1055: Remove PopBlock from With/AsyncWith exception unwinding or add it to break/continue path for consistency.

The unwind_fblock() method emits Instruction::PopBlock for With/AsyncWith blocks (line 1031), but the compile_break_continue() method does not emit it when unwinding the same block type (line 6870). This is inconsistent with the pattern used elsewhere in the codebase: TryExcept, FinallyTry, and other block setup methods explicitly avoid emitting PopBlock with comments stating "exception table handles this". Since the VM treats PopBlock as a pseudo-instruction (per frame.rs), this instruction should either be removed from the exception unwinding path entirely, or compile_break_continue should emit it as well for consistency.

@youknowone youknowone changed the title Replace CallFunction/LoadMethod opcodes with CallWithSelf Replace CallMethod/LoadMethod opcodes with plain Call Jan 9, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codegen/src/compile.rs (1)

3694-3777: Generic class creation: panic! on **kwargs is a functional regression (should compile or error gracefully).

keyword.arg == None is reachable for **kwargs in class bases/keywords; panicking in codegen is too harsh and likely breaks valid code. Prefer routing **kwargs through the Ex call path (the new “WithSelfEx” opcode) or returning a structured CodegenError.

Proposed direction (sketch)
  • If arguments.keywords.iter().any(|k| k.arg.is_none()):
    • Build positional args as today (including .generic_base)
    • Build kwargs map via compile_keywords(&arguments.keywords)?
    • Emit the new Ex call opcode (with self slot already present due to PushNull after LoadBuildClass)
  • Else keep the CallKw fast-path.
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d61ff1 and 5a37003.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_xml_etree.py is excluded by !Lib/**
  • crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/vm/method.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/vm/method.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/jit/src/instructions.rs
  • crates/codegen/src/compile.rs
  • crates/vm/src/frame.rs
  • crates/compiler-core/src/bytecode.rs
🧬 Code graph analysis (2)
crates/codegen/src/compile.rs (2)
crates/compiler-core/src/bytecode.rs (3)
  • try_from (1123-1129)
  • try_from (1150-1155)
  • try_from (1164-1170)
crates/vm/src/builtins/code.rs (5)
  • args (434-443)
  • args (446-455)
  • args (458-467)
  • args (470-479)
  • args (497-505)
crates/vm/src/frame.rs (2)
crates/vm/src/protocol/object.rs (1)
  • s (379-379)
crates/vm/src/vm/method.rs (1)
  • get (22-88)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Check Rust code with clippy
🔇 Additional comments (32)
crates/jit/src/instructions.rs (3)

30-30: LGTM! Null variant properly integrated.

The JitValue::Null variant is consistently handled across all value operations:

  • to_jit_type correctly returns None (not JIT-representable)
  • into_value returns None (no Cranelift Value)
  • boolean_val returns NotSupported (cannot convert to boolean)

This aligns with the new self_or_null calling convention where NULL is a placeholder, not an actual value.

Also applies to: 49-49, 56-56, 143-145


460-485: Verify: JIT only supports direct recursive calls, not method calls.

The Call instruction now requires self_or_null to be Null, returning NotSupported for any other case. This means the JIT currently cannot compile method calls (where self_or_null would be Some(self)).

This is a reasonable limitation for the initial implementation, but consider:

  1. Adding a comment explaining this limitation
  2. Tracking this as a future enhancement (JIT support for method calls)

Based on the PR context, is method call support in JIT planned for future work? If so, consider adding a TODO comment.


486-489: LGTM! PushNull instruction correctly implemented.

The instruction properly pushes a JitValue::Null onto the JIT stack, matching the VM's push_null semantics for the self_or_null calling convention.

crates/vm/src/frame.rs (12)

56-56: Major change: Stack is now NULL-aware.

The stack type changed from BoxVec<PyObjectRef> to BoxVec<Option<PyObjectRef>>, enabling explicit NULL slots for the self_or_null calling convention. This is a fundamental change that affects all stack operations throughout the file.


613-619: LGTM! BinarySubscript cleaned up.

The instruction now properly uses push_value(result) instead of direct stack manipulation, which automatically wraps the value in Some() for the NULL-aware stack.


677-686: LGTM! BuildString correctly handles NULL-aware stack.

The instruction now collects parts using pop_multiple (which panics on NULL) and builds the string from the collected parts. This is correct since the compiler should never emit BuildString with NULL arguments on the stack.


708-722: LGTM! Unified call semantics with self_or_null protocol.

All call variants (Call, CallKw, CallFunctionEx) now follow the unified calling convention:

  1. Pop self_or_null using pop_value_opt() (returns Option<PyObjectRef>)
  2. If Some(self_val), prepend to args (method call)
  3. If None (NULL), use args as-is (regular function call)

This correctly implements the new self_or_null protocol described in the PR objectives.

Also applies to: 2163-2185


1437-1441: LGTM! PushNull instruction correctly implemented.

The instruction properly pushes a NULL slot onto the stack using push_null(), which wraps None in the Option-based stack representation.


1169-1171: LGTM! LoadAttrMethod implements the new method loading protocol.

The instruction correctly distinguishes between:

  1. Method descriptors: Pushes [func, self] for method calls
  2. Regular attributes: Pushes [attr, NULL] for attribute access

This enables the unified Call instruction to handle both cases by checking the self_or_null slot. The implementation aligns with the method resolution logic shown in the relevant code snippets from crates/vm/src/vm/method.rs.

Also applies to: 2553-2572


2592-2627: LGTM! Stack operations properly handle NULL slots.

The stack operation methods are well-designed:

  • push_value(obj) wraps in Some() for regular values
  • push_null() pushes None for NULL slots
  • pop_value_opt() returns Option<PyObjectRef> for NULL-aware pops
  • pop_value() panics on NULL (for instructions that expect values)

The separation between NULL-aware and NULL-asserting operations makes the API clear and helps catch compiler bugs in debug mode.


2854-2860: Verify: unchecked unwrap relies on compiler correctness.

The expect_unchecked helper uses unwrap_unchecked() in release builds for performance, relying on the compiler to never emit bytecode that puts NULL where a value is expected. This is a common optimization pattern but has safety implications:

Debug builds: Panics with helpful message on NULL
Release builds: Undefined behavior if NULL is encountered

This is acceptable if the compiler is thoroughly tested and the bytecode validator prevents invalid bytecode. However, consider:

  1. Is there a bytecode validator that runs before execution?
  2. Are there comprehensive tests covering all bytecode patterns with the new NULL semantics?

Please confirm that the compiler is designed to never emit NULL in value positions, or that there's validation to catch such cases.


767-781: LGTM! CopyItem correctly handles NULL slots.

The instruction now preserves NULL slots when copying stack elements, using push_value_opt() to maintain the Option<PyObjectRef> semantics. This is correct since NULL is a valid stack value in the new calling convention.


1650-1654: LGTM! WithExceptStart safely accesses exit.

The instruction uses expect_unchecked to access __exit__ from a known stack position. This is correct since the compiler guarantees __exit__ is placed at stack_len - 4 by BeforeWith, and it should never be NULL.


2754-2802: LGTM! Stack accessor methods correctly handle NULL slots.

The accessor methods properly handle the NULL-aware stack:

  • pop_multiple: Panics on NULL (used for collecting arguments)
  • replace_top: Preserves NULL (allows swapping NULL slots)
  • top_value / nth_value: Panic on NULL (used for value access)

The panic-on-NULL behavior for value accessors is correct since these are used in contexts where the compiler should guarantee a value, not NULL.


2813-2839: LGTM! Debug formatting shows NULL slots.

The debug implementation now properly renders NULL slots as "> NULL", making it easier to debug stack states and identify issues with the NULL-aware calling convention.

crates/codegen/src/compile.rs (10)

1027-1055: With/AsyncWith unwind: PushNull + Call { nargs: 3 } stack shape looks correct.

The __exit__(None, None, None) emission now matches the [callable, self_or_null, args…] convention and preserves preserve_tos semantics via the surrounding Swaps.


1871-1894: Assert error path: PushNull placement is correct for calling AssertionError(...).

This aligns the stack for Call { nargs: 0/1 } without relying on legacy call opcodes.


2097-2111: Decorator call convention: pushing NULL per decorator is consistent with later Call { nargs: 1 }.

The resulting stack evolution correctly applies decorators from the topmost decorator down.


2113-2154: Type-param bound/default thunk: PushNull + Call { nargs: 0 } is the right convention for immediate invocation.

LGTM; the thunk creation + immediate call sequence is now self_or_null-safe.


3778-3792: Non-generic class creation: switching to PushNull + compile_call_with_self(...) is the right consolidation.

Nice simplification vs duplicating call-shape logic here.


3937-3957: With-statement normal exit: PushNull insertion matches the new call ABI.

This keeps the __exit__ call consistent with unwind-time __exit__ emission.


6116-6137: compile_call: method vs non-method split using LoadAttrMethod / PushNull is consistent with the new ABI.

This is the right place to centralize the “self_or_null slot” decision.


6432-6450: Comprehension call site: PushNull + Call { nargs: 1 } matches the new convention.

LGTM with the iterator argument evaluation following the inserted NULL slot.


6862-6877: break/continue unwind through with: PushNull + Call { nargs: 3 } is consistent with other __exit__ call sites.

This keeps unwind-time __exit__ calls ABI-consistent.


6139-6147: The current code is correct. CallFunctionEx already handles the self_or_null slot, as documented in the bytecode definition (stack layout: [func, self_or_null, callargs, kwargs?]) and confirmed by the runtime handler in frame.rs:2165 which explicitly pops and conditionally prepends self_or_null to the arguments. No CallWithSelfEx opcode exists, and no change to line 6145 is needed.

crates/compiler-core/src/bytecode.rs (7)

802-821: LGTM: Clear instruction definitions with explicit self_or_null calling convention.

The new Call, CallFunctionEx, and CallKw instructions are well-documented with clear stack layout comments that explicitly show the self_or_null slot positioning. This makes the new calling convention easy to understand for anyone working with these opcodes.


1046-1049: LGTM: Reserved opcodes properly positioned.

The Reserved125-127 and Reserved135 opcodes are correctly positioned in the instruction enum and maintain the opcode numbering scheme. These placeholders are handled consistently throughout the codebase.

Also applies to: 1072-1073


1086-1089: LGTM: LoadAttrMethod properly defined for the new calling convention.

The LoadAttrMethod instruction is correctly defined with clear documentation showing it pushes both the method and self_or_null onto the stack, establishing the calling convention expected by the new Call instructions.


1902-1907: LGTM: Correct stack effects for reserved and placeholder instructions.

The stack effect calculations are accurate:

  • Reserved opcodes correctly return 0 (never executed)
  • PushNull correctly returns +1 (pushes NULL for self_or_null slot in plain function calls)

1937-1938: LGTM: Stack effects correctly implement the new calling convention.

The stack effect calculations are mathematically correct and consistent with the documented stack layouts:

  • LoadAttrMethod: Correctly pops 1 (obj) and pushes 2 (method + self_or_null), net +1
  • Call: Correctly pops nargs + 2 (self_or_null + callable) and pushes 1, net -(nargs+1)
  • CallKw: Correctly pops nargs + 3 (kw_names + self_or_null + callable) and pushes 1, net -(nargs+2)
  • CallFunctionEx: Correctly pops has_kwargs + 3 (kwargs? + args + self_or_null + callable) and pushes 1, net -(has_kwargs+2)

Also applies to: 1971-1976


2142-2143: LGTM: Consistent display logic for reserved opcodes.

All reserved opcodes (3, 17, 125-127, 135, 140, 142-148) consistently display as "RESERVED" in disassembly output, making it clear these are placeholder opcodes not used in execution.

Also applies to: 2250-2252


2193-2194: LGTM: Clear and consistent display names for new instructions.

The display logic properly formats the new opcodes with clear, CPython-compatible names:

  • Call → "CALL"
  • CallFunctionEx → "CALL_FUNCTION_EX"
  • CallKw → "CALL_KW"
  • LoadAttrMethod → "LOAD_ATTR_METHOD"

All arguments are correctly formatted using the appropriate getters.

Also applies to: 2197-2197, 2233-2233

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/codegen/src/compile.rs (3)

3929-3944: with normal-exit path: PushNull + Call(3) matches the new call convention.
Minor: the big comment block above still shows CALL 2 for __exit__(None, None, None); consider updating it to avoid confusion.


3703-3770: Generic class build path breaks *bases and **kwargs (and panics on **kwargs).

  • for arg in &arguments.args { self.compile_expression(arg)?; } rejects Expr::Starred (*bases) with error "cannot use starred expression here"
  • panic!("keyword argument name must be set") crashes when **kwargs is present in class definitions

Both *bases and **kwargs are valid Python syntax that must be supported. Use the CallFunctionEx instruction with gather_elements and compile_keywords (which already handle these cases correctly) when unpacking is present, and preserve the fast Call/CallKw path for simple cases.


3297-3337: Incorrect stack argument order when calling type-params closure with multiple arguments.

For num_typeparam_args >= 2, the current Swap/PushNull/Swap/Reverse sequence produces arguments in reverse order. For N=2, the code produces [closure, NULL, arg2, arg1] instead of [closure, NULL, arg1, arg2]. This breaks generic functions with multiple type parameters that have defaults.

Note: The proposed fix in the diff also does not produce the correct result and requires further revision.

🧹 Nitpick comments (4)
crates/vm/src/frame.rs (2)

2553-2572: Verify the unused target in the Function variant.

In the PyMethod::Function { target: _, func } arm, the target is extracted from the PyMethod but immediately discarded via _. However, on line 2563, parent (the original object) is pushed as self instead of target.

According to PyMethod::get, target would be the same as the original object passed in, but the naming discrepancy is confusing. Consider using target directly for clarity:

-        PyMethod::Function { target: _, func } => {
+        PyMethod::Function { target, func } => {
             // Method descriptor found: push [method, self]
             self.push_value(func);
-            self.push_value(parent); // self
+            self.push_value(target); // self
         }

This makes the code self-documenting and avoids holding onto parent longer than needed.


2381-2384: Consider using pop_value() for consistency.

Since func on the stack should never be NULL at this point (it was just created by MakeFunction), using pop_value_opt() is slightly misleading. The expect() on line 2384 confirms the expectation that the value is non-NULL.

♻️ Suggested refactor for consistency
-let func = self.pop_value_opt();
-let attr_value = self
-    .replace_top(func)
-    .expect("replace_top encountered NULL");
+let func = self.pop_value();
+let attr_value = self
+    .replace_top(Some(func))
+    .expect("replace_top encountered NULL");
crates/codegen/src/compile.rs (2)

2091-2105: Decorator protocol is consistent: prepare_decorators pushes NULL, apply_decorators uses Call(1).
The resulting runtime stack shape [decorator, NULL, decorated_obj] per decorator application matches the comment.


6128-6184: compile_call_helper: good consolidation, but double-check EX-call opcode naming + avoid unchecked arg count math.

  • The helper emits Instruction::CallFunctionEx { has_kwargs }; ensure this is the intended opcode in this PR (some parts mention CallWithSelfEx).
  • let count = u32::try_from(arguments.len()).unwrap() + additional_positional; can wrap on overflow in release builds; prefer checked_add + a compilation error.
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a37003 and 55fbd0e.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_xml_etree.py is excluded by !Lib/**
  • crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/vm/method.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/stdlib/src/opcode.rs
  • crates/vm/src/vm/method.rs
  • crates/jit/src/instructions.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/codegen/src/compile.rs
  • crates/vm/src/frame.rs
🧬 Code graph analysis (1)
crates/compiler-core/src/bytecode.rs (2)
crates/codegen/src/compile.rs (1)
  • name (1205-1207)
crates/codegen/src/ir.rs (1)
  • idx (47-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run tests under miri
  • GitHub Check: Check the WASM package and demo
🔇 Additional comments (20)
crates/stdlib/src/opcode.rs (1)

77-77: LGTM!

The opcode variant rename from LoadMethod to LoadAttrMethod is consistent with the broader instruction consolidation in this PR.

crates/vm/src/vm/method.rs (1)

41-49: Correct handling of classmethod descriptors.

The logic correctly routes classmethods through descr_get to ensure the method is bound to the class rather than the instance. For PyMethodDescriptor with the CLASS flag, the descriptor protocol is invoked to properly convert instance → class, while other method descriptors continue to use direct method binding.

crates/jit/src/instructions.rs (2)

30-30: LGTM - JitValue::Null aligns with the NULL-aware call protocol.

Adding the Null variant properly models the self_or_null slot in the new call convention.


460-489: JIT correctly implements the new call protocol with an expected limitation.

The implementation correctly:

  1. Pops self_or_null after collecting arguments (lines 469-473)
  2. Requires self_or_null to be Null for JIT-compiled recursive calls
  3. Returns NotSupported for method calls where self_or_null is not Null

This means JIT currently only supports simple function calls (via PushNullCall), not method calls. This is a reasonable constraint for the initial implementation since method calls would require runtime dispatch logic.

crates/vm/src/frame.rs (5)

56-56: Core architectural change: NULL-aware stack.

The stack type change from BoxVec<PyObjectRef> to BoxVec<Option<PyObjectRef>> enables the new call protocol where None represents a NULL slot for the self_or_null position. This is a significant but well-contained change that aligns with the new Call/CallKw/LoadAttrMethod instruction semantics.


2163-2185: Clean implementation of the self_or_null call protocol.

The execute_call implementation correctly handles both cases:

  • Some(self_val): Method call - prepends self to args
  • None: Regular function call - uses args as-is

This provides a unified call path that works for both method and function calls based on what LoadAttrMethod pushed onto the stack.


2854-2860: Good performance optimization with debug safety net.

The expect_unchecked helper appropriately uses unwrap_unchecked() in release builds while maintaining safety checks in debug builds. This pattern is reasonable for hot paths where the compiler guarantees non-NULL values.


1437-1441: PushNull instruction correctly implemented.

The PushNull instruction simply calls push_null() to place a NULL marker on the stack, which is used by the call protocol to indicate "no self argument".


708-722: Call instructions correctly delegate to execute_call.

All three call variants (Call, CallKw, CallFunctionEx) now share the same execute_call path, with the difference only in how arguments are collected. This is a clean unification of the call handling logic.

crates/compiler-core/src/bytecode.rs (4)

1046-1098: LGTM: Reserved placeholders and LoadAttrMethod properly defined.

The reserved instruction slots (125-127 for old CallMethod* opcodes, 135 for LoadMethod) maintain opcode numbering stability during the transition. The new LoadAttrMethod instruction at opcode 141 is well-positioned and clearly documented.


2139-2252: LGTM: Display formatting is consistent and follows CPython conventions.

The instruction display names (CALL, CALL_KW, CALL_FUNCTION_EX, LOAD_ATTR_METHOD) align with CPython 3.13 opcode naming. Reserved placeholder instructions are uniformly displayed as RESERVED, which is clear and appropriate.


1899-1976: Stack effect calculations are correct.

The stack depth calculations correctly account for the self_or_null protocol used in call instructions:

  • Call: -(nargs + 1) (pops callable, self_or_null, and nargs arguments; pushes result)
  • CallKw: -(nargs + 2) (additionally pops kw_names_tuple)
  • CallFunctionEx: -(has_kwargs + 2) (pops callable, self_or_null, args_tuple, and optionally kwargs)
  • LoadAttrMethod: +1 (pops obj, pushes method and self_or_null)
  • PushNull: +1 (pushes NULL)

These stack effects are actively used in the max_stackdepth() calculation in crates/codegen/src/ir.rs, which validates stack depth by iterating through all instructions and tracking cumulative stack changes with overflow/underflow checks.


802-821: New call instruction definitions are correctly implemented with proper stack semantics.

The Call, CallFunctionEx, and CallKw instructions properly define the updated calling convention with the self_or_null protocol. Stack layout comments accurately describe the operand order, stack effects are correctly calculated, and both the compiler (codegen/src/compile.rs) and VM (vm/src/frame.rs) correctly emit and handle these instructions with the updated stack semantics. The deprecated CallMethod* instructions have been completely removed from the codebase.

crates/codegen/src/compile.rs (7)

1021-1049: with/async with unwind: PushNull + Call(3) stack shape looks correct.
The preserve_tos swap puts __exit__ back on TOS before PushNull, so Call { nargs: 3 } consumes [__exit__, NULL, None, None, None] and leaves the preserved TOS intact.


1866-1892: Assert raising path: PushNull + Call(0/1) is consistent with the new call convention.
This correctly creates AssertionError(...) via [callable, NULL, args...].


2106-2148: Type-param bound/default closure: immediate PushNull + Call(0) looks correct.
This ensures the bound/default thunk is invoked under the same [callable, NULL] contract as other non-method calls.


3771-3784: Non-generic class build: PushNull + compile_call_helper(2, ...) is the right direction.
This reuses the centralized call logic and should correctly support *bases/**kwargs here.


6108-6126: compile_call: method calls via LoadAttrMethod, regular calls via PushNull look consistent.
This cleanly enforces a single stack contract across both cases.


6414-6434: Comprehension call site: PushNull before Call(1) matches the unified call ABI.


6840-6858: Break/continue unwind through with: PushNull + Call(3) is consistent with other __exit__ call sites.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codegen/src/compile.rs (1)

3687-3784: Generic class creation with type parameters likely regresses support for *bases and **kwargs, and crashes codegen with a panic on the latter.

  • Lines 3718: for arg in &arguments.args { self.compile_expression(arg)?; } will fail with InvalidStarExpr if any base is Expr::Starred (e.g., class C[T](*bases): ...), whereas class C(*bases): ... works via compile_call_helpergather_elements, which explicitly handles starred expressions.
  • Lines 3735–3741: When keyword.arg.is_none() (indicating **kwargs unpacking), the code panics instead of routing through proper handling. The non-generic path correctly detects this via has_double_star and calls compile_keywords, which uses CallFunctionEx. Example: class C[T](**kw): ... will panic at codegen time, while class C(**kw): ... works (confirmed in extra_tests/snippets/syntax_class.py line 265).

Align the generic path with the non-generic path: replace the manual loop and keyword handling with calls to gather_elements and compile_keywords, and never panic on user input (return CodegenError instead). Add tests for these cases with type parameters.

🤖 Fix all issues with AI agents
In @crates/codegen/src/compile.rs:
- Around line 3304-3337: The current Swap/PushNull/Swap + Reverse sequence can
reorder arguments for num_typeparam_args >= 3; replace it with a stable "bubble
down" sequence so NULL is inserted under the closure without disturbing arg
order: after computing swap_depth and emitting Instruction::Swap { index:
swap_depth } to bring the closure to the top and emitting Instruction::PushNull,
emit a series of Instruction::Swap operations that iteratively bubble the NULL
down one slot at a time across the num_typeparam_args positions (i.e., perform
num_typeparam_args swaps with appropriate decreasing indices) and then call
Instruction::Call { nargs: num_typeparam_args as u32 }; remove the existing
single Swap+Reverse approach and use this loop-based swap sequence using the
same swap_depth and num_typeparam_args symbols.
🧹 Nitpick comments (1)
crates/vm/src/frame.rs (1)

2790-2798: Consider using expect_unchecked for consistency and safety.

The nth_value method uses unreachable_unchecked() for NULL slots, which results in undefined behavior if a NULL is encountered in release builds. For consistency with top_value and better safety, consider using expect_unchecked (defined at line 2850) which provides debug assertions:

♻️ Proposed refactor
 #[inline]
 #[track_caller]
 fn nth_value(&self, depth: u32) -> &PyObject {
     let stack = &self.state.stack;
-    match &stack[stack.len() - depth as usize - 1] {
-        Some(obj) => obj,
-        None => unsafe { std::hint::unreachable_unchecked() },
-    }
+    expect_unchecked(
+        stack[stack.len() - depth as usize - 1].as_ref(),
+        "nth_value but null found. This is a compiler bug.",
+    )
 }
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55fbd0e and bf3e813.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_xml_etree.py is excluded by !Lib/**
  • crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/vm/method.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/stdlib/src/opcode.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/vm/method.rs
  • crates/jit/src/instructions.rs
  • crates/codegen/src/compile.rs
  • crates/vm/src/frame.rs
  • crates/compiler-core/src/bytecode.rs
🧬 Code graph analysis (1)
crates/codegen/src/compile.rs (1)
crates/vm/src/builtins/code.rs (5)
  • args (434-443)
  • args (446-455)
  • args (458-467)
  • args (470-479)
  • args (497-505)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests (macos-latest)
🔇 Additional comments (36)
crates/vm/src/vm/method.rs (1)

41-49: LGTM! Classmethod descriptor handling is well-structured.

The conditional logic correctly differentiates between classmethod descriptors (which need descr_get to convert the instance to a class) and regular method descriptors (which preserve the existing is_method behavior). This aligns with the broader self_or_null protocol introduced in this PR.

crates/jit/src/instructions.rs (4)

30-30: LGTM! Null variant integrated consistently.

The JitValue::Null variant is properly integrated across all relevant methods (to_jit_type, into_value), treating it as a non-representable type similar to None, Tuple, and FuncRef. This aligns with the new NULL-aware stack protocol.

Also applies to: 49-49, 56-56


143-145: LGTM! Appropriate error handling for Null.

The boolean_val method correctly returns NotSupported for Null, treating it consistently with other non-boolean-convertible types like Tuple and FuncRef.


486-489: LGTM! PushNull instruction is straightforward.

The new PushNull instruction correctly pushes JitValue::Null onto the stack, supporting the explicit Null construction needed for the new calling convention.


460-485: The existing comment at line 469 already explains this is intentional: "should be Null for JIT-compiled recursive calls." This restriction is by design, not an oversight. No changes needed.

Likely an incorrect or invalid review comment.

crates/compiler-core/src/bytecode.rs (6)

802-805: LGTM! Call instruction well-documented.

The new Call instruction with its stack layout comment [callable, self_or_null, args...] clearly documents the calling convention. The instruction definition is clean and straightforward.


806-821: LGTM! CallFunctionEx and CallKw instructions are consistent.

Both CallFunctionEx and CallKw instructions include clear stack layout comments showing the self_or_null slot, maintaining consistency with the new calling convention.


1899-1904: LGTM! Stack effects for reserved instructions and PushNull are correct.

Reserved instructions appropriately return 0 stack effect, and PushNull correctly returns +1 as it pushes one NULL value onto the stack.


1934-1936: LGTM! Stack effect calculations are correct for the new calling convention.

The stack effects for LoadAttrMethod, Call, CallKw, and CallFunctionEx correctly account for the self_or_null slot in the new calling convention:

  • LoadAttrMethod: pops obj, pushes method + self_or_null (net +1) ✓
  • Call: pops callable + self_or_null + nargs, pushes result (net -nargs - 1) ✓
  • CallKw: pops callable + self_or_null + nargs + kw_names, pushes result (net -nargs - 2) ✓
  • CallFunctionEx: pops callable + self_or_null + args + kwargs?, pushes result (net -(has_kwargs) - 2) ✓

Also applies to: 1968-1973


2139-2141: LGTM! Display formatting is consistent.

The fmt_dis method correctly maps all new instructions (Call, CallKw, LoadAttrMethod) and reserved slots to their appropriate display names. The reserved instructions consistently show as "RESERVED".

Also applies to: 2190-2190, 2194-2194, 2230-2230, 2247-2249


1046-1049: LoadAttrMethod is a distinct instruction with its own implementation, not a conversion to LOAD_ATTR.

LoadAttrMethod has a separate implementation in crates/vm/src/frame.rs (line 1167) that calls load_attr_method(), not a conversion to LOAD_ATTR. It also has distinct stack semantics (documented at line 1934–1935 in bytecode.rs): it pops an object and pushes both a method and a self_or_null slot, unlike regular LOAD_ATTR.

Regarding reserved instruction slots: Reserved140–148 are clearly documented as "padding to keep RESUME at 149," while Reserved125–127 lack explicit purpose comments. All reserved slots are designated as "dummy/placeholder instructions (never executed)" in the codebase. The total count is 12 for the two ranges mentioned (3 + 9), not 13.

Likely an incorrect or invalid review comment.

crates/codegen/src/compile.rs (7)

1021-1049: with/async with unwind: PushNull placement looks correct for the new [callable, self_or_null, args…] convention.
PushNull occurs after any Swap used to preserve TOS, so the call consumes __exit__ + NULL + 3 args without disturbing the preserved value.


1865-1889: assert failure path correctly adds PushNull before calling AssertionError(...).
This matches the new call ABI for both nargs: 0 and nargs: 1.


2091-2105: Decorator stack protocol is consistent ([decorator, NULL] then CALL nargs=1).
The comment in apply_decorators matches the actual stack shape produced by prepare_decorators.


2106-2147: Type-param bound/default closure execution correctly uses PushNull + Call { nargs: 0 }.


3932-4042: Other updated call-sites (with normal exit, comprehension call, break/continue unwind) follow the new PushNull + Call { nargs } convention consistently.

Also applies to: 6414-6434, 6844-6858


1-7460: Repo hygiene: please run cargo fmt + cargo clippy for the Rust changes.

Ensure all formatting and linting passes before finalizing this PR.


6108-6184: Stack order and instruction naming assumptions are correct. No issues found with the compile_call/compile_call_helper refactor.

  • LoadAttrMethod correctly leaves [method, self_or_null] on the stack (confirmed in bytecode definition with stack effect +1 after popping object).
  • CallFunctionEx is the correct instruction name; no rename occurred in this PR series.
  • Both regular calls (via PushNull) and method calls (via LoadAttrMethod) produce matching stack layouts for CallFunctionEx consumption.
crates/vm/src/frame.rs (18)

56-56: LGTM: Stack type updated to support NULL slots.

The stack field is now BoxVec<Option<PyObjectRef>>, enabling the NULL-aware self_or_null call protocol. This is a foundational change that coordinates with the new instruction set.


613-619: LGTM: BinarySubscript correctly adapted for new stack semantics.

The instruction properly pops two values, calls get_item, and pushes the result. The push_value method now internally wraps the object in Some, maintaining correctness.


677-684: LGTM: BuildString correctly adapted.

The instruction collects PyStr objects from the stack and assembles them into a Wtf8Buf. The pop_multiple method panics on NULL slots, which is appropriate here since the compiler guarantees non-NULL PyStr objects on the stack.


706-720: LGTM: Call instructions updated for new call protocol.

The three call instructions (Call, CallKw, CallFunctionEx) are correctly dispatched with appropriate argument collection. The stack comments accurately reflect the self_or_null protocol. These instructions coordinate with the updated execute_call method to handle method calls.


765-780: LGTM: CopyItem correctly handles NULL-aware slots.

The instruction now copies Option<PyObjectRef> slots directly using push_value_opt, which correctly propagates NULL slots when present.


1435-1439: LGTM: PushNull instruction added for call protocol.

This new instruction pushes a NULL placeholder onto the stack, used in the self_or_null slot when calling regular attributes (non-methods). The implementation correctly calls push_null().


1642-1664: LGTM: WithExceptStart correctly accesses exit with NULL guard.

The instruction uses expect_unchecked to safely unwrap the __exit__ slot from the stack. This is appropriate since __exit__ is guaranteed to be non-NULL by the BeforeWith instruction. The debug assertion provides additional safety.


1795-1797: LGTM: Reserved opcodes added for replaced instructions.

The reserved opcodes (125-127) likely correspond to the removed CallMethod/LoadMethod instructions. Marking them as unreachable is correct to prevent accidental execution.


2540-2568: LGTM: Attribute access correctly split into load_attr and load_attr_method.

The implementation properly distinguishes between:

  • load_attr: Regular attribute access (pop obj, push attr)
  • load_attr_method: Method call preparation (pop obj, push [callable, self_or_null])

The load_attr_method correctly uses PyMethod::get to determine if a method descriptor is found:

  • Method descriptor: pushes [method, self]
  • Regular attribute: pushes [attr, NULL]

This coordinates with the execute_call method which expects [callable, self_or_null, ...].


2161-2183: LGTM: execute_call correctly implements self_or_null protocol.

The method properly handles both method calls and regular attribute calls:

  1. Pops self_or_null (Option) from TOS
  2. Pops callable from stack
  3. If self_or_null is Some: prepends self to args (method call)
  4. If self_or_null is None (NULL): uses args as-is (regular call)

The stack order and logic are correct.


2587-2623: LGTM: Push/pop API correctly refactored for NULL-aware stack.

The helper methods provide a clear API:

  • push_value_opt(Option<PyObjectRef>): Pushes an optional value
  • push_value(PyObjectRef): Pushes a value (wraps in Some internally)
  • push_null(): Pushes a NULL placeholder
  • pop_value_opt() -> Option<PyObjectRef>: Pops an optional value
  • pop_value() -> PyObjectRef: Pops a value (asserts non-NULL via expect_unchecked)

The use of expect_unchecked in pop_value provides debug assertions while being optimized in release builds.


2750-2771: LGTM: pop_multiple correctly guards against NULL slots.

The method drains multiple values from the stack and maps each Option to PyObjectRef using expect_unchecked. This correctly panics if any NULL slot is encountered, which is appropriate since most instructions expect non-NULL values. Instructions that need to handle NULL should use pop_value_opt directly.


2773-2778: LGTM: replace_top correctly handles Option-wrapped slots.

The method swaps TOS with a provided Option<PyObjectRef>, correctly supporting both Some and None values.


2809-2835: LGTM: Debug formatting updated for NULL-aware stack.

The Debug implementation correctly formats Option<PyObjectRef> slots, explicitly displaying NULL slots as "NULL". This is helpful for debugging the new NULL-aware stack semantics.


2850-2856: LGTM: expect_unchecked provides debug safety with release optimization.

The helper function uses expect in debug builds (with assertions) and unwrap_unchecked in release builds (for performance). This is an appropriate trade-off since NULL slots in expected non-NULL positions indicate compiler bugs. The helper is used consistently throughout the file.


2259-2289: LGTM: execute_unpack_ex correctly wraps elements in Some.

The method correctly unpacks sequences with variable-length middle sections, wrapping each element in Some before pushing onto the NULL-aware stack.


2462-2492: LGTM: unpack_sequence correctly wraps elements in Some.

The method correctly unpacks fixed-size sequences, wrapping each element in Some before pushing onto the NULL-aware stack.


1920-2005: LGTM: unwind_blocks correctly handles NULL slots during stack cleanup.

The exception unwinding logic correctly pops stack slots to the handler depth without checking for NULL. This is appropriate since the values are being discarded during cleanup. The implementation properly handles raising, returning, breaking, and continuing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/vm/src/frame.rs (1)

2780-2797: Remove UB: nth_value() must not use unreachable_unchecked() on NULL.
If a NULL slot is ever observed here (malformed bytecode, logic bug, or future refactor), this becomes immediate undefined behavior. Prefer fatal() (or expect_unchecked) to keep failure as a controlled panic.

Proposed fix
     fn nth_value(&self, depth: u32) -> &PyObject {
         let stack = &self.state.stack;
         match &stack[stack.len() - depth as usize - 1] {
             Some(obj) => obj,
-            None => unsafe { std::hint::unreachable_unchecked() },
+            None => self.fatal("tried to get stack item but got NULL"),
         }
     }
crates/jit/src/instructions.rs (1)

460-485: Fix CALL argument order + remove unwrap() panics + return-type handling.

The implementation has three critical bugs:

  1. Reversed argument order: Stack pops in the loop collect args in reverse order (argN, ..., arg1), but Cranelift's call() expects arguments in left-to-right order (arg1, ..., argN). This corrupts all function call arguments.

  2. Unprotected unwrap(): into_value() returns None for Null, Tuple, FuncRef, and None variants. Calling unwrap() on these will panic instead of returning an error.

  3. Hardcoded return type: Line 479 always pushes JitValue::Int(returns[0]), ignoring the actual function signature's return type. This should use self.sig.ret (which exists and is used in return_value()) combined with JitValue::from_type_and_value() (which is already defined and used elsewhere).

Proposed fix
             Instruction::Call { nargs } => {
                 let nargs = nargs.get(arg);

-                let mut args = Vec::new();
+                let mut args = Vec::with_capacity(nargs as usize);
                 for _ in 0..nargs {
                     let arg = self.stack.pop().ok_or(JitCompileError::BadBytecode)?;
-                    args.push(arg.into_value().unwrap());
+                    let val = arg.into_value().ok_or(JitCompileError::NotSupported)?;
+                    args.push(val);
                 }
+                // Stack is [callable, self_or_null, arg1, ..., argN] with argN on top.
+                // We popped argN..arg1; reverse to pass arg1..argN to Cranelift.
+                args.reverse();

                 // Pop self_or_null (should be Null for JIT-compiled recursive calls)
                 let self_or_null = self.stack.pop().ok_or(JitCompileError::BadBytecode)?;
                 if !matches!(self_or_null, JitValue::Null) {
                     return Err(JitCompileError::NotSupported);
                 }

                 match self.stack.pop().ok_or(JitCompileError::BadBytecode)? {
                     JitValue::FuncRef(reference) => {
                         let call = self.builder.ins().call(reference, &args);
                         let returns = self.builder.inst_results(call);
-                        self.stack.push(JitValue::Int(returns[0]));
+                        let ret = *returns.get(0).ok_or(JitCompileError::NotSupported)?;
+                        let ret_ty = self.sig.ret.clone().ok_or(JitCompileError::NotSupported)?;
+                        self.stack.push(JitValue::from_type_and_value(ret_ty, ret));

                         Ok(())
                     }
                     _ => Err(JitCompileError::BadBytecode),
                 }
             }
crates/codegen/src/compile.rs (1)

3702-3762: Critical: generic class call path likely breaks starred bases and **kwargs (and uses panic!).

In the generic branch, bases are compiled via for arg in &arguments.args { self.compile_expression(arg)?; } (Line 3716-3723). If any base is starred (*bases), that’s typically represented as Expr::Starred(...) and compile_expression will error outside annotation context—whereas the normal call machinery handles it via gather_elements. Also, keyword.arg == None (**kwargs) currently hard-panics (Line 3741-3742) instead of compiling.

Recommendation: avoid the ad-hoc base/keyword emission and reuse the same star/** machinery as calls (either by extending compile_call_helper for this classdef+generic_base case, or by building a positional tuple + optional kwargs map and using the new CallWithSelfEx-style opcode).

🤖 Fix all issues with AI agents
In @crates/codegen/src/compile.rs:
- Around line 3731-3756: Replace the panic!("keyword argument name must be set")
in the keyword iteration with returning a structured CodegenError (e.g.,
CodegenError::new(CodegenErrorType::SyntaxError(...)) or similar) so compiler
input errors don't abort the process; locate the block handling
arguments.keywords in compile.rs (the loop over for keyword in
&arguments.keywords) and when keyword.arg is None return the error with an
appropriate span/range for the keyword (use existing range/span available on
`keyword` or `arguments`), keeping the rest of the logic (pushing
ConstantData::Str, self.compile_expression, emit_load_const,
Instruction::CallKw) unchanged.

In @crates/vm/src/frame.rs:
- Around line 2379-2381: The SET_FUNCTION_ATTRIBUTE handling must not accept a
NULL `func`: replace the current use of pop_value_opt() with a non-optional pop
(e.g., call pop_value() instead of pop_value_opt()), or explicitly check the
popped Option and immediately treat a null/None as a bytecode/compiler bug by
failing fast (use expect_unchecked or an assert with a clear message). Ensure
this change is applied to the code that calls pop_value_opt(), replace_top(func)
and expect_unchecked so `func` is guaranteed non-null before continuing.
🧹 Nitpick comments (2)
crates/vm/src/frame.rs (1)

677-684: BuildString: consider avoiding unwrap() if this can be reached by non-compiler bytecode.
If stack contents can be malformed (e.g., unmarshaled), .downcast::<PyStr>().unwrap() will panic instead of raising a VM error.

crates/compiler-core/src/bytecode.rs (1)

2136-2250: Disassembly formatting updates are coherent (CALL, CALL_KW, LOAD_ATTR_METHOD, reserved opcodes).
Minor: the reserved formatting path is slightly inconsistent (some go through the padded macro, some write "RESERVED" directly), but functionally fine.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf3e813 and f2c310a.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_xml_etree.py is excluded by !Lib/**
  • crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/vm/method.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/vm/method.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/jit/src/instructions.rs
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/vm/src/frame.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run tests under miri
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check Rust code with clippy
🔇 Additional comments (32)
crates/jit/src/instructions.rs (3)

24-33: Nice separation of Null vs None (representability stays explicit).
Null consistently maps to NotSupported for typed lowering (to_jit_type/into_value), which keeps the JIT type model honest.

Also applies to: 44-58


129-146: boolean_val() correctly rejects Null (and other non-scalars).
This avoids accidentally treating the self-or-null sentinel as falsy/truthy in compiled control-flow.


486-489: PushNull instruction is a clean fit for the self-or-null protocol.

crates/vm/src/frame.rs (14)

52-60: Good: stack element type change is minimal and localized (Option<PyObjectRef>).


613-619: BinarySubscript: good switch to push_value() (keeps NULL-aware stack invariants centralized).


706-720: CALL/CALL_KW/CALL_FUNCTION_EX handlers match the new stack contract.
The “callable, self_or_null, …” comment aligns with execute_call() popping self_or_null first after arg collection.


765-780: CopyItem: push_value_opt() is the right API now.


1167-1169: LoadAttrMethod + load_attr_method() wiring looks consistent: pushes [callable, self_or_null].
Nice that “attribute call” and “method call” become uniform for CALL*.

Also applies to: 2549-2567


1435-1439: PushNull execution path is clear and self-documenting.


1648-1653: WithExceptStart: good explicit NULL check for __exit__.


1938-1940: Unwind stack trimming: OK with Option stack slots (NULLs discarded safely).


2160-2183: execute_call() correctly prepends self only when present.


2274-2287: UnpackEx: correct wrapping into Some(...) when pushing back onto the Option stack.


2474-2480: UnpackSequence: correct wrapping into Some(...) for stack push-back.


2586-2622: Stack helper APIs are coherent (push/pop/replace + “NULL means None slot”).
Given the move to Option, this is a solid centralization of invariants and error messages.

Also applies to: 2749-2770, 2772-2777


2811-2824: Debug formatting of NULL stack slots is helpful for tracing.


2849-2855: expect_unchecked() is a reasonable pattern, but please ensure bytecode input is trusted.
If unmarshaled/untrusted bytecode can reach here, unwrap_unchecked() becomes a safety hazard (UB).

crates/compiler-core/src/bytecode.rs (3)

795-822: Opcode additions look consistent with the new call protocol; please double-check CPython-number alignment.
This enum’s ordering is correctness-critical for marshal/disassembly compatibility.

Also applies to: 1046-1076


1896-1974: stack_effect() updates for PushNull / LoadAttrMethod / Call look correct.*
The Call/CallKw/CallFunctionEx formulas match the documented “callable + self_or_null + …” layouts.


1103-1106: Good: LAST_INSTRUCTION and size asserts remain intact after enum changes.

crates/codegen/src/compile.rs (12)

1032-1049: Correctly updated __exit__(None, None, None) to the new Call stack protocol.

The inserted PushNull ensures the call site matches [callable, self_or_null, args...] before Call { nargs: 3 }.


1871-1882: Assert-path now matches new call protocol (good PushNull placement).

LoadGlobal(AssertionError) + PushNull + optional message + Call { nargs: 0/1 } looks consistent with the new stack layout.


2091-2104: Decorator push/apply sequence matches new protocol; comment is helpful.

Pushing PushNull per decorator and then applying Call { nargs: 1 } per decorator should preserve correct order (top-most decorator applied first).


2140-2147: Type-param bound/default closure call updated correctly (PushNull + Call { nargs: 0 }).

This aligns the “immediate invoke” path with the new self-or-null slot requirement.


3303-3337: Type-params closure invocation: Reverse/PushNull sequence is subtle—please add/confirm coverage for N=0/1/2 args.

The stack juggling looks plausible, but it’s easy to regress; I’d like to see a focused bytecode snapshot test that covers:

  • no defaults/kwdefaults (nargs=0)
  • defaults only (nargs=1)
  • defaults + kwdefaults (nargs=2)

3771-3784: Non-generic class call path: good use of compile_call_helper (keeps star/ behavior consistent).**

This is the shape I’d expect the generic branch to converge towards as well (modulo generic_base insertion).


3933-3944: with normal-exit __exit__ call updated correctly for self_or_null.

PushNull before Call { nargs: 3 } matches the updated calling convention.


6108-6126: compile_call now cleanly distinguishes method calls vs regular calls.

Using LoadAttrMethod for obj.attr(...) and PushNull for plain func(...) matches the new “explicit self_or_null” protocol.


6417-6439: Comprehension invocation updated correctly (PushNull before Call { nargs: 1 }).

This preserves [callable, NULL, iter] just before the call.


6847-6861: Break/continue unwind for with: correct PushNull insertion before calling __exit__.

This matches the same with-exit calling convention used elsewhere.


1-20: Please ensure cargo fmt + cargo clippy are clean for this opcode migration.

As per repo guidelines for **/*.rs, this change is wide enough that a clippy/fmt pass is important to catch stale opcode variants and stack-protocol mismatches early.


6128-6187: Remove: The CallFunctionEx instruction is valid and properly defined.

The instruction enum in crates/compiler-core/src/bytecode.rs defines CallFunctionEx { has_kwargs: Arg<bool> } as variant 54, with the correct stack layout already documented as [func, self_or_null, callargs, kwargs?]. There is no CallWithSelfEx variant, and CallFunctionEx has not been renamed or removed. The code will compile correctly as-is.

Comment on lines 3731 to 3749
if let Some(arguments) = arguments
&& !arguments.keywords.is_empty()
{
let mut kwarg_names = vec![];
for keyword in &arguments.keywords {
if let Some(name) = &keyword.arg {
self.emit_load_const(ConstantData::Str {
kwarg_names.push(ConstantData::Str {
value: name.as_str().into(),
});
} else {
panic!("keyword argument name must be set");
}
self.compile_expression(&keyword.value)?;
}
self.emit_load_const(ConstantData::Tuple {
elements: kwarg_names,
});
emit!(
self,
Instruction::CallFunctionKeyword {
Instruction::CallKw {
nargs: nargs
+ u32::try_from(arguments.keywords.len())
.expect("too many keyword arguments")
}
);
} else {
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 | 🟠 Major

Avoid panic! in compiler input paths; return a CodegenError instead.

panic!("keyword argument name must be set") turns valid Python syntax errors (or unsupported constructs) into compiler crashes. Prefer emitting a structured CodegenErrorType::SyntaxError(...) (or a dedicated error) with a proper range.

🤖 Prompt for AI Agents
In @crates/codegen/src/compile.rs around lines 3731 - 3756, Replace the
panic!("keyword argument name must be set") in the keyword iteration with
returning a structured CodegenError (e.g.,
CodegenError::new(CodegenErrorType::SyntaxError(...)) or similar) so compiler
input errors don't abort the process; locate the block handling
arguments.keywords in compile.rs (the loop over for keyword in
&arguments.keywords) and when keyword.arg is None return the error with an
appropriate span/range for the keyword (use existing range/span available on
`keyword` or `arguments`), keeping the rest of the logic (pushing
ConstantData::Str, self.compile_expression, emit_load_const,
Instruction::CallKw) unchanged.

@youknowone youknowone requested a review from ShaharNaveh January 9, 2026 12:41
value: name.as_str().into(),
});
} else {
panic!("keyword argument name must be set");
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can use:

&keyword.arg.expect("...")

self.assertEqual(t.find('.//paragraph').text,
'A new cultivar of Begonia plant named \u2018BCT9801BEG\u2019.')

@unittest.expectedFailure # TODO: RUSTPYTHON
Copy link
Collaborator

Choose a reason for hiding this comment

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

nice

Copy link
Collaborator

@ShaharNaveh ShaharNaveh left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/codegen/src/compile.rs (1)

3702-3760: Generic class call: keyword handling will panic on **kwargs, and likely regresses valid syntax.
keyword.arg.expect(...) makes class C(**kw): ... (and metaclass=..., **kw) a hard panic. Also, this path doesn’t appear to have a *_EX fallback for **kwargs like compile_call_helper does.

Proposed direction (no full diff): handle `**kwargs` via the same map-building path as `compile_call_helper`
  • Detect arguments.keywords.iter().any(|k| k.arg.is_none()).
  • If true, build kwargs via self.compile_keywords(&arguments.keywords)?; (supports ** groups) and emit the new EX-call opcode (likely Instruction::CallWithSelfEx { has_kwargs: true }) after packing positional args into a tuple.
  • Otherwise keep the CallKw fast-path.

This keeps parity with regular call compilation and avoids panics.

crates/vm/src/frame.rs (1)

52-60: Avoid UB on malformed/hostile bytecode: remove unwrap_unchecked() / unreachable_unchecked() from stack access.

expect_unchecked() uses unwrap_unchecked() in release, and nth_value() uses unreachable_unchecked() on None. If a crafted code object (e.g., via marshal) hits these paths, this can become memory-unsafe UB instead of a controlled panic/error.

Safer alternatives (panic in all builds)
-fn expect_unchecked(optional: Option<PyObjectRef>, err_msg: &'static str) -> PyObjectRef {
-    if cfg!(debug_assertions) {
-        optional.expect(err_msg)
-    } else {
-        unsafe { optional.unwrap_unchecked() }
-    }
-}
+fn expect_unchecked(optional: Option<PyObjectRef>, err_msg: &'static str) -> PyObjectRef {
+    optional.expect(err_msg)
+}
 fn nth_value(&self, depth: u32) -> &PyObject {
     let stack = &self.state.stack;
     match &stack[stack.len() - depth as usize - 1] {
         Some(obj) => obj,
-        None => unsafe { std::hint::unreachable_unchecked() },
+        None => self.fatal("tried to get stack value but got NULL"),
     }
 }

Also applies to: 2586-2622, 2749-2797, 2849-2855

crates/jit/src/instructions.rs (1)

460-489: Fix CALL arg order and remove unwrap() panics in JIT call lowering.

The stack layout for CALL is [callable, self_or_null, args...], a LIFO stack where arguments are already in reverse order when popped. The current loop pops arguments without reversing, so the callee receives them in reversed order. Additionally, arg.into_value().unwrap() will panic when the stack value is Null, Tuple, or FuncRef rather than returning NotSupported.

Proposed fix
             Instruction::Call { nargs } => {
                 let nargs = nargs.get(arg);

-                let mut args = Vec::new();
-                for _ in 0..nargs {
-                    let arg = self.stack.pop().ok_or(JitCompileError::BadBytecode)?;
-                    args.push(arg.into_value().unwrap());
-                }
+                let mut args = Vec::with_capacity(nargs as usize);
+                for _ in 0..nargs {
+                    let arg = self.stack.pop().ok_or(JitCompileError::BadBytecode)?;
+                    let val = arg.into_value().ok_or(JitCompileError::NotSupported)?;
+                    args.push(val);
+                }
+                args.reverse();

                 // Pop self_or_null (should be Null for JIT-compiled recursive calls)
                 let self_or_null = self.stack.pop().ok_or(JitCompileError::BadBytecode)?;
                 if !matches!(self_or_null, JitValue::Null) {
                     return Err(JitCompileError::NotSupported);
                 }
🤖 Fix all issues with AI agents
In @crates/vm/src/frame.rs:
- Around line 2810-2824: The debug branch that matches Some(elem) if
elem.downcastable::<Self>() currently pushes the literal string "\n  > {frame}"
— replace that with a real representation of the frame: downcast the element to
a &Self (e.g., via elem.downcast_ref::<Self>()), then format a meaningful
identifier (pointer, id, or Debug) into the string using format_args! or format!
(for example "\n  > <frame {:p}>" or "\n  > <frame {:?}>") so the output shows
the frame info; if downcasting fails or you prefer a minimal change, use a clear
marker like "\n  > <frame>" instead of the literal "{frame}".
- Around line 677-684: The BuildString instruction currently uses
.downcast::<PyStr>().unwrap() after pop_multiple, which will panic on non-str
results; replace the unwrap with a safe downcast/match for each popped PyObject
from pop_multiple in the bytecode::Instruction::BuildString arm, and if any item
fails to downcast to PyStr return a TypeError via the VM (e.g., raise/return a
VM TypeError) instead of panicking; include a CPython-like error message
identifying the actual returned type (e.g., "__format__() must return str, not
<type>") and keep the successful path pushing vm.ctx.new_str(s).into().
🧹 Nitpick comments (1)
crates/compiler-core/src/bytecode.rs (1)

802-821: Clarify whether LoadAttrMethod is truly “pseudo” vs a real executed opcode.

Line 1072 says “pseudo instruction converted…”, but the opcode is fully represented (enum variant + stack effect + disassembly) and is executed in crates/vm/src/frame.rs. Please align the comment with actual semantics (or document the conversion point if it’s transformed before execution/marshaling).

Also applies to: 1046-1075

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2c310a and 4d82ef4.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_xml_etree.py is excluded by !Lib/**
  • crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/vm/method.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/stdlib/src/opcode.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/vm/method.rs
  • crates/jit/src/instructions.rs
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/vm/src/frame.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run tests under miri
🔇 Additional comments (18)
crates/codegen/src/compile.rs (12)

1032-1049: With/AsyncWith unwind: PushNull placement looks consistent with new [callable, self_or_null, args…] contract.
The resulting stack should be [..., __exit__, NULL, None, None, None] before Call { nargs: 3 }, which matches the comment.


1871-1882: Assert path: good update to push NULL before calling AssertionError(...).
This keeps the stack layout aligned with the unified Call opcode.


2091-2104: Decorator protocol seems correct: prepare_decorators pushes decorator, NULL and apply_decorators consumes decorator, NULL, arg.
The stack discipline should work for chained decorators.


2140-2147: Type-param bound/default thunk: PushNull + Call { nargs: 0 } is consistent.


3772-3784: Non-generic class call: good switch to PushNull + compile_call_helper(2, ...) (and Call { nargs: 2 } when no args).


3932-3944: with normal-exit __exit__(None, None, None) call: PushNull addition matches the new Call ABI.


6416-6439: Comprehension call site: PushNull before Call { nargs: 1 } is consistent with the new ABI.


6844-6861: break/continue unwind through with: PushNull before calling __exit__ matches the updated call ABI.


3304-3337: Type-params closure invocation: verify Reverse/PushNull sequence matches Reverse semantics and call ABI.

This is very sensitive to exact Instruction::Reverse { amount } behavior; a small mismatch will corrupt argument order.

To verify this sequence:

  1. Confirm Reverse implementation in the bytecode instruction set—specifically how it reverses the top N stack elements
  2. Verify the Call instruction ABI expectations for argument order: [callable, self_or_null, arg1, ..., argN]
  3. Trace through the sequence with concrete examples (e.g., N=2 args) to ensure the double-Reverse with PushNull in between produces the correct final order

1-50: Process: please ensure cargo fmt and cargo clippy are run for this opcode-heavy refactor.
As per coding guidelines, this should be done before merging.

#!/bin/bash
set -euo pipefail
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -D warnings

6108-6126: compile_call: nice split between method calls (LoadAttrMethod) and regular calls (PushNull).

Verified: both code paths correctly produce [callable, self_or_null, args...] on the stack as expected by CALL. LoadAttrMethod pushes [method, self_or_null] (per VM implementation at frame.rs:2549-2567), and PushNull pushes null for the self_or_null slot. CALL instruction expectation and both VM and JIT implementations align on this stack layout.


6128-6187: Incorrect: CallFunctionEx is the actual variant name—no rename to CallWithSelfEx occurred.

The CallFunctionEx instruction variant exists and is correctly named in the bytecode. There is no variant named CallWithSelfEx in the codebase. The code at line 6157 is correct.

Additionally, Arguments::len() returns the count of positional arguments, and the nargs value passed to CallKw (the count of positional arguments) is correct. The keyword names tuple is emitted separately on the stack before the CallKw instruction.

Likely an incorrect or invalid review comment.

crates/compiler-core/src/bytecode.rs (2)

1896-1974: stack_effect() updates for Call/CallKw/CallFunctionEx/LoadAttrMethod/PushNull look internally consistent.

The net effects match the documented stack layouts, including the self_or_null slot and kw_names_tuple / kwargs optionality.


2136-2232: Disassembly mappings cover the new opcodes and reserved slots appropriately.

CALL, CALL_KW, CALL_FUNCTION_EX, LOAD_ATTR_METHOD, and PUSH_NULL are surfaced; reserved opcodes print as RESERVED.

crates/jit/src/instructions.rs (2)

24-59: JitValue::Null integration looks consistent, but keep it out of “value-required” paths.
to_jit_type()/into_value() correctly treat Null as non-representable; boolean_val() rejects it.

Also applies to: 129-147


486-489: PushNull opcode support is straightforward.

crates/vm/src/frame.rs (1)

706-720: Call protocol integration (PushNull + LoadAttrMethod + Call*) is coherent.

  • LoadAttrMethod pushes [callable, self] or [callable, NULL].
  • PushNull fills self_or_null for non-method calls.
  • execute_call() correctly prepends self when present.

Nice simplification vs older CallMethod* split.

Also applies to: 1435-1439, 2161-2183, 2549-2567

crates/vm/src/vm/method.rs (1)

5-10: Ensure PyMethodFlags::CLASS descriptor path behaves sanely if descr_get is absent.

Right now classmethod descriptors route to descr_cls.slots.descr_get.load(). If that’s ever None, the call site will treat it like a plain attribute (since is_method stays false), which may be a silent behavioral shift. Consider asserting/guarding (or falling back to is_method = true) if CLASS is set but descr_get is missing.

Also applies to: 36-49

⛔ Skipped due to learnings
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-27T14:03:49.034Z
Learning: Applies to **/*.rs : Use the macro system (`pyclass`, `pymodule`, `pyfunction`, etc.) when implementing Python functionality in Rust

Comment on lines 677 to 684
bytecode::Instruction::BuildString { size } => {
let s = self
let s: Wtf8Buf = self
.pop_multiple(size.get(arg) as usize)
.as_slice()
.iter()
.map(|pyobj| pyobj.downcast_ref::<PyStr>().unwrap())
.collect::<Wtf8Buf>();
let str_obj = vm.ctx.new_str(s);
self.push_value(str_obj.into());
.map(|pyobj| pyobj.downcast::<PyStr>().unwrap())
.collect();
self.push_value(vm.ctx.new_str(s).into());
Ok(None)
}
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 | 🟠 Major

BUILD_STRING now panics on non-str; should raise TypeError instead.

.downcast::<PyStr>().unwrap() will crash the VM if a bad __format__ implementation returns a non-str (CPython raises TypeError).

Proposed fix
             bytecode::Instruction::BuildString { size } => {
-                let s: Wtf8Buf = self
-                    .pop_multiple(size.get(arg) as usize)
-                    .map(|pyobj| pyobj.downcast::<PyStr>().unwrap())
-                    .collect();
+                let mut parts = Vec::with_capacity(size.get(arg) as usize);
+                for obj in self.pop_multiple(size.get(arg) as usize) {
+                    let s = obj.downcast::<PyStr>().map_err(|obj| {
+                        vm.new_type_error(format!(
+                            "BUILD_STRING expected str, got {:.200}",
+                            obj.class().name()
+                        ))
+                    })?;
+                    parts.push(s);
+                }
+                let s: Wtf8Buf = parts.into_iter().collect();
                 self.push_value(vm.ctx.new_str(s).into());
                 Ok(None)
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bytecode::Instruction::BuildString { size } => {
let s = self
let s: Wtf8Buf = self
.pop_multiple(size.get(arg) as usize)
.as_slice()
.iter()
.map(|pyobj| pyobj.downcast_ref::<PyStr>().unwrap())
.collect::<Wtf8Buf>();
let str_obj = vm.ctx.new_str(s);
self.push_value(str_obj.into());
.map(|pyobj| pyobj.downcast::<PyStr>().unwrap())
.collect();
self.push_value(vm.ctx.new_str(s).into());
Ok(None)
}
bytecode::Instruction::BuildString { size } => {
let mut parts = Vec::with_capacity(size.get(arg) as usize);
for obj in self.pop_multiple(size.get(arg) as usize) {
let s = obj.downcast::<PyStr>().map_err(|obj| {
vm.new_type_error(format!(
"BUILD_STRING expected str, got {:.200}",
obj.class().name()
))
})?;
parts.push(s);
}
let s: Wtf8Buf = parts.into_iter().collect();
self.push_value(vm.ctx.new_str(s).into());
Ok(None)
}
🤖 Prompt for AI Agents
In @crates/vm/src/frame.rs around lines 677 - 684, The BuildString instruction
currently uses .downcast::<PyStr>().unwrap() after pop_multiple, which will
panic on non-str results; replace the unwrap with a safe downcast/match for each
popped PyObject from pop_multiple in the bytecode::Instruction::BuildString arm,
and if any item fails to downcast to PyStr return a TypeError via the VM (e.g.,
raise/return a VM TypeError) instead of panicking; include a CPython-like error
message identifying the actual returned type (e.g., "__format__() must return
str, not <type>") and keep the successful path pushing vm.ctx.new_str(s).into().

Comment on lines 2810 to 2780
let state = self.state.lock();
let stack_str = state.stack.iter().fold(String::new(), |mut s, elem| {
if elem.downcastable::<Self>() {
s.push_str("\n > {frame}");
} else {
core::fmt::write(&mut s, format_args!("\n > {elem:?}")).unwrap();
let stack_str = state.stack.iter().fold(String::new(), |mut s, slot| {
match slot {
Some(elem) if elem.downcastable::<Self>() => {
s.push_str("\n > {frame}");
}
Some(elem) => {
core::fmt::write(&mut s, format_args!("\n > {elem:?}")).unwrap();
}
None => {
s.push_str("\n > NULL");
}
}
s
});
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

Debug output prints literal {frame} instead of something meaningful.

This branch currently does s.push_str("\n > {frame}"), which looks unintended. Consider a literal marker (e.g., <frame>) or formatting the frame pointer/id.

Possible tweak
                 Some(elem) if elem.downcastable::<Self>() => {
-                    s.push_str("\n  > {frame}");
+                    s.push_str("\n  > <frame>");
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let state = self.state.lock();
let stack_str = state.stack.iter().fold(String::new(), |mut s, elem| {
if elem.downcastable::<Self>() {
s.push_str("\n > {frame}");
} else {
core::fmt::write(&mut s, format_args!("\n > {elem:?}")).unwrap();
let stack_str = state.stack.iter().fold(String::new(), |mut s, slot| {
match slot {
Some(elem) if elem.downcastable::<Self>() => {
s.push_str("\n > {frame}");
}
Some(elem) => {
core::fmt::write(&mut s, format_args!("\n > {elem:?}")).unwrap();
}
None => {
s.push_str("\n > NULL");
}
}
s
});
let state = self.state.lock();
let stack_str = state.stack.iter().fold(String::new(), |mut s, slot| {
match slot {
Some(elem) if elem.downcastable::<Self>() => {
s.push_str("\n > <frame>");
}
Some(elem) => {
core::fmt::write(&mut s, format_args!("\n > {elem:?}")).unwrap();
}
None => {
s.push_str("\n > NULL");
}
}
s
});
🤖 Prompt for AI Agents
In @crates/vm/src/frame.rs around lines 2810 - 2824, The debug branch that
matches Some(elem) if elem.downcastable::<Self>() currently pushes the literal
string "\n  > {frame}" — replace that with a real representation of the frame:
downcast the element to a &Self (e.g., via elem.downcast_ref::<Self>()), then
format a meaningful identifier (pointer, id, or Debug) into the string using
format_args! or format! (for example "\n  > <frame {:p}>" or "\n  > <frame
{:?}>") so the output shows the frame info; if downcasting fails or you prefer a
minimal change, use a clear marker like "\n  > <frame>" instead of the literal
"{frame}".

stack is [Option<PyObjectRef>]
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/codegen/src/compile.rs (2)

3277-3331: Critical: type-params closure call builds the wrong stack (arg order + NULL placement).
Reverse flips .defaults/.kwdefaults, and PushNull at TOS will be treated as an argument, not self_or_null.

Proposed fix (keep arg order; bubble NULL under args)
-            if num_typeparam_args > 0 {
-                // Stack: [arg1, ..., argN, closure]
-                // Need: [closure, NULL, arg1, ..., argN]
-                let reverse_amount = (num_typeparam_args + 1) as u32;
-                emit!(
-                    self,
-                    Instruction::Reverse {
-                        amount: reverse_amount
-                    }
-                );
-                // Stack: [closure, argN, ..., arg1]
-                emit!(self, Instruction::PushNull);
-                // Stack: [closure, argN, ..., arg1, NULL]
-                emit!(
-                    self,
-                    Instruction::Call {
-                        nargs: num_typeparam_args as u32
-                    }
-                );
-            } else {
+            if num_typeparam_args > 0 {
+                // Stack: [arg1, ..., argN, closure]
+                // Goal:  [closure, NULL, arg1, ..., argN]
+                let n = num_typeparam_args as u32;
+                emit!(self, Instruction::Reverse { amount: n + 1 }); // [closure, argN, ..., arg1]
+                if n > 1 {
+                    emit!(self, Instruction::Reverse { amount: n }); // [closure, arg1, ..., argN]
+                }
+                emit!(self, Instruction::PushNull); // [closure, arg1, ..., argN, NULL]
+                for _ in 0..n {
+                    emit!(self, Instruction::Swap { index: 2 }); // bubble NULL beneath args
+                }
+                emit!(self, Instruction::Call { nargs: n });
+            } else {
                 // Stack: [closure]
                 emit!(self, Instruction::PushNull);
                 // Stack: [closure, NULL]
                 // Call pops: args (0), then self_or_null (NULL), then callable (closure)
                 emit!(self, Instruction::Call { nargs: 0 });
             }

3648-3752: Major: generic class creation path likely breaks *bases and **kwargs (and may panic).
The manual arguments.args/arguments.keywords handling bypasses gather_elements/compile_keywords semantics and expect() will panic on **kwargs.

One possible direction: always build an args tuple + (optional) kwargs map and use `CallFunctionEx`
             // Compile original bases
-            let base_count = if let Some(arguments) = arguments {
-                for arg in &arguments.args {
-                    self.compile_expression(arg)?;
-                }
-                arguments.args.len()
-            } else {
-                0
-            };
-
-            // Load .generic_base as the last base
-            emit!(self, Instruction::LoadName(dot_generic_base));
-
-            let nargs = 2 + u32::try_from(base_count).expect("too many base classes") + 1; // function, name, bases..., generic_base
-
-            // Handle keyword arguments
-            if let Some(arguments) = arguments
-                && !arguments.keywords.is_empty()
-            {
-                let mut kwarg_names = vec![];
-                for keyword in &arguments.keywords {
-                    let name = keyword
-                        .arg
-                        .as_ref()
-                        .expect("keyword argument name must be set");
-                    kwarg_names.push(ConstantData::Str {
-                        value: name.as_str().into(),
-                    });
-                    self.compile_expression(&keyword.value)?;
-                }
-                self.emit_load_const(ConstantData::Tuple {
-                    elements: kwarg_names,
-                });
-                emit!(
-                    self,
-                    Instruction::CallKw {
-                        nargs: nargs
-                            + u32::try_from(arguments.keywords.len())
-                                .expect("too many keyword arguments")
-                    }
-                );
-            } else {
-                emit!(self, Instruction::Call { nargs });
-            }
+            // Build positional args tuple for __build_class__(func, name, *bases, generic_base)
+            if let Some(arguments) = arguments {
+                let (size, unpack) = self.gather_elements(2, &arguments.args)?;
+                if unpack {
+                    emit!(self, Instruction::BuildTupleFromTuples { size });
+                } else {
+                    emit!(self, Instruction::BuildTuple { size });
+                }
+                // append generic_base
+                emit!(self, Instruction::LoadName(dot_generic_base));
+                emit!(self, Instruction::BuildTuple { size: 1 });
+                emit!(self, Instruction::BuildTupleFromTuples { size: 2 });
+
+                let has_kwargs = !arguments.keywords.is_empty();
+                if has_kwargs {
+                    self.compile_keywords(&arguments.keywords)?;
+                }
+                emit!(self, Instruction::CallFunctionEx { has_kwargs });
+            } else {
+                emit!(self, Instruction::BuildTuple { size: 2 }); // (func, name)
+                emit!(self, Instruction::LoadName(dot_generic_base));
+                emit!(self, Instruction::BuildTuple { size: 1 });
+                emit!(self, Instruction::BuildTupleFromTuples { size: 2 });
+                emit!(self, Instruction::CallFunctionEx { has_kwargs: false });
+            }
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d82ef4 and 29d2d88.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_xml_etree.py is excluded by !Lib/**
  • crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/jit/src/instructions.rs
  • crates/stdlib/src/opcode.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/vm/method.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/jit/src/instructions.rs
  • crates/codegen/src/compile.rs
  • crates/stdlib/src/opcode.rs
  • crates/compiler-core/src/bytecode.rs
  • crates/vm/src/vm/method.rs
  • crates/vm/src/frame.rs
🧠 Learnings (1)
📚 Learning: 2025-11-10T06:27:41.954Z
Learnt from: youknowone
Repo: RustPython/RustPython PR: 6243
File: vm/src/function/buffer.rs:123-129
Timestamp: 2025-11-10T06:27:41.954Z
Learning: In vm/src/function/buffer.rs, line 36 in the try_rw_bytes_like method intentionally uses TypeError (not BufferError) for the "buffer is not a read-write bytes-like object" error case, even though a similar error message in ArgMemoryBuffer::try_from_borrowed_object uses BufferError. The TypeError is the intended exception type for this specific code path.

Applied to files:

  • crates/vm/src/frame.rs
🧬 Code graph analysis (1)
crates/compiler-core/src/bytecode.rs (2)
crates/codegen/src/compile.rs (1)
  • name (1205-1207)
crates/codegen/src/ir.rs (1)
  • idx (47-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Run rust tests (windows-2025)
🔇 Additional comments (38)
crates/stdlib/src/opcode.rs (1)

77-77: LGTM: Instruction variant renamed to align with bytecode changes.

The rename from LoadMethod to LoadAttrMethod correctly reflects the broader opcode consolidation across the codebase.

crates/vm/src/vm/method.rs (2)

6-7: LGTM: Imports added for classmethod resolution enhancement.

The new imports (PyMethodDescriptor and PyMethodFlags) are correctly added to support the enhanced descriptor handling logic below.


41-49: LGTM: Proper classmethod descriptor resolution added.

The logic correctly distinguishes classmethods (which need descr_get for instance-to-class conversion) from regular method descriptors (which maintain the existing is_method = true behavior). The let-chain pattern safely handles the downcast and flag check.

crates/jit/src/instructions.rs (5)

30-30: LGTM: Null variant added to support the new call protocol.

The Null variant correctly extends JitValue to represent the NULL marker used in the self_or_null calling convention.


44-50: LGTM: Null correctly treated as non-representable type.

The addition of JitValue::Null to the pattern that returns None is correct, as NULL markers (like None, Tuple, FuncRef) don't map to primitive JIT types.


53-58: LGTM: Null correctly has no Cranelift value representation.

Adding JitValue::Null to return None is appropriate, as the NULL marker (like None, Tuple, FuncRef) doesn't have an underlying Cranelift Value.


129-146: LGTM: Null correctly unsupported for boolean conversion.

Adding JitValue::Null to the NotSupported error path is appropriate - NULL markers shouldn't be converted to booleans, consistent with Tuple and FuncRef handling.


460-489: LGTM: Call instruction updated for new calling convention with known limitation.

The changes correctly implement the new call protocol:

  1. Renamed CallFunctionPositional to Call
  2. Added logic to pop and verify self_or_null is Null
  3. Added PushNull instruction to push NULL markers

The JIT currently only supports calls with NULL self (function calls), not method calls with a real self object. This is indicated by returning NotSupported for non-NULL self, which is a reasonable limitation for the initial implementation.

crates/compiler-core/src/bytecode.rs (6)

954-956: LGTM: LoadAttrMethod instruction definition.

The new LoadAttrMethod instruction is well-defined and appropriately placed in the pseudo-opcode range. The structure is consistent with similar attribute-loading instructions.


749-763: LGTM: Unified call instructions.

The new Call, CallKw, and CallFunctionEx instructions properly consolidate the call protocol. The argument types and opcode assignments are correct.


1778-1779: LGTM: LoadAttrMethod stack effect is correct.

The stack effect calculation properly accounts for popping the object and pushing both the method and the self_or_null slot, resulting in a net increase of 1.


1813-1818: LGTM: Call stack effects are correctly calculated.

The stack effects for Call, CallKw, and CallFunctionEx properly account for the self_or_null protocol:

  • Call: pops nargs + self_or_null + callable, pushes result → net effect: -nargs - 1
  • CallKw: additionally pops kw_names_tuple → net effect: -nargs - 2
  • CallFunctionEx: pops kwargs (if any) + args_tuple + self_or_null + callable, pushes result → net effect: -has_kwargs - 2

1913-1913: LGTM: PushNull stack effect is correct.

The stack effect of +1 properly reflects pushing a NULL sentinel value for the self_or_null slot in the call protocol.


2042-2042: LGTM: LoadAttrMethod display is consistent.

The display format properly shows the instruction name and attribute index, consistent with other attribute instructions.

crates/vm/src/frame.rs (18)

56-56: LGTM: Stack type updated to support NULL slots.

The stack type change from BoxVec<PyObjectRef> to BoxVec<Option<PyObjectRef>> is essential for the NULL-aware call protocol. This allows representing the self_or_null slot as None when no self argument is present.


613-619: LGTM: BinarySubscr correctly handles the new stack semantics.

The instruction properly uses pop_value() to unwrap the Option<PyObjectRef> stack slots, which is correct since subscript operations should never have NULL operands.


677-684: LGTM: BuildString correctly processes stack elements.

The instruction properly uses pop_multiple which unwraps the Option stack slots and panics on NULL (as expected, since BuildString operands should never be NULL). The downcast and string building logic is sound.


706-720: LGTM: Call instructions properly delegate to execute_call.

The three call variants correctly collect their arguments and delegate to execute_call, which handles the unified self_or_null protocol. The stack layout comments accurately document the expected state.


765-780: LGTM: CopyItem preserves NULL slots correctly.

The instruction properly uses push_value_opt to copy stack slots, which correctly handles NULL values (represented as None). This ensures that NULL slots are preserved when copied.


1435-1439: LGTM: PushNull correctly implements NULL pushing.

The instruction properly calls push_null() to push a None value onto the stack, implementing the NULL sentinel for the self_or_null slot in the call protocol.


1642-1664: LGTM: WithExceptStart correctly guards against NULL exit.

The use of expect_unchecked to unwrap __exit__ is appropriate here. The __exit__ method should never be NULL in valid bytecode, so this catches compiler bugs in debug builds while maintaining performance in release builds.


1893-1896: LGTM: Exception unwinding safely discards stack slots.

The unwinding logic correctly pops stack slots without requiring them to be non-NULL. This is appropriate since exception handlers must handle partially-constructed stack states.


2117-2139: LGTM: execute_call correctly implements the self_or_null protocol.

The implementation properly handles both method calls and regular calls:

  • When self_or_null is Some, it prepends the self argument to the call arguments (method call)
  • When self_or_null is None (NULL), it uses the arguments as-is (regular attribute/function call)

This correctly implements the unified call protocol described in the PR.


2496-2523: LGTM: load_attr_method correctly implements method loading with self_or_null.

The implementation properly distinguishes between:

  1. Method descriptors (PyMethod::Function): Pushes the unbound method and the self object
  2. Regular attributes (PyMethod::Attribute): Pushes the attribute value and a NULL sentinel

This correctly implements the self_or_null protocol, where the NULL sentinel indicates no self argument for the subsequent call.


2542-2578: LGTM: Stack manipulation primitives correctly implement NULL-aware operations.

The updated stack manipulation methods properly handle the Option<PyObjectRef> stack:

  • push_value_opt: Directly pushes an Option<PyObjectRef>
  • push_value: Wraps PyObjectRef in Some before pushing
  • push_null: Pushes None as the NULL sentinel
  • pop_value_opt: Returns Option<PyObjectRef> for nullable operations
  • pop_value: Unwraps with expect_unchecked, catching NULL in debug builds

These primitives form a sound foundation for the NULL-aware call protocol.


2705-2726: LGTM: pop_multiple correctly unwraps non-NULL stack slots.

The implementation properly returns PyObjectRef instead of Option<PyObjectRef>, using expect_unchecked to unwrap each slot. This is appropriate for operations that expect all operands to be non-NULL, catching compiler bugs in debug builds while maintaining performance in release.


2729-2753: LGTM: Stack access methods correctly handle Option unwrapping.

The implementations properly handle the Option<PyObjectRef> stack:

  • replace_top: Correctly swaps Option<PyObjectRef> values
  • top_value: Pattern matches and panics on NULL (compiler bug)
  • nth_value: Uses unreachable_unchecked for NULL case (optimization hint)

These methods are used in contexts where NULL is unexpected, so the panic/unreachable behavior is appropriate for catching compiler bugs.


2764-2790: LGTM: Debug implementation visualizes NULL slots.

The updated debug output properly handles Option<PyObjectRef> stack slots, displaying NULL entries as "NULL". This aids in debugging the NULL-aware call protocol.


2805-2811: LGTM: expect_unchecked helper balances safety and performance.

The helper function provides a good balance:

  • Debug builds: Uses expect() to catch NULL values with informative messages (compiler bugs)
  • Release builds: Uses unwrap_unchecked() for performance after the debug validation

This is an appropriate pattern for "should never happen" cases in compiled bytecode.


2418-2448: LGTM: unpack_sequence correctly wraps elements in Some.

The implementation properly wraps each unpacked element in Some() before pushing onto the Option<PyObjectRef> stack. This ensures all unpacked values are non-NULL as expected.


2215-2245: LGTM: execute_unpack_ex correctly wraps all elements.

The implementation properly wraps all unpacked elements in Some() before pushing onto the stack. Both the after-elements and before-elements are correctly wrapped, ensuring proper stack semantics.


2326-2353: LGTM: execute_set_function_attribute correctly manipulates the stack.

The implementation properly:

  1. Pops the function using pop_value_opt() (handles NULL-aware stack)
  2. Replaces TOS with the function
  3. Uses expect_unchecked to unwrap the old TOS (attr_value), which should never be NULL

The stack manipulation is correct for the SET_FUNCTION_ATTRIBUTE operation.

crates/codegen/src/compile.rs (6)

1021-1049: __exit__ callsites: PushNull placement looks correct.
You’re pushing NULL immediately after the __exit__ callable and before the 3 args, which matches the new call stack contract.

Also applies to: 3922-3943, 6839-6854


1866-1883: assert: correct PushNull insertion before Call.
This aligns AssertionError(...) with the unified call protocol.


2091-2104: Decorators: stack protocol + application order look consistent.
prepare_decorators() leaves [decorator, NULL] pairs so apply_decorators() can repeatedly Call(1) with the produced function value.


2106-2147: Type-param bound/default closure: correct PushNull before Call(0).


6101-6180: compile_call/compile_call_helper: good consolidation; verify CallFunctionEx naming + invariant.

  • The LoadAttrMethod vs PushNull split is clear and keeps argument emission shared.
  • Please verify the actual opcode variant name is still Instruction::CallFunctionEx (vs CallWithSelfEx) in the updated rustpython_compiler_core::bytecode::Instruction, and that all callers satisfy the “stack already has [callable, self_or_null]” precondition.

6409-6432: Comprehension call: correct PushNull before Call(1).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/compiler-core/src/bytecode.rs (1)

1777-1819: Remove or explicitly handle legacy CallMethodPositional, CallMethodKeyword, and CallMethodEx opcodes in stack_effect.

These enum variants (lines 924–932) exist but are never constructed or emitted anywhere in the codebase. In the stack_effect method, they fall through to the catch-all _ => 0 pattern (line 1914), silently reporting zero stack depth change when they should either be explicitly removed or properly specified. If any old bytecode contains these instructions, the stack analysis will be incorrect.

Either:

  1. Remove the enum variants entirely if they're truly obsolete
  2. Change them to Reserved variants
  3. Add explicit stack_effect cases with correct formulas if they might appear in legacy bytecode
crates/codegen/src/compile.rs (1)

1021-1049: Ensure PushNull placement matches the new Call stack contract in with unwind.

The new PushNull before calling __exit__ looks consistent with the stated [callable, self_or_null, args…] layout, but this path is extremely sensitive to stack order (especially with preserve_tos swaps). Consider adding a short stack-shape comment right above PushNull to capture the exact expected top-of-stack layout after the conditional swaps.

🧹 Nitpick comments (4)
crates/compiler-core/src/bytecode.rs (1)

2042-2043: Disassembly names added for LOAD_ATTR_METHOD and PUSH_NULL—consider explicit formatting for legacy CallMethod too.*

Nice to have these show up clearly in fmt_dis. If CallMethodPositional/Keyword/Ex are meant to be reserved going forward, it’d be better for debugging to print an explicit RESERVED_* opcode name rather than RUSTPYTHON_PLACEHOLDER when disassembling older artifacts.

Also applies to: 2065-2066

crates/codegen/src/compile.rs (3)

2091-2104: Decorator application order looks correct; comment could be more explicit about why the loop is forward.

Because decorators are pushed in source order but popped from the stack, iterating for _ in decorator_list still applies them in reverse (last decorator runs first). It may be worth stating that explicitly to prevent future “fixes” that reverse the loop and break behavior.


3291-3336: Manual stack shuffling for calling the type-params closure is correct but brittle.

The Swap/PushNull sequences for num_typeparam_args 1 and 2 look consistent with the target layout [closure, NULL, arg1, ...]. However, this is easy to regress and hard to audit.

Consider extracting a small helper like fn reorder_for_null_call(nargs: u32) (or a more general “insert NULL under top N args”) to centralize these swap recipes.


3641-3830: Class creation call emission: good migration to Call/CallKw/CallFunctionEx, but generic path duplicates call-assembly logic.

  • LoadBuildClass + PushNull is consistent with the new protocol.
  • The non-generic branch’s compile_call_helper(2, arguments) is a nice reuse.
  • The generic branch re-implements a parallel call-assembly flow to append .generic_base as a base (including starred/**kwargs handling). That duplication is a maintenance risk (e.g., keyword validation / tuple-building behavior can drift from compile_call_helper).

If feasible, consider refactoring so the generic branch also uses compile_call_helper (e.g., by supporting an “extra trailing positional” expression like .generic_base, or by building an Arguments-like view that includes it).

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29d2d88 and 012a6fa.

📒 Files selected for processing (2)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
🧬 Code graph analysis (1)
crates/compiler-core/src/bytecode.rs (1)
crates/codegen/src/ir.rs (1)
  • idx (47-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run tests under miri
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
🔇 Additional comments (7)
crates/compiler-core/src/bytecode.rs (1)

954-957: LoadAttrMethod opcode 254 is safely and consistently integrated. No conflicts or unsafe assumptions detected. Opcode 254 is exclusively assigned to LoadAttrMethod with proper stack_effect (1) and disassembly formatting (LOAD_ATTR_METHOD). The instruction is correctly emitted by codegen, executed in the frame interpreter, and handled throughout the codebase with no gaps.

crates/codegen/src/compile.rs (6)

1866-1882: AssertionError call path now correctly follows the PushNull + Call convention.

LoadGlobal(AssertionError) + PushNull + Call { nargs: 0/1 } matches the new protocol and is clearer than mixing old call opcodes.


2106-2147: Type-param bound/default closure call: PushNull addition looks correct.

After make_closure(...) the PushNull + Call { nargs: 0 } sequence aligns with the new call convention.


3864-3990: with normal-exit __exit__ call updated correctly with PushNull.

PushNull before pushing the three Nones matches the new Call { nargs: 3 } contract.


6149-6228: compile_call + compile_call_helper: argument counting and opcode selection look consistent with the new call protocol.

  • Method call path (LoadAttrMethod) + compile_call_helper(0, ...) matches the intended [method, self_or_null] setup.
  • Regular call path now explicitly inserts PushNull.
  • CallFunctionEx is selected for *args or **kwargs, and CallKw is used for named keywords; both seem consistent with the emitted stack shapes.
  • count = arguments.len() + additional_positional looks right for Call/CallKw since arguments.len() includes both positional args and keywords, and additional_positional accounts for pre-pushed positional args (e.g., __build_class__(func, name, ...)).

Minor: args_count/count could be computed lazily only in the Call/CallKw branches, but this is fine.


6457-6474: Comprehension wrapper call updated correctly with PushNull.

make_closure(...) followed by PushNull and then Call { nargs: 1 } (after iterator setup) matches the new stack convention.


6884-6902: Break/continue unwinding through with: PushNull before calling __exit__ matches the new call convention.

This mirrors the normal-exit/return unwinds and keeps the call protocol consistent across control-flow exits.

Copy link
Collaborator

@ShaharNaveh ShaharNaveh left a comment

Choose a reason for hiding this comment

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

Please generate opcode metadata again.

We should add a step in the CI to validate that's updated tbh

@youknowone
Copy link
Member Author

adding one to lint step and autocommit will make sense

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codegen/src/compile.rs (1)

6169-6228: Remove NameUsage::Store check for keyword argument names; use debug_assert for unreachable branch.

The check_forbidden_name call with NameUsage::Store at line 6187 incorrectly restricts keyword argument names. Keyword arguments are not assignment targets—the __debug__ restriction applies only to assignments and deletions, not to parameter names. This causes RustPython to reject valid calls like f(__debug__=1) even though CPython allows them.

Additionally, the panic!("name must be set") at line 6065 is unreachable by construction: the branch only executes when !has_double_star, which means all keywords have arg.is_some(). Replace with debug_assert!(keyword.arg.is_some()) for safety in release builds.

Proposed change
-        for keyword in &arguments.keywords {
-            if let Some(name) = &keyword.arg {
-                self.check_forbidden_name(name.as_str(), NameUsage::Store)?;
-            }
-        }

And:

                 } else {
-                    // This means **kwargs!
-                    panic!("name must be set");
+                    debug_assert!(false, "keyword.arg must be Some when has_double_star is false");
+                    unreachable!()
                 }
🧹 Nitpick comments (3)
crates/codegen/src/compile.rs (3)

2091-2104: Decorator application stack discipline is consistent; consider clarifying the ordering contract.

Pushing PushNull right after each decorator expression and then emitting Call { nargs: 1 } N times correctly applies decorators from the top of stack downward (i.e., last decorator is applied first). A short invariant comment like “stack grows as [d1,NULL,d2,NULL,...,value]” would reduce footguns.


3304-3336: Type-params wrapper call: the stack shuffles look correct but are brittle—please add coverage.

The swap sequences for num_typeparam_args 1 and 2 appear to correctly transform:

  • [arg1, closure] -> [closure, NULL, arg1]
  • [arg1, arg2, closure] -> [closure, NULL, arg1, arg2]

Given how easy this is to regress, it’d be good to add a disassembly snapshot test covering:

  • generic function with positional defaults only
  • generic function with kwonly defaults only
  • generic function with both (arg count = 2)

3701-3830: PushNull/Call migration correct; nargs math verified against bytecode spec.*

  • No legacy call ops remain in compile.rs. ✓
  • Generic path: LoadBuildClass + PushNull is correct.
  • CallFunctionEx branch correctly builds tuple and optional kwargs map.
  • "Simple case" branch: CallKw { nargs } is correct—nargs includes all positional args and keyword values on stack, with kw_names_tuple as a separate item pushed immediately before CallKw. Per bytecode spec (-1 - (nargs) - 2 + 1), this pops kw_names_tuple + nargs items + self_or_null + callable. ✓
  • Stack math is correct in both class creation branches.

Consider adding snapshot tests for generic classes with starred bases, **kwargs, and mixed keyword args to cover these edge cases.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 012a6fa and 6fd34eb.

📒 Files selected for processing (2)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/compiler-core/src/bytecode.rs
  • crates/codegen/src/compile.rs
🔇 Additional comments (14)
crates/compiler-core/src/bytecode.rs (6)

945-948: LGTM!

The LoadAttrMethod instruction is properly defined as a pseudo-opcode (254) with the appropriate NameIdx argument type, consistent with related instructions like LoadAttr and LoadClosure.


1769-1770: LGTM!

Stack effect calculation is correct: pops 1 (object) and pushes 2 (method + self_or_null), yielding a net effect of +1.


1804-1809: LGTM!

The stack effect calculations correctly implement the CPython 3.13+ call protocol:

  • Call: pops nargs + 2 (args + self_or_null + callable), pushes 1 result
  • CallKw: adds the kw_names tuple to the pop count
  • CallFunctionEx: correctly handles the optional kwargs dict

1904-1905: LGTM!

The stack effect of +1 is correct for PushNull, which pushes a NULL placeholder for the call protocol.


2033-2033: LGTM!

The display formatting for LoadAttrMethod follows the established pattern, using the name mapper consistent with LoadAttr.


2056-2056: LGTM!

Simple, no-argument display formatting consistent with other nullary instructions.

crates/codegen/src/compile.rs (8)

1021-1049: With/AsyncWith unwind: PushNull + Call nargs=3 stack shape looks correct.

Given the stack comment (“[..., exit]”), inserting PushNull before loading Nones aligns with the unified [callable, self_or_null, ...] protocol.


1866-1882: Assert statement now matches unified Call protocol.

LoadGlobal(AssertionError) + PushNull + Call { nargs: 0/1 } is consistent with “regular call” semantics.


2106-2147: Type-param bound/default closure invocation: PushNull placement is correct.

After make_closure(...) the stack is [closure]; PushNull + Call { nargs: 0 } matches the new calling convention.


3864-3991: With statement normal-exit exit call updated correctly.

PushNull before the 3 Nones + Call { nargs: 3 } matches the new call stack layout.


6149-6167: compile_call: LoadAttrMethod vs PushNull split is coherent with self_or_null semantics.

  • Attribute calls: LoadAttrMethod { idx } then compile_call_helper(...) leverages [method, self_or_null].
  • Non-attribute calls: explicit PushNull ensures the slot exists.

Would be good to ensure coverage for obj.attr(...) where attr is not a descriptor method (so self_or_null should be NULL).


6458-6474: Comprehension invocation updated correctly with PushNull.

make_closure(...) + PushNull + iterator + Call { nargs: 1 } aligns with the unified call protocol.


6884-6902: break/continue unwind through with: exit call now follows unified call protocol.

The PushNull insertion before Call { nargs: 3 } matches other exit call sites and should keep the unwind behavior consistent.


6149-6228: Run cargo fmt --all and cargo clippy --all-targets --all-features -- -D warnings before merging.

This code introduces stack-shape logic changes and contains multiple panic!() and expect() calls (lines 6176, 6179, 6214) that may warrant clippy review to ensure error handling aligns with Rust best practices.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codegen/src/compile.rs (1)

6158-6237: Use explicit field counts instead of Arguments::len()—it may not include keyword arguments, causing VM stack corruption with CallKw.

Instruction::CallKw { nargs } pops nargs values from the stack in collect_keyword_args(). Currently, nargs derives from arguments.len(), which is an external API detail that may count only positional arguments, not keywords. The class compilation code at line ~2080 shows the correct pattern: nargs + arguments.keywords.len(). Apply the same fix here.

Proposed fix
 fn compile_call_helper(
     &mut self,
     additional_positional: u32,
     arguments: &Arguments,
 ) -> CompileResult<()> {
-    let args_count = u32::try_from(arguments.len()).expect("too many arguments");
-    let count = args_count
-        .checked_add(additional_positional)
-        .expect("too many arguments");
+    let positional_count = u32::try_from(arguments.args.len())
+        .expect("too many positional arguments")
+        .checked_add(additional_positional)
+        .expect("too many arguments");
+    let keyword_count =
+        u32::try_from(arguments.keywords.len()).expect("too many keyword arguments");
+    let total_count = positional_count
+        .checked_add(keyword_count)
+        .expect("too many arguments");
 
     // Normal arguments:
     let (size, unpack) = self.gather_elements(additional_positional, &arguments.args)?;
     let has_double_star = arguments.keywords.iter().any(|k| k.arg.is_none());
@@
     } else if !arguments.keywords.is_empty() {
@@
-        emit!(self, Instruction::CallKw { nargs: count });
+        emit!(self, Instruction::CallKw { nargs: total_count });
     } else {
-        emit!(self, Instruction::Call { nargs: count });
+        emit!(self, Instruction::Call { nargs: positional_count });
     }
 
     Ok(())
 }
🧹 Nitpick comments (4)
crates/compiler-core/src/bytecode.rs (1)

1804-1809: Stack effects are mathematically correct for the new calling convention.

The calculations properly account for:

  • Call: -(nargs + 2) + 1 items
  • CallKw: -(nargs + 3) + 1 items
  • CallFunctionEx: -(3 or 4) + 1 items
📝 Optional: Clarify stack layout order in comments

The comments could be more explicit about the stack order for clarity. For example:

-            // Call: pops nargs + self_or_null + callable, pushes result
+            // Call: pops nargs + self_or_null + callable (top to bottom: argN, ..., arg1, callable, self_or_null), pushes result
             Call { nargs } => -(nargs.get(arg) as i32) - 2 + 1,
-            // CallKw: pops kw_names_tuple + nargs + self_or_null + callable, pushes result
+            // CallKw: pops kw_names_tuple + nargs + self_or_null + callable (top to bottom: kw_names, argN, ..., arg1, callable, self_or_null), pushes result
             CallKw { nargs } => -1 - (nargs.get(arg) as i32) - 2 + 1,
-            // CallFunctionEx: pops kwargs(if any) + args_tuple + self_or_null + callable, pushes result
+            // CallFunctionEx: pops kwargs(if any) + args_tuple + self_or_null + callable (top to bottom: kwargs?, args_tuple, callable, self_or_null), pushes result
             CallFunctionEx { has_kwargs } => -1 - (has_kwargs.get(arg) as i32) - 2 + 1,
crates/codegen/src/compile.rs (3)

2094-2112: Decorator stack discipline looks correct; consider making the “reverse application” intent explicit.

prepare_decorators() pushing dec, NULL pairs and apply_decorators() issuing Call { nargs: 1 } N times correctly applies decorators from the top of stack. For readability, you could iterate .iter().rev() in apply_decorators (even though the loop variable isn’t used) to match the doc comment’s “reverse order” wording.


3311-3346: Reduce footguns: the manual SWAP/NULL insertion for type-param-closure calls is correct but brittle.

The reordering logic for num_typeparam_args in {0,1,2} looks stack-correct, but this is the kind of sequence that’s easy to subtly break later. Consider extracting a small helper like emit_call_with_null_and_n_args(n) / reorder_call_with_null(n) to centralize the swap patterns.


3711-3839: Class creation paths: NULL handling looks consistent; consider consolidating the “build_class call” assembly.

Both generic and non-generic class creation now do LoadBuildClass then PushNull, which matches the new protocol. The generic branch’s CallFunctionEx / CallKw / Call split for starred bases / kwargs also looks coherent, but the call assembly is duplicated and complex (especially tuple-from-tuples building + .generic_base). If this stabilizes, a focused helper for “build_class + generic_base” would reduce the chance of future stack mistakes.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6fd34eb and 8c097df.

⛔ Files ignored due to path filters (1)
  • Lib/_opcode_metadata.py is excluded by !Lib/**
📒 Files selected for processing (2)
  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/codegen/src/compile.rs
  • crates/compiler-core/src/bytecode.rs
🧬 Code graph analysis (1)
crates/compiler-core/src/bytecode.rs (2)
crates/codegen/src/compile.rs (1)
  • name (1208-1210)
crates/codegen/src/ir.rs (1)
  • idx (47-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check the WASM package and demo
🔇 Additional comments (10)
crates/compiler-core/src/bytecode.rs (4)

1769-1770: Stack effect correctly accounts for the new calling convention.

The calculation is accurate: popping the object (-1) and pushing both the method and self_or_null marker (+2) results in a net effect of +1.


1904-1904: PushNull stack effect is correct.

The +1 effect correctly represents pushing a NULL marker onto the stack for the new call protocol.


1992-1992: Display formatting is consistent and follows CPython naming conventions.

The instruction display implementations correctly use the w! macro consistently across all four instructions. The LoadAttrMethod instruction properly displays the name index parameter using the same name = idx pattern as LoadAttr and other similar instructions. All CPython instruction names (CALL, CALL_KW, LOAD_ATTR_METHOD, PUSH_NULL) match the Python 3.11+ instruction set conventions.

The codegen properly emits all four instructions throughout compile.rs, and the VM has corresponding execution handlers in frame.rs that correctly process each instruction's stack protocol.


945-947: LGTM - LoadAttrMethod instruction definition and calling convention are correct.

Opcode 254 is appropriately placed in the pseudo-opcode range, the parameter type matches similar attribute instructions, and the stack effects correctly implement the new calling convention:

  • LoadAttrMethod pops the object and pushes method + self_or_null (net +1)
  • Call, CallKw, and CallFunctionEx properly account for callable + self_or_null + arguments on the stack
  • PushNull correctly adds the NULL sentinel for non-method calls

The opcode assignments (Call=53, CallKw=57) align with CPython 3.13's bytecode specification, and the self_or_null mechanism matches CPython's updated calling convention for consistent callable positioning.

crates/codegen/src/compile.rs (6)

1021-1052: Confirm PushNull placement matches the new CALL protocol in With/AsyncWith unwind.

The added Instruction::PushNull before calling __exit__ aligns with the new [callable, self_or_null, arg1..] protocol, and the preserve_tos swap keeps the return value below the callable. I’d just ensure the VM’s Call { nargs: 3 } consumes __exit__ + NULL + 3 args and leaves the preserved value untouched (as intended).


1869-1895: Good: AssertionError(...) call now pushes NULL.

Adding Instruction::PushNull before Call { nargs: 0/1 } makes this a “regular call” under the new protocol, consistent with other non-method calls.


2149-2156: Good: type-param bound/default closures are invoked with NULL.

make_closure(...) followed by PushNull then Call { nargs: 0 } matches the new call ABI.


3981-4000: Good: with normal-exit __exit__(None,None,None) now pushes NULL.

Instruction::PushNull before Call { nargs: 3 } matches the new calling convention for the __exit__ callable sitting on the stack.


6466-6489: Good: comprehension function invocation now pushes NULL.

After make_closure(...), inserting PushNull before pushing the iterator arg keeps the call stack shape consistent with other regular calls.


6893-6911: Good: break/continue unwind path calls __exit__ using NULL.

The inserted PushNull in UnwindAction::With matches the same __exit__ calling convention used elsewhere (unwind/normal exit).

@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2026

Code has been automatically formatted

The code in this PR has been formatted using cargo fmt --all.
Please pull the latest changes before pushing again:

git pull origin call

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
crates/compiler-core/src/bytecode.rs (1)

974-1008: Consider using a const array or match for O(1) opcode validation.

The current implementation uses custom_ops.contains(&value) which is O(n) on each TryFrom call. While the list is small (14 elements), this function may be called frequently during bytecode deserialization.

♻️ Optional: Use a const lookup table for O(1) validation
+// At module level or inside impl block
+const fn is_custom_op(value: u8) -> bool {
+    matches!(value, 119..=124 | 128..=135)
+}

 // In TryFrom implementation
-let custom_ops: &[u8] = &[
-    // ... all the explicit opcodes
-];
-
 if (cpython_start..=cpython_end).contains(&value)
     || value == resume_id
-    || custom_ops.contains(&value)
+    || is_custom_op(value)
     || (pseudo_start..=pseudo_end).contains(&value)
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c097df and 6dbe436.

📒 Files selected for processing (1)
  • crates/compiler-core/src/bytecode.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/compiler-core/src/bytecode.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (windows-2025)
  • GitHub Check: Run rust tests (windows-2025)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Check the WASM package and demo
🔇 Additional comments (6)
crates/compiler-core/src/bytecode.rs (6)

945-948: LGTM!

The LoadAttrMethod instruction is correctly defined as a pseudo opcode (254) with the appropriate NameIdx argument, aligning with CPython's LOAD_ATTR with method semantics.


1804-1805: LGTM!

The stack effect of +1 for LoadAttrMethod is correct: it pops 1 object and pushes 2 values (method + self_or_null), resulting in a net effect of +1.


1839-1844: LGTM!

The stack effects for the new call protocol are correctly calculated:

  • Call: pops nargs + 2 (args + self_or_null + callable), pushes 1 result
  • CallKw: pops nargs + 3 (kw_names + args + self_or_null + callable), pushes 1 result
  • CallFunctionEx: pops 3 + has_kwargs (args_tuple + optional kwargs + self_or_null + callable), pushes 1 result

The comments accurately describe the stack operations.


1939-1939: LGTM!

Stack effect of +1 for PushNull is correct—it pushes a NULL placeholder onto the stack for the call protocol's self_or_null semantics.


2068-2068: LGTM!

The display formatting for LoadAttrMethod correctly uses LOAD_ATTR_METHOD as the opcode name and properly resolves the name index for display.


2091-2091: LGTM!

The display formatting for PushNull follows the standard pattern for no-argument instructions.

});
let custom_end = u8::from(Self::Subscript);
// RustPython-only opcodes (explicit list to avoid gaps like 125-127)
let custom_ops: &[u8] = &[
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice, I did exactly this on #6675 lol. I'll rebase after this gets merged

@youknowone youknowone merged commit de0bc73 into RustPython:main Jan 9, 2026
13 checks passed
@youknowone youknowone deleted the call branch January 9, 2026 23:25
This was referenced Jan 9, 2026
terryluan12 pushed a commit to terryluan12/RustPython that referenced this pull request Jan 15, 2026
* implementing call the correct way

stack is [Option<PyObjectRef>]

* fix again

* list custom instructions

* Auto-format: cargo fmt --all

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants