Instrumented instruction#7212
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a runtime monitoring subsystem (sys.monitoring), per-code instrumentation metadata and versioning, interpreter- and frame-level event emission with instrumented-opcode paths, codegen/IR changes to preserve line markers, bytecode/instruction helpers, and new VM/global state fields for coordination. Changes
Sequence Diagram(s)sequenceDiagram
participant Tool as Tool/Debugger
participant SysMon as SysMonitoring
participant VM as VM (PyGlobalState)
participant Code as PyCode
participant Frame as FrameExecutor
Tool->>SysMon: use_tool_id(tool_id, name)
Tool->>SysMon: register_callback(event, func)
SysMon->>VM: update_events_mask()
VM->>Code: check instrumentation_version
alt version mismatch
VM->>Code: instrument_code(events)
Code->>Code: rewrite bytecode (insert instrumented ops)
Code->>VM: bump instrumentation_version
end
VM->>Frame: execute bytecode
Frame->>SysMon: fire_py_start()
loop per instruction
Frame->>Code: consult monitoring_data
alt instrumented opcode
Frame->>SysMon: fire_instruction()/fire_line()/fire_call()/fire_branch...
SysMon->>Tool: invoke callback(event, payload)
Frame->>Frame: remap to base opcode and resume
else normal opcode
Frame->>Frame: execute opcode normally
end
end
Frame->>SysMon: fire_py_return()
SysMon->>Tool: invoke callback(PY_RETURN)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Sorry, something went wrong.
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] lib: cpython/Lib/opcode.py dependencies:
dependent tests: (39 tests)
[ ] lib: cpython/Lib/bdb.py dependencies:
dependent tests: (1 tests)
[x] test: cpython/Lib/test/test_compile.py (TODO: 31) dependencies: dependent tests: (no tests depend on compile) [x] lib: cpython/Lib/dis.py dependencies:
dependent tests: (56 tests)
[ ] test: cpython/Lib/test/test_monitoring.py (TODO: 13) dependencies: dependent tests: (no tests depend on monitoring) [x] test: cpython/Lib/test/test_sys.py (TODO: 8) dependencies: dependent tests: (215 tests)
Legend:
|
Sorry, something went wrong.
cff3dfc to
c975125
Compare
February 24, 2026 11:37
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
crates/vm/src/frame.rs (1)
2568-2584: Consider consolidating identical jump instrumentation arms.
InstrumentedJumpForwardandInstrumentedJumpBackwardare identical aside from the label variable. You can merge them to keep the match smaller and avoid duplication.Refactor sketch
- Instruction::InstrumentedJumpForward => { - let src_offset = (self.lasti() - 1) * 2; - let target = bytecode::Label::from(u32::from(arg)); - self.jump(target); - if self.monitoring_mask & monitoring::EVENT_JUMP != 0 { - monitoring::fire_jump(vm, self.code, src_offset, target.0 * 2)?; - } - Ok(None) - } - Instruction::InstrumentedJumpBackward => { - let src_offset = (self.lasti() - 1) * 2; - let dest = bytecode::Label::from(u32::from(arg)); - self.jump(dest); - if self.monitoring_mask & monitoring::EVENT_JUMP != 0 { - monitoring::fire_jump(vm, self.code, src_offset, dest.0 * 2)?; - } - Ok(None) - } + Instruction::InstrumentedJumpForward | Instruction::InstrumentedJumpBackward => { + let src_offset = (self.lasti() - 1) * 2; + let target = bytecode::Label::from(u32::from(arg)); + self.jump(target); + if self.monitoring_mask & monitoring::EVENT_JUMP != 0 { + monitoring::fire_jump(vm, self.code, src_offset, target.0 * 2)?; + } + Ok(None) + }As per coding guidelines, "When branches differ only in a value but share common logic, extract the differing value first, then call the common logic once to avoid duplicate code."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` around lines 2568 - 2584, The two match arms for Instruction::InstrumentedJumpForward and Instruction::InstrumentedJumpBackward are identical except for the label variable; consolidate them by matching both variants together (Instruction::InstrumentedJumpForward | Instruction::InstrumentedJumpBackward) and compute the label once from arg (e.g., let label = bytecode::Label::from(u32::from(arg))), then reuse the shared logic: compute src_offset via self.lasti(), call self.jump(label), and conditionally call monitoring::fire_jump(vm, self.code, src_offset, label.0 * 2) using the existing monitoring_mask check to avoid duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/compiler-core/src/bytecode.rs`:
- Around line 102-128: The computation end: start + size in
decode_exception_table can overflow on malformed input; replace the unchecked
addition with a checked add (e.g., start.checked_add(size)) and abort decoding
when it returns None (break/return) so you never push an entry where end <
start; update the ExceptionTableEntry creation to use the checked result and
keep the rest of the loop logic (read_varint_with_start/read_varint and
depth_lasti handling) unchanged.
- Around line 401-415: The safety docs for CodeUnits::replace_op must state that
mutation occurs through a shared reference and therefore the caller must ensure
exclusive access (no other threads or aliases access the same CodeUnits) or that
interior mutability/synchronization is used (e.g. stored in an
UnsafeCell/Mutex/RwLock). Update the unsafe fn replace_op documentation to
explicitly require exclusive/unique ownership or external synchronization to
prevent data races, and mention that the caller must guarantee the CodeUnit
memory is not concurrently accessed while calling replace_op; reference the
CodeUnits type and the replace_op method in the wording.
In `@crates/compiler-core/src/bytecode/instruction.rs`:
- Around line 422-511: The debug assertion in to_instrumented currently checks
self.to_base().is_none() which misses InstrumentedLine and
InstrumentedInstruction; change the guard to
debug_assert!(!self.is_instrumented(), "to_instrumented called on
already-instrumented opcode {self:?}") so it uses the canonical
is_instrumented() check; update the assertion message if desired, then run cargo
fmt, cargo clippy, and perform a clean bytecode build (remove relevant target
build dirs and any .pyc files) to ensure the bytecode changes compile and lint
cleanly.
In `@crates/vm/src/builtins/code.rs`:
- Around line 916-979: co_branches assumes a fixed fall-through of (i + 2) * 2
for POP_JUMP_IF_* but NOT_TAKEN may be absent; update the POP_JUMP_IF_* arm in
co_branches to compute the fall-through from the actual next opcode instead of
using (i + 2): inspect instructions.get(i + 1).map(|u| u.op) and if that opcode
is the NOT_TAKEN sentinel (or its instrumented variant) use (i + 2) as the
fall-through index, otherwise use (i + 1); keep the jump target as oparg * 2 and
return the tuple creation logic unchanged for co_branches and the
Instruction::PopJumpIfFalse/PopJumpIfTrue/PopJumpIfNone/PopJumpIfNotNone match.
In `@crates/vm/src/builtins/frame.rs`:
- Around line 74-104: set_f_lineno currently only ensures the requested line
exists in self.code.locations and then calls self.set_lasti(idx as u32) without
validating the resulting instruction index; add explicit bounds checks and
execution-safety checks before calling set_lasti: verify the computed idx is
within the valid instruction range (compare against the code's
instruction/bytecode length or an explicit instruction count on self.code),
return a ValueError if out of range, and additionally refuse or validate targets
that would jump into the middle of an exception-handler setup or otherwise
unsafe region (use the code's exception table/handler metadata on self.code to
detect unsafe indices) so that set_lasti is only called for safe, in-bounds
instruction indices.
In `@crates/vm/src/frame.rs`:
- Around line 261-270: set_lasti currently only updates the atomic lasti when
threading is enabled, leaving the mutex-protected FrameState.lasti out of sync
with external f_lasti and ExecutingFrame::lasti; modify set_lasti to also
acquire the state mutex and update FrameState.lasti (the same field read by
ExecutingFrame::lasti) when #[cfg(feature = "threading")] so both the atomic
(self.lasti.store(...)) and the mutex-protected value are kept in sync, ensuring
reads via ExecutingFrame::lasti observe external updates.
---
Nitpick comments:
In `@crates/vm/src/frame.rs`:
- Around line 2568-2584: The two match arms for
Instruction::InstrumentedJumpForward and Instruction::InstrumentedJumpBackward
are identical except for the label variable; consolidate them by matching both
variants together (Instruction::InstrumentedJumpForward |
Instruction::InstrumentedJumpBackward) and compute the label once from arg
(e.g., let label = bytecode::Label::from(u32::from(arg))), then reuse the shared
logic: compute src_offset via self.lasti(), call self.jump(label), and
conditionally call monitoring::fire_jump(vm, self.code, src_offset, label.0 * 2)
using the existing monitoring_mask check to avoid duplication.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (8)
Lib/opcode.pyis excluded by!Lib/**Lib/test/dis_module.pyis excluded by!Lib/**Lib/test/profilee.pyis excluded by!Lib/**Lib/test/test_compile.pyis excluded by!Lib/**Lib/test/test_dis.pyis excluded by!Lib/**Lib/test/test_monitoring.pyis excluded by!Lib/**Lib/test/test_peepholer.pyis excluded by!Lib/**crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snapis excluded by!**/*.snap
📒 Files selected for processing (14)
.cspell.dict/cpython.txtcrates/codegen/src/compile.rscrates/codegen/src/ir.rscrates/compiler-core/src/bytecode.rscrates/compiler-core/src/bytecode/instruction.rscrates/vm/src/builtins/code.rscrates/vm/src/builtins/frame.rscrates/vm/src/frame.rscrates/vm/src/stdlib/sys.rscrates/vm/src/stdlib/sys/monitoring.rscrates/vm/src/types/slot.rscrates/vm/src/vm/interpreter.rscrates/vm/src/vm/mod.rsscripts/update_lib/deps.py
Sorry, something went wrong.
ShaharNaveh
left a comment
There was a problem hiding this comment.
Great! we now have instrumented opcodes 🥳
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 3
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)
513-527: ⚠️ Potential issue | 🟠 Major
label_arg()does not handle instrumented jump variants, causinglabel_targets()to miss jump targets.When monitoring is enabled, instructions are replaced with
Instrumented*variants in the bytecode (viareplace_op()in monitoring.rs). However,label_arg()only matches base variants (JumpForward,JumpBackward,PopJumpIf*,ForIter,Send), so it returnsNoneforInstrumentedJumpForward,InstrumentedJumpBackward,InstrumentedForIter,InstrumentedPopJumpIf*, andInstrumentedSend. Sincelabel_targets()iteratesself.instructionsand callslabel_arg()on each instruction, it will silently miss jump targets when instrumented bytecode is present. This affects bytecode display and JIT compilation.Delegate to
to_base()(which already handles all these variants correctly) to ensure instrumented instructions are properly analyzed:Suggested implementation
fn label_arg(&self) -> Option<Arg<Label>> { match self { Self::JumpBackward { target: l } | Self::JumpBackwardNoInterrupt { target: l } | Self::JumpForward { target: l } | Self::PopJumpIfTrue { target: l } | Self::PopJumpIfFalse { target: l } | Self::PopJumpIfNone { target: l } | Self::PopJumpIfNotNone { target: l } | Self::ForIter { target: l } | Self::Send { target: l } => Some(*l), _ => { if let Some(base) = self.to_base() { base.label_arg() } else { None } } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/compiler-core/src/bytecode/instruction.rs` around lines 513 - 527, label_arg() on Instruction (impl InstructionMetadata) only matches base jump/send variants and thus skips Instrumented* variants; update label_arg() to delegate to self.to_base() for non-matching cases so instrumented instructions return the same Arg<Label> as their base. Specifically, inside Instruction::label_arg(), keep the existing match for JumpBackward/JumpForward/PopJumpIf*/ForIter/Send, and in the _ arm call self.to_base() and, if Some(base), return base.label_arg(), otherwise None; this ensures label_targets() (which iterates self.instructions) and monitoring.replace_op() handled instrumented variants are analyzed correctly.
♻️ Duplicate comments (2)
crates/compiler-core/src/bytecode.rs (1)
119-125: ⚠️ Potential issue | 🟡 MinorUnchecked
start + sizecan wrap on malformed input.Line 121 computes
end: start + sizewithout overflow protection. On a crafted/corrupted varint table, this can produceend < start, yielding a nonsensical entry. The sibling functionfind_exception_handler(line 85) has the same issue, but sincedecode_exception_tablereturns entries for external consumption, a bogusendis more dangerous here.Suggested fix
+ let end = match start.checked_add(size) { + Some(end) => end, + None => break, + }; entries.push(ExceptionTableEntry { start, - end: start + size, + end, target,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/compiler-core/src/bytecode.rs` around lines 119 - 125, The code unconditionally computes end = start + size which can overflow on malformed input; in decode_exception_table (and similarly in find_exception_handler) replace the unchecked addition with a checked addition (use start.checked_add(size)) and handle the None case by returning an error (or skipping the malformed entry) instead of producing an end < start; ensure the handling is consistent for ExceptionTableEntry creation (and its depth/push_lasti logic) so no bogus entries are emitted to external consumers.crates/vm/src/frame.rs (1)
261-270:FrameState.lastistill not synced in threading builds.
set_lastionly stores to the atomicself.lasti, leavingself.state.lasti(the value read byExecutingFrame::lasti()) stale in threading builds.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` around lines 261 - 270, In threading builds set_lasti currently only writes the atomic self.lasti, leaving self.state.lasti stale; update set_lasti to write both places so ExecutingFrame::lasti() (which reads self.state.lasti) sees the new value — i.e., in the #[cfg(feature = "threading")] branch keep the atomic store(self.lasti.store(val, ...)) and also update the backing FrameState.lasti (self.state.lasti.set(val) or assign accordingly) so both the atomic and self.state.lasti are kept in sync.
🧹 Nitpick comments (3)
crates/vm/src/frame.rs (3)
395-397:monitoring_maskandprev_lineset but never read inyield_from_target.The
ExecutingFramebuilt here only callsexec.yield_from_target()which neither readsmonitoring_masknorprev_line. These initializations are dead code in this context.♻️ Proposed cleanup
- monitoring_mask: 0, - prev_line: 0,Remove the two dead fields, or if
ExecutingFramerequires them to be set (struct exhaustiveness), document why.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` around lines 395 - 397, The ExecutingFrame initialization sets monitoring_mask and prev_line but yield_from_target never reads them, so drop these dead initializations: remove monitoring_mask and prev_line from the ExecutingFrame literal (or from the struct if they truly are unused) and rebuild; if ExecutingFrame must maintain those fields for exhaustiveness, either derive/implement Default and use ..Default::default() or add a clear comment in the ExecutingFrame/ yield_from_target caller explaining why the fields are intentionally set despite not being read. Ensure references to monitoring_mask, prev_line, ExecutingFrame, and yield_from_target are updated accordingly.
503-511: Redundantprev_lineupdate when tracing already set it.When the tracing block (lines 490-497) fires and updates
self.prev_line, the block at lines 503-511 unconditionally writes the same value again. The second write is always a no-op but adds unnecessary noise.♻️ Proposed cleanup
- if !matches!( - op, - Instruction::Resume { .. } - | Instruction::ExtendedArg - | Instruction::InstrumentedLine - ) && let Some((loc, _)) = self.code.locations.get(idx) - { - self.prev_line = loc.line.get() as u32; - } + if !vm.use_tracing.get() + && !matches!( + op, + Instruction::Resume { .. } + | Instruction::ExtendedArg + | Instruction::InstrumentedLine + ) + { + if let Some((loc, _)) = self.code.locations.get(idx) { + self.prev_line = loc.line.get() as u32; + } + }Or simply remove the
else-redundant guard; both writes are idempotent. Consider adding a short comment explaining the write's purpose (LINE event deduplication inInstrumentedLine).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` around lines 503 - 511, The block that unconditionally sets self.prev_line using self.code.locations.get(idx) after the tracing branch is redundant because the tracing branch (which also updates self.prev_line) already wrote the same value; remove the second update in the matching block (the code that checks !matches!(op, Instruction::Resume | Instruction::ExtendedArg | Instruction::InstrumentedLine) && let Some((loc, _)) = self.code.locations.get(idx)) so self.prev_line is only set in the tracing branch, or convert that assignment into an else-branch of the tracing logic; also add a short comment by the remaining write (referencing self.prev_line and InstrumentedLine) explaining it's for LINE event deduplication.
2673-2684:offset.saturating_sub(2)inInstrumentedNotTakenassumes preceding instruction is always the conditional jump.The
src_offsetis computed as(lasti - 1) * 2(this instruction's position), then reduced by 2 to point at the preceding instruction. This implicitly requires the compiler to always emitNOT_TAKENimmediately after its paired conditional jump with no intervening opcodes, and that the instruction width is always 2 bytes.This is currently guaranteed by the codegen, but it's worth a brief comment explaining the invariant to prevent future breakage.
📝 Suggested comment
Instruction::InstrumentedNotTaken => { if self.monitoring_mask & monitoring::EVENT_BRANCH_LEFT != 0 { let offset = (self.lasti() - 1) * 2; let dest_offset = self.lasti() * 2; + // NOT_TAKEN is always emitted immediately after its paired POP_JUMP_IF_*, + // so src_offset points at that conditional jump (offset - 2). monitoring::fire_branch_left( vm, self.code, offset.saturating_sub(2), dest_offset, )?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` around lines 2673 - 2684, Add a short explanatory comment immediately above the InstrumentedNotTaken handling block (Instruction::InstrumentedNotTaken) that documents the invariant: the NOT_TAKEN marker is emitted directly after its paired conditional jump with no intervening opcodes and all instruction slots are 2 bytes, which is why the src_offset is computed as (lasti - 1) * 2 and then adjusted with .saturating_sub(2) before calling monitoring::fire_branch_left; additionally consider a debug-time check (e.g., using debug_assert!) that self.lasti() >= 1 to catch violations early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/codegen/src/ir.rs`:
- Around line 260-267: The code uses std::collections::HashSet for non_nop_lines
and kept_nop_lines which breaks no_std builds; replace those with the
already-imported IndexSet so the crate remains no_std-compatible: change the
declarations for non_nop_lines and kept_nop_lines to use IndexSet (e.g.
IndexSet<_> and IndexSet<OneIndexed>) and adjust any type annotations/usages
accordingly (membership checks, .new(), .collect()) to use IndexSet APIs.
In `@crates/compiler-core/src/bytecode.rs`:
- Around line 401-418: The current unsafe mutation in CodeUnits::replace_op
casts self.0.as_ptr() to *mut CodeUnit and writes through a shared &self, which
is unsound; wrap the inner buffer in UnsafeCell (e.g., make CodeUnits hold
UnsafeCell<Box<[CodeUnit]>> or UnsafeCell<Vec<CodeUnit>>) so interior mutation
through &self is legal, update uses of self.0 to access the UnsafeCell (e.g.,
get a raw pointer via UnsafeCell::get) inside replace_op, and replace
write_volatile with core::ptr::write since your safety contract requires
exclusive access; ensure the function docs and safety comment reflect the new
UnsafeCell-based design and exclusive-access requirement.
In `@crates/vm/src/frame.rs`:
- Around line 3266-3307: The is_python_call detection is too strict (it only
checks callable.downcast_ref::<PyFunction>()) and mislabels wrapped Python
callables (PyBoundMethod, PyStaticMethod, PyPartial, etc.), causing duplicate
monitoring events; update execute_call_instrumented to replace the single-line
is_python_call check with a small helper (e.g., is_underlying_py_function or
is_python_callable) that recursively unwraps known wrapper types (PyBoundMethod,
PyStaticMethod, PyPartial, and any other delegating types in your codebase) to
inspect the wrapped callable and return true if the ultimate target is a
PyFunction, then use that helper where is_python_call is used (for
monitoring::fire_c_return and monitoring::fire_c_raise) so only true C calls
trigger C_RETURN/C_RAISE.
---
Outside diff comments:
In `@crates/compiler-core/src/bytecode/instruction.rs`:
- Around line 513-527: label_arg() on Instruction (impl InstructionMetadata)
only matches base jump/send variants and thus skips Instrumented* variants;
update label_arg() to delegate to self.to_base() for non-matching cases so
instrumented instructions return the same Arg<Label> as their base.
Specifically, inside Instruction::label_arg(), keep the existing match for
JumpBackward/JumpForward/PopJumpIf*/ForIter/Send, and in the _ arm call
self.to_base() and, if Some(base), return base.label_arg(), otherwise None; this
ensures label_targets() (which iterates self.instructions) and
monitoring.replace_op() handled instrumented variants are analyzed correctly.
---
Duplicate comments:
In `@crates/compiler-core/src/bytecode.rs`:
- Around line 119-125: The code unconditionally computes end = start + size
which can overflow on malformed input; in decode_exception_table (and similarly
in find_exception_handler) replace the unchecked addition with a checked
addition (use start.checked_add(size)) and handle the None case by returning an
error (or skipping the malformed entry) instead of producing an end < start;
ensure the handling is consistent for ExceptionTableEntry creation (and its
depth/push_lasti logic) so no bogus entries are emitted to external consumers.
In `@crates/vm/src/frame.rs`:
- Around line 261-270: In threading builds set_lasti currently only writes the
atomic self.lasti, leaving self.state.lasti stale; update set_lasti to write
both places so ExecutingFrame::lasti() (which reads self.state.lasti) sees the
new value — i.e., in the #[cfg(feature = "threading")] branch keep the atomic
store(self.lasti.store(val, ...)) and also update the backing FrameState.lasti
(self.state.lasti.set(val) or assign accordingly) so both the atomic and
self.state.lasti are kept in sync.
---
Nitpick comments:
In `@crates/vm/src/frame.rs`:
- Around line 395-397: The ExecutingFrame initialization sets monitoring_mask
and prev_line but yield_from_target never reads them, so drop these dead
initializations: remove monitoring_mask and prev_line from the ExecutingFrame
literal (or from the struct if they truly are unused) and rebuild; if
ExecutingFrame must maintain those fields for exhaustiveness, either
derive/implement Default and use ..Default::default() or add a clear comment in
the ExecutingFrame/ yield_from_target caller explaining why the fields are
intentionally set despite not being read. Ensure references to monitoring_mask,
prev_line, ExecutingFrame, and yield_from_target are updated accordingly.
- Around line 503-511: The block that unconditionally sets self.prev_line using
self.code.locations.get(idx) after the tracing branch is redundant because the
tracing branch (which also updates self.prev_line) already wrote the same value;
remove the second update in the matching block (the code that checks
!matches!(op, Instruction::Resume | Instruction::ExtendedArg |
Instruction::InstrumentedLine) && let Some((loc, _)) =
self.code.locations.get(idx)) so self.prev_line is only set in the tracing
branch, or convert that assignment into an else-branch of the tracing logic;
also add a short comment by the remaining write (referencing self.prev_line and
InstrumentedLine) explaining it's for LINE event deduplication.
- Around line 2673-2684: Add a short explanatory comment immediately above the
InstrumentedNotTaken handling block (Instruction::InstrumentedNotTaken) that
documents the invariant: the NOT_TAKEN marker is emitted directly after its
paired conditional jump with no intervening opcodes and all instruction slots
are 2 bytes, which is why the src_offset is computed as (lasti - 1) * 2 and then
adjusted with .saturating_sub(2) before calling monitoring::fire_branch_left;
additionally consider a debug-time check (e.g., using debug_assert!) that
self.lasti() >= 1 to catch violations early.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Lib/test/test_peepholer.pyis excluded by!Lib/**
📒 Files selected for processing (4)
crates/codegen/src/ir.rscrates/compiler-core/src/bytecode.rscrates/compiler-core/src/bytecode/instruction.rscrates/vm/src/frame.rs
Sorry, something went wrong.
97bd821 to
56bb7b8
Compare
February 26, 2026 09:05
|
Code has been automatically formatted The code in this PR has been formatted using:
git pull origin instrumented-instruction |
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
crates/vm/src/frame.rs (2)
261-270: ⚠️ Potential issue | 🟠 Major
set_lastistill leavesFrameState.lastistale in threading mode.Line 264 updates only the atomic field. But
ExecutingFrame::lasti()readsstate.lasti(Line 469), so externalf_lastiwrites can be ignored while executing.Suggested fix
pub fn set_lasti(&self, val: u32) { #[cfg(feature = "threading")] { self.lasti.store(val, atomic::Ordering::Relaxed); + self.state.lock().lasti = val; } #[cfg(not(feature = "threading"))] { self.lasti.set(val); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` around lines 261 - 270, The threading build currently updates only the atomic lasti in Frame::set_lasti, leaving FrameState.lasti stale and causing ExecutingFrame::lasti to read an outdated value; change Frame::set_lasti so that in the #[cfg(feature = "threading")] branch it writes the atomic (self.lasti.store(...)) and also updates the non-atomic backing field (state.lasti or self.state.lasti.set/assign) so both places remain consistent, or alternatively change ExecutingFrame::lasti to read the atomic lasti; update the code paths involving set_lasti, FrameState.lasti, and ExecutingFrame::lasti accordingly.
3271-3271: ⚠️ Potential issue | 🟠 Major
is_python_callstill misclassifies wrapped Python callables as C calls.Line 3271 only matches bare
PyFunction. Wrappers (e.g. bound/static/partial-like delegates) can still execute Python frames, which can lead to wrong C_* event classification.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` at line 3271, is_python_call currently only checks callable.downcast_ref::<PyFunction>(), misclassifying wrapped Python callables; change the detection to treat any object whose __call__ is a PyFunction as a Python call too by replacing the single downcast_ref check with a two-step check: first keep callable.downcast_ref::<PyFunction>().is_some(), and if not, attempt to retrieve callable.getattr("__call__") and test whether that attribute downcasts to PyFunction (handling/propagating errors safely), so bound/static/partial-like wrappers are classified as Python calls; reference the local variables is_python_call and callable when making the change.
🧹 Nitpick comments (1)
crates/vm/src/builtins/code.rs (1)
916-987: LGTM! Previous NOT_TAKEN concern addressed; minor robustness consideration.The implementation correctly handles de-instrumentation via
to_base()and properly checks forNotTakenpresence (lines 958-965) before computing the fall-through offset.One minor consideration: the
EndAsyncForbranch (lines 970-971) performsnext_i - oparg as usizewhich could panic on subtraction underflow if the bytecode is malformed. While valid bytecode should never trigger this, usingsaturating_suborchecked_subwould make this more defensive against corrupted inputs.Optional defensive fix
Instruction::EndAsyncFor => { // src = END_SEND position (next_i - oparg) let next_i = i + 1; - let src_i = next_i - oparg as usize; + let src_i = next_i.saturating_sub(oparg as usize); (src_i * 2, (src_i + 2) * 2, next_i * 2) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/builtins/code.rs` around lines 916 - 987, co_branches currently computes src_i for the EndAsyncFor case as next_i - oparg as usize which can underflow on malformed bytecode; update the EndAsyncFor branch in co_branches to use a safe subtraction (e.g., saturating_sub or checked_sub with an early continue or error) when deriving src_i from next_i and oparg so you avoid panics on underflow while preserving existing behavior for valid bytecode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/compiler-core/src/bytecode/instruction.rs`:
- Around line 464-469: Update the doc comment for the to_base method to clarify
that returning None means “there is no corresponding base opcode” rather than
implying the variant is not instrumented; explicitly note that some instrumented
variants (e.g., InstrumentedLine and InstrumentedInstruction) do not have base
equivalents and will return None, and advise callers not to use
to_base().is_some() as a general instrumentation check but to check the opcode
variant (or provide/point to an is_instrumented helper) when determining
instrumentation. Reference the to_base function and the
InstrumentedLine/InstrumentedInstruction variants in the comment so callers
understand the intended contract.
In `@crates/vm/src/frame.rs`:
- Around line 3274-3304: call_arg0 is only computed when monitoring::EVENT_CALL
is enabled, which prevents emitting C_RETURN/C_RAISE when only those events are
requested; compute/extract the argument whenever any of the relevant events are
enabled (EVENT_CALL, EVENT_C_RETURN, EVENT_C_RAISE) by checking
self.monitoring_mask for that combined set and calling
final_args.args.first().cloned().unwrap_or_else(|| monitoring::get_missing(vm))
into a variable (e.g., call_arg0) before invoking callable.call; then, only call
monitoring::fire_call when EVENT_CALL is set, but still pass the extracted
call_arg0 to monitoring::fire_c_return and monitoring::fire_c_raise so those
events are emitted even if EVENT_CALL was disabled (use existing symbols:
monitoring::fire_call, monitoring::fire_c_return, monitoring::fire_c_raise,
monitoring::get_missing, call_arg0, self.lasti(), callable.call).
---
Duplicate comments:
In `@crates/vm/src/frame.rs`:
- Around line 261-270: The threading build currently updates only the atomic
lasti in Frame::set_lasti, leaving FrameState.lasti stale and causing
ExecutingFrame::lasti to read an outdated value; change Frame::set_lasti so that
in the #[cfg(feature = "threading")] branch it writes the atomic
(self.lasti.store(...)) and also updates the non-atomic backing field
(state.lasti or self.state.lasti.set/assign) so both places remain consistent,
or alternatively change ExecutingFrame::lasti to read the atomic lasti; update
the code paths involving set_lasti, FrameState.lasti, and ExecutingFrame::lasti
accordingly.
- Line 3271: is_python_call currently only checks
callable.downcast_ref::<PyFunction>(), misclassifying wrapped Python callables;
change the detection to treat any object whose __call__ is a PyFunction as a
Python call too by replacing the single downcast_ref check with a two-step
check: first keep callable.downcast_ref::<PyFunction>().is_some(), and if not,
attempt to retrieve callable.getattr("__call__") and test whether that attribute
downcasts to PyFunction (handling/propagating errors safely), so
bound/static/partial-like wrappers are classified as Python calls; reference the
local variables is_python_call and callable when making the change.
---
Nitpick comments:
In `@crates/vm/src/builtins/code.rs`:
- Around line 916-987: co_branches currently computes src_i for the EndAsyncFor
case as next_i - oparg as usize which can underflow on malformed bytecode;
update the EndAsyncFor branch in co_branches to use a safe subtraction (e.g.,
saturating_sub or checked_sub with an early continue or error) when deriving
src_i from next_i and oparg so you avoid panics on underflow while preserving
existing behavior for valid bytecode.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Lib/test/test_peepholer.pyis excluded by!Lib/**
📒 Files selected for processing (5)
crates/codegen/src/ir.rscrates/compiler-core/src/bytecode.rscrates/compiler-core/src/bytecode/instruction.rscrates/vm/src/builtins/code.rscrates/vm/src/frame.rs
Sorry, something went wrong.
d647b47 to
f17b4da
Compare
February 26, 2026 12:37
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (4)
crates/vm/src/frame.rs (2)
3269-3269: ⚠️ Potential issue | 🟠 Major
is_python_callmisclassifies wrapped Python callables.Line 3269 only detects bare
PyFunction, so wrappers (e.g. bound/static/partial wrappers around Python functions) can incorrectly emitC_RETURN/C_RAISEin addition to Python-level events.[suggested fix: unwrap known wrapper callables before classification, then check the terminal callable type.]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` at line 3269, The current is_python_call check only tests for PyFunction and thus misclassifies wrapped callables; add logic to unwrap known wrapper types (e.g. bound method wrappers, functools.partial-like wrappers, staticmethod/classmethod wrappers) before testing the terminal callable type. Implement or call a helper (e.g. unwrap_callable or similar) that strips PyMethod/partial/descriptor wrappers and then replace the direct downcast_ref::<PyFunction>() check with a downcast on the unwrapped object so is_python_call reflects the terminal callable type.
2531-2554: ⚠️ Potential issue | 🟠 Major
C_RETURN/C_RAISEemission is incorrectly tied toEVENT_CALL.At Line 3272 and Line 2531, arg capture is guarded by
EVENT_CALL; when a tool enables onlyEVENT_C_RETURN/EVENT_C_RAISE, those events can be dropped.Suggested fix pattern
- let call_arg0 = if self.monitoring_mask & monitoring::EVENT_CALL != 0 { + let monitor_call = self.monitoring_mask & monitoring::EVENT_CALL != 0; + let monitor_c_return = self.monitoring_mask & monitoring::EVENT_C_RETURN != 0; + let monitor_c_raise = self.monitoring_mask & monitoring::EVENT_C_RAISE != 0; + let need_arg0 = monitor_call || (monitor_c_return || monitor_c_raise); + let call_arg0 = if need_arg0 { ... } else { None }; - if let Some(arg0) = call_arg0 && !is_python_call { + if let Some(arg0) = call_arg0.clone() && !is_python_call && monitor_c_return { ... } - if let Some(arg0) = call_arg0 && !is_python_call { + if let Some(arg0) = call_arg0 && !is_python_call && monitor_c_raise { ... }Also applies to: 3271-3303
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` around lines 2531 - 2554, The code currently ties capturing call-site arguments to monitoring::EVENT_CALL (via monitoring_mask and call_args), so handlers that enable only EVENT_C_RETURN or EVENT_C_RAISE will not receive the captured args; change the logic so capturing of global_super/arg0 is performed whenever EVENT_C_RETURN or EVENT_C_RAISE is enabled (i.e. check monitoring::EVENT_C_RETURN|monitoring::EVENT_C_RAISE in addition to or instead of EVENT_CALL), ensure nth_value(2)/nth_value(1)/monitoring::get_missing are used to populate the tuple used by monitoring::fire_c_return and monitoring::fire_c_raise, and keep fire_call only under EVENT_CALL while using the newly-captured call_args for the C_RETURN/C_RAISE paths in load_super_attr success and Err branches (symbols: monitoring_mask, EVENT_CALL, EVENT_C_RETURN, EVENT_C_RAISE, call_args, fire_call, fire_c_return, fire_c_raise, load_super_attr, nth_value, get_missing).crates/compiler-core/src/bytecode/instruction.rs (1)
464-466: ⚠️ Potential issue | 🟡 MinorClarify
to_base()Nonesemantics.Line 465 currently implies
Nonemeans “not instrumented,” butInstruction::InstrumentedLineandInstruction::InstrumentedInstructionare instrumented and also returnNone. Please document thatNonecan also mean “instrumented opcode without a base equivalent.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/compiler-core/src/bytecode/instruction.rs` around lines 464 - 466, Update the doc comment for the to_base() method to clarify the semantics of a None return: state that None is returned both for opcodes that are not instrumented and for instrumented opcodes that do not have a base equivalent (specifically mention Instruction::InstrumentedLine and Instruction::InstrumentedInstruction as examples of instrumented variants that also return None), so callers know None can mean either “not instrumented” or “instrumented without a base variant.”crates/compiler-core/src/bytecode.rs (1)
420-423: ⚠️ Potential issue | 🟡 MinorUnify the safety contract between
Derefandreplace_op.Lines 420-423 say shared reads are valid while
replace_opmutates, but Lines 433-434 require exclusive access with no concurrent reads/writes. These guarantees conflict; please keep a single contract (exclusive access) to avoid implying racy reads are permitted.#!/bin/bash # Verify the conflicting safety statements in CodeUnits. rg -n "Shared references to the slice are valid even while replace_op|exclusive access to the instruction buffer" crates/compiler-core/src/bytecode.rs -C2Also applies to: 433-435
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/compiler-core/src/bytecode.rs` around lines 420 - 423, Unify the safety contract by requiring exclusive access for both the Deref path and replace_op: update the unsafe comment in the Deref implementation (the unsafe { &*self.0.get() } block) to state that callers must ensure exclusive, no-concurrent-reads/writes access to the underlying instruction buffer (remove the line claiming shared reads are valid), and make the replace_op documentation/unsafe contract match (i.e., both must assert exclusive access to the instruction buffer); reference the Deref impl around the unsafe get() and the replace_op function so the two comments are consistent and no longer imply racy shared reads are permitted.
🧹 Nitpick comments (1)
crates/vm/src/stdlib/sys/monitoring.rs (1)
341-347: Redundant.take(len)in iterator chain.The iterator
code.code.instructions.iter()already yields exactlylenitems (sincelen = code.code.instructions.len()), making.take(len)unnecessary.Suggested simplification
for (i, unit) in code .code .instructions .iter() .enumerate() - .take(len) .skip(first_traceable) {The same applies at line 437-442.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/stdlib/sys/monitoring.rs` around lines 341 - 347, Remove the redundant .take(len) from the iterator chain(s) that enumerate code.code.instructions (where len = code.code.instructions.len())—e.g., in the loop using code.code.instructions.iter().enumerate().take(len).skip(first_traceable) remove .take(len) so the chain becomes enumerate().skip(first_traceable); do the same for the second occurrence referenced around the later loop (the other enumerate over code.code.instructions with .take(len)). This removes unnecessary truncation while preserving use of len and first_traceable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/codegen/src/compile.rs`:
- Around line 5190-5206: At the start of compile_for, capture the
for-statement's full source range (e.g., let stmt_range = stmt.range() or
equivalent) and then use that captured stmt_range wherever the code currently
calls iter.range() for loop-boundary attribution; specifically replace
iter.range() in the set_source_range calls before emitting
Instruction::EndAsyncFor / Instruction::EndFor / Instruction::PopIter and for
the implicit-return attribution so the EndFor/PopIter and implicit return are
attributed to the for statement line rather than the iterator expression. Ensure
the new stmt_range is used instead of iter.range() in both places and keep
preserving/restoring saved_range behavior as before.
In `@crates/vm/src/builtins/code.rs`:
- Around line 968-972: The subtraction in the Instruction::EndAsyncFor arm can
underflow when computing src_i (next_i - oparg); modify the arm in the code that
builds branch offsets to use checked_sub on next_i and oparg, validate the
result before computing (src_i * 2, (src_i + 2) * 2, next_i * 2), and skip or
return an error for entries where checked_sub returns None so malformed bytecode
cannot produce invalid offsets (reference Instruction::EndAsyncFor, next_i,
src_i, and oparg).
- Around line 925-927: co_branches() currently de-instruments opcodes using
unit.op.to_base() which loses InstrumentedLine/InstrumentedInstruction variants
and thus skips displaced branch opcodes; change the de-instrument step to look
up the original opcode from self.monitoring_data by the instruction index (the
same index used to read unit/op) and use that restored opcode for matching
(instead of or in addition to to_base()) when computing op and when performing
the target-op checks around the logic that uses unit.op and unit.arg (this also
applies to the code paths around the checks at the later target-op logic), so
branch tuples include instrumented variants and displaced branches are not
missed.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 886-903: The RERAISE_PENDING flag is set true before calling
fire(...) in fire_reraise, but if fire returns an Err the flag remains true and
suppresses future reraise events; change fire_reraise to capture the result of
fire(...) and, on error, reset RERAISE_PENDING.with(|f| f.set(false)) before
returning the Err so the flag is not left stuck; keep references to
RERAISE_PENDING, fire_reraise, fire, and EVENT_RERAISE when locating the change.
---
Duplicate comments:
In `@crates/compiler-core/src/bytecode.rs`:
- Around line 420-423: Unify the safety contract by requiring exclusive access
for both the Deref path and replace_op: update the unsafe comment in the Deref
implementation (the unsafe { &*self.0.get() } block) to state that callers must
ensure exclusive, no-concurrent-reads/writes access to the underlying
instruction buffer (remove the line claiming shared reads are valid), and make
the replace_op documentation/unsafe contract match (i.e., both must assert
exclusive access to the instruction buffer); reference the Deref impl around the
unsafe get() and the replace_op function so the two comments are consistent and
no longer imply racy shared reads are permitted.
In `@crates/compiler-core/src/bytecode/instruction.rs`:
- Around line 464-466: Update the doc comment for the to_base() method to
clarify the semantics of a None return: state that None is returned both for
opcodes that are not instrumented and for instrumented opcodes that do not have
a base equivalent (specifically mention Instruction::InstrumentedLine and
Instruction::InstrumentedInstruction as examples of instrumented variants that
also return None), so callers know None can mean either “not instrumented” or
“instrumented without a base variant.”
In `@crates/vm/src/frame.rs`:
- Line 3269: The current is_python_call check only tests for PyFunction and thus
misclassifies wrapped callables; add logic to unwrap known wrapper types (e.g.
bound method wrappers, functools.partial-like wrappers, staticmethod/classmethod
wrappers) before testing the terminal callable type. Implement or call a helper
(e.g. unwrap_callable or similar) that strips PyMethod/partial/descriptor
wrappers and then replace the direct downcast_ref::<PyFunction>() check with a
downcast on the unwrapped object so is_python_call reflects the terminal
callable type.
- Around line 2531-2554: The code currently ties capturing call-site arguments
to monitoring::EVENT_CALL (via monitoring_mask and call_args), so handlers that
enable only EVENT_C_RETURN or EVENT_C_RAISE will not receive the captured args;
change the logic so capturing of global_super/arg0 is performed whenever
EVENT_C_RETURN or EVENT_C_RAISE is enabled (i.e. check
monitoring::EVENT_C_RETURN|monitoring::EVENT_C_RAISE in addition to or instead
of EVENT_CALL), ensure nth_value(2)/nth_value(1)/monitoring::get_missing are
used to populate the tuple used by monitoring::fire_c_return and
monitoring::fire_c_raise, and keep fire_call only under EVENT_CALL while using
the newly-captured call_args for the C_RETURN/C_RAISE paths in load_super_attr
success and Err branches (symbols: monitoring_mask, EVENT_CALL, EVENT_C_RETURN,
EVENT_C_RAISE, call_args, fire_call, fire_c_return, fire_c_raise,
load_super_attr, nth_value, get_missing).
---
Nitpick comments:
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 341-347: Remove the redundant .take(len) from the iterator
chain(s) that enumerate code.code.instructions (where len =
code.code.instructions.len())—e.g., in the loop using
code.code.instructions.iter().enumerate().take(len).skip(first_traceable) remove
.take(len) so the chain becomes enumerate().skip(first_traceable); do the same
for the second occurrence referenced around the later loop (the other enumerate
over code.code.instructions with .take(len)). This removes unnecessary
truncation while preserving use of len and first_traceable.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (8)
Lib/opcode.pyis excluded by!Lib/**Lib/test/dis_module.pyis excluded by!Lib/**Lib/test/profilee.pyis excluded by!Lib/**Lib/test/test_compile.pyis excluded by!Lib/**Lib/test/test_dis.pyis excluded by!Lib/**Lib/test/test_monitoring.pyis excluded by!Lib/**Lib/test/test_peepholer.pyis excluded by!Lib/**crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snapis excluded by!**/*.snap
📒 Files selected for processing (14)
.cspell.dict/cpython.txtcrates/codegen/src/compile.rscrates/codegen/src/ir.rscrates/compiler-core/src/bytecode.rscrates/compiler-core/src/bytecode/instruction.rscrates/vm/src/builtins/code.rscrates/vm/src/builtins/frame.rscrates/vm/src/frame.rscrates/vm/src/stdlib/sys.rscrates/vm/src/stdlib/sys/monitoring.rscrates/vm/src/types/slot.rscrates/vm/src/vm/interpreter.rscrates/vm/src/vm/mod.rsscripts/update_lib/deps.py
🚧 Files skipped from review as they are similar to previous changes (5)
- crates/vm/src/types/slot.rs
- crates/vm/src/stdlib/sys.rs
- crates/vm/src/vm/mod.rs
- crates/vm/src/builtins/frame.rs
- scripts/update_lib/deps.py
Sorry, something went wrong.
d0d1d1e to
39c06eb
Compare
February 26, 2026 17:11
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
crates/vm/src/builtins/code.rs (1)
925-927: ⚠️ Potential issue | 🟠 MajorRestore displaced opcodes from
monitoring_datainco_branches().Line 926 only uses
to_base(), soInstrumentedLine/InstrumentedInstructioncannot be mapped back to their original opcodes. That makes branch extraction incomplete on instrumented code, including target checks at Line 942 and NOT_TAKEN fallthrough detection at Line 961.Proposed fix
pub fn co_branches(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> { use bytecode::Instruction; let instructions = &self.code.instructions; + let monitoring = self.monitoring_data.lock(); + let original_op = |idx: usize, op: Instruction| -> Instruction { + match op { + Instruction::InstrumentedLine => monitoring + .as_ref() + .and_then(|m| m.line_opcodes.get(idx).copied()) + .filter(|b| *b != 0) + .and_then(|b| Instruction::try_from(b).ok()) + .unwrap_or(op), + Instruction::InstrumentedInstruction => monitoring + .as_ref() + .and_then(|m| m.per_instruction_opcodes.get(idx).copied()) + .filter(|b| *b != 0) + .and_then(|b| Instruction::try_from(b).ok()) + .unwrap_or(op), + _ => op.to_base().unwrap_or(op), + } + }; let mut branches = Vec::new(); let mut extended_arg: u32 = 0; for (i, unit) in instructions.iter().enumerate() { - let op = unit.op.to_base().unwrap_or(unit.op); + let op = original_op(i, unit.op); let raw_arg = u32::from(u8::from(unit.arg)); @@ - let right = if matches!( - instructions.get(target).map(|u| u.op), + let right = if matches!( + instructions.get(target).map(|u| original_op(target, u.op)), Some(Instruction::EndFor) | Some(Instruction::InstrumentedEndFor) ) { @@ let next_op = instructions .get(i + 1) - .map(|u| u.op.to_base().unwrap_or(u.op)); + .map(|u| original_op(i + 1, u.op));Also applies to: 942-945, 958-961
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/builtins/code.rs` around lines 925 - 927, In co_branches(), when de-instrumenting opcodes (currently using unit.op.to_base().unwrap_or(unit.op)), consult monitoring_data to restore any displaced InstrumentedLine/InstrumentedInstruction back to their original opcode first (e.g., look up the original opcode by the unit's offset/index in monitoring_data and use it), then fall back to to_base() and finally unit.op; apply the same restoration logic at the other locations that compute branch targets and NOT_TAKEN fallthrough (the checks around the target computation and fallthrough detection currently at the later blocks referenced in the comment) so instrumented instructions map correctly to their original opcodes.crates/vm/src/frame.rs (1)
3378-3379: ⚠️ Potential issue | 🟠 MajorUnwrap delegated Python callables before deciding C-return/C-raise emission.
Line 3378 only recognizes bare
PyFunction. Wrapper callables that delegate to Python functions can be misclassified as C calls, which can produce incorrect/duplicate monitoring events on Line 3396 and Line 3406.Also applies to: 3396-3410
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/frame.rs` around lines 3378 - 3379, The code currently sets is_python_call via callable.downcast_ref::<PyFunction>().is_some(), which misclassifies wrapper/delegating callables as non-Python and causes incorrect duplicate monitoring events at the C-return/C-raise emission sites; change the logic that computes is_python_call to first unwrap delegated Python callables (detect known wrapper/proxy types and obtain their inner target, or use the existing unwrap utility if present) and then test the unwrapped value with downcast_ref::<PyFunction>() so the subsequent C-return/C-raise emission branches (the code paths that emit the monitoring events) correctly treat delegated Python functions as Python calls.
🧹 Nitpick comments (2)
crates/compiler-core/src/bytecode/instruction.rs (1)
435-513: Consider a single source of truth for opcode pair mappings.Line 440 onward and Line 473 onward duplicate the same mapping pairs in opposite directions. This is easy to desync on future opcode additions. Consider defining pairs once (macro/table) and deriving both
to_instrumentedandto_basematches from it.As per coding guidelines: "When branches differ only in a value but share common logic, extract the differing value first, then call the common logic once to avoid duplicate code."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/compiler-core/src/bytecode/instruction.rs` around lines 435 - 513, The two match lists in to_instrumented and to_base duplicate opcode pair mappings and should be centralized; create a single source-of-truth mapping (e.g., a static array, const slice, or macro) that lists tuples of (base_variant, instrumented_variant) and then implement to_instrumented and to_base by looking up that mapping (construct the base variants with Arg::marker() where needed and match on variant discriminants or pattern keys). Update to_instrumented to return the instrumented variant from the shared table and to_base to return the base variant (using Arg::marker() for typed fields) so new opcode pairs are added only once and both functions derive their behavior from the same mapping.crates/vm/src/stdlib/sys/monitoring.rs (1)
230-230: Replace decorative section separators with plain comments.Please remove
// === ... ===separators and keep short plain comments/doc-comments only.As per coding guidelines "Do not add decorative section separators (e.g.
// -----------,// ===,/* *** */). Use///doc-comments or short//comments only when they add value".Also applies to: 277-277
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/stdlib/sys/monitoring.rs` at line 230, Replace the decorative section separator comment "// === Phase 1-3: De-instrument all layers (outermost first) ===" (and the similar "// === ... ===" at the other occurrence) with a short plain comment or doc-comment—e.g., change to "// Phase 1-3: De-instrument all layers (outermost first)" or "/// Phase 1-3: De-instrument all layers"—so remove the "===...===" decoration; update the comment in the surrounding function/section in monitoring.rs where that text appears to follow the project's short/plain comment guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/vm/src/builtins/frame.rs`:
- Around line 169-180: The loop that accumulates EXTENDED_ARG prefixes can leave
i == len if the bytecode ends with an ExtendedArg, causing the final access
instructions[i] to panic; update the logic around Instruction::ExtendedArg in
the frame analysis (the while handling of Instruction::ExtendedArg, variables
instructions, i, len, oparg, stacks, next_stack) to guard the trailing case by
checking i < len before doing the final oparg combine and accessing
instructions[i] (e.g., break out and handle the incomplete EXTENDED_ARG safely
or skip the final combination when i >= len) so no out-of-bounds indexing
occurs.
- Around line 181-183: mark_stacks() currently calls
opcode.to_base().unwrap_or(opcode) which leaves
InstrumentedLine/InstrumentedInstruction placeholders (stack effect 0,0) when
to_base() returns None; instead have mark_stacks consult the real opcode stored
in CoMonitoringData (from PyCode.monitoring_data) to compute correct stack
effects. Update the mark_stacks signature to accept a reference to the
monitoring data (or a &PyCode that exposes monitoring_data), and where you
currently call to_base()/unwrap_or(opcode) (and the similar spots noted around
lines 243-256 and 483-590) add logic to look up the underlying opcode in
monitoring_data and use that opcode’s stack effect when available; fall back to
opcode.to_base() only if monitoring data is absent. Ensure callers are updated
to pass the monitoring_data reference.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 691-712: The snapshot captures state.disable but doesn't ensure
the DISABLE sentinel exists, so if it was None a callback returning
sys.monitoring.DISABLE won't be recognized; before taking the snapshot in the
block that builds (callbacks, disable_sentinel) (the code reading state.disable
and producing disable_sentinel) ensure state.disable is initialized (create and
store the sentinel PyObjectRef for sys.monitoring.DISABLE when None) then clone
it into disable_sentinel; apply the same initialization to the analogous
snapshot at lines 727-742 so both dispatch sites see a valid disable sentinel.
---
Duplicate comments:
In `@crates/vm/src/builtins/code.rs`:
- Around line 925-927: In co_branches(), when de-instrumenting opcodes
(currently using unit.op.to_base().unwrap_or(unit.op)), consult monitoring_data
to restore any displaced InstrumentedLine/InstrumentedInstruction back to their
original opcode first (e.g., look up the original opcode by the unit's
offset/index in monitoring_data and use it), then fall back to to_base() and
finally unit.op; apply the same restoration logic at the other locations that
compute branch targets and NOT_TAKEN fallthrough (the checks around the target
computation and fallthrough detection currently at the later blocks referenced
in the comment) so instrumented instructions map correctly to their original
opcodes.
In `@crates/vm/src/frame.rs`:
- Around line 3378-3379: The code currently sets is_python_call via
callable.downcast_ref::<PyFunction>().is_some(), which misclassifies
wrapper/delegating callables as non-Python and causes incorrect duplicate
monitoring events at the C-return/C-raise emission sites; change the logic that
computes is_python_call to first unwrap delegated Python callables (detect known
wrapper/proxy types and obtain their inner target, or use the existing unwrap
utility if present) and then test the unwrapped value with
downcast_ref::<PyFunction>() so the subsequent C-return/C-raise emission
branches (the code paths that emit the monitoring events) correctly treat
delegated Python functions as Python calls.
---
Nitpick comments:
In `@crates/compiler-core/src/bytecode/instruction.rs`:
- Around line 435-513: The two match lists in to_instrumented and to_base
duplicate opcode pair mappings and should be centralized; create a single
source-of-truth mapping (e.g., a static array, const slice, or macro) that lists
tuples of (base_variant, instrumented_variant) and then implement
to_instrumented and to_base by looking up that mapping (construct the base
variants with Arg::marker() where needed and match on variant discriminants or
pattern keys). Update to_instrumented to return the instrumented variant from
the shared table and to_base to return the base variant (using Arg::marker() for
typed fields) so new opcode pairs are added only once and both functions derive
their behavior from the same mapping.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Line 230: Replace the decorative section separator comment "// === Phase 1-3:
De-instrument all layers (outermost first) ===" (and the similar "// === ...
===" at the other occurrence) with a short plain comment or doc-comment—e.g.,
change to "// Phase 1-3: De-instrument all layers (outermost first)" or "///
Phase 1-3: De-instrument all layers"—so remove the "===...===" decoration;
update the comment in the surrounding function/section in monitoring.rs where
that text appears to follow the project's short/plain comment guideline.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Lib/test/test_sys_settrace.pyis excluded by!Lib/**
📒 Files selected for processing (5)
crates/compiler-core/src/bytecode/instruction.rscrates/vm/src/builtins/code.rscrates/vm/src/builtins/frame.rscrates/vm/src/frame.rscrates/vm/src/stdlib/sys/monitoring.rs
Sorry, something went wrong.
71ebc4a to
13a7676
Compare
February 26, 2026 19:26
13a7676 to
8c74023
Compare
February 26, 2026 19:37
Add CoMonitoringData with line_opcodes and per_instruction_opcodes side-tables. INSTRUMENTED_LINE and INSTRUMENTED_INSTRUCTION read original opcodes from side-tables and re-dispatch after firing events. - Add decode_exception_table() and CodeUnits::replace_op() - Add Instruction::to_instrumented/to_base/is_instrumented mappings - Three-phase instrument_code: de-instrument, re-instrument regular, then layer INSTRUCTION and LINE with side-table storage - Mark exception handler targets as line starts in LINE placement - InstrumentedLine resolves side-table chain atomically when wrapping InstrumentedInstruction - InstrumentedForIter fires both BRANCH_LEFT and BRANCH_RIGHT - Remove callback on DISABLE return for non-local events
- Implement set_f_lineno with stack analysis and deferred unwinding - Add Frame::set_lasti() for trace callback line jumps - Implement co_branches() on code objects - Clear _cache_format in opcode.py (no inline caches) - Fix getattro slot inheritance: preserve native slot from inherit_slots - Fix BRANCH_RIGHT src_offset in InstrumentedPopJumpIf* - Move lasti increment before line event for correct f_lineno - Skip RESUME instruction from generating line events - Defer stack pops via pending_stack_pops/pending_unwind_from_stack to avoid deadlock with state mutex - Fix ForIter exhaust target in mark_stacks to skip END_FOR - Prevent exception handler paths from overwriting normal-flow stacks - Replace #[cfg(feature = "threading")] duplication with PyAtomic<T> from rustpython_common::atomic (Radium-based unified API) - Remove expectedFailure from 31 now-passing jump tests
test_peepholer test_bdb
8c74023 to
4567c68
Compare
February 27, 2026 00:07
684001f
into
RustPython:main
Feb 27, 2026
* Update test_monitoring from CPython 3.14.3 * patch test_monitoring not to entirely skipped * impl sys.monitoring * Update test_compile from v3.14.3 * instrumented instructions * Implement side-table instrumented opcode execution Add CoMonitoringData with line_opcodes and per_instruction_opcodes side-tables. INSTRUMENTED_LINE and INSTRUMENTED_INSTRUCTION read original opcodes from side-tables and re-dispatch after firing events. - Add decode_exception_table() and CodeUnits::replace_op() - Add Instruction::to_instrumented/to_base/is_instrumented mappings - Three-phase instrument_code: de-instrument, re-instrument regular, then layer INSTRUCTION and LINE with side-table storage - Mark exception handler targets as line starts in LINE placement - InstrumentedLine resolves side-table chain atomically when wrapping InstrumentedInstruction - InstrumentedForIter fires both BRANCH_LEFT and BRANCH_RIGHT - Remove callback on DISABLE return for non-local events * mark failing tests * set_f_lineno, set_f_lasti, PyAtomic refactor - Implement set_f_lineno with stack analysis and deferred unwinding - Add Frame::set_lasti() for trace callback line jumps - Implement co_branches() on code objects - Clear _cache_format in opcode.py (no inline caches) - Fix getattro slot inheritance: preserve native slot from inherit_slots - Fix BRANCH_RIGHT src_offset in InstrumentedPopJumpIf* - Move lasti increment before line event for correct f_lineno - Skip RESUME instruction from generating line events - Defer stack pops via pending_stack_pops/pending_unwind_from_stack to avoid deadlock with state mutex - Fix ForIter exhaust target in mark_stacks to skip END_FOR - Prevent exception handler paths from overwriting normal-flow stacks - Replace #[cfg(feature = "threading")] duplication with PyAtomic<T> from rustpython_common::atomic (Radium-based unified API) - Remove expectedFailure from 31 now-passing jump tests * Unmark successful tests test_peepholer test_bdb --------- Co-authored-by: CPython Developers <>
close #7197 #7206
Summary by CodeRabbit
New Features
Improvements