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), instruments interpreter frames to emit many runtime events, extends VM global state with monitoring fields, preserves/per-line NOP markers and source ranges in the compiler, and adds nested #[pymodule] submodule initialization support. Changes
Sequence Diagram(s)sequenceDiagram
participant Frame as Frame (interpreter)
participant VM as VirtualMachine (global state)
participant Monitor as sys.monitoring
participant Tool as ToolCallback (Python)
Frame->>VM: compute code, offset, args
Frame->>Monitor: fire_* (e.g., fire_line(code, offset, line))
Monitor->>VM: read monitoring_events mask
alt tools enabled
Monitor->>Monitor: per-tool filtering (global/local/disabled)
Monitor->>Tool: invoke callback(args)
Tool-->>Monitor: return or raise
end
Monitor-->>Frame: return (may propagate exceptions)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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 |
d9eb9ed to
5cfece8
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
crates/vm/src/frame.rs (2)
715-730:⚠️ Potential issue | 🟡 MinorEXCEPTION_HANDLED callback error prevents handler setup — may differ from CPython.
If
fire_exception_handledreturnsErr(...)(because the callback raises), the?on line 729 causesunwind_blocksto return that error before setting up the exception handler (lines 732–752 never run). This means a buggy monitoring callback can prevent exception handlers from executing. CPython's exception handlers still run even if monitoring callbacks fail. Consider wrapping the fire call so errors are logged but don't abort handler setup.Proposed fix
if vm.state.monitoring_events.load() & crate::stdlib::sys::monitoring::EVENT_EXCEPTION_HANDLED != 0 { let byte_offset = offset * 2; let exc_obj: PyObjectRef = exception.clone().into(); - crate::stdlib::sys::monitoring::fire_exception_handled( + let _ = crate::stdlib::sys::monitoring::fire_exception_handled( vm, self.code, byte_offset, &exc_obj, - )?; + ); }🤖 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 715 - 730, The call to vm.fire_exception_handled(...) inside unwind_blocks (triggered when closing a generator in the is_gen_exit branch) must not use ? or propagate errors because a failing EXCEPTION_HANDLED callback can abort handler setup; change the code around the fire_exception_handled invocation so it catches Err(e) locally (e.g., match or if let Err(e) = vm.fire_exception_handled(...)) and logs or records the error instead of returning it, allowing the subsequent exception-handler setup code in unwind_blocks (the handler creation and attachment logic that follows the close_result path) to always run; reference vm.fire_exception_handled, unwind_blocks, is_gen_exit, and the local variable err when implementing the local error handling.
818-860:⚠️ Potential issue | 🟡 MinorInconsistent error handling for monitoring callbacks across
gen_throw.In the main
runloop,fire_raise/fire_py_unwinderrors replace the original exception (lines 595–619, 625–640). But ingen_throw, monitoring errors are silently discarded (let _ = ...at lines 825, 831, 854). This inconsistency could make debugging monitoring callbacks harder — errors ingen_throwpaths vanish silently while the same callback errors propagate elsewhere.Consider at least logging discarded errors, or using the same replacement pattern as the main loop.
🤖 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 818 - 860, The gen_throw path currently swallows errors from monitoring callbacks (fire_py_throw, fire_raise, fire_py_unwind) using "let _ = …", creating an inconsistency with the main run loop which replaces the original exception when a monitoring callback errors; change the gen_throw calls to handle the Result the same way as in run: capture the Result from monitoring::fire_py_throw / monitoring::fire_raise / monitoring::fire_py_unwind and if it Errs, propagate that error (return Err(err) or otherwise replace the current exception with the monitoring error) instead of discarding it; locate these calls in gen_throw (the block that checks vm.state.monitoring_events and calls fire_py_throw/fire_raise/fire_py_unwind) and apply the same replacement/propagation pattern used in run.
🧹 Nitpick comments (4)
crates/derive-impl/src/pymodule.rs (1)
840-847:meta.module()?is called twice; consider caching the result.Both lines 840 and 841 call
meta.module()?. While the result is deterministic, it parses metadata twice. You could also align this more closely with theClassItempattern (lines 742–749) that handles both extraction and fill in a single branch:♻️ Suggested simplification
- let has_module = meta.module()?.is_some(); - let module_name = meta.module()?.unwrap_or_else(|| args.context.name.clone()); - if !has_module { - let structseq_attr = &mut args.attrs[self.inner.index]; - structseq_attr.fill_nested_meta("module", || { - parse_quote! {module = `#module_name`} - })?; - } + let module_name = if let Some(module_name) = meta.module()? { + module_name + } else { + let module_name = args.context.name.clone(); + let structseq_attr = &mut args.attrs[self.inner.index]; + structseq_attr.fill_nested_meta("module", || { + parse_quote! {module = `#module_name`} + })?; + module_name + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/derive-impl/src/pymodule.rs` around lines 840 - 847, meta.module()? is being called twice (for has_module and module_name); cache its result into a local like let module_opt = meta.module()? and use that for both checks, then refactor to a single branch: if module_opt.is_none() { let structseq_attr = &mut args.attrs[self.inner.index]; structseq_attr.fill_nested_meta("module", || parse_quote! { module = `#module_value` })? } else set module_name from module_opt.unwrap_or_else(...). Replace both meta.module()? uses with the cached module_opt and update the module_name/has_module logic accordingly (referencing meta.module, has_module, module_name, structseq_attr, args.attrs, self.inner.index, and fill_nested_meta).crates/vm/src/vm/interpreter.rs (1)
127-128:AtomicCellis already imported — prefer the short name.Line 50 already imports
crossbeam_utils::atomic::AtomicCell, so line 128 can useAtomicCell::new(0)directly instead of the fully-qualified path.Proposed fix
monitoring: PyMutex::default(), - monitoring_events: crossbeam_utils::atomic::AtomicCell::new(0), + monitoring_events: AtomicCell::new(0),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/vm/interpreter.rs` around lines 127 - 128, Replace the fully-qualified type used to initialize monitoring_events with the already-imported short name: change the initializer from crossbeam_utils::atomic::AtomicCell::new(0) to AtomicCell::new(0) so the field monitoring_events uses the existing AtomicCell import; no other behavior change required.crates/vm/src/stdlib/sys/monitoring.rs (2)
356-356: Remove decorative section separator.Line 356 uses a decorative
// ── Event dispatch ──...separator. As per coding guidelines: "Do not add decorative section separators (e.g.// -----------,// ===,/* *** */)."-// ── Event dispatch ──────────────────────────────────────────────────────🤖 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 356, Remove the decorative comment line "// ── Event dispatch ──────────────────────────────────────────────────────" from crates/vm/src/stdlib/sys/monitoring.rs; locate the "Event dispatch" separator comment and delete that single line so the file conforms to the guideline disallowing decorative section separators (no other logic changes required).
265-275: Magic numbers for BRANCH event mapping.The hardcoded
18,8, and9for BRANCH, BRANCH_LEFT, and BRANCH_RIGHT event IDs are brittle and will silently break if the event enum order changes. Consider deriving these from the bitflags constants.Proposed fix
+const EVENT_ID_BRANCH: usize = MonitoringEvents::BRANCH.bits().trailing_zeros() as usize; +const EVENT_ID_BRANCH_LEFT: usize = MonitoringEvents::BRANCH_LEFT.bits().trailing_zeros() as usize; +const EVENT_ID_BRANCH_RIGHT: usize = MonitoringEvents::BRANCH_RIGHT.bits().trailing_zeros() as usize; if !vm.is_none(&func) { state.callbacks.insert((tool, event_id), func.clone()); - if event_id == 18 { - state.callbacks.insert((tool, 8), func.clone()); - state.callbacks.insert((tool, 9), func); + if event_id == EVENT_ID_BRANCH { + state.callbacks.insert((tool, EVENT_ID_BRANCH_LEFT), func.clone()); + state.callbacks.insert((tool, EVENT_ID_BRANCH_RIGHT), func); } } else { - if event_id == 18 { - state.callbacks.remove(&(tool, 8)); - state.callbacks.remove(&(tool, 9)); + if event_id == EVENT_ID_BRANCH { + state.callbacks.remove(&(tool, EVENT_ID_BRANCH_LEFT)); + state.callbacks.remove(&(tool, EVENT_ID_BRANCH_RIGHT)); } }🤖 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 265 - 275, Replace the hardcoded magic numbers (18, 8, 9) used around the state.callbacks.insert/remove calls in monitoring.rs with named constants derived from the event bitflags/enum (e.g., BRANCH, BRANCH_LEFT, BRANCH_RIGHT) so the mapping is stable if enum order changes; update the checks using event_id and the tuple keys passed to state.callbacks.insert and state.callbacks.remove to use EventFlags::BRANCH.bits() / EventFlags::BRANCH_LEFT.bits() / EventFlags::BRANCH_RIGHT.bits() or the equivalent event ID constants used by your event enum, and apply the same change in both the insert (register) and remove (unregister) branches where tool, func, and event_id are used.
🤖 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 5186-5192: The END_FOR/PopIter emission uses iter.range() which
attributes line events to the iterable expression instead of the for-statement;
in compile_for capture the for statement's range at the start (e.g. let
stmt_range = self.current_source_range or similar) and then replace the later
set_source_range(iter.range()) calls with set_source_range(stmt_range) before
emitting Instruction::EndFor and Instruction::PopIter (also update the similar
block at the other spot referenced), ensuring you reference compile_for,
iter.range(), Instruction::EndFor, Instruction::PopIter, set_source_range and
current_source_range when making the change.
In `@crates/vm/src/frame.rs`:
- Around line 484-502: The tracing path (vm.use_tracing) updates prev_line
before the monitoring block, causing monitoring::fire_line to be suppressed when
tracing also fires a Line event; change the update so prev_line is only set once
after both tracing and monitoring checks (i.e., move the prev_line = line
assignment to after the monitoring block) or add a clear comment if sharing the
"new line" state is intentional; refer to symbols vm.use_tracing, prev_line,
monitoring::fire_line, monitoring_mask, monitoring::EVENT_LINE,
Instruction::Resume, and Instruction::ExtendedArg to locate the code to modify.
- Around line 2046-2121: Extract the duplicated branch-monitoring block into a
shared helper (suggested name fire_branch_event) and replace the repeated blocks
in Instruction::PopJumpIfNone, Instruction::PopJumpIfNotNone and the pop_jump_if
implementation with a single call: compute src_offset, branch_taken, taken_dest
(target.0 * 2) and fallthrough_dest (self.lasti() * 2) at each site, then call
self.fire_branch_event(vm, src_offset, branch_taken, taken_dest,
fallthrough_dest). The helper should read vm.state.monitoring_events, decide
dest_offset from branch_taken, and call fire_branch_right or fire_branch_left as
before; mark it #[inline] and keep the same semantics and arguments (vm,
self.code, src_offset, dest_offset).
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 372-378: The is_disable function relies on fragile class name
checks and should instead compare the callback result by identity to the actual
DISABLE sentinel; update MonitoringState (similar to how missing/MISSING is
stored) to hold the DISABLE singleton and initialize it where monitor module
singletons are created, then change is_disable to accept the VM/MonitoringState
and return true only when the passed obj pointer/identity equals
MonitoringState::DISABLE (and still exclude None via vm.is_none(obj)); reference
the is_disable function, MonitoringState, and the DISABLE sentinel when making
the change.
---
Outside diff comments:
In `@crates/vm/src/frame.rs`:
- Around line 715-730: The call to vm.fire_exception_handled(...) inside
unwind_blocks (triggered when closing a generator in the is_gen_exit branch)
must not use ? or propagate errors because a failing EXCEPTION_HANDLED callback
can abort handler setup; change the code around the fire_exception_handled
invocation so it catches Err(e) locally (e.g., match or if let Err(e) =
vm.fire_exception_handled(...)) and logs or records the error instead of
returning it, allowing the subsequent exception-handler setup code in
unwind_blocks (the handler creation and attachment logic that follows the
close_result path) to always run; reference vm.fire_exception_handled,
unwind_blocks, is_gen_exit, and the local variable err when implementing the
local error handling.
- Around line 818-860: The gen_throw path currently swallows errors from
monitoring callbacks (fire_py_throw, fire_raise, fire_py_unwind) using "let _ =
…", creating an inconsistency with the main run loop which replaces the original
exception when a monitoring callback errors; change the gen_throw calls to
handle the Result the same way as in run: capture the Result from
monitoring::fire_py_throw / monitoring::fire_raise / monitoring::fire_py_unwind
and if it Errs, propagate that error (return Err(err) or otherwise replace the
current exception with the monitoring error) instead of discarding it; locate
these calls in gen_throw (the block that checks vm.state.monitoring_events and
calls fire_py_throw/fire_raise/fire_py_unwind) and apply the same
replacement/propagation pattern used in run.
---
Nitpick comments:
In `@crates/derive-impl/src/pymodule.rs`:
- Around line 840-847: meta.module()? is being called twice (for has_module and
module_name); cache its result into a local like let module_opt = meta.module()?
and use that for both checks, then refactor to a single branch: if
module_opt.is_none() { let structseq_attr = &mut args.attrs[self.inner.index];
structseq_attr.fill_nested_meta("module", || parse_quote! { module =
`#module_value` })? } else set module_name from module_opt.unwrap_or_else(...).
Replace both meta.module()? uses with the cached module_opt and update the
module_name/has_module logic accordingly (referencing meta.module, has_module,
module_name, structseq_attr, args.attrs, self.inner.index, and
fill_nested_meta).
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Line 356: Remove the decorative comment line "// ── Event dispatch
──────────────────────────────────────────────────────" from
crates/vm/src/stdlib/sys/monitoring.rs; locate the "Event dispatch" separator
comment and delete that single line so the file conforms to the guideline
disallowing decorative section separators (no other logic changes required).
- Around line 265-275: Replace the hardcoded magic numbers (18, 8, 9) used
around the state.callbacks.insert/remove calls in monitoring.rs with named
constants derived from the event bitflags/enum (e.g., BRANCH, BRANCH_LEFT,
BRANCH_RIGHT) so the mapping is stable if enum order changes; update the checks
using event_id and the tuple keys passed to state.callbacks.insert and
state.callbacks.remove to use EventFlags::BRANCH.bits() /
EventFlags::BRANCH_LEFT.bits() / EventFlags::BRANCH_RIGHT.bits() or the
equivalent event ID constants used by your event enum, and apply the same change
in both the insert (register) and remove (unregister) branches where tool, func,
and event_id are used.
In `@crates/vm/src/vm/interpreter.rs`:
- Around line 127-128: Replace the fully-qualified type used to initialize
monitoring_events with the already-imported short name: change the initializer
from crossbeam_utils::atomic::AtomicCell::new(0) to AtomicCell::new(0) so the
field monitoring_events uses the existing AtomicCell import; no other behavior
change required.
| // END_FOR + POP_ITER are on the `for` line, not the body's last line | ||
| let saved_range = self.current_source_range; | ||
| self.set_source_range(iter.range()); | ||
| emit!(self, Instruction::EndFor); | ||
| emit!(self, Instruction::PopIter); | ||
| self.set_source_range(saved_range); | ||
| } |
There was a problem hiding this comment.
Use the for statement range, not iter.range(), for line attribution.
Line 5188 and Line 5200 currently pin END_FOR/implicit-return to the iterable expression’s range. In multiline for statements, iter.range() can be on a different line than the for keyword, so LINE events can shift. Capture the statement range at the start of compile_for and reuse it here.
Suggested fix
fn compile_for(
&mut self,
target: &ast::Expr,
iter: &ast::Expr,
body: &[ast::Stmt],
orelse: &[ast::Stmt],
is_async: bool,
) -> CompileResult<()> {
self.enter_conditional_block();
+ let for_range = self.current_source_range;
// Start loop
let for_block = self.new_block();
@@
} else {
- // END_FOR + POP_ITER are on the `for` line, not the body's last line
+ // END_FOR + POP_ITER are on the `for` line, not the body's last line
let saved_range = self.current_source_range;
- self.set_source_range(iter.range());
+ self.set_source_range(for_range);
emit!(self, Instruction::EndFor);
emit!(self, Instruction::PopIter);
self.set_source_range(saved_range);
}
@@
- self.set_source_range(iter.range());
+ self.set_source_range(for_range);Also applies to: 5197-5201
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/codegen/src/compile.rs` around lines 5186 - 5192, The END_FOR/PopIter
emission uses iter.range() which attributes line events to the iterable
expression instead of the for-statement; in compile_for capture the for
statement's range at the start (e.g. let stmt_range = self.current_source_range
or similar) and then replace the later set_source_range(iter.range()) calls with
set_source_range(stmt_range) before emitting Instruction::EndFor and
Instruction::PopIter (also update the similar block at the other spot
referenced), ensuring you reference compile_for, iter.range(),
Instruction::EndFor, Instruction::PopIter, set_source_range and
current_source_range when making the change.
5cfece8 to
1d608f0
Compare
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 (2)
crates/vm/src/frame.rs (2)
2878-2935:⚠️ Potential issue | 🟠 MajorPropagate C_RAISE callback failures.
fire_c_raiseerrors are currently ignored, so monitoring failures are dropped whilefire_c_returnpropagates. Consider handling it consistently.Suggested fix
- let _ = monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0); + monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0)?;🤖 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 2878 - 2935, The C_RAISE monitoring call is currently ignored in the Err branch; update the Err arm of the callable.call match to propagate errors from monitoring::fire_c_raise just like the Ok arm does for monitoring::fire_c_return — i.e., replace the discarded call (let _ = monitoring::fire_c_raise(...);) with a ?-propagated call (monitoring::fire_c_raise(...)?;) so that errors from fire_c_raise bubble up; ensure this change is applied around the call_arg0 check in the Err branch where call_arg0, is_python_call, and offset are used.
797-833:⚠️ Potential issue | 🟠 MajorPropagate monitoring callback failures in the generator throw path.
fire_py_throw/fire_raise(and the PY_UNWIND branch below) currently discard errors, so callback failures are silently ignored. Consider replacing the active exception when a monitoring callback raises, consistent with the main RAISE/RERAISE handling.Suggested fix
- let exception = match ctor.instantiate_value(exc_val, vm) { + let mut exception = match ctor.instantiate_value(exc_val, vm) {- let _ = monitoring::fire_py_throw(vm, self.code, offset, &exc_obj); + if let Err(monitor_exc) = monitoring::fire_py_throw(vm, self.code, offset, &exc_obj) { + exception = monitor_exc; + }- let _ = monitoring::fire_raise(vm, self.code, offset, &exc_obj); + if let Err(monitor_exc) = monitoring::fire_raise(vm, self.code, offset, &exc_obj) { + exception = monitor_exc; + }- Err(exception) => { - // Fire PY_UNWIND: exception escapes the generator frame - if vm.state.monitoring_events.load() - & crate::stdlib::sys::monitoring::EVENT_PY_UNWIND - != 0 - { - let offset = idx as u32 * 2; - let exc_obj: PyObjectRef = exception.clone().into(); - let _ = crate::stdlib::sys::monitoring::fire_py_unwind( - vm, self.code, offset, &exc_obj, - ); - } - Err(exception) - } + Err(exception) => { + // Fire PY_UNWIND: exception escapes the generator frame + let exception = if vm.state.monitoring_events.load() + & crate::stdlib::sys::monitoring::EVENT_PY_UNWIND + != 0 + { + let offset = idx as u32 * 2; + let exc_obj: PyObjectRef = exception.clone().into(); + match crate::stdlib::sys::monitoring::fire_py_unwind( + vm, self.code, offset, &exc_obj, + ) { + Ok(()) => exception, + Err(monitor_exc) => monitor_exc, + } + } else { + exception + }; + Err(exception) + }Also applies to: 846-857
🤖 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 797 - 833, The monitoring callbacks (fire_py_throw, fire_raise and the PY_UNWIND branch) currently ignore returned errors; change each call site in the generator throw path to check the Result and, on Err, replace the active exception with the returned error (same behavior as the main RAISE/RERAISE handling): after calling monitoring::fire_py_throw or monitoring::fire_raise, match the Result, and on Err(err) set exception = err (and update traceback via exception.set_traceback_typed(...) as done earlier), then stop ignoring the error so the new exception is raised; apply the same pattern for the PY_UNWIND branch mentioned (lines near fire_py_throw/fire_raise), referencing the functions fire_py_throw, fire_raise, PY_UNWIND, and the local exception variable to locate edits.
🤖 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/frame.rs`:
- Around line 2048-2066: The monitoring helper fire_branch_event should not
swallow errors: change its signature to return PyResult<()> and propagate
failures with the ? operator from within it; then update all call sites (e.g.,
in Instruction::PopJumpIfNone, Instruction::PopJumpIfNotNone, pop_jump_if,
execute_for_iter and any places that call fire_branch_left/fire_branch_right
variants) to use the ?-propagating call instead of ignoring results, adjust
those functions to return PyResult where needed, and ensure branch-taking logic
still performs self.jump(target) before returning Ok(None) so the propagated
error bubbles up to the instruction executor.
- Around line 1437-1450: The JUMP monitoring callback result is currently
ignored (let _ = crate::stdlib::sys::monitoring::fire_jump(...)) which swallows
errors; replace that ignored call with a propagated result (use the ? operator
or otherwise return the Err) so failures in fire_jump bubble up like other
monitoring events. Specifically, update the block in the same scope that calls
fire_jump so it calls crate::stdlib::sys::monitoring::fire_jump(vm, self.code,
src_offset, dest.0 * 2)?; and ensure the enclosing function’s signature and call
sites return Result/propagate errors accordingly (adjust types where necessary
to match other monitoring event handlers).
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Line 371: Remove the decorative separator comment "// ── Event dispatch
──────────────────────────────────────────────────────" in
crates/vm/src/stdlib/sys/monitoring.rs and either delete it or replace it with a
short, value-adding comment (e.g., "// Event dispatch") using a plain `//` or a
doc comment if documentation is needed; ensure no decorative characters remain
so the section follows the project's comment style.
---
Outside diff comments:
In `@crates/vm/src/frame.rs`:
- Around line 2878-2935: The C_RAISE monitoring call is currently ignored in the
Err branch; update the Err arm of the callable.call match to propagate errors
from monitoring::fire_c_raise just like the Ok arm does for
monitoring::fire_c_return — i.e., replace the discarded call (let _ =
monitoring::fire_c_raise(...);) with a ?-propagated call
(monitoring::fire_c_raise(...)?;) so that errors from fire_c_raise bubble up;
ensure this change is applied around the call_arg0 check in the Err branch where
call_arg0, is_python_call, and offset are used.
- Around line 797-833: The monitoring callbacks (fire_py_throw, fire_raise and
the PY_UNWIND branch) currently ignore returned errors; change each call site in
the generator throw path to check the Result and, on Err, replace the active
exception with the returned error (same behavior as the main RAISE/RERAISE
handling): after calling monitoring::fire_py_throw or monitoring::fire_raise,
match the Result, and on Err(err) set exception = err (and update traceback via
exception.set_traceback_typed(...) as done earlier), then stop ignoring the
error so the new exception is raised; apply the same pattern for the PY_UNWIND
branch mentioned (lines near fire_py_throw/fire_raise), referencing the
functions fire_py_throw, fire_raise, PY_UNWIND, and the local exception variable
to locate edits.
---
Duplicate comments:
In `@crates/codegen/src/compile.rs`:
- Around line 5186-5191: The END_FOR and PopIter emissions are using
iter.range() for source attribution which misattributes multiline
for-statements; in compile_for capture the for-statement's source range at the
start (e.g. let stmt_range = self.current_source_range or similar) and reuse
that stored stmt_range when calling self.set_source_range(...) before
emit!(self, Instruction::EndFor) and emit!(self, Instruction::PopIter) (also
update the other occurrence around the second END_FOR/implicit-return block),
rather than using iter.range().
In `@crates/vm/src/frame.rs`:
- Around line 485-502: The code updates prev_line inside the tracing block
before running the monitoring checks, which can suppress LINE monitoring
(prev_line influences the monitoring::fire_line call); move the prev_line
assignment so it only happens once after both the tracing and monitoring blocks
(i.e., compute line as now, run the monitoring checks that use prev_line and
possibly call monitoring::fire_line, then set prev_line = line), ensuring
Instruction::Resume / Instruction::ExtendedArg exclusions remain unchanged and
keeping monitoring_mask and monitoring::fire_instruction/fire_line usage intact.
| Ok(all_events) | ||
| } | ||
|
|
||
| // ── Event dispatch ────────────────────────────────────────────────────── |
There was a problem hiding this comment.
Avoid decorative section separators.
The // ── Event dispatch ── line is a decorative separator; prefer a simple comment or remove it.
As per coding guidelines "Do not add decorative section separators (e.g. // -----------, // ===, /* *** */). Use /// doc-comments or short // comments only when they add value".
Suggested fix
-// ── Event dispatch ──────────────────────────────────────────────────────
+// Event dispatch📝 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.
| // ── Event dispatch ────────────────────────────────────────────────────── | |
| // Event dispatch |
🤖 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 371, Remove the decorative
separator comment "// ── Event dispatch
──────────────────────────────────────────────────────" in
crates/vm/src/stdlib/sys/monitoring.rs and either delete it or replace it with a
short, value-adding comment (e.g., "// Event dispatch") using a plain `//` or a
doc comment if documentation is needed; ensure no decorative characters remain
so the section follows the project's comment style.
1d608f0 to
bc87d1e
Compare
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 (2)
crates/vm/src/frame.rs (2)
818-858:⚠️ Potential issue | 🟠 MajorDon’t swallow monitoring callback failures in generator throw/unwind.
fire_py_throw,fire_raise, andfire_py_unwinderrors are ignored here, but other monitoring paths replace the original exception on callback failure. This can silently drop monitoring errors (e.g., invalid DISABLE). Consider mirroring the replacement behavior used in the main run loop.🐛 Suggested fix (replace exception on callback failure)
- { - use crate::stdlib::sys::monitoring; - let monitoring_mask = vm.state.monitoring_events.load(); - if monitoring_mask & monitoring::EVENT_PY_THROW != 0 { - let offset = idx as u32 * 2; - let exc_obj: PyObjectRef = exception.clone().into(); - let _ = monitoring::fire_py_throw(vm, self.code, offset, &exc_obj); - } - // Fire RAISE: the thrown exception is raised in this frame - if monitoring_mask & monitoring::EVENT_RAISE != 0 { - let offset = idx as u32 * 2; - let exc_obj: PyObjectRef = exception.clone().into(); - let _ = monitoring::fire_raise(vm, self.code, offset, &exc_obj); - } - } + let exception = { + use crate::stdlib::sys::monitoring; + let monitoring_mask = vm.state.monitoring_events.load(); + let mut exception = exception; + if monitoring_mask & monitoring::EVENT_PY_THROW != 0 { + let offset = idx as u32 * 2; + let exc_obj: PyObjectRef = exception.clone().into(); + if let Err(monitor_exc) = + monitoring::fire_py_throw(vm, self.code, offset, &exc_obj) + { + exception = monitor_exc; + } + } + // Fire RAISE: the thrown exception is raised in this frame + if monitoring_mask & monitoring::EVENT_RAISE != 0 { + let offset = idx as u32 * 2; + let exc_obj: PyObjectRef = exception.clone().into(); + if let Err(monitor_exc) = + monitoring::fire_raise(vm, self.code, offset, &exc_obj) + { + exception = monitor_exc; + } + } + exception + }; @@ - Err(exception) => { - // Fire PY_UNWIND: exception escapes the generator frame - if vm.state.monitoring_events.load() - & crate::stdlib::sys::monitoring::EVENT_PY_UNWIND - != 0 - { - let offset = idx as u32 * 2; - let exc_obj: PyObjectRef = exception.clone().into(); - let _ = crate::stdlib::sys::monitoring::fire_py_unwind( - vm, self.code, offset, &exc_obj, - ); - } - Err(exception) - } + Err(exception) => { + // Fire PY_UNWIND: exception escapes the generator frame + let exception = if vm.state.monitoring_events.load() + & crate::stdlib::sys::monitoring::EVENT_PY_UNWIND + != 0 + { + let offset = idx as u32 * 2; + let exc_obj: PyObjectRef = exception.clone().into(); + match crate::stdlib::sys::monitoring::fire_py_unwind( + vm, self.code, offset, &exc_obj, + ) { + Ok(()) => exception, + Err(monitor_exc) => monitor_exc, + } + } else { + exception + }; + Err(exception) + }🤖 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 818 - 858, The monitoring callback results (fire_py_throw, fire_raise, fire_py_unwind) are being ignored, which can drop monitoring errors; change these call sites inside the generator throw/unwind path to mirror the main run loop behavior by checking the callback return and replacing the current exception when the callback returns a new/failed exception: call fire_py_throw and fire_raise, inspect their Result/Option, and if they indicate an error or provide a replacement exception, set the local exception (the value passed into unwind_blocks / used for reporting) to that returned exception before continuing; do the same for fire_py_unwind in the Err branch so the exception propagated by unwind_blocks/run reflects any replacement from the monitoring callback.
2878-2935:⚠️ Potential issue | 🟠 MajorC_RETURN/C_RAISE gating and error handling need adjustment.
Two issues in this block:
call_arg0is only computed when EVENT_CALL is enabled, so C_RETURN/C_RAISE won’t fire if only those bits are enabled.fire_c_raiseerrors are ignored, unlike other monitoring callbacks that replace the active exception.Consider computing arg0 when any of CALL/C_RETURN/C_RAISE is enabled, gating C_RETURN/C_RAISE on their own bits, and replacing the exception when
fire_c_raisefails.🐛 Suggested fix
- let monitoring_mask = vm.state.monitoring_events.load(); - let is_python_call = callable.downcast_ref::<PyFunction>().is_some(); - - // Compute arg0 once for CALL, C_RETURN, and C_RAISE events - let call_arg0 = if monitoring_mask & monitoring::EVENT_CALL != 0 { - let arg0 = final_args - .args - .first() - .cloned() - .unwrap_or_else(|| monitoring::get_missing(vm)); - let offset = (self.lasti() - 1) * 2; - monitoring::fire_call(vm, self.code, offset, &callable, arg0.clone())?; - Some(arg0) - } else { - None - }; + let monitoring_mask = vm.state.monitoring_events.load(); + let is_python_call = callable.downcast_ref::<PyFunction>().is_some(); + let wants_arg0 = monitoring_mask + & (monitoring::EVENT_CALL | monitoring::EVENT_C_RETURN | monitoring::EVENT_C_RAISE) + != 0; + + // Compute arg0 once for CALL, C_RETURN, and C_RAISE events + let call_arg0 = if wants_arg0 { + let arg0 = final_args + .args + .first() + .cloned() + .unwrap_or_else(|| monitoring::get_missing(vm)); + if monitoring_mask & monitoring::EVENT_CALL != 0 { + let offset = (self.lasti() - 1) * 2; + monitoring::fire_call(vm, self.code, offset, &callable, arg0.clone())?; + } + Some(arg0) + } else { + None + }; @@ - if let Some(arg0) = call_arg0 - && !is_python_call - { + if let Some(arg0) = call_arg0 + && !is_python_call + && (monitoring_mask & monitoring::EVENT_C_RETURN != 0) + { let offset = (self.lasti() - 1) * 2; monitoring::fire_c_return(vm, self.code, offset, &callable, arg0)?; } @@ - if let Some(arg0) = call_arg0 - && !is_python_call - { - let offset = (self.lasti() - 1) * 2; - let _ = monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0); - } - Err(exc) + let exc = if let Some(arg0) = call_arg0 + && !is_python_call + && (monitoring_mask & monitoring::EVENT_C_RAISE != 0) + { + let offset = (self.lasti() - 1) * 2; + match monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0) { + Ok(()) => exc, + Err(monitor_exc) => monitor_exc, + } + } else { + exc + }; + Err(exc)🤖 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 2878 - 2935, Compute call_arg0 whenever any of the monitor bits monitoring::EVENT_CALL, monitoring::EVENT_C_RETURN, or monitoring::EVENT_C_RAISE are enabled (so arg0 is available for all three callbacks), but only invoke monitoring::fire_call if EVENT_CALL is set and only invoke monitoring::fire_c_return / monitoring::fire_c_raise when their respective bits (EVENT_C_RETURN, EVENT_C_RAISE) are set; when calling monitoring::fire_c_raise, propagate/replace the active exception if the callback returns an Err (i.e., return that error instead of discarding it). Ensure you update the code around call_arg0 creation and the match arms for Ok/Err to check the specific event bits (use monitoring::EVENT_C_RETURN and monitoring::EVENT_C_RAISE) and to handle a failing fire_c_raise by replacing the returned Err as the function’s Err.
🧹 Nitpick comments (4)
crates/derive-impl/src/pymodule.rs (1)
840-841:meta.module()is called twice — collapse into a single binding.Both calls parse the same attribute; the first discards the value to check
is_some()and the second re-parses it to unwrap. Merge them:♻️ Suggested refactor
- let has_module = meta.module()?.is_some(); - let module_name = meta.module()?.unwrap_or_else(|| args.context.name.clone()); + let module_opt = meta.module()?; + let has_module = module_opt.is_some(); + let module_name = module_opt.unwrap_or_else(|| args.context.name.clone());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/derive-impl/src/pymodule.rs` around lines 840 - 841, Collapse the duplicate calls to meta.module() by calling it once into a local binding (e.g., let module_opt = meta.module()?) and then derive has_module from module_opt.is_some() and module_name from module_opt.unwrap_or_else(|| args.context.name.clone()); update the existing variables has_module and module_name to use that single binding so the attribute is parsed only once.crates/vm/src/frame.rs (1)
1437-1450: Reminder: run a clean build after bytecode instruction changes.This block modifies bytecode instruction handling; please run the clean build steps before finalizing. As per coding guidelines: "When modifying bytecode instructions, perform a clean build by running:
rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r".🤖 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 1437 - 1450, This change touches bytecode instruction handling around lasti(), jump(), and the call to crate::stdlib::sys::monitoring::fire_jump guarded by EVENT_JUMP; before finalizing, perform a clean build to ensure generated artifacts reflect the instruction changes by removing affected cargo build caches and stale .pyc files (run the prescribed commands to delete target/debug/build/rustpython-* and remove .pyc files), then rebuild and run tests to verify no bytecode/monitoring regressions occur.crates/vm/src/stdlib/sys/monitoring.rs (2)
676-677:fire_stop_iterationis suppressed with#[allow(dead_code)]— STOP_ITERATION events are not wired up.The
STOP_ITERATIONevent (bit 10) is defined inMonitoringEvents, included inLOCAL_EVENTS_COUNT, and appears inEVENT_NAMES, butfire_stop_iterationis never called fromframe.rs. The#[allow(dead_code)]attribute is a placeholder. Without the call site, tools that registerSTOP_ITERATIONcallbacks will never receive them, which is a silent gap in compatibility with CPython'ssys.monitoring.Do you want me to identify the correct
frame.rscall sites (implicitStopIterationraises fromFOR_ITER/generator exhaustion) and open a tracking issue?🤖 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 676 - 677, The function fire_stop_iteration is currently unused (marked with #[allow(dead_code)]) so STOP_ITERATION events are never emitted; remove the allow attribute and call fire_stop_iteration from the StopIteration raise sites in frame.rs — specifically where FOR_ITER implicitly raises and where generator/iterator exhaustion is handled — passing the same context/arguments used by other monitoring calls (keep the function signature of fire_stop_iteration intact), and ensure the STOP_ITERATION bit (bit 10 / MonitoringEvents::STOP_ITERATION) is emitted so registered callbacks receive the event.
280-290: Replace magic event-ID literals with named constants.The literal values
18,8, and9for BRANCH/BRANCH_LEFT/BRANCH_RIGHT event IDs are fragile. If theMonitoringEventsflags are ever reordered, these silently produce wrong behavior. Derive them from the bitflags instead:♻️ Proposed refactor
+const BRANCH_EVENT_ID: usize = MonitoringEvents::BRANCH.bits().trailing_zeros() as usize; +const BRANCH_LEFT_EVENT_ID: usize = MonitoringEvents::BRANCH_LEFT.bits().trailing_zeros() as usize; +const BRANCH_RIGHT_EVENT_ID: usize = MonitoringEvents::BRANCH_RIGHT.bits().trailing_zeros() as usize;Then in
register_callback:- if event_id == 18 { - // BRANCH → BRANCH_LEFT (8) + BRANCH_RIGHT (9) - state.callbacks.insert((tool, 8), func.clone()); - state.callbacks.insert((tool, 9), func); + if event_id == BRANCH_EVENT_ID { + state.callbacks.insert((tool, BRANCH_LEFT_EVENT_ID), func.clone()); + state.callbacks.insert((tool, BRANCH_RIGHT_EVENT_ID), func); } } else { - if event_id == 18 { - state.callbacks.remove(&(tool, 8)); - state.callbacks.remove(&(tool, 9)); + if event_id == BRANCH_EVENT_ID { + state.callbacks.remove(&(tool, BRANCH_LEFT_EVENT_ID)); + state.callbacks.remove(&(tool, BRANCH_RIGHT_EVENT_ID)); } }🤖 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 280 - 290, Replace the magic numeric literals (18, 8, 9) in the register_callback logic with named values from the MonitoringEvents bitflags: compute the branch-related IDs from MonitoringEvents::BRANCH, MonitoringEvents::BRANCH_LEFT and MonitoringEvents::BRANCH_RIGHT (e.g. use their .bits() or test membership via MonitoringEvents::from_bits_truncate(event_id) / .contains(...)) and use those constants when inserting/removing entries in state.callbacks and when testing event_id; update the conditional that currently reads `if event_id == 18` and the two inserts/removes that use 8 and 9 to reference the corresponding MonitoringEvents-derived identifiers or contains checks instead of raw numbers.
🤖 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/derive-impl/src/pymodule.rs`:
- Around line 233-243: Insert a call to PyModule::__init_dict_from_def after
creating the child module and before calling child.__init_methods so the
sequence in the generated block around `#mod_ident`::module_def(ctx) ->
create_module(vm) becomes: create_module(vm).unwrap();
PyModule::__init_dict_from_def(vm, &child); child.__init_methods(vm).unwrap();
`#mod_ident`::module_exec(vm, &child).unwrap(); and keep the subsequent conversion
and vm.__module_set_attr as-is; also replace the duplicate meta.module()? usage
by capturing it once (e.g., let module_info = meta.module()?; let has_module =
module_info.is_some();) and use module_info for the unwrap_or_else call.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 353-368: The monitoring lock is held while calling into the Python
runtime in all_events (vm.state.monitoring.lock() surrounding vm.ctx.new_int and
all_events.set_item), which risks deadlock; instead, while holding the lock
iterate EVENT_NAMES.iter().take(UNGROUPED_EVENTS_COUNT) and TOOL_LIMIT to
compute and store (event_name, tools_mask) pairs in a local Vec, then drop the
lock and loop over that Vec to call vm.ctx.new_int(...) and
all_events.set_item(...). Keep references to EVENT_NAMES,
UNGROUPED_EVENTS_COUNT, TOOL_LIMIT, and the all_events dict unchanged, only
moving the Python calls after the lock is released.
- Around line 249-255: free_tool_id has a TOCTOU race because it calls
clear_tool_id (which locks and releases) and then re-locks to clear tool_names,
allowing concurrent set_events/set_local_events to re-populate events for a
freed slot; fix by extracting the clearing logic from clear_tool_id into an
internal helper that accepts &mut MonitoringState (e.g., fn
clear_tool_state_locked(state: &mut MonitoringState, tool: usize) ) and then
change free_tool_id to acquire vm.state.monitoring.lock() once, call
check_valid_tool without releasing, invoke the new helper to clear
global_events/local_events/callbacks/disabled and update_events_mask while still
holding the lock, and finally set state.tool_names[tool] = None; update or
replace callers of clear_tool_id to either use the new helper when they already
hold the lock or keep clear_tool_id as a thin wrapper that locks and calls the
helper.
---
Outside diff comments:
In `@crates/vm/src/frame.rs`:
- Around line 818-858: The monitoring callback results (fire_py_throw,
fire_raise, fire_py_unwind) are being ignored, which can drop monitoring errors;
change these call sites inside the generator throw/unwind path to mirror the
main run loop behavior by checking the callback return and replacing the current
exception when the callback returns a new/failed exception: call fire_py_throw
and fire_raise, inspect their Result/Option, and if they indicate an error or
provide a replacement exception, set the local exception (the value passed into
unwind_blocks / used for reporting) to that returned exception before
continuing; do the same for fire_py_unwind in the Err branch so the exception
propagated by unwind_blocks/run reflects any replacement from the monitoring
callback.
- Around line 2878-2935: Compute call_arg0 whenever any of the monitor bits
monitoring::EVENT_CALL, monitoring::EVENT_C_RETURN, or monitoring::EVENT_C_RAISE
are enabled (so arg0 is available for all three callbacks), but only invoke
monitoring::fire_call if EVENT_CALL is set and only invoke
monitoring::fire_c_return / monitoring::fire_c_raise when their respective bits
(EVENT_C_RETURN, EVENT_C_RAISE) are set; when calling monitoring::fire_c_raise,
propagate/replace the active exception if the callback returns an Err (i.e.,
return that error instead of discarding it). Ensure you update the code around
call_arg0 creation and the match arms for Ok/Err to check the specific event
bits (use monitoring::EVENT_C_RETURN and monitoring::EVENT_C_RAISE) and to
handle a failing fire_c_raise by replacing the returned Err as the function’s
Err.
---
Duplicate comments:
In `@crates/codegen/src/compile.rs`:
- Around line 5186-5201: In compile_for, the code still uses iter.range() for
source attribution causing END_FOR/implicit return to be misattributed on
multiline for statements; capture the for statement's range at the start of
compile_for (e.g., let stmt_range = for_stmt.range() or similar) and replace
uses of iter.range() in the places shown (the set_source_range calls before
emitting Instruction::EndFor/PopIter and the final set_source_range call) with
stmt_range so all END_FOR/implicit return attribution uses the for statement
range rather than the iterator expression's range.
In `@crates/vm/src/frame.rs`:
- Around line 485-502: The tracing block currently updates prev_line before the
monitoring block causing LINE events to be suppressed when both tracing and
monitoring are enabled; introduce a separate prev_line_monitor (initialized to
the same starting line where prev_line is initialized) and use it in the
monitoring comparison and update (replace uses of prev_line in the monitoring
block with prev_line_monitor and set prev_line_monitor = line after firing
monitoring events) while leaving prev_line behavior for tracing unchanged;
update references around Instruction::Resume / Instruction::ExtendedArg,
self.code.locations[idx].0.line.get(), monitoring::fire_line and
monitoring::fire_instruction, and vm.state.monitoring_events.load() to use the
new monitor-specific tracker.
- Around line 3124-3181: In execute_for_iter (the match on next_obj) the
branch-monitoring logic is duplicated in both Ok(PyIterReturn::Return(...)) and
Ok(PyIterReturn::StopIteration(...)); extract the common monitoring block by
first computing the differing pieces (e.g., src_offset = (self.lasti() - 1) * 2,
a boolean/flag for branch_taken or an enum indicating Left vs Right, and the
dest_offset which is either self.lasti() * 2 or jump_target.0 * 2) and then call
a single helper (reuse fire_branch_event or create one) that takes vm,
self.code, src_offset, dest_offset and the branch type; replace the two
identical monitoring if-blocks with this single call so only the differing
values are computed per arm and the shared logic is centralized.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Line 371: Remove the decorative separator comment "// ── Event dispatch
──────────────────────────────────────────────────────" and replace it with a
concise, meaningful comment (e.g. "// Event dispatch" ) or a doc-comment if
documentation is needed; locate the separator in the event dispatch section
where the comment text "Event dispatch" appears and update the comment to follow
the no-decorative-separators guideline (affecting the event dispatch block).
---
Nitpick comments:
In `@crates/derive-impl/src/pymodule.rs`:
- Around line 840-841: Collapse the duplicate calls to meta.module() by calling
it once into a local binding (e.g., let module_opt = meta.module()?) and then
derive has_module from module_opt.is_some() and module_name from
module_opt.unwrap_or_else(|| args.context.name.clone()); update the existing
variables has_module and module_name to use that single binding so the attribute
is parsed only once.
In `@crates/vm/src/frame.rs`:
- Around line 1437-1450: This change touches bytecode instruction handling
around lasti(), jump(), and the call to
crate::stdlib::sys::monitoring::fire_jump guarded by EVENT_JUMP; before
finalizing, perform a clean build to ensure generated artifacts reflect the
instruction changes by removing affected cargo build caches and stale .pyc files
(run the prescribed commands to delete target/debug/build/rustpython-* and
remove .pyc files), then rebuild and run tests to verify no bytecode/monitoring
regressions occur.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 676-677: The function fire_stop_iteration is currently unused
(marked with #[allow(dead_code)]) so STOP_ITERATION events are never emitted;
remove the allow attribute and call fire_stop_iteration from the StopIteration
raise sites in frame.rs — specifically where FOR_ITER implicitly raises and
where generator/iterator exhaustion is handled — passing the same
context/arguments used by other monitoring calls (keep the function signature of
fire_stop_iteration intact), and ensure the STOP_ITERATION bit (bit 10 /
MonitoringEvents::STOP_ITERATION) is emitted so registered callbacks receive the
event.
- Around line 280-290: Replace the magic numeric literals (18, 8, 9) in the
register_callback logic with named values from the MonitoringEvents bitflags:
compute the branch-related IDs from MonitoringEvents::BRANCH,
MonitoringEvents::BRANCH_LEFT and MonitoringEvents::BRANCH_RIGHT (e.g. use their
.bits() or test membership via MonitoringEvents::from_bits_truncate(event_id) /
.contains(...)) and use those constants when inserting/removing entries in
state.callbacks and when testing event_id; update the conditional that currently
reads `if event_id == 18` and the two inserts/removes that use 8 and 9 to
reference the corresponding MonitoringEvents-derived identifiers or contains
checks instead of raw numbers.
| fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> { | ||
| let tool = check_valid_tool(tool_id, vm)?; | ||
| clear_tool_id(tool_id, vm)?; | ||
| let mut state = vm.state.monitoring.lock(); | ||
| state.tool_names[tool] = None; | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
TOCTOU gap in free_tool_id: two separate lock acquisitions leave a window for state corruption.
clear_tool_id acquires the lock, clears global_events/local_events/callbacks/disabled, calls update_events_mask, and releases the lock — but deliberately leaves tool_names[tool] intact. Then free_tool_id re-acquires the lock to set tool_names[tool] = None. In the gap between the two releases, a concurrent thread can:
- See
tool_names[tool].is_some()→ successfully callset_eventsorset_local_eventsfor that tool. - Then
free_tool_idclears the name, leaving active events inglobal_eventsorlocal_eventsowned by a now-freed tool slot.
The fix is to perform both the clear and the name reset in a single critical section, e.g. by extracting the clear logic into an internal helper that takes &mut MonitoringState:
🛡️ Proposed fix
+fn clear_tool_state(tool: usize, state: &mut MonitoringState) {
+ state.global_events[tool] = 0;
+ state.local_events.retain(|(t, _), _| *t != tool);
+ state.callbacks.retain(|(t, _), _| *t != tool);
+ state.disabled.retain(|&(_, _, t)| t != tool);
+}
+
fn clear_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
let tool = check_valid_tool(tool_id, vm)?;
let mut state = vm.state.monitoring.lock();
if state.tool_names[tool].is_some() {
- state.global_events[tool] = 0;
- state.local_events.retain(|(local_tool, _), _| *local_tool != tool);
- state.callbacks.retain(|(cb_tool, _), _| *cb_tool != tool);
- state.disabled.retain(|&(_, _, t)| t != tool);
+ clear_tool_state(tool, &mut state);
}
update_events_mask(vm, &state);
Ok(())
}
fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> {
let tool = check_valid_tool(tool_id, vm)?;
- clear_tool_id(tool_id, vm)?;
- let mut state = vm.state.monitoring.lock();
+ let mut state = vm.state.monitoring.lock();
+ clear_tool_state(tool, &mut state);
state.tool_names[tool] = None;
+ update_events_mask(vm, &state);
Ok(())
}📝 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.
| fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> { | |
| let tool = check_valid_tool(tool_id, vm)?; | |
| clear_tool_id(tool_id, vm)?; | |
| let mut state = vm.state.monitoring.lock(); | |
| state.tool_names[tool] = None; | |
| Ok(()) | |
| } | |
| fn clear_tool_state(tool: usize, state: &mut MonitoringState) { | |
| state.global_events[tool] = 0; | |
| state.local_events.retain(|(t, _), _| *t != tool); | |
| state.callbacks.retain(|(t, _), _| *t != tool); | |
| state.disabled.retain(|&(_, _, t)| t != tool); | |
| } | |
| fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> { | |
| let tool = check_valid_tool(tool_id, vm)?; | |
| let mut state = vm.state.monitoring.lock(); | |
| clear_tool_state(tool, &mut state); | |
| state.tool_names[tool] = None; | |
| update_events_mask(vm, &state); | |
| Ok(()) | |
| } |
🤖 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 249 - 255, free_tool_id
has a TOCTOU race because it calls clear_tool_id (which locks and releases) and
then re-locks to clear tool_names, allowing concurrent
set_events/set_local_events to re-populate events for a freed slot; fix by
extracting the clearing logic from clear_tool_id into an internal helper that
accepts &mut MonitoringState (e.g., fn clear_tool_state_locked(state: &mut
MonitoringState, tool: usize) ) and then change free_tool_id to acquire
vm.state.monitoring.lock() once, call check_valid_tool without releasing, invoke
the new helper to clear global_events/local_events/callbacks/disabled and
update_events_mask while still holding the lock, and finally set
state.tool_names[tool] = None; update or replace callers of clear_tool_id to
either use the new helper when they already hold the lock or keep clear_tool_id
as a thin wrapper that locks and calls the helper.
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] test: cpython/Lib/test/test_compile.py (TODO: 23) dependencies: dependent tests: (no tests depend on compile) [x] test: cpython/Lib/test/test_monitoring.py (TODO: 8) dependencies: dependent tests: (no tests depend on monitoring) Legend:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
crates/vm/src/stdlib/sys/monitoring.rs (2)
473-473: Remove decorative section separator.
// ── Public dispatch functions (called from frame.rs) ─────────────────is a decorative separator.Proposed fix
-// ── Public dispatch functions (called from frame.rs) ───────────────── +// Public dispatch functions (called from frame.rs)As per coding guidelines: "Do not add decorative section separators (e.g.
// -----------,// ===,/* *** */). Use///doc-comments or short//comments only when they add value."🤖 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 473, Remove the decorative separator comment line "// ── Public dispatch functions (called from frame.rs) ─────────────────" from monitoring.rs; either delete it entirely or replace it with a concise, value-adding comment or a short doc-comment that briefly states "Public dispatch functions (called from frame.rs)" to comply with the guideline against decorative section separators and keep the intent clear for the pub dispatch functions section.
280-290: Replace magic numbers 18, 8, 9 with named constants.The hardcoded
event_id == 18(BRANCH),8(BRANCH_LEFT),9(BRANCH_RIGHT) are fragile. Use the event-bit constants derived from trailing-zeros to keep this in sync withEVENT_NAMES.♻️ Proposed fix
+ let branch_id = EVENT_BRANCH.trailing_zeros() as usize; + let branch_left_id = EVENT_BRANCH_LEFT.trailing_zeros() as usize; + let branch_right_id = EVENT_BRANCH_RIGHT.trailing_zeros() as usize; + if !vm.is_none(&func) { state.callbacks.insert((tool, event_id), func.clone()); - if event_id == 18 { - state.callbacks.insert((tool, 8), func.clone()); - state.callbacks.insert((tool, 9), func); + if event_id == branch_id { + state.callbacks.insert((tool, branch_left_id), func.clone()); + state.callbacks.insert((tool, branch_right_id), func); } } else { - if event_id == 18 { - state.callbacks.remove(&(tool, 8)); - state.callbacks.remove(&(tool, 9)); + if event_id == branch_id { + state.callbacks.remove(&(tool, branch_left_id)); + state.callbacks.remove(&(tool, branch_right_id)); } }🤖 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 280 - 290, Replace the magic numeric literals 18, 8, and 9 with named constants (e.g., BRANCH, BRANCH_LEFT, BRANCH_RIGHT) and use the same derivation as the rest of the module so they stay in sync with EVENT_NAMES; compute those constants from the event-bit/trailing-zeros logic used elsewhere (derive the index/bit for "BRANCH", "BRANCH_LEFT", "BRANCH_RIGHT" the same way the module derives event bits) and then use those constants in the state.callbacks.insert/remove and the event_id comparison instead of raw numbers (references: event_id, state.callbacks, tool, func, and the EVENT_NAMES/trailing-zeros derivation).crates/vm/src/frame.rs (1)
2106-2116: Inconsistent event-bit guard: use specific bits to match the LINE/INSTRUCTION pattern.
Resume,ReturnValue, andYieldValuegate onmonitoring_events.load() != 0, whileLINEandINSTRUCTION(lines 494–498) correctly check their specific bits. Any registered event (e.g., onlyLINE) triggers a lock acquisition insidefire_py_start/fire_py_resume/fire_py_return/fire_py_yieldon every function entry/return/yield, even when those events are not subscribed.♻️ Proposed fix
Instruction::Resume { arg: resume_arg } => { let resume_type = resume_arg.get(arg); - if vm.state.monitoring_events.load() != 0 { + let event_bit = if resume_type == 0 { + crate::stdlib::sys::monitoring::EVENT_PY_START + } else { + crate::stdlib::sys::monitoring::EVENT_PY_RESUME + }; + if vm.state.monitoring_events.load() & event_bit != 0 { let offset = (self.lasti() - 1) * 2; if resume_type == 0 { crate::stdlib::sys::monitoring::fire_py_start(vm, self.code, offset)?; } else { crate::stdlib::sys::monitoring::fire_py_resume(vm, self.code, offset)?; } }Instruction::ReturnValue => { let value = self.pop_value(); - if vm.state.monitoring_events.load() != 0 { + if vm.state.monitoring_events.load() & crate::stdlib::sys::monitoring::EVENT_PY_RETURN != 0 { let offset = (self.lasti() - 1) * 2; crate::stdlib::sys::monitoring::fire_py_return(vm, self.code, offset, &value)?; }Instruction::YieldValue { arg: oparg } => { let value = self.pop_value(); - if vm.state.monitoring_events.load() != 0 { + if vm.state.monitoring_events.load() & crate::stdlib::sys::monitoring::EVENT_PY_YIELD != 0 { let offset = (self.lasti() - 1) * 2; crate::stdlib::sys::monitoring::fire_py_yield(vm, self.code, offset, &value)?; }Also applies to: 2118-2124, 2344-2349
🤖 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 2106 - 2116, The code currently tests vm.state.monitoring_events.load() != 0 for Instruction::Resume, ReturnValue, and YieldValue which causes any subscribed event to trigger monitoring locks; change these guards to test only the specific monitoring bits relevant to the events being fired (use a bitmask check like vm.state.monitoring_events.load() & (MONITOR_LINE|MONITOR_INSTRUCTION) != 0 or the exact constants for RETURN/YIELD/RESUME as defined) before calling fire_py_start/fire_py_resume/fire_py_return/fire_py_yield so locks are only acquired when the matching event bits are set; update the checks in the Instruction::Resume, ReturnValue, and YieldValue handlers (and the analogous spots at the other mentioned ranges) to use the bitmask test rather than != 0.
🤖 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/frame.rs`:
- Around line 2943-2950: The monitoring callback fire_c_raise is currently
invoked with "let _ =" which drops any error; change the call in the Err(exc)
arm so that if monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0)
returns Err(mon_exc) you propagate or replace the current exception (i.e.,
return Err(mon_exc) or otherwise replace exc with the callback's error) instead
of discarding it; update the invocation around the symbols call_arg0,
is_python_call, self.lasti(), code, and callable so it mirrors the
error-handling behavior used by the other monitoring handlers (RAISE, PY_UNWIND,
PY_THROW) in this file.
---
Duplicate comments:
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 353-368: In all_events, avoid holding vm.state.monitoring.lock()
across calls into the Python VM: while iterating
EVENT_NAMES/UNGROUPED_EVENTS_COUNT and TOOL_LIMIT you should collect the
(event_name, tools_mask) pairs into a temporary Vec under the lock (compute
event_bit and tools_mask as now), then drop the lock and iterate the Vec to call
all_events.set_item(*event_name, vm.ctx.new_int(tools_mask).into(), vm); this
prevents deadlocks if set_item triggers code that re-acquires
vm.state.monitoring.
- Line 371: Remove the decorative separator comment "// ── Event dispatch
──────────────────────────────────────────────────────" in
crates/vm/src/stdlib/sys/monitoring.rs (the section labeled "Event dispatch");
simply delete that line so the file adheres to the coding guideline prohibiting
decorative section separators and leave surrounding code and any real comments
intact.
- Around line 249-255: free_tool_id currently calls clear_tool_id (which locks
and unlocks vm.state.monitoring) and then re-locks to clear state.tool_names,
causing a TOCTOU race with concurrent use_tool_id; fix by extracting a helper
clear_tool_state(&mut MonitoringState, tool: usize) that performs both the
event/slot clearing and tool_names[tool]=None while a single lock is held, then
change clear_tool_id to acquire the lock and call clear_tool_state and change
free_tool_id to acquire the lock once and call clear_tool_state directly so both
operations happen under one lock.
---
Nitpick comments:
In `@crates/vm/src/frame.rs`:
- Around line 2106-2116: The code currently tests
vm.state.monitoring_events.load() != 0 for Instruction::Resume, ReturnValue, and
YieldValue which causes any subscribed event to trigger monitoring locks; change
these guards to test only the specific monitoring bits relevant to the events
being fired (use a bitmask check like vm.state.monitoring_events.load() &
(MONITOR_LINE|MONITOR_INSTRUCTION) != 0 or the exact constants for
RETURN/YIELD/RESUME as defined) before calling
fire_py_start/fire_py_resume/fire_py_return/fire_py_yield so locks are only
acquired when the matching event bits are set; update the checks in the
Instruction::Resume, ReturnValue, and YieldValue handlers (and the analogous
spots at the other mentioned ranges) to use the bitmask test rather than != 0.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Line 473: Remove the decorative separator comment line "// ── Public dispatch
functions (called from frame.rs) ─────────────────" from monitoring.rs; either
delete it entirely or replace it with a concise, value-adding comment or a short
doc-comment that briefly states "Public dispatch functions (called from
frame.rs)" to comply with the guideline against decorative section separators
and keep the intent clear for the pub dispatch functions section.
- Around line 280-290: Replace the magic numeric literals 18, 8, and 9 with
named constants (e.g., BRANCH, BRANCH_LEFT, BRANCH_RIGHT) and use the same
derivation as the rest of the module so they stay in sync with EVENT_NAMES;
compute those constants from the event-bit/trailing-zeros logic used elsewhere
(derive the index/bit for "BRANCH", "BRANCH_LEFT", "BRANCH_RIGHT" the same way
the module derives event bits) and then use those constants in the
state.callbacks.insert/remove and the event_id comparison instead of raw numbers
(references: event_id, state.callbacks, tool, func, and the
EVENT_NAMES/trailing-zeros derivation).
55f97b4 to
02c549a
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
crates/codegen/src/ir.rs (1)
254-261: Consider importingHashSetat the top of the file.
std::collections::HashSetis referenced three times in this block. Ause std::collections::HashSet;import at the top would be cleaner and more consistent with the existing import style.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/codegen/src/ir.rs` around lines 254 - 261, The code repeats std::collections::HashSet three times when building non_nop_lines and kept_nop_lines; add a top-level import `use std::collections::HashSet;` and then replace `std::collections::HashSet<_>` and `std::collections::HashSet::new()` with `HashSet<_>` and `HashSet::new()` respectively in the non_nop_lines and kept_nop_lines declarations so the block reads using the imported type (affecting symbols non_nop_lines and kept_nop_lines).crates/vm/src/vm/interpreter.rs (1)
128-128: Redundant full path —AtomicCellis already in scope.
use crossbeam_utils::atomic::AtomicCell;is imported at Line 50 of the same function; the fully-qualified path is unnecessary here.♻️ Proposed simplification
- monitoring_events: crossbeam_utils::atomic::AtomicCell::new(0), + monitoring_events: AtomicCell::new(0),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/vm/interpreter.rs` at line 128, The field initialization for monitoring_events uses the redundant fully-qualified path crossbeam_utils::atomic::AtomicCell::new(0); replace it with the already-imported AtomicCell::new(0) in the struct/initializer that contains monitoring_events, keeping the behavior unchanged and relying on the existing use statement for AtomicCell.crates/vm/src/stdlib/sys/monitoring.rs (4)
696-710:fire_stop_iterationis dead code.The
#[allow(dead_code)]annotation on apub fnsignals that this function is not yet called anywhere. Either hook it up inframe.rs(STOP_ITERATION should fire when a generator returns its value via StopIteration), or remove it and add it back when needed to avoid accumulating unused code.🤖 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 696 - 710, The pub function fire_stop_iteration is currently unused (marked with #[allow(dead_code]); either remove it to avoid dead code or wire it into the generator return path: call fire_stop_iteration(vm, code, offset, exception) from the appropriate place in frame.rs where generators raise/return via StopIteration (e.g., in the function that handles generator completion/exception unwrapping), ensuring you pass the current VirtualMachine, the PyCode for the frame, the bytecode offset, and the StopIteration exception so EVENT_STOP_ITERATION is emitted at the correct moment.
437-492:is_disablere-acquires the monitoring lock on every callback iteration.Inside
fire(), the monitoring lock is released after collecting callbacks, thenis_disablere-acquires it for each callback result. Sincestate.disableis immutable during callback invocations (callbacks can't change DISABLE), the sentinel can be snapshotted once before the loop.♻️ Proposed refactor
+ // Snapshot the DISABLE sentinel once before invoking callbacks. + let disable_obj = { + let state = vm.state.monitoring.lock(); + state.disable.clone() + }; FIRING.with(|f| f.set(true)); let result = (|| { for (tool, cb) in callbacks { let result = cb.call(args.clone(), vm)?; - if is_disable(&result, vm) { + if disable_obj.as_ref().is_some_and(|d| result.is(d)) {🤖 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 437 - 492, The fire() function currently releases vm.state.monitoring then calls is_disable(...) inside the callback loop which re-acquires the monitoring lock on every iteration; instead snapshot the disabled set once before invoking callbacks and use that snapshot for disable-checks to avoid repeated locking—capture let disabled_snapshot = { let s = vm.state.monitoring.lock(); s.disabled.clone() } (or extract the relevant keys for code_id/offset), then replace calls to is_disable(...) with a check against disabled_snapshot (or change is_disable signature to accept the snapshot) while keeping the existing logic that only allows disabling for event_id < LOCAL_EVENTS_COUNT and preserving error creation via EVENT_NAMES.
285-298: Magic numbers for BRANCH/BRANCH_LEFT/BRANCH_RIGHT event IDs inregister_callback.The literals
18,8, and9are the bit-trailing-zero indices ofEVENT_BRANCH,EVENT_BRANCH_LEFT, andEVENT_BRANCH_RIGHTrespectively. They'll silently go stale if event bit positions ever change.♻️ Proposed refactor
+const EVENT_ID_BRANCH: usize = MonitoringEvents::BRANCH.bits().trailing_zeros() as usize; +const EVENT_ID_BRANCH_LEFT: usize = MonitoringEvents::BRANCH_LEFT.bits().trailing_zeros() as usize; +const EVENT_ID_BRANCH_RIGHT: usize = MonitoringEvents::BRANCH_RIGHT.bits().trailing_zeros() as usize;Then replace the literals:
- if event_id == 18 { - state.callbacks.insert((tool, 8), func.clone()); - state.callbacks.insert((tool, 9), func); - } + if event_id == EVENT_ID_BRANCH { + state.callbacks.insert((tool, EVENT_ID_BRANCH_LEFT), func.clone()); + state.callbacks.insert((tool, EVENT_ID_BRANCH_RIGHT), func); + }Same for the removal path at lines 295-298.
🤖 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 285 - 298, The code uses magic numeric event IDs (18, 8, 9) inside register_callback when handling the composite BRANCH event; replace those literals with the proper event constants or derived indices (e.g., use the trailing-zero index of EVENT_BRANCH, EVENT_BRANCH_LEFT, and EVENT_BRANCH_RIGHT or existing constants like EVENT_BRANCH_IDX/ EVENT_BRANCH_LEFT_IDX/ EVENT_BRANCH_RIGHT_IDX) when comparing event_id and when inserting/removing from state.callbacks (references: the check "if event_id == 18", the inserts "state.callbacks.insert((tool, 8), ...)" and "state.callbacks.insert((tool, 9), ...)" and the removals "state.callbacks.remove(&(tool, 8))"/"remove(&(tool, 9))"); update both the registration and clearing branches so they use the named constants/derived indices instead of raw numbers.
494-494: Decorative section separator.
// ── Public dispatch functions ... ─────uses box-drawing characters. Replace with a plain//comment or remove it entirely.As per coding guidelines: "Do not add decorative section separators (e.g.
// -----------,// ===). Use///doc-comments or short//comments only when they add value."🤖 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 494, Replace the decorative box-drawing comment line "// ── Public dispatch functions (called from frame.rs) ─────────────────" with a plain non-decorative comment (e.g. "// Public dispatch functions (called from frame.rs)") or remove it entirely; locate this line in crates/vm/src/stdlib/sys/monitoring.rs near the public dispatch functions and update the comment so it no longer uses box-drawing characters.crates/vm/src/frame.rs (1)
3148-3215: Inline branch monitoring inexecute_for_itercould use thefire_branch_eventhelper.The two inline branch-monitoring blocks in
execute_for_iterduplicate the same guard/dispatch pattern already extracted intofire_branch_event. Both cases map cleanly: the "continue" path isbranch_taken = false(BRANCH_LEFT) and the "exhausted" path isbranch_taken = true(BRANCH_RIGHT) withjump_targetas the label.♻️ Proposed refactor
Ok(PyIterReturn::Return(value)) => { self.push_value(value); - // Fire BRANCH_LEFT: iterator has more items, continue loop - let monitoring_mask = vm.state.monitoring_events.load(); - if monitoring_mask - & (crate::stdlib::sys::monitoring::EVENT_BRANCH_LEFT - | crate::stdlib::sys::monitoring::EVENT_BRANCH_RIGHT) - != 0 - { - let dest_offset = self.lasti() * 2; - crate::stdlib::sys::monitoring::fire_branch_left( - vm, - self.code, - src_offset, - dest_offset, - )?; - } + self.fire_branch_event(vm, src_offset, false, target)?; Ok(None) } Ok(PyIterReturn::StopIteration(_)) => { // ... self.jump(jump_target); - // Fire BRANCH_RIGHT: iterator exhausted, exit loop - let monitoring_mask = vm.state.monitoring_events.load(); - if monitoring_mask - & (crate::stdlib::sys::monitoring::EVENT_BRANCH_LEFT - | crate::stdlib::sys::monitoring::EVENT_BRANCH_RIGHT) - != 0 - { - let dest_offset = jump_target.0 * 2; - crate::stdlib::sys::monitoring::fire_branch_right( - vm, - self.code, - src_offset, - dest_offset, - )?; - } + self.fire_branch_event(vm, src_offset, true, jump_target)?; Ok(None) }
🤖 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/stdlib/sys/monitoring.rs`:
- Around line 303-307: get_events currently returns the normalized mask (with
EVENT_C_RETURN_MASK/EVENT_C_RAISE stripped) because normalize_event_set is
applied before storing; change the monitoring data model to keep both the
normalized mask used for dispatch and the original raw mask used for API
round-trips (e.g., add a raw_global_events array or raw_events field alongside
global_events), update the code path that calls normalize_event_set (the writer
used by set_events) to store the normalized version in global_events and the
original unmodified mask in raw_global_events, and modify get_events (which
currently reads state.global_events[tool]) to return the stored original/raw
mask so callers see the exact bits they set.
- Around line 361-390: The all_events function only checks state.global_events
and therefore omits tool registrations present in state.local_events (registered
via set_local_events); update all_events to include local_events when
constructing each event's tools_mask by combining bits from
state.global_events[tool] and any state.local_events entries: when building
masks inside all_events, after computing event_bit and tools_mask from
state.global_events, iterate state.local_events (or otherwise aggregate per-tool
masks stored there) and OR in any bits that indicate the event is enabled
locally for that tool so EVENT_NAMES / UNGROUPED_EVENTS_COUNT and TOOL_LIMIT
usage remains the same and the resulting all_events dict reflects both global
and per-code-object local registrations.
---
Duplicate comments:
In `@crates/codegen/src/compile.rs`:
- Around line 5186-5191: The END_FOR/POP_ITER and implicit-return are being
attributed to iter.range() instead of the for-statement line; in compile_for
capture the for-statement's source range at the start (e.g. a variable like
stmt_range) and use that when calling set_source_range before emitting
Instruction::EndFor, Instruction::PopIter and the implicit return emission
instead of using iter.range(); make the same change for the second occurrence
around the other block (the one noted at 5197-5201) so both places use the saved
for-statement range rather than the iterable's range.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Line 392: The decorative box-drawing comment line "// ── Event dispatch
──────────────────────────────────────────────────────" should be replaced with
a plain comment that follows guidelines (e.g., "// Event dispatch") to remove
the box-drawing characters; locate the separator in the monitoring.rs file and
update that comment so it no longer contains box-drawing characters or
decorative separators.
---
Nitpick comments:
In `@crates/codegen/src/ir.rs`:
- Around line 254-261: The code repeats std::collections::HashSet three times
when building non_nop_lines and kept_nop_lines; add a top-level import `use
std::collections::HashSet;` and then replace `std::collections::HashSet<_>` and
`std::collections::HashSet::new()` with `HashSet<_>` and `HashSet::new()`
respectively in the non_nop_lines and kept_nop_lines declarations so the block
reads using the imported type (affecting symbols non_nop_lines and
kept_nop_lines).
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 696-710: The pub function fire_stop_iteration is currently unused
(marked with #[allow(dead_code]); either remove it to avoid dead code or wire it
into the generator return path: call fire_stop_iteration(vm, code, offset,
exception) from the appropriate place in frame.rs where generators raise/return
via StopIteration (e.g., in the function that handles generator
completion/exception unwrapping), ensuring you pass the current VirtualMachine,
the PyCode for the frame, the bytecode offset, and the StopIteration exception
so EVENT_STOP_ITERATION is emitted at the correct moment.
- Around line 437-492: The fire() function currently releases
vm.state.monitoring then calls is_disable(...) inside the callback loop which
re-acquires the monitoring lock on every iteration; instead snapshot the
disabled set once before invoking callbacks and use that snapshot for
disable-checks to avoid repeated locking—capture let disabled_snapshot = { let s
= vm.state.monitoring.lock(); s.disabled.clone() } (or extract the relevant keys
for code_id/offset), then replace calls to is_disable(...) with a check against
disabled_snapshot (or change is_disable signature to accept the snapshot) while
keeping the existing logic that only allows disabling for event_id <
LOCAL_EVENTS_COUNT and preserving error creation via EVENT_NAMES.
- Around line 285-298: The code uses magic numeric event IDs (18, 8, 9) inside
register_callback when handling the composite BRANCH event; replace those
literals with the proper event constants or derived indices (e.g., use the
trailing-zero index of EVENT_BRANCH, EVENT_BRANCH_LEFT, and EVENT_BRANCH_RIGHT
or existing constants like EVENT_BRANCH_IDX/ EVENT_BRANCH_LEFT_IDX/
EVENT_BRANCH_RIGHT_IDX) when comparing event_id and when inserting/removing from
state.callbacks (references: the check "if event_id == 18", the inserts
"state.callbacks.insert((tool, 8), ...)" and "state.callbacks.insert((tool, 9),
...)" and the removals "state.callbacks.remove(&(tool, 8))"/"remove(&(tool,
9))"); update both the registration and clearing branches so they use the named
constants/derived indices instead of raw numbers.
- Line 494: Replace the decorative box-drawing comment line "// ── Public
dispatch functions (called from frame.rs) ─────────────────" with a plain
non-decorative comment (e.g. "// Public dispatch functions (called from
frame.rs)") or remove it entirely; locate this line in
crates/vm/src/stdlib/sys/monitoring.rs near the public dispatch functions and
update the comment so it no longer uses box-drawing characters.
In `@crates/vm/src/vm/interpreter.rs`:
- Line 128: The field initialization for monitoring_events uses the redundant
fully-qualified path crossbeam_utils::atomic::AtomicCell::new(0); replace it
with the already-imported AtomicCell::new(0) in the struct/initializer that
contains monitoring_events, keeping the behavior unchanged and relying on the
existing use statement for AtomicCell.
| Instruction::Resume { arg: resume_arg } => { | ||
| // Resume execution after yield, await, or at function start | ||
| // In CPython, this checks instrumentation and eval breaker | ||
| // For now, we just check for signals/interrupts | ||
| let _resume_type = resume_arg.get(arg); | ||
|
|
||
| // Check for interrupts if not resuming from yield_from | ||
| // if resume_type < bytecode::ResumeType::AfterYieldFrom as u32 { | ||
| // vm.check_signals()?; | ||
| // } | ||
| let resume_type = resume_arg.get(arg); | ||
| if vm.state.monitoring_events.load() != 0 { | ||
| let offset = (self.lasti() - 1) * 2; | ||
| if resume_type == 0 { | ||
| crate::stdlib::sys::monitoring::fire_py_start(vm, self.code, offset)?; | ||
| } else { | ||
| crate::stdlib::sys::monitoring::fire_py_resume(vm, self.code, offset)?; | ||
| } | ||
| } | ||
| Ok(None) |
There was a problem hiding this comment.
Overly broad monitoring guard fires on every function entry/return/yield regardless of which events are registered.
All three hot-path sites use monitoring_events.load() != 0 as the guard instead of checking the specific event bit. When only, e.g., EVENT_LINE is registered (common for coverage tools), every function call still invokes fire_py_start/py_return/py_yield → acquires the monitoring lock → iterates TOOL_LIMIT tools → finds no callbacks → returns. Every other monitoring point in this file (lines 494, 497, 1458, etc.) correctly checks the specific bit.
🛠️ Proposed fix
- if vm.state.monitoring_events.load() != 0 {
- let offset = (self.lasti() - 1) * 2;
- if resume_type == 0 {
- crate::stdlib::sys::monitoring::fire_py_start(vm, self.code, offset)?;
- } else {
- crate::stdlib::sys::monitoring::fire_py_resume(vm, self.code, offset)?;
- }
- }
+ {
+ use crate::stdlib::sys::monitoring;
+ let monitoring_mask = vm.state.monitoring_events.load();
+ let offset = (self.lasti() - 1) * 2;
+ if resume_type == 0 {
+ if monitoring_mask & monitoring::EVENT_PY_START != 0 {
+ monitoring::fire_py_start(vm, self.code, offset)?;
+ }
+ } else if monitoring_mask & monitoring::EVENT_PY_RESUME != 0 {
+ monitoring::fire_py_resume(vm, self.code, offset)?;
+ }
+ }- if vm.state.monitoring_events.load() != 0 {
- let offset = (self.lasti() - 1) * 2;
- crate::stdlib::sys::monitoring::fire_py_return(vm, self.code, offset, &value)?;
- }
+ if vm.state.monitoring_events.load()
+ & crate::stdlib::sys::monitoring::EVENT_PY_RETURN
+ != 0
+ {
+ let offset = (self.lasti() - 1) * 2;
+ crate::stdlib::sys::monitoring::fire_py_return(vm, self.code, offset, &value)?;
+ }- if vm.state.monitoring_events.load() != 0 {
- let offset = (self.lasti() - 1) * 2;
- crate::stdlib::sys::monitoring::fire_py_yield(vm, self.code, offset, &value)?;
- }
+ if vm.state.monitoring_events.load()
+ & crate::stdlib::sys::monitoring::EVENT_PY_YIELD
+ != 0
+ {
+ let offset = (self.lasti() - 1) * 2;
+ crate::stdlib::sys::monitoring::fire_py_yield(vm, self.code, offset, &value)?;
+ }Also applies to: 2120-2124, 2346-2349
| fn get_events(tool_id: i32, vm: &VirtualMachine) -> PyResult<u32> { | ||
| let tool = check_valid_tool(tool_id, vm)?; | ||
| let state = vm.state.monitoring.lock(); | ||
| Ok(state.global_events[tool]) | ||
| } |
There was a problem hiding this comment.
get_events returns the normalized (C_RETURN/C_RAISE-stripped) event set.
normalize_event_set removes C_RETURN and C_RAISE bits before storing (line 205: event_set &= !EVENT_C_RETURN_MASK), so get_events returns CALL when the user originally set CALL | C_RETURN. CPython's get_events returns "the int representing all the active events" — the original set including C_RETURN/C_RAISE. This mismatch would break any tool that does a round-trip set_events(tool, mask) followed by get_events(tool) == mask.
Consider storing the original (pre-normalization) mask separately for get_events while using the normalized mask internally for dispatch.
🤖 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 303 - 307, get_events
currently returns the normalized mask (with EVENT_C_RETURN_MASK/EVENT_C_RAISE
stripped) because normalize_event_set is applied before storing; change the
monitoring data model to keep both the normalized mask used for dispatch and the
original raw mask used for API round-trips (e.g., add a raw_global_events array
or raw_events field alongside global_events), update the code path that calls
normalize_event_set (the writer used by set_events) to store the normalized
version in global_events and the original unmodified mask in raw_global_events,
and modify get_events (which currently reads state.global_events[tool]) to
return the stored original/raw mask so callers see the exact bits they set.
| fn all_events(vm: &VirtualMachine) -> PyResult<PyDictRef> { | ||
| // Collect data under the lock, then release before calling into Python VM. | ||
| let masks: Vec<(&str, u8)> = { | ||
| let state = vm.state.monitoring.lock(); | ||
| EVENT_NAMES | ||
| .iter() | ||
| .take(UNGROUPED_EVENTS_COUNT) | ||
| .enumerate() | ||
| .filter_map(|(event_id, event_name)| { | ||
| let event_bit = 1u32 << event_id; | ||
| let mut tools_mask = 0u8; | ||
| for tool in 0..TOOL_LIMIT { | ||
| if (state.global_events[tool] & event_bit) != 0 { | ||
| tools_mask |= 1 << tool; | ||
| } | ||
| } | ||
| if tools_mask != 0 { | ||
| Some((*event_name, tools_mask)) | ||
| } else { | ||
| None | ||
| } | ||
| }) | ||
| .collect() | ||
| }; | ||
| let all_events = vm.ctx.new_dict(); | ||
| for (name, mask) in masks { | ||
| all_events.set_item(name, vm.ctx.new_int(mask).into(), vm)?; | ||
| } | ||
| Ok(all_events) | ||
| } |
There was a problem hiding this comment.
all_events omits per-code-object local events.
Only state.global_events[tool] is consulted; state.local_events entries (registered via set_local_events) are ignored. A tool that registers events only locally would show up as having no events in all_events, potentially confusing introspection tools.
🐛 Proposed fix sketch
let event_bit = 1u32 << event_id;
let mut tools_mask = 0u8;
for tool in 0..TOOL_LIMIT {
- if (state.global_events[tool] & event_bit) != 0 {
+ let local_union: u32 = state.local_events
+ .iter()
+ .filter(|((t, _), _)| *t == tool)
+ .fold(0, |acc, (_, &v)| acc | v);
+ if ((state.global_events[tool] | local_union) & event_bit) != 0 {
tools_mask |= 1 << tool;
}
}🤖 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 361 - 390, The
all_events function only checks state.global_events and therefore omits tool
registrations present in state.local_events (registered via set_local_events);
update all_events to include local_events when constructing each event's
tools_mask by combining bits from state.global_events[tool] and any
state.local_events entries: when building masks inside all_events, after
computing event_bit and tools_mask from state.global_events, iterate
state.local_events (or otherwise aggregate per-tool masks stored there) and OR
in any bits that indicate the event is enabled locally for that tool so
EVENT_NAMES / UNGROUPED_EVENTS_COUNT and TOOL_LIMIT usage remains the same and
the resulting all_events dict reflects both global and per-code-object local
registrations.
9fecfef to
6e0ef9c
Compare
6e0ef9c to
6f29d09
Compare
cc @terryluan12
requiring instrumented instructions
Summary by CodeRabbit
New Features
Improved Behavior