Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Jan 27, 2026

Summary by CodeRabbit

  • New Features

    • Further performance optimizations for local-variable access and class-scope resolution.
  • Bug Fixes

    • Improved in-place Unicode concatenation handling.
    • More robust lookup behavior for class/dict vs. mapping fallbacks.
  • Tests

    • Relaxed I/O test read-size assertions to allow larger reads.
  • Chores

    • Expanded spell-check/dictionary word lists with many new tokens.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

This PR adds new bytecode instruction variants, a LOAD_FAST→LOAD_FAST_BORROW optimization, modifies class-scope name emission to use a dict-aware load path, updates VM/JIT handlers for the new opcodes, and adds many cspell dictionary entries.

Changes

Cohort / File(s) Summary
Spell-check dictionaries
\.cspell.dict/cpython.txt, \.cspell.dict/python-more.txt, \.cspell.dict/rust-more.txt, \.cspell.json
Added many new tokens/words and config entries; purely additive dictionary/config changes.
Bytecode instruction definitions
crates/compiler-core/src/bytecode/instruction.rs
Converted placeholder opcodes into real variants and added new instruction variants (e.g., LoadFastBorrow, LoadFastBorrowLoadFastBorrow, LoadFromDictOrGlobals) with updated stack-effect and display logic.
Compiler codegen
crates/codegen/src/compile.rs
Changed class-scope name emission: emit LoadLocals + LoadFromDictOrDeref inside class blocks, optionally loading __classdict__ when visible.
IR optimization
crates/codegen/src/ir.rs
Added optimize_load_fast_borrow() pass to convert LOAD_FASTLOAD_FAST_BORROW when safe (per-block stack simulation) and invoke it from finalize_code().
VM & JIT execution
crates/vm/src/frame.rs, crates/jit/src/instructions.rs
VM and JIT updated to handle new opcodes (borrow variants, LoadFromDictOrDeref dict-path handling, BinaryOpInplaceAddUnicode, and several NOP-like/instrumented variants).
Tests
extra_tests/snippets/stdlib_io.py
Relaxed read-size assertions from 8KB to 16KB and fixed inner variable reference.

Sequence Diagram(s)

sequenceDiagram
    participant Source as Source (AST/Parser)
    participant Compiler as Compiler/Codegen
    participant IR as IR optimizer
    participant Bytecode as BytecodeEmitter
    participant VM as VM/JIT

    Source->>Compiler: emit name ops (class scope -> LoadLocals / LoadFromDictOrDeref)
    Compiler->>IR: produce instruction stream (LOAD_FAST, etc.)
    IR->>IR: optimize_load_fast_borrow() (convert LOAD_FAST -> LOAD_FAST_BORROW when safe)
    IR->>Bytecode: finalize and emit new opcodes
    Bytecode->>VM: execute bytecode
    VM->>VM: handle LoadFastBorrow / LoadFromDictOrDeref at runtime
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

skip:ci

Suggested reviewers

  • ShaharNaveh
  • moreal

Poem

🐇 I hopped through code and bytecode stacks,
I borrowed fast and traced the tracks.
Class dicts now whisper names anew,
New ops dance, and tests grew too.
A carrot-cheer for this small crew!

🚥 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 'More Load instructions' accurately describes the main changes across the PR, which involve adding new load-related instruction variants (LoadFastBorrow, LoadFastBorrowLoadFastBorrow, LoadFromDictOrGlobals, LoadLocals, LoadFromDictOrDeref) and extending load instruction handling throughout the codebase.
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 load-ops branch 3 times, most recently from b78584b to 29ec496 Compare January 29, 2026 00:39
@youknowone youknowone marked this pull request as ready for review January 29, 2026 00:39
@github-actions
Copy link
Contributor

github-actions bot commented Jan 29, 2026

Code has been automatically formatted

The code in this PR has been formatted using:

  • cargo fmt --all
  • ruff format
    Please pull the latest changes before pushing again:
git pull origin load-ops

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/vm/src/frame.rs (1)

1178-1194: Preserve non‑KeyError exceptions from class-dict lookups.
class_dict.get_item(...).ok() converts any error into “not found,” which can mask real exceptions from custom mappings returned by __prepare__. The globals path already treats only KeyError as “miss”; this should do the same.

