Update test_sys from v3.14.3 and impl more sys module#7075
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:
📝 WalkthroughWalkthroughThis PR introduces a comprehensive interpreter-level event monitoring subsystem with per-code-object event masks and callback dispatch, refactors frame ownership from reference-counted to non-owning pointers with per-thread tracking, and modifies exception handling to use per-frame stacks alongside extensive instrumentation throughout frame execution. Changes
Sequence Diagram(s)Skipped—changes span heterogeneous subsystems (codegen, frame management, monitoring, stdlib) without a single coherent control flow suitable for visualization. Estimated code review effort🎯 5 (Critical) | ⏱️ ~105 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 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.
|
Code has been automatically formatted The code in this PR has been formatted using:
git pull origin sys |
Sorry, something went wrong.
71af7b0 to
33b66f9
Compare
February 12, 2026 07:58
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/vm/src/frame.rs (1)
788-829: ⚠️ Potential issue | 🟠 MajorHandle monitoring callback errors in
gen_throwconsistently with other code paths.In the
runfunction, monitoring callback errors fromfire_raiseandfire_py_unwindreplace the original exception. However, ingen_throw, errors fromfire_py_throw,fire_raise, andfire_py_unwindare silently ignored withlet _, creating an inconsistency. Apply the same error-handling pattern to all monitoring callbacks here.
🤖 Fix all issues with AI agents
In `@crates/vm/src/builtins/frame.rs`:
- Around line 197-235: Extract the duplicated pointer-comparison logic in f_back
into a small helper (e.g., find_frame_by_ptr or frame_matches_ptr) that takes an
iterator/collection of &PyRef<Frame> (or a closure to produce such an iterator)
and the previous pointer from previous_frame(), performs the pointer equality
using the correct deref chain (&***f) and returns Option<PyRef<Frame>>; then
call this helper for vm.frames.borrow().iter() and for each
slot.frames.lock().iter() inside the threading block, replacing the duplicated
closures (do not use &****f).
In `@crates/vm/src/stdlib/io.rs`:
- Around line 2619-2679: The tuple validation in
StatelessIncrementalEncoder::encode and StatelessIncrementalDecoder::decode
relies on try_into_value(), which produces different TypeError messages than the
codec helpers; update both methods to perform the same downcast + length check
used in crate::codecs::{encode, decode} (or call those helpers) so non-tuple
inputs and wrong-length tuples produce the identical CPython-compatible error
text. Concretely, replace res.try_into_value(vm)? with
res.downcast::<PyTupleRef>(vm).map_err(|_| vm.new_type_error("encoder must
return a tuple (object, integer)")) (and the corresponding decoder message for
decode) and then check tuple.len() exactly as in the codec helpers to keep
messages and behavior consistent.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 371-378: The is_disable function currently checks only the class
name and .is_none(), which treats both DISABLE and MISSING (both
MonitoringSentinel/_Sentinel) as disable; change it to perform an identity
comparison against the actual DISABLE singleton instead: obtain the DISABLE
object (either by fetching the module-level DISABLE singleton or by caching it
on MonitoringState like missing) and return obj.is(disable_obj) (or
pointer/identity equality) rather than comparing class name; update is_disable
to use that cached/obtained DISABLE symbol so MISSING is not misclassified.
In `@crates/vm/src/vm/python_run.rs`:
- Around line 47-62: cache_source_for_traceback currently builds the `lines`
list via split_inclusive('\n') which can leave the final line without a trailing
newline; update the logic in cache_source_for_traceback to post-process the
collected `lines` (before converting to Py objects) and ensure the last element
ends with '\n' (append one if missing) so the resulting tuple `entry` matches
CPython's linecache invariant `(size, mtime=None, lines, path)` with every line
ending in newline; keep the existing creation of `entry` and call to
cache.set_item as-is.
🧹 Nitpick comments (8)
crates/derive-impl/src/pymodule.rs (1)
233-243: Generated submodule init code panics on failure with no diagnostic context.The four
.unwrap()calls in the generated block will panic at runtime without any indication of which submodule failed or why. While the rest of the file also uses.unwrap()for__module_set_attr, those are single-attribute operations; here you have an entire module lifecycle (create_module→__init_methods→module_exec→set_attr) that could fail at multiple stages.Consider using
.expect(...)with the submodule name to improve debuggability:Suggested improvement
submodule_inits.push(quote! { #(`#cfgs`)* { let child_def = `#mod_ident`::module_def(ctx); - let child = child_def.create_module(vm).unwrap(); - child.__init_methods(vm).unwrap(); - `#mod_ident`::module_exec(vm, &child).unwrap(); + let child = child_def.create_module(vm) + .expect(concat!("failed to create submodule ", `#py_name`)); + child.__init_methods(vm) + .expect(concat!("failed to init methods for submodule ", `#py_name`)); + `#mod_ident`::module_exec(vm, &child) + .expect(concat!("failed to exec submodule ", `#py_name`)); let child: ::rustpython_vm::PyObjectRef = child.into(); vm.__module_set_attr(module, ctx.intern_str(`#py_name`), child).unwrap(); } });crates/codegen/src/ir.rs (1)
247-273: Second NOP-removal pass looks correct; consider inlining auseforHashSet.The two-pass approach makes sense:
remove_nops()(line 195) handles pre-existing NOPs before optimization passes, while this pass handles NOPs freshly created byconvert_pseudo_opsandlabel_exception_targets. The set-based deduplication logic is sound — a NOP is kept only when no real instruction covers its line and no other NOP for that line was already retained.Minor nit:
std::collections::HashSetis spelled out three times. A localusewould reduce noise.♻️ Optional: local import for readability
+ use std::collections::HashSet; // Collect lines that have non-NOP instructions in this block - let non_nop_lines: std::collections::HashSet<_> = block + let non_nop_lines: HashSet<_> = block .instructions .iter() .filter(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop))) .map(|ins| ins.location.line) .collect(); - let mut kept_nop_lines: std::collections::HashSet<OneIndexed> = - std::collections::HashSet::new(); + let mut kept_nop_lines: HashSet<OneIndexed> = HashSet::new();crates/vm/src/stdlib/thread.rs (1)
890-895: Avoid holding the registry lock while locking per-thread frames.
thread_frames.lock()is held whileslot.frames.lock()is acquired for each entry. This creates lock contention and prevents thread registration/cleanup during the snapshot. The faulthandler.rs timeout handler demonstrates the correct pattern: clone slot references under the registry lock, drop the registry lock, then lock individual slot frames.♻️ Suggested refactor (drop registry lock before per-slot locks)
- let registry = vm.state.thread_frames.lock(); - registry - .iter() - .filter_map(|(id, slot)| slot.frames.lock().last().cloned().map(|f| (*id, f))) - .collect() + let registry = vm.state.thread_frames.lock(); + let slots: Vec<_> = registry.iter().map(|(id, slot)| (*id, slot.clone())).collect(); + drop(registry); + slots + .into_iter() + .filter_map(|(id, slot)| slot.frames.lock().last().cloned().map(|f| (id, f))) + .collect()crates/vm/src/frame.rs (1)
466-503: Sharedprev_linebetween tracing and monitoring may suppress first Line event after mid-framesettrace()if still on the same line.The code unconditionally updates
prev_lineat line 502 outside themonitoring_maskcheck. While this is intentional (per the comment on line 484 to prevent spurious LINE events when monitoring is enabled mid-function), it means ifsys.settrace()is called mid-frame while still on the same line, the firstLineevent will not fire until the line changes. This design prioritizes shared state efficiency over system independence. Clarify whether this matches CPython 3.12+ behavior; if stricter separation is needed, trackprev_trace_lineindependently for tracing while retainingprev_linefor monitoring's mid-function safety.crates/vm/src/stdlib/sys/monitoring.rs (4)
430-451:FIRINGguard is not panic-safe — use a RAII drop guard instead.If a Rust panic occurs inside the callback loop (lines 432–448), line 450 is never reached and
FIRINGstaystruefor the thread permanently, silently suppressing all future monitoring events on that thread.Proposed fix using a drop guard
+struct FiringGuard; +impl FiringGuard { + fn new() -> Self { + FIRING.with(|f| f.set(true)); + Self + } +} +impl Drop for FiringGuard { + fn drop(&mut self) { + FIRING.with(|f| f.set(false)); + } +} + // ... inside fire_event_inner: - FIRING.with(|f| f.set(true)); - let result = (|| { + let _guard = FiringGuard::new(); + let result = (|| { for (tool, cb) in callbacks { // ... } Ok(()) })(); - FIRING.with(|f| f.set(false)); result
265-275: Replace magic event-ID literals with derived constants.
18,8,9correspond to the bit positions ofBRANCH,BRANCH_LEFT, andBRANCH_RIGHTrespectively, but bare literals are fragile and will silently break if bit assignments change.Proposed fix
You could define constants at the module level:
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 replace all magic numbers in
register_callbackand thefire_*functions accordingly.
356-356: Remove decorative section separator.As per coding guidelines, "Do not add decorative section separators (e.g.
// -----------,// ===,/* *** */). Use///doc-comments or short//comments only when they add value".
457-726: Consider a macro to reduce boilerplate acrossfire_*functions.All 17
fire_*functions follow the same pattern: extractcode_id, buildFuncArgs, callfire_event_inner. The differences are the event ID, event bit constant, and the argument list. A declarative macro could eliminate the repetition while keeping each variant's distinct arguments clear.This is a readability/maintainability suggestion — no functional concern.
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@crates/codegen/src/compile.rs`:
- Around line 5186-5201: Capture the for-statement's source range at the start
of compile_for (e.g., store self.current_source_range into a local like
for_range) and replace uses of iter.range() used for setting attribution with
that for_range; specifically update the calls to
self.set_source_range(iter.range()) around the loop epilogue and implicit return
attribution to use for_range so LINE events remain tied to the for-statement
header rather than the iterable expression.
In `@crates/vm/src/frame.rs`:
- Around line 590-619: The current block replaces the original exception with
any error returned from monitoring::fire_reraise/fire_raise and other sites like
fire_py_unwind; change this so that only RAISE/RERAISE events replace the
propagated exception, but PY_UNWIND (and similar unwind events) preserve the
original exception on monitoring callback failure—i.e., call
monitoring::fire_py_unwind (or other unwind callers) and if it returns Err,
ignore or log the monitoring error and return the original exception instead of
monitor_exc. Update the logic around monitoring::EVENT_PY_UNWIND and the
corresponding match/Err handling (referencing fire_reraise, fire_raise,
fire_py_unwind, and monitoring::EVENT_* masks) to implement this differentiated
behavior.
- Around line 788-803: In gen_throw, don't silently discard
monitoring::fire_py_throw and monitoring::fire_raise results; capture their
Result and, on Err, propagate/replace the current exception just like the run()
loop does—i.e., call monitoring::fire_py_throw(vm, self.code, offset, &exc_obj)
and monitoring::fire_raise(vm, self.code, offset, &exc_obj), check the Result,
and if it returns Err(e) return or propagate that error (or replace the
propagating exception) so monitoring errors are not swallowed; update the
gen_throw logic accordingly to mirror run()'s error-handling behavior.
In `@crates/vm/src/stdlib/sys/monitoring.rs`:
- Around line 430-451: The FIRING TLS flag is set before running callbacks but
is reset only after the closure returns, so a panic inside the callback loop
will skip FIRING.with(|f| f.set(false)) and leave monitoring disabled for that
thread; wrap the FIRING set/reset in a panic-safe RAII guard (or use the
scopeguard crate) so that FIRING.with(|f| f.set(false)) runs in Drop even if a
callback panics: set FIRING to true, create a guard type or scopeguard that
calls FIRING.with(|f| f.set(false)) in its Drop, then run the existing callbacks
loop (the code using callbacks, cb.call(...), is unchanged) and let the guard
reset FIRING regardless of success/failure before returning the result and
before modifying vm.state.monitoring.lock().
- Around line 696-726: The doc comments for fire_branch_left and
fire_branch_right are reversed; update the comments so fire_branch_left
documents "fired when a branch is not taken (falls through)" and
fire_branch_right documents "fired when a branch is taken (jumps to
destination)" to match how frame.rs invokes these functions (reference
fire_branch_left and fire_branch_right to locate them and adjust their
docstrings accordingly).
🧹 Nitpick comments (11)
crates/derive-impl/src/pymodule.rs (1)
197-248: Submodule initialization looks correct; consider a minor robustness note on theNameValuecase.The overall logic — detect nested
#[pymodule]mods, skipsubmodules, generate cfg-guarded init code — is clean and consistent with the rest of the file.One small observation: at Line 213,
syn::Meta::NameValuesilently returnsOk(()), meaning a malformed#[pymodule = "..."]annotation would be silently ignored rather than producing a compile-time diagnostic. This is unlikely in practice but could cause confusion if someone misuses the attribute.Optional: emit an error for the NameValue case
- _ => return Ok(()), + syn::Meta::NameValue(nv) => { + bail_span!(nv, "#[pymodule] does not support `=` syntax"); + }crates/vm/src/stdlib/sys/mod.rs (1)
1072-1079: Inconsistency with non-threading_current_framesstub.The non-threading
_current_exceptionsreturns a dict with key0and the actual exception, while the non-threading_current_frames(line 1084) returns an empty dict. Consider aligning them — either both return meaningful data for the main thread or both return an empty dict. Currently CPython returns both with the main thread's data.crates/vm/src/vm/mod.rs (1)
1109-1155:resume_gen_framelacksscopeguardforrecursion_depthdecrement.If
f(frame)panics,recursion_depthat line 1153 won't be decremented, and the exception stack / frame chain won't be cleaned up either (lines 1141–1151). While panics from Python execution are rare,with_frame_excat least protects the recursion depth viawith_recursion'sscopeguard::defer!. Consider wrapping the cleanup in ascopeguardor a drop guard for consistency.Proposed fix — use scopeguard for cleanup
self.recursion_depth.update(|d| d + 1); self.frames.borrow_mut().push(frame.clone()); let frame_ptr: *const Frame = &***frame; let old_frame = crate::vm::thread::set_current_frame(frame_ptr); frame.previous.store( old_frame as *mut Frame, core::sync::atomic::Ordering::Relaxed, ); - // Inline exception push without thread exception update self.exceptions.borrow_mut().stack.push(exc); let old_owner = frame.owner.swap( crate::frame::FrameOwner::Thread as i8, core::sync::atomic::Ordering::AcqRel, ); let result = f(frame); - // SAFETY: frame_ptr is valid because self.frames holds a clone - unsafe { &*frame_ptr } - .owner - .store(old_owner, core::sync::atomic::Ordering::Release); - // Inline exception pop without thread exception update - self.exceptions - .borrow_mut() - .stack - .pop() - .expect("pop_exception() without nested exc stack"); - crate::vm::thread::set_current_frame(old_frame); - let _popped = self.frames.borrow_mut().pop(); - - self.recursion_depth.update(|d| d - 1); + scopeguard::defer! { + // SAFETY: frame_ptr is valid because self.frames holds a clone + unsafe { &*frame_ptr } + .owner + .store(old_owner, core::sync::atomic::Ordering::Release); + self.exceptions + .borrow_mut() + .stack + .pop() + .expect("pop_exception() without nested exc stack"); + crate::vm::thread::set_current_frame(old_frame); + let _popped = self.frames.borrow_mut().pop(); + self.recursion_depth.update(|d| d - 1); + } + resultcrates/vm/src/frame.rs (3)
2021-2056: Duplicated branch monitoring code — extract into a helper.The exact same branch-monitoring pattern (compute
src_offset/dest_offset, check mask, firebranch_left/branch_rightdepending onbranch_taken) appears in:
PopJumpIfNone(Lines 2029-2056)PopJumpIfNotNone(Lines 2067-2094)pop_jump_if(Lines 3109-3136)execute_for_iter(Lines 3150-3164, 3188-3202)This violates DRY — extract a small helper like
fire_branch_event(&self, vm, src_offset, dest_offset, branch_taken).♻️ Sketch of helper
/// Fire BRANCH_LEFT or BRANCH_RIGHT monitoring event. fn fire_branch_monitoring( &self, vm: &VirtualMachine, src_offset: u32, dest_offset: u32, branch_taken: bool, ) { 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 { if branch_taken { let _ = crate::stdlib::sys::monitoring::fire_branch_right( vm, self.code, src_offset, dest_offset, ); } else { let _ = crate::stdlib::sys::monitoring::fire_branch_left( vm, self.code, src_offset, dest_offset, ); } } }Then each call site becomes a single line:
-// Fire BRANCH monitoring events -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 = if branch_taken { target.0 * 2 } else { self.lasti() * 2 }; - if branch_taken { - let _ = crate::stdlib::sys::monitoring::fire_branch_right( - vm, self.code, src_offset, dest_offset, - ); - } else { - let _ = crate::stdlib::sys::monitoring::fire_branch_left( - vm, self.code, src_offset, dest_offset, - ); - } -} +let dest_offset = if branch_taken { target.0 * 2 } else { self.lasti() * 2 }; +self.fire_branch_monitoring(vm, src_offset, dest_offset, branch_taken);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."
Also applies to: 2059-2094, 3096-3137
2908-2966:execute_callmonitoring:is_python_callcheck is shallow.Line 2930 uses
callable.downcast_ref::<PyFunction>().is_some()to decide whether to fireC_RETURN/C_RAISE. This misses cases where aPyFunctionis wrapped (e.g., bound methods,functools.partial, descriptors). A bound method wrapping a Python function would still be classified as a "C call" and fireC_RETURN/C_RAISEevents incorrectly. CPython checksPy_TYPE(callable) == &PyFunction_Typewhich is similarly shallow, so this may be acceptable for now, but it's worth noting.
2929-2944:call_arg0computation always allocates the MISSING sentinel when no args present, even when CALL monitoring is disabled.The
MISSINGsentinel is only fetched whenmonitoring_mask & EVENT_CALL != 0(Line 2933), so the guard is correct. However,get_missing(vm)acquires the monitoring lock every time. For the hot path (CALL with no args but monitoring active), consider caching MISSING at VM init time to avoid repeated lock acquisition.crates/vm/src/vm/thread.rs (2)
133-142:get_all_current_exceptionsholds the registry lock while cloning all exceptions.Line 137 acquires
vm.state.thread_frames.lock()and then iterates all slots callingslot.exception.to_owned()(Line 140). Ifto_owned()involves cloningPyObjectRef(ref-count bump), this holds the registry lock for O(threads) clone operations. For typical thread counts this is fine, but consider collecting just theArc<ThreadSlot>references first, then releasing the lock before reading exceptions, to minimize contention.
15-21:ThreadSlotfields arepubbut the struct itself should probably restrict visibility.Both
framesandexceptionarepub, allowing any code to directly lock/swap them. Consider restricting topub(crate)to match the existing visibility pattern in this module (e.g.,pub(crate) static CURRENT_FRAME).crates/vm/src/stdlib/sys/monitoring.rs (3)
248-278: Magic event IDs: hardcoded18,8,9for BRANCH/BRANCH_LEFT/BRANCH_RIGHT.Lines 265, 267, 268, 272-274 use raw numeric event IDs. These should use named constants derived from the bitflags to prevent silent breakage if event positions change.
♻️ Proposed fix
+// Event ID constants (bit position = trailing_zeros of the corresponding bit) +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; +const EVENT_ID_BRANCH: usize = MonitoringEvents::BRANCH.bits().trailing_zeros() as usize; // 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 == EVENT_ID_BRANCH { + state.callbacks.insert((tool, EVENT_ID_BRANCH_LEFT), func.clone()); + state.callbacks.insert((tool, EVENT_ID_BRANCH_RIGHT), func); }
356-356: Remove decorative section separator.Line 356
// ── Event dispatch ──...is a decorative separator. As per coding guidelines: "Do not add decorative section separators (e.g.// -----------,// ===,/* *** */). Use///doc-comments or short//comments only when they add value."
84-107:MonitoringStateusesHashMapforlocal_events,callbacks, anddisabled— considerFxHashMapfor better performance.These maps are accessed under the monitoring lock on every event dispatch (
fire_event_inner, Lines 407-418). The standardHashMapuses SipHash which is slower thanFxHashMapfor small integer keys(usize, usize). Since this is a runtime VM, usingrustc_hash::FxHashMap(or similar) could reduce overhead when monitoring is active.
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
crates/vm/src/frame.rs (2)
466-502: ⚠️ Potential issue | 🟠 MajorSeparate trace vs monitoring line tracking to avoid suppressing LINE events
prev_lineis updated in the tracing block before the monitoring block, so when tracing is enabled the monitoringEVENT_LINEcheck sees no change and never fires. Track trace and monitoring line numbers independently (or update ordering) so both subsystems get line-change notifications.💡 Suggested fix
- let mut prev_line: u32 = 0; + let mut prev_trace_line: u32 = 0; + let mut prev_monitor_line: u32 = 0; ... - && loc.line.get() as u32 != prev_line + && loc.line.get() as u32 != prev_trace_line { - prev_line = loc.line.get() as u32; + prev_trace_line = loc.line.get() as u32; vm.trace_event(crate::protocol::TraceEvent::Line, None)?; } ... - if monitoring_mask & monitoring::EVENT_LINE != 0 && line != prev_line { + if monitoring_mask & monitoring::EVENT_LINE != 0 && line != prev_monitor_line { monitoring::fire_line(vm, self.code, offset, line)?; } ... - prev_line = line; + prev_monitor_line = line; }
2909-2966: ⚠️ Potential issue | 🟠 MajorC_RETURN/C_RAISE should fire even when EVENT_CALL is off
call_arg0is computed only whenEVENT_CALLis set, so C_RETURN/C_RAISE never fire unless EVENT_CALL is enabled. Also,fire_c_raiseerrors are discarded. Computearg0when any of CALL/C_RETURN/C_RAISE is enabled, and handlefire_c_raiseconsistently.💡 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 monitoring_mask = vm.state.monitoring_events.load(); + let is_python_call = callable.downcast_ref::<PyFunction>().is_some(); + let wants_call = monitoring_mask & monitoring::EVENT_CALL != 0; + let wants_c = monitoring_mask + & (monitoring::EVENT_C_RETURN | monitoring::EVENT_C_RAISE) + != 0; + + // Compute arg0 once for CALL / C_RETURN / C_RAISE + let call_arg0 = if wants_call || wants_c { 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())?; + if wants_call { + 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 + && wants_c + { 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 - { + if let Some(arg0) = call_arg0 + && !is_python_call + && wants_c + { let offset = (self.lasti() - 1) * 2; - let _ = monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0); + monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0)?; }
🤖 Fix all issues with AI agents
In `@crates/vm/src/frame.rs`:
- Around line 1407-1420: The monitoring call currently swallows errors when
firing jump events; update the block in function(s) using self.lasti(),
target.get(arg), self.jump(dest) so the call to
crate::stdlib::sys::monitoring::fire_jump returns its Result and any Err is
propagated instead of ignored—e.g., check vm.state.monitoring_events for
EVENT_JUMP as now but replace the ignored call with a propagating call (use the
? operator or map_err to convert/return the error) so failures from fire_jump
are not silently dropped; apply the same change to the other occurrences
referenced (around the blocks that call fire_* and inspect monitoring_events).
In `@crates/vm/src/vm/mod.rs`:
- Around line 1129-1176: resume_gen_frame currently performs manual cleanup
(recursion_depth.update decrement, frames.pop(), exceptions.stack.pop(),
owner.store(old_owner), crate::vm::thread::set_current_frame(old_frame)) after
calling f(frame), which will be skipped if f panics; wrap the post-call cleanup
in a panic-safe guard (either use scopeguard::defer! immediately after
successfully performing the setup steps or create a small RAII guard struct) so
that on unwind you always: decrement recursion_depth, pop self.frames, pop the
exceptions stack, restore frame.owner to old_owner, and reset the current thread
frame via crate::vm::thread::set_current_frame(old_frame); place the guard right
after pushing the FramePtr and pushing the exception so it has access to
old_owner and old_frame and remove the manual cleanup at the end or let the
guard perform it.
🧹 Nitpick comments (6)
crates/codegen/src/ir.rs (1)
253-273: Consider using an import forstd::collections::HashSet.
std::collections::HashSetis used three times inline (lines 254, 260, 261). A localuseor a module-level import would reduce verbosity.♻️ Suggested refactor
+ use std::collections::HashSet; + // Collect lines that have non-NOP instructions in this block - let non_nop_lines: std::collections::HashSet<_> = block + let non_nop_lines: HashSet<_> = block .instructions .iter() .filter(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop))) .map(|ins| ins.location.line) .collect(); - let mut kept_nop_lines: std::collections::HashSet<OneIndexed> = - std::collections::HashSet::new(); + let mut kept_nop_lines: HashSet<OneIndexed> = HashSet::new();crates/vm/src/frame.rs (1)
2692-2707: EXCEPTION_HANDLED hook placement looks correctPlease also confirm fmt/clippy and the bytecode clean-build command were run since instruction handling changed.
Commands to run locally:
cargo fmt cargo clippy --all-targets --all-features rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -rAs per coding guidelines: "Follow the default rustfmt code style by running
cargo fmtto format Rust code", "Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints that are introduced by your changes", and "When modifying bytecode instructions, perform a clean build by running:rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r".crates/vm/src/vm/mod.rs (1)
1148-1148: Triple deref(&***frame)is hard to follow — consider a helper or a brief inline comment.
frameis&FrameRef(&PyRef<Frame>), so&***framedereferences through&→PyRef<Frame>→Py<Frame>→Framethen re-borrows to&Frame. It works, but is a readability hazard. A named binding or a brief// &FrameRef → &Framecomment would help future readers.crates/vm/src/stdlib/sys/monitoring.rs (3)
456-726: Magic event-id literals scattered across allfire_*functions.Every
fire_*function passes a raw integer (e.g.,0,1,11,16) as theevent_idtofire_event_inner. A single typo would silently route callbacks to the wrong event with no compile-time protection. Consider defining named constants (e.g.,const EVENT_ID_PY_START: usize = 0;) or deriving the id from the bit position of the correspondingEVENT_*constant (EVENT_PY_START.trailing_zeros() as usize).Example using trailing_zeros
pub fn fire_py_start(vm: &VirtualMachine, code: &PyRef<PyCode>, offset: u32) -> PyResult<()> { let code_id = code.get_id(); let args = FuncArgs::from(vec![code.clone().into(), vm.ctx.new_int(offset).into()]); - fire_event_inner(vm, 0, EVENT_PY_START, code_id, offset, &args) + fire_event_inner(vm, EVENT_PY_START.trailing_zeros() as usize, EVENT_PY_START, code_id, offset, &args) }Or, have
fire_event_innerderiveevent_idfromevent_bitinternally, eliminating the redundant parameter.
234-240:free_tool_iddouble-locks:clear_tool_idlocks+unlocks, then line 237 locks again.This is both a minor performance issue and the same TOCTOU pattern flagged on
set_events. Consider inlining the clear logic or taking the lock once.Sketch
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(); + 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); + } state.tool_names[tool] = None; + update_events_mask(vm, &state); Ok(()) }
84-107:MonitoringStatefields are allpub— consider restricting visibility.All fields of
MonitoringStateare public, allowing any code to mutate internal invariants (e.g.,global_events,disabled) without going through the validation functions. Since this struct is behind aPyMutex, direct field access from outside this module could bypass the event normalization and mask update logic. Making fieldspub(crate)or private with accessors would tighten the contract.
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@crates/vm/src/frame.rs`:
- Around line 2957-2963: The monitoring::fire_c_raise call currently ignores its
Result in the Err(exc) branch, causing monitoring errors to be silently dropped;
update the Err(exc) handling so the result of monitoring::fire_c_raise (called
with monitoring::fire_c_raise(vm, self.code, offset, &callable, arg0)) is
checked and its error is either propagated (use the ? operator or map the
monitoring error into the current Err to return) or explicitly handled/logged so
failures aren’t discarded—apply this change in the same block that references
call_arg0, is_python_call, self.lasti(), self.code, offset, callable, and arg0
to match CALL/C_RETURN behavior.
🧹 Nitpick comments (3)
crates/codegen/src/ir.rs (1)
247-273: Consider using an importedHashSetinstead of repeated inline paths.
std::collections::HashSetis spelled out in full three times within this block. Auseimport at the top of the file (or the function) would be slightly cleaner.Also, a minor observation on correctness: this second NOP-removal pass (post-
convert_pseudo_ops) correctly complements the earlierremove_nopspass (pre-pseudo-conversion). New NOPs introduced byconvert_pseudo_ops(fromSetupFinally,SetupWith,SetupCleanup,PopBlock) are properly filtered here againstnon_nop_lines, so line-marker uniqueness is preserved.♻️ Suggested import cleanup
Add to the existing imports at the top of the file:
use std::collections::HashSet;Then simplify the block:
- let non_nop_lines: std::collections::HashSet<_> = block + let non_nop_lines: HashSet<_> = block .instructions .iter() .filter(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop))) .map(|ins| ins.location.line) .collect(); - let mut kept_nop_lines: std::collections::HashSet<OneIndexed> = - std::collections::HashSet::new(); + let mut kept_nop_lines: HashSet<OneIndexed> = HashSet::new();crates/vm/src/coroutine.rs (1)
103-112: Unsafe raw-pointer alias of&self.exception— sound but brittle.The raw pointer on line 105 is used to sidestep the borrow checker:
selfis already borrowed, and the closure passed toresume_gen_frameneeds mutable-like access toself.exception. Therunningflag logically guarantees exclusive access. This is sound providedresume_gen_frameinvokes the closure synchronously (before the&selfborrow expires) and never stores it.Two observations:
The
_oldvalue discarded on line 110 should always beNone(swapped in on line 104, with no intervening writes due to therunningguard). If that invariant matters, adebug_assert!(_old.is_none())would make it explicit and catch regressions.Consider whether
resume_gen_framecould be refactored to return the exception state separately, eliminating the need for the raw pointer entirely. That would remove bothunsafeblocks here.💡 Optional: add a debug assertion
// SAFETY: exclusive access guaranteed by running flag - let _old = unsafe { (*exception_ptr).swap(vm.current_exception()) }; + let old = unsafe { (*exception_ptr).swap(vm.current_exception()) }; + debug_assert!(old.is_none(), "exception slot should have been None"); resultcrates/vm/src/stdlib/io.rs (1)
2608-2692: Stateless incremental encoder/decoder: significant code duplication.
StatelessIncrementalEncoder::encodeandStatelessIncrementalDecoder::decodeshare nearly identical logic (build args → call function → validate tuple → return first element). Consider extracting a shared helper to reduce duplication.♻️ Suggested shared helper
+ fn call_stateless_codec( + func: &PyObjectRef, + input: PyObjectRef, + errors: &Option<PyStrRef>, + role: &str, // "encoder" or "decoder" + vm: &VirtualMachine, + ) -> PyResult { + let mut args: Vec<PyObjectRef> = vec![input]; + if let Some(errors) = errors { + args.push(errors.to_owned().into()); + } + let res = func.call(args, vm)?; + let tuple: PyTupleRef = res.try_into_value(vm)?; + if tuple.len() != 2 { + return Err(vm.new_type_error(format!( + "{role} must return a tuple (object, integer)" + ))); + } + Ok(tuple[0].clone()) + }Then both
encodeanddecodebecome one-liners delegating to this helper.
Sorry, something went wrong.
3bfaffd to
f67094d
Compare
February 20, 2026 14:45
- Add sys.call_tracing as func(*args) dispatch - Add sys._current_exceptions with per-thread exception tracking via thread_exceptions registry synced on push/pop/set_exception - Fix f_back to use previous_frame pointer and cross-thread lookup - Add module="sys" to float_info struct sequence - Fix sys.exit to unpack tuple args - Set default stdio_errors to surrogateescape - Remove noisy __del__ warning in object core
slot_repr now checks the __module__ attribute set by #[pymodule] at runtime, so explicit module = "..." in #[pystruct_sequence] is no longer needed for types defined inside a #[pymodule].
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] test: cpython/Lib/test/test_c_locale_coercion.py dependencies: dependent tests: (no tests depend on c_locale_coercion) [x] test: cpython/Lib/test/test_sys.py (TODO: 10) dependencies: dependent tests: (213 tests)
Legend:
|
Sorry, something went wrong.
f5eadae
into
RustPython:main
Feb 21, 2026
Update test_sys from v3.14.3 and impl more sys module
Summary by CodeRabbit
New Features
Improvements