🔧 Suggested fix
-                // First try to find in the dict
-                let value = class_dict.get_item(name, vm).ok();
+                // First try to find in the dict
+                let value = if let Some(dict_obj) = class_dict.downcast_ref::<PyDict>() {
+                    dict_obj.get_item_opt(name, vm)?
+                } else {
+                    match class_dict.get_item(name, vm) {
+                        Ok(v) => Some(v),
+                        Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => None,
+                        Err(e) => return Err(e),
+                    }
+                };
🧹 Nitpick comments (1)
crates/codegen/src/ir.rs (1)

693-795: Make the borrow optimization conservative on stack underflow and method loads.
The underflow path says “don’t optimize this block,” but the loop continues, so the analysis still runs and may convert unsafely. Also, LOAD_ATTR with method-flag pushes two values; treating it as (1,1) undercounts pushes and can mis-mark LOAD_FAST as fully consumed. Consider using AnyInstruction::stack_effect for pseudo ops and bailing out on underflow, plus a precise LOAD_ATTR case.

🔧 Suggested change
-            for (i, info) in block.instructions.iter().enumerate() {
-                let Some(instr) = info.instr.real() else {
-                    continue;
-                };
-
-                // Get stack effect
-                let effect = instr.stack_effect(info.arg);
+            let mut valid_block = true;
+            for (i, info) in block.instructions.iter().enumerate() {
+                let instr_real = info.instr.real();
+
+                // Get stack effect (includes pseudo ops)
+                let effect = info.instr.stack_effect(info.arg);
                 let num_popped = if effect < 0 { (-effect) as usize } else { 0 };
                 let num_pushed = if effect > 0 { effect as usize } else { 0 };
 
                 // More precise: calculate actual pops and pushes
                 // For most instructions: pops = max(0, -effect), pushes = max(0, effect)
                 // But some instructions have both pops and pushes
-                let (pops, pushes) = match instr {
+                let (pops, pushes) = match instr_real {
+                    Some(Instruction::LoadAttr { idx }) => {
+                        let (_, is_method) = bytecode::decode_load_attr_arg(idx.get(info.arg));
+                        if is_method { (1, 2) } else { (1, 1) }
+                    }
                     // Instructions that both pop and push
-                    Instruction::BinaryOp { .. } => (2, 1),
-                    Instruction::CompareOp { .. } => (2, 1),
-                    Instruction::ContainsOp(_) => (2, 1),
-                    Instruction::IsOp(_) => (2, 1),
-                    Instruction::UnaryInvert
-                    | Instruction::UnaryNegative
-                    | Instruction::UnaryNot
-                    | Instruction::ToBool => (1, 1),
-                    Instruction::GetIter | Instruction::GetAIter => (1, 1),
-                    Instruction::LoadAttr { .. } => (1, 1), // simplified
-                    Instruction::Call { nargs } => (nargs.get(info.arg) as usize + 2, 1),
-                    Instruction::CallKw { nargs } => (nargs.get(info.arg) as usize + 3, 1),
+                    Some(Instruction::BinaryOp { .. }) => (2, 1),
+                    Some(Instruction::CompareOp { .. }) => (2, 1),
+                    Some(Instruction::ContainsOp(_)) => (2, 1),
+                    Some(Instruction::IsOp(_)) => (2, 1),
+                    Some(Instruction::UnaryInvert)
+                    | Some(Instruction::UnaryNegative)
+                    | Some(Instruction::UnaryNot)
+                    | Some(Instruction::ToBool) => (1, 1),
+                    Some(Instruction::GetIter) | Some(Instruction::GetAIter) => (1, 1),
+                    Some(Instruction::Call { nargs }) => (nargs.get(info.arg) as usize + 2, 1),
+                    Some(Instruction::CallKw { nargs }) => (nargs.get(info.arg) as usize + 3, 1),
                     // Use stack effect for others
                     _ => (num_popped, num_pushed),
                 };
 
                 // Pop values from stack
                 for _ in 0..pops {
                     if stack.pop().is_none() {
                         // Stack underflow in simulation - block receives values from elsewhere
                         // Conservative: don't optimize this block
-                        break;
+                        valid_block = false;
+                        break;
                     }
                 }
+                if !valid_block {
+                    break;
+                }
 
                 // Push values to stack with source instruction index
-                let source = match instr {
-                    Instruction::LoadFast(_) | Instruction::LoadFastLoadFast { .. } => i,
+                let source = match instr_real {
+                    Some(Instruction::LoadFast(_))
+                    | Some(Instruction::LoadFastLoadFast { .. }) => i,
                     _ => NOT_LOCAL,
                 };
                 for _ in 0..pushes {
                     stack.push(source);
                 }
             }
 
+            if !valid_block {
+                continue;
+            }
             // Mark instructions whose values remain on stack at block end
             for &src in &stack {
                 if src != NOT_LOCAL {
                     unconsumed[src] = true;
                 }
             }

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: 3

🤖 Fix all issues with AI agents
In `@crates/codegen/src/ir.rs`:
- Around line 731-747: The current special-casing in the match over instr (the
Instruction enum) is incomplete and can misinterpret the stack delta because
num_popped/num_pushed fallback is ambiguous; update the logic in this match to
be conservative: either enumerate and correctly handle all Instruction variants
with non-trivial pop/push behavior (e.g., BinaryOpInplaceAddUnicode,
LoadFromDictOrDeref, LoadFromDictOrGlobals, Swap, Copy, ForIter, all Build*
instructions) by returning explicit (pops, pushes) tuples, or change the default
branch to return (0, 0) for unknown/complex instructions to avoid incorrect
assumptions; adjust the Call/CallKw handling to still use nargs.get(info.arg)
and keep using info.arg where needed, and add a short comment above the match
noting the conservative assumption so future readers of ir.rs and the match over
instr know why unknown cases return (0,0).

In `@extra_tests/snippets/stdlib_io.py`:
- Around line 17-21: The assertions inside the FileIO context are checking the
wrong variable: change the three assertions that reference result to use the
local variable res (read via FileIO.read()) so they validate the bytes returned
by fio.read(); update the checks near the FileIO block (the assertions currently
using result) to assert len(res) <= 16 * 1024, len(res) >= 0, and
isinstance(res, bytes) respectively.
🧹 Nitpick comments (1)
crates/vm/src/frame.rs (1)

1341-1391: Consider extracting common error-handling logic.

The LoadFastBorrow and LoadFastBorrowLoadFastBorrow handlers duplicate the unbound-local error creation pattern from LoadFast and LoadFastLoadFast. While the TODO comments indicate future divergence for the actual load semantics, the error-handling logic will likely remain the same.

You could extract a helper to reduce duplication:

fn unbound_local_error(varname: &'static PyStrInterned, vm: &VirtualMachine) -> PyBaseExceptionRef {
    vm.new_exception_msg(
        vm.ctx.exceptions.unbound_local_error.to_owned(),
        format!("local variable '{varname}' referenced before assignment"),
    )
}

This is optional since the current implementation is correct and the duplication may be intentional for future optimization work.

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/jit/src/instructions.rs (1)

581-589: Handle LoadFastBorrowLoadFastBorrow in the JIT.

With the new borrow optimization, this opcode can be emitted and will currently fall through to NotSupported, disabling JIT for otherwise valid bytecode. Consider mirroring the LOAD_FAST_LOAD_FAST behavior here.

🛠️ Proposed fix
             Instruction::LoadFast(idx) | Instruction::LoadFastBorrow(idx) => {
                 let local = self.variables[idx.get(arg) as usize]
                     .as_ref()
                     .ok_or(JitCompileError::BadBytecode)?;
                 self.stack.push(JitValue::from_type_and_value(
                     local.ty.clone(),
                     self.builder.use_var(local.var),
                 ));
                 Ok(())
             }
+            Instruction::LoadFastBorrowLoadFastBorrow { arg: packed } => {
+                let oparg = packed.get(arg);
+                let idx1 = oparg >> 4;
+                let idx2 = oparg & 0xF;
+                for idx in [idx1, idx2] {
+                    let local = self.variables[idx as usize]
+                        .as_ref()
+                        .ok_or(JitCompileError::BadBytecode)?;
+                    self.stack.push(JitValue::from_type_and_value(
+                        local.ty.clone(),
+                        self.builder.use_var(local.var),
+                    ));
+                }
+                Ok(())
+            }
🤖 Fix all issues with AI agents
In `@crates/codegen/src/ir.rs`:
- Around line 758-760: The stack-effect mapping for Instruction::LoadAttr is
wrong for method-form attribute access: update the match arm handling
Instruction::LoadAttr in the stack-effect calculation in ir.rs so that it
returns (1, 2) when the LoadAttr instance has its method flag set (pop 1, push
method+self_or_null = 2) and remains (1, 1) otherwise; locate the match on
Instruction::LoadAttr and branch on the method flag to adjust the push count to
avoid underestimating the simulated stack and unsafe LOAD_FAST_BORROW
conversions.
🧹 Nitpick comments (1)
.cspell.dict/rust-more.txt (1)

92-93: bitvec/Bitvec entries break alphabetical ordering.

The dictionary appears to be alphabetically sorted, but bitvec and Bitvec are appended at the end after winsock. They should be placed between bitor and bstr to maintain consistency.

📋 Suggested placement
 bitflags
 bitor
+bitvec
+Bitvec
 bitxor
 bstr

And remove from end of file:

 winresource
 winsock
-bitvec
-Bitvec

@github-actions
Copy link
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] test: cpython/Lib/test/test_peepholer.py (TODO: 32)

dependencies:

dependent tests: (no tests depend on peepholer)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

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/compiler-core/src/bytecode/instruction.rs (1)

26-55: Update BinaryOpInplaceAddUnicode stack effect to -1.

The instruction pops 2 values and pushes 1 result (matching the codegen grouping at line 738-742 of crates/codegen/src/ir.rs), but InstructionMetadata::stack_effect at line 608 still returns 0. This mismatch can skew stack-depth analysis and other compiler passes.

🛠️ Proposed fix
-            Self::BinaryOpInplaceAddUnicode => 0,
+            Self::BinaryOpInplaceAddUnicode => -1,
🤖 Fix all issues with AI agents
In `@crates/codegen/src/ir.rs`:
- Around line 724-727: The borrow-analysis in optimize_load_fast_borrow
currently ignores pseudo-ops by using info.instr.real() and continuing, which
can mis-model the stack (pseudo ops like LoadClosure/StoreFastMaybeNull affect
push/pop); change the logic to conservatively bail out for the whole block when
a pseudo-op is encountered: inside the loop over block.instructions detect when
info.instr.real() is None and skip further processing of that block (e.g., set a
flag and break/continue to the next block) so the pass does not make unsafe
assumptions about LOAD_FAST liveness, or alternatively move
optimize_load_fast_borrow to run after pseudo lowering if feasible.
🧹 Nitpick comments (1)
crates/vm/src/frame.rs (1)

1341-1391: Consider tracking the borrow-optimization TODO.

If this is intentionally a placeholder, linking it to an issue will make the follow‑up easier. I can help draft the issue or a concrete implementation plan if you want.

Comment on lines +724 to +727
for (i, info) in block.instructions.iter().enumerate() {
let Some(instr) = info.instr.real() else {
continue;
};
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

Handle pseudo-instructions in the borrow analysis.

optimize_load_fast_borrow runs before pseudo lowering, but info.instr.real() skips pseudo ops (e.g., LoadClosure, StoreFastMaybeNull) that can push/pop. That can mis-model the stack and mark a LOAD_FAST as consumed when it actually survives to block end, which would make a future borrow implementation unsafe. Consider bailing out on pseudo ops or moving this pass after pseudo conversion.

🛡️ Conservative fix (skip block on pseudo ops)
-            let Some(instr) = info.instr.real() else {
-                continue;
-            };
+            let Some(instr) = info.instr.real() else {
+                // Pseudo ops can change stack depth; skip this block to stay conservative.
+                underflow = true;
+                break;
+            };
🤖 Prompt for AI Agents
In `@crates/codegen/src/ir.rs` around lines 724 - 727, The borrow-analysis in
optimize_load_fast_borrow currently ignores pseudo-ops by using
info.instr.real() and continuing, which can mis-model the stack (pseudo ops like
LoadClosure/StoreFastMaybeNull affect push/pop); change the logic to
conservatively bail out for the whole block when a pseudo-op is encountered:
inside the loop over block.instructions detect when info.instr.real() is None
and skip further processing of that block (e.g., set a flag and break/continue
to the next block) so the pass does not make unsafe assumptions about LOAD_FAST
liveness, or alternatively move optimize_load_fast_borrow to run after pseudo
lowering if feasible.

@youknowone youknowone merged commit 95abec4 into RustPython:main Jan 29, 2026
14 checks passed
@youknowone youknowone deleted the load-ops branch January 29, 2026 10:33
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.

1 participant