diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 5f0454086..f96537ec0 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1012,8 +1012,6 @@ if 1: code_lines = self.get_code_lines(genexp_code) self.assertEqual(genexp_lines, code_lines) - # TODO: RUSTPYTHON; implicit return line number after async for - @unittest.expectedFailure def test_line_number_implicit_return_after_async_for(self): async def test(aseq): diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 2bed4cde7..2edb09de2 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -2865,6 +2865,9 @@ impl Compiler { // Normal path jumps here to skip exception path blocks let end_block = self.new_block(); + // Emit NOP at the try: line so LINE events fire for it + emit!(self, Instruction::Nop); + // Setup a finally block if we have a finally statement. // Push fblock with handler info for exception table generation // IMPORTANT: handler goes to finally_except_block (exception path), not finally_block @@ -3018,8 +3021,10 @@ impl Compiler { type_, name, body, + range: handler_range, .. }) = &handler; + self.set_source_range(*handler_range); let next_handler = self.new_block(); // If we gave a typ, @@ -3313,6 +3318,9 @@ impl Compiler { }; let exit_block = self.new_block(); + // Emit NOP at the try: line so LINE events fire for it + emit!(self, Instruction::Nop); + // Push fblock with handler info for exception table generation if !finalbody.is_empty() { emit!( @@ -3743,6 +3751,9 @@ impl Compiler { is_async: bool, funcflags: bytecode::MakeFunctionFlags, ) -> CompileResult<()> { + // Save source range so MAKE_FUNCTION gets the `def` line, not the body's last line + let saved_range = self.current_source_range; + // Always enter function scope self.enter_function(name, parameters)?; self.current_code_info() @@ -3796,6 +3807,9 @@ impl Compiler { let code = self.exit_scope(); self.ctx = prev_ctx; + // Restore source range so MAKE_FUNCTION is attributed to the `def` line + self.set_source_range(saved_range); + // Create function object with closure self.make_closure(code, funcflags)?; @@ -5169,15 +5183,22 @@ impl Compiler { if is_async { emit!(self, Instruction::EndAsyncFor); } else { - // END_FOR + POP_ITER pattern (CPython 3.14) - // FOR_ITER jumps to END_FOR, but VM skips it (+1) to reach POP_ITER + // 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); } self.compile_statements(orelse)?; self.switch_to_block(after_block); + // Restore source range to the `for` line so any implicit return + // (LOAD_CONST None, RETURN_VALUE) is attributed to the `for` line, + // not the loop body's last line. + self.set_source_range(iter.range()); + self.leave_conditional_block(); Ok(()) } diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 651eb5910..317fddca6 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -250,15 +250,33 @@ impl CodeInfo { let mut locations = Vec::new(); let mut linetable_locations: Vec = Vec::new(); - // Convert pseudo ops and remove resulting NOPs + // Convert pseudo ops and remove resulting NOPs (keep line-marker NOPs) convert_pseudo_ops(&mut blocks, varname_cache.len() as u32); for block in blocks .iter_mut() .filter(|b| b.next != BlockIdx::NULL || !b.instructions.is_empty()) { - block + // Collect lines that have non-NOP instructions in this block + let non_nop_lines: std::collections::HashSet<_> = block .instructions - .retain(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop))); + .iter() + .filter(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop))) + .map(|ins| ins.location.line) + .collect(); + let mut kept_nop_lines: std::collections::HashSet = + std::collections::HashSet::new(); + block.instructions.retain(|ins| { + if matches!(ins.instr.real(), Some(Instruction::Nop)) { + let line = ins.location.line; + // Remove if another instruction covers this line, + // or if we already kept a NOP for this line + if non_nop_lines.contains(&line) || kept_nop_lines.contains(&line) { + return false; + } + kept_nop_lines.insert(line); + } + true + }); } let mut block_to_offset = vec![Label(0); blocks.len()]; @@ -501,9 +519,14 @@ impl CodeInfo { let tuple_const = ConstantData::Tuple { elements }; let (const_idx, _) = self.metadata.consts.insert_full(tuple_const); - // Replace preceding LOAD instructions with NOP + // Replace preceding LOAD instructions with NOP, using the + // BUILD_TUPLE location so remove_nops() treats them as + // same-line and removes them (multi-line tuple literals + // would otherwise leave line-introducing NOPs behind). + let folded_loc = block.instructions[i].location; for j in start_idx..i { block.instructions[j].instr = Instruction::Nop.into(); + block.instructions[j].location = folded_loc; } // Replace BUILD_TUPLE with LOAD_CONST @@ -673,12 +696,23 @@ impl CodeInfo { } } - /// Remove NOP instructions from all blocks + /// Remove NOP instructions from all blocks, but keep NOPs that introduce + /// a new source line (they serve as line markers for monitoring LINE events). fn remove_nops(&mut self) { for block in &mut self.blocks { - block - .instructions - .retain(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop))); + let mut prev_line = None; + block.instructions.retain(|ins| { + if matches!(ins.instr.real(), Some(Instruction::Nop)) { + let line = ins.location.line; + if prev_line == Some(line) { + // Same line as previous instruction — safe to remove + return false; + } + // This NOP introduces a new line — keep it + } + prev_line = Some(ins.location.line); + true + }); } } diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 313266eef..10815c2fc 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -369,6 +369,7 @@ impl Py { lasti: &self.lasti, object: self, state: &mut state, + monitoring_mask: 0, }; f(exec) } @@ -414,6 +415,7 @@ impl Py { lasti: &self.lasti, object: self, state: &mut state, + monitoring_mask: 0, }; exec.yield_from_target().map(PyObject::to_owned) } @@ -448,6 +450,8 @@ struct ExecutingFrame<'a> { object: &'a Py, lasti: &'a Lasti, state: &'a mut FrameState, + /// Cached monitoring events mask. Reloaded at Resume instruction only, + monitoring_mask: u32, } impl fmt::Debug for ExecutingFrame<'_> { @@ -498,6 +502,7 @@ impl ExecutingFrame<'_> { let instructions = &self.code.instructions; let mut arg_state = bytecode::OpArgState::default(); let mut prev_line: u32 = 0; + self.monitoring_mask = vm.state.monitoring_events.load(); loop { let idx = self.lasti() as usize; // Fire 'line' trace event when line number changes. @@ -515,6 +520,25 @@ impl ExecutingFrame<'_> { let bytecode::CodeUnit { op, arg } = instructions[idx]; let arg = arg_state.extend(arg); let mut do_extend_arg = false; + + // Always track current line for LINE monitoring, even when + // monitoring is off. This prevents spurious LINE events when + // monitoring is enabled mid-function. + if !matches!(op, Instruction::Resume { .. } | Instruction::ExtendedArg) { + let line = self.code.locations[idx].0.line.get() as u32; + if self.monitoring_mask != 0 { + use crate::stdlib::sys::monitoring; + let offset = idx as u32 * 2; + if self.monitoring_mask & monitoring::EVENT_LINE != 0 && line != prev_line { + monitoring::fire_line(vm, self.code, offset, line)?; + } + if self.monitoring_mask & monitoring::EVENT_INSTRUCTION != 0 { + monitoring::fire_instruction(vm, self.code, offset)?; + } + } + prev_line = line; + } + let result = self.execute_instruction(op, arg, &mut do_extend_arg, vm); match result { Ok(None) => {} @@ -601,10 +625,57 @@ impl ExecutingFrame<'_> { ) ); + // Fire RAISE or RERAISE monitoring event. + // fire_reraise internally deduplicates: only the first + // re-raise after each EXCEPTION_HANDLED fires the event. + // If the callback raises (e.g. ValueError for illegal DISABLE), + // replace the original exception. + let exception = { + use crate::stdlib::sys::monitoring; + if is_reraise { + if self.monitoring_mask & monitoring::EVENT_RERAISE != 0 { + let offset = idx as u32 * 2; + let exc_obj: PyObjectRef = exception.clone().into(); + match monitoring::fire_reraise(vm, self.code, offset, &exc_obj) { + Ok(()) => exception, + Err(monitor_exc) => monitor_exc, + } + } else { + exception + } + } else if self.monitoring_mask & monitoring::EVENT_RAISE != 0 { + let offset = idx as u32 * 2; + let exc_obj: PyObjectRef = exception.clone().into(); + match monitoring::fire_raise(vm, self.code, offset, &exc_obj) { + Ok(()) => exception, + Err(monitor_exc) => monitor_exc, + } + } else { + exception + } + }; + match handle_exception(self, exception, idx, is_reraise, is_new_raise, vm) { Ok(None) => {} Ok(Some(result)) => break Ok(result), Err(exception) => { + // Fire PY_UNWIND: exception escapes this frame + let exception = if self.monitoring_mask + & 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 + }; + // Restore lasti from traceback so frame.f_lineno matches tb_lineno // The traceback was created with the correct lasti when exception // was first raised, but frame.lasti may have changed during cleanup @@ -671,6 +742,7 @@ impl ExecutingFrame<'_> { exc_val: PyObjectRef, exc_tb: PyObjectRef, ) -> PyResult { + self.monitoring_mask = vm.state.monitoring_events.load(); if let Some(jen) = self.yield_from_target() { // Check if the exception is GeneratorExit (type or instance). // For GeneratorExit, close the sub-iterator instead of throwing. @@ -781,6 +853,33 @@ impl ExecutingFrame<'_> { exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx))); } + // Fire PY_THROW and RAISE events before raising the exception in the + // generator. do_monitor_exc in CPython replaces the active exception + // when a callback fails, so we mirror that here. + let exception = { + use crate::stdlib::sys::monitoring; + let exception = if self.monitoring_mask & monitoring::EVENT_PY_THROW != 0 { + let offset = idx as u32 * 2; + let exc_obj: PyObjectRef = exception.clone().into(); + match monitoring::fire_py_throw(vm, self.code, offset, &exc_obj) { + Ok(()) => exception, + Err(monitor_exc) => monitor_exc, + } + } else { + exception + }; + if self.monitoring_mask & monitoring::EVENT_RAISE != 0 { + let offset = idx as u32 * 2; + let exc_obj: PyObjectRef = exception.clone().into(); + match monitoring::fire_raise(vm, self.code, offset, &exc_obj) { + Ok(()) => exception, + Err(monitor_exc) => monitor_exc, + } + } else { + exception + } + }; + // when raising an exception, set __context__ to the current exception // This is done in _PyErr_SetObject vm.contextualize_exception(&exception); @@ -792,7 +891,26 @@ impl ExecutingFrame<'_> { match self.unwind_blocks(vm, UnwindReason::Raising { exception }) { Ok(None) => self.run(vm), Ok(Some(result)) => Ok(result), - Err(exception) => Err(exception), + Err(exception) => { + // Fire PY_UNWIND: exception escapes the generator frame. + // do_monitor_exc replaces the exception on callback failure. + let exception = if self.monitoring_mask + & 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) + } } } @@ -1371,7 +1489,18 @@ impl ExecutingFrame<'_> { Ok(None) } Instruction::JumpBackward { target } => { - self.jump(target.get(arg)); + let src_offset = (self.lasti() - 1) * 2; + let dest = target.get(arg); + self.jump(dest); + // JUMP events fire only for backward jumps (loop iterations) + if self.monitoring_mask & crate::stdlib::sys::monitoring::EVENT_JUMP != 0 { + crate::stdlib::sys::monitoring::fire_jump( + vm, + self.code, + src_offset, + dest.0 * 2, + )?; + } Ok(None) } Instruction::JumpBackwardNoInterrupt { target } => { @@ -1976,16 +2105,24 @@ impl ExecutingFrame<'_> { Instruction::PopJumpIfTrue { target } => self.pop_jump_if(vm, target.get(arg), true), Instruction::PopJumpIfNone { target } => { let value = self.pop_value(); - if vm.is_none(&value) { - self.jump(target.get(arg)); + let branch_taken = vm.is_none(&value); + let target = target.get(arg); + let src_offset = (self.lasti() - 1) * 2; + if branch_taken { + self.jump(target); } + self.fire_branch_event(vm, src_offset, branch_taken, target)?; Ok(None) } Instruction::PopJumpIfNotNone { target } => { let value = self.pop_value(); - if !vm.is_none(&value) { - self.jump(target.get(arg)); + let branch_taken = !vm.is_none(&value); + let target = target.get(arg); + let src_offset = (self.lasti() - 1) * 2; + if branch_taken { + self.jump(target); } + self.fire_branch_event(vm, src_offset, branch_taken, target)?; Ok(None) } Instruction::PopTop => { @@ -2010,19 +2147,25 @@ impl ExecutingFrame<'_> { } Instruction::RaiseVarargs { kind } => self.execute_raise(vm, kind.get(arg)), 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()?; - // } + use crate::stdlib::sys::monitoring; + let resume_type = resume_arg.get(arg); + self.monitoring_mask = vm.state.monitoring_events.load(); + let offset = (self.lasti() - 1) * 2; + if resume_type == 0 { + if self.monitoring_mask & monitoring::EVENT_PY_START != 0 { + monitoring::fire_py_start(vm, self.code, offset)?; + } + } else if self.monitoring_mask & monitoring::EVENT_PY_RESUME != 0 { + monitoring::fire_py_resume(vm, self.code, offset)?; + } Ok(None) } Instruction::ReturnValue => { let value = self.pop_value(); + if self.monitoring_mask & 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)?; + } self.unwind_blocks(vm, UnwindReason::Returning { value }) } Instruction::SetAdd { i } => { @@ -2061,8 +2204,10 @@ impl ExecutingFrame<'_> { vm.set_exception(Some(exc_ref.to_owned())); } + // Complete stack operations self.push_value(prev_exc); self.push_value(exc); + Ok(None) } Instruction::CheckExcMatch => { @@ -2243,6 +2388,10 @@ impl ExecutingFrame<'_> { } Instruction::YieldValue { arg: oparg } => { let value = self.pop_value(); + if self.monitoring_mask & 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)?; + } // arg=0: direct yield (wrapped for async generators) // arg=1: yield from await/yield-from (NOT wrapped) let wrap = oparg.get(arg) == 0; @@ -2571,6 +2720,23 @@ impl ExecutingFrame<'_> { if let Some(entry) = bytecode::find_exception_handler(&self.code.exceptiontable, offset) { + // Fire EXCEPTION_HANDLED before setting up handler. + // If the callback raises, the handler is NOT set up and the + // new exception propagates instead. + if self.monitoring_mask + & 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( + vm, + self.code, + byte_offset, + &exc_obj, + )?; + } + // 1. Pop stack to entry.depth while self.state.stack.len() > entry.depth as usize { self.state.stack.pop(); @@ -2774,6 +2940,8 @@ impl ExecutingFrame<'_> { #[inline] fn execute_call(&mut self, args: FuncArgs, vm: &VirtualMachine) -> FrameResult { + use crate::stdlib::sys::monitoring; + // Stack: [callable, self_or_null, ...] let self_or_null = self.pop_value_opt(); // Option let callable = self.pop_value(); @@ -2792,9 +2960,50 @@ impl ExecutingFrame<'_> { args }; - let value = callable.call(final_args, vm)?; - self.push_value(value); - Ok(None) + let is_python_call = callable.downcast_ref::().is_some(); + + // Compute arg0 once for CALL, C_RETURN, and C_RAISE events + let call_arg0 = if self.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 + }; + + match callable.call(final_args, vm) { + Ok(value) => { + if let Some(arg0) = call_arg0 + && !is_python_call + { + let offset = (self.lasti() - 1) * 2; + monitoring::fire_c_return(vm, self.code, offset, &callable, arg0)?; + } + self.push_value(value); + Ok(None) + } + Err(exc) => { + // call_instrumentation_vector_protected replaces the active + // exception when the callback itself raises. + let exc = if let Some(arg0) = call_arg0 + && !is_python_call + { + 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) + } + } } fn execute_raise(&mut self, vm: &VirtualMachine, kind: bytecode::RaiseKind) -> FrameResult { @@ -2923,6 +3132,43 @@ impl ExecutingFrame<'_> { self.update_lasti(|i| *i = target_pc); } + #[inline] + fn fire_branch_event( + &self, + vm: &VirtualMachine, + src_offset: u32, + branch_taken: bool, + target: bytecode::Label, + ) -> PyResult<()> { + if self.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 { + crate::stdlib::sys::monitoring::fire_branch_right( + vm, + self.code, + src_offset, + dest_offset, + )?; + } else { + crate::stdlib::sys::monitoring::fire_branch_left( + vm, + self.code, + src_offset, + dest_offset, + )?; + } + } + Ok(()) + } + #[inline] fn pop_jump_if( &mut self, @@ -2932,9 +3178,12 @@ impl ExecutingFrame<'_> { ) -> FrameResult { let obj = self.pop_value(); let value = obj.try_to_bool(vm)?; - if value == flag { + let branch_taken = value == flag; + let src_offset = (self.lasti() - 1) * 2; + if branch_taken { self.jump(target); } + self.fire_branch_event(vm, src_offset, branch_taken, target)?; Ok(None) } @@ -2942,11 +3191,13 @@ impl ExecutingFrame<'_> { fn execute_for_iter(&mut self, vm: &VirtualMachine, target: bytecode::Label) -> FrameResult { let top_of_stack = PyIter::new(self.top_value()); let next_obj = top_of_stack.next(vm); + let src_offset = (self.lasti() - 1) * 2; // Check the next object: match next_obj { Ok(PyIterReturn::Return(value)) => { self.push_value(value); + self.fire_branch_event(vm, src_offset, false, target)?; Ok(None) } Ok(PyIterReturn::StopIteration(_)) => { @@ -2970,6 +3221,7 @@ impl ExecutingFrame<'_> { target }; self.jump(jump_target); + self.fire_branch_event(vm, src_offset, true, jump_target)?; Ok(None) } Err(next_error) => { diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index ec21cc7f4..68734f946 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -1,3 +1,5 @@ +pub(crate) mod monitoring; + use crate::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule, convert::ToPyObject}; #[cfg(all(not(feature = "host_env"), feature = "stdio"))] @@ -73,6 +75,9 @@ mod sys { RUST_MULTIARCH.replace("-unknown", "") } + #[pymodule(name = "monitoring", with(super::monitoring::sys_monitoring))] + pub(super) mod monitoring {} + #[pyclass(no_attr, name = "_BootstrapStderr")] #[derive(Debug, PyPayload)] pub(super) struct BootstrapStderr; diff --git a/crates/vm/src/stdlib/sys/monitoring.rs b/crates/vm/src/stdlib/sys/monitoring.rs new file mode 100644 index 000000000..f024e9dad --- /dev/null +++ b/crates/vm/src/stdlib/sys/monitoring.rs @@ -0,0 +1,872 @@ +use crate::{ + AsObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + builtins::{PyCode, PyDictRef, PyNamespace, PyUtf8StrRef}, + function::FuncArgs, +}; +use crossbeam_utils::atomic::AtomicCell; +use std::collections::{HashMap, HashSet}; + +pub const TOOL_LIMIT: usize = 6; +const EVENTS_COUNT: usize = 19; +const LOCAL_EVENTS_COUNT: usize = 11; +const UNGROUPED_EVENTS_COUNT: usize = 18; + +// Event bit positions +bitflags::bitflags! { + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct MonitoringEvents: u32 { + const PY_START = 1 << 0; + const PY_RESUME = 1 << 1; + const PY_RETURN = 1 << 2; + const PY_YIELD = 1 << 3; + const CALL = 1 << 4; + const LINE = 1 << 5; + const INSTRUCTION = 1 << 6; + const JUMP = 1 << 7; + const BRANCH_LEFT = 1 << 8; + const BRANCH_RIGHT = 1 << 9; + const STOP_ITERATION = 1 << 10; + const RAISE = 1 << 11; + const EXCEPTION_HANDLED = 1 << 12; + const PY_UNWIND = 1 << 13; + const PY_THROW = 1 << 14; + const RERAISE = 1 << 15; + const C_RETURN = 1 << 16; + const C_RAISE = 1 << 17; + const BRANCH = 1 << 18; + } +} + +// Re-export as plain u32 constants for use in frame.rs hot-path checks +pub const EVENT_PY_START: u32 = MonitoringEvents::PY_START.bits(); +pub const EVENT_PY_RESUME: u32 = MonitoringEvents::PY_RESUME.bits(); +pub const EVENT_PY_RETURN: u32 = MonitoringEvents::PY_RETURN.bits(); +pub const EVENT_PY_YIELD: u32 = MonitoringEvents::PY_YIELD.bits(); +pub const EVENT_CALL: u32 = MonitoringEvents::CALL.bits(); +pub const EVENT_LINE: u32 = MonitoringEvents::LINE.bits(); +pub const EVENT_INSTRUCTION: u32 = MonitoringEvents::INSTRUCTION.bits(); +pub const EVENT_JUMP: u32 = MonitoringEvents::JUMP.bits(); +pub const EVENT_BRANCH_LEFT: u32 = MonitoringEvents::BRANCH_LEFT.bits(); +pub const EVENT_BRANCH_RIGHT: u32 = MonitoringEvents::BRANCH_RIGHT.bits(); +pub const EVENT_RAISE: u32 = MonitoringEvents::RAISE.bits(); +pub const EVENT_EXCEPTION_HANDLED: u32 = MonitoringEvents::EXCEPTION_HANDLED.bits(); +pub const EVENT_PY_UNWIND: u32 = MonitoringEvents::PY_UNWIND.bits(); +pub const EVENT_C_RETURN: u32 = MonitoringEvents::C_RETURN.bits(); +const EVENT_C_RAISE: u32 = MonitoringEvents::C_RAISE.bits(); +const EVENT_STOP_ITERATION: u32 = MonitoringEvents::STOP_ITERATION.bits(); +pub const EVENT_PY_THROW: u32 = MonitoringEvents::PY_THROW.bits(); +const EVENT_BRANCH: u32 = MonitoringEvents::BRANCH.bits(); +pub const EVENT_RERAISE: u32 = MonitoringEvents::RERAISE.bits(); +const EVENT_C_RETURN_MASK: u32 = EVENT_C_RETURN | EVENT_C_RAISE; + +const EVENT_NAMES: [&str; EVENTS_COUNT] = [ + "PY_START", + "PY_RESUME", + "PY_RETURN", + "PY_YIELD", + "CALL", + "LINE", + "INSTRUCTION", + "JUMP", + "BRANCH_LEFT", + "BRANCH_RIGHT", + "STOP_ITERATION", + "RAISE", + "EXCEPTION_HANDLED", + "PY_UNWIND", + "PY_THROW", + "RERAISE", + "C_RETURN", + "C_RAISE", + "BRANCH", +]; + +/// Interpreter-level monitoring state, shared by all threads. +pub struct MonitoringState { + pub tool_names: [Option; TOOL_LIMIT], + pub global_events: [u32; TOOL_LIMIT], + pub local_events: HashMap<(usize, usize), u32>, + pub callbacks: HashMap<(usize, usize), PyObjectRef>, + /// Per-instruction disabled tools: (code_id, offset, tool) + pub disabled: HashSet<(usize, usize, usize)>, + /// Cached MISSING sentinel singleton + pub missing: Option, + /// Cached DISABLE sentinel singleton + pub disable: Option, +} + +impl Default for MonitoringState { + fn default() -> Self { + Self { + tool_names: Default::default(), + global_events: [0; TOOL_LIMIT], + local_events: HashMap::new(), + callbacks: HashMap::new(), + disabled: HashSet::new(), + missing: None, + disable: None, + } + } +} + +impl MonitoringState { + /// Compute the OR of all tools' global_events + local_events. + /// This is used for the fast-path atomic mask to skip monitoring + /// when no events are registered at all. + pub fn combined_events(&self) -> u32 { + let global = self.global_events.iter().fold(0, |acc, &e| acc | e); + let local = self.local_events.values().fold(0, |acc, &e| acc | e); + global | local + } +} + +/// Global atomic mask: OR of all tools' events. Checked in the hot path +/// to skip monitoring overhead when no events are registered. +/// Lives in PyGlobalState alongside the PyMutex. +pub type MonitoringEventsMask = AtomicCell; + +/// Get the MISSING sentinel, creating it if necessary. +pub fn get_missing(vm: &VirtualMachine) -> PyObjectRef { + let mut state = vm.state.monitoring.lock(); + if let Some(ref m) = state.missing { + m.clone() + } else { + let m: PyObjectRef = sys_monitoring::MonitoringSentinel.into_ref(&vm.ctx).into(); + state.missing = Some(m.clone()); + m + } +} + +/// Get the DISABLE sentinel, creating it if necessary. +pub fn get_disable(vm: &VirtualMachine) -> PyObjectRef { + let mut state = vm.state.monitoring.lock(); + if let Some(ref d) = state.disable { + d.clone() + } else { + let d: PyObjectRef = sys_monitoring::MonitoringSentinel.into_ref(&vm.ctx).into(); + state.disable = Some(d.clone()); + d + } +} + +fn check_valid_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult { + if !(0..TOOL_LIMIT as i32).contains(&tool_id) { + return Err(vm.new_value_error(format!("invalid tool {tool_id} (must be between 0 and 5)"))); + } + Ok(tool_id as usize) +} + +fn check_tool_in_use(tool: usize, vm: &VirtualMachine) -> PyResult<()> { + let state = vm.state.monitoring.lock(); + if state.tool_names[tool].is_some() { + Ok(()) + } else { + Err(vm.new_value_error(format!("tool {tool} is not in use"))) + } +} + +fn parse_single_event(event: i32, vm: &VirtualMachine) -> PyResult { + let event = u32::try_from(event) + .map_err(|_| vm.new_value_error("The callback can only be set for one event at a time"))?; + if event.count_ones() != 1 { + return Err(vm.new_value_error("The callback can only be set for one event at a time")); + } + let event_id = event.trailing_zeros() as usize; + if event_id >= EVENTS_COUNT { + return Err(vm.new_value_error(format!("invalid event {event}"))); + } + Ok(event_id) +} + +fn normalize_event_set(event_set: i32, local: bool, vm: &VirtualMachine) -> PyResult { + if event_set < 0 { + let kind = if local { + "local event set" + } else { + "event set" + }; + return Err(vm.new_value_error(format!("invalid {kind} 0x{event_set:x}"))); + } + + let mut event_set = event_set as u32; + if event_set >= (1 << EVENTS_COUNT) { + let kind = if local { + "local event set" + } else { + "event set" + }; + return Err(vm.new_value_error(format!("invalid {kind} 0x{event_set:x}"))); + } + + if (event_set & EVENT_C_RETURN_MASK) != 0 && (event_set & EVENT_CALL) != EVENT_CALL { + return Err(vm.new_value_error("cannot set C_RETURN or C_RAISE events independently")); + } + + event_set &= !EVENT_C_RETURN_MASK; + + if (event_set & EVENT_BRANCH) != 0 { + event_set &= !EVENT_BRANCH; + event_set |= EVENT_BRANCH_LEFT | EVENT_BRANCH_RIGHT; + } + + if local && event_set >= (1 << LOCAL_EVENTS_COUNT) { + return Err(vm.new_value_error(format!("invalid local event set 0x{event_set:x}"))); + } + + Ok(event_set) +} + +/// Update the global monitoring_events atomic mask from current state. +fn update_events_mask(vm: &VirtualMachine, state: &MonitoringState) { + vm.state.monitoring_events.store(state.combined_events()); +} + +fn use_tool_id(tool_id: i32, name: &str, 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() { + return Err(vm.new_value_error(format!("tool {tool_id} is already in use"))); + } + state.tool_names[tool] = Some(name.to_owned()); + Ok(()) +} + +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); + } + update_events_mask(vm, &state); + Ok(()) +} + +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(); + 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(()) +} + +fn get_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult> { + let tool = check_valid_tool(tool_id, vm)?; + let state = vm.state.monitoring.lock(); + Ok(state.tool_names[tool].clone()) +} + +fn register_callback( + tool_id: i32, + event: i32, + func: PyObjectRef, + vm: &VirtualMachine, +) -> PyResult { + let tool = check_valid_tool(tool_id, vm)?; + let event_id = parse_single_event(event, vm)?; + + let mut state = vm.state.monitoring.lock(); + let prev = state + .callbacks + .remove(&(tool, event_id)) + .unwrap_or_else(|| vm.ctx.none()); + 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()); + // BRANCH is a composite event: also register for BRANCH_LEFT/RIGHT + if event_id == branch_id { + state.callbacks.insert((tool, branch_left_id), func.clone()); + state.callbacks.insert((tool, branch_right_id), func); + } + } else { + // Also clear BRANCH_LEFT/RIGHT when clearing BRANCH + if event_id == branch_id { + state.callbacks.remove(&(tool, branch_left_id)); + state.callbacks.remove(&(tool, branch_right_id)); + } + } + Ok(prev) +} + +fn get_events(tool_id: i32, vm: &VirtualMachine) -> PyResult { + let tool = check_valid_tool(tool_id, vm)?; + let state = vm.state.monitoring.lock(); + Ok(state.global_events[tool]) +} + +fn set_events(tool_id: i32, event_set: i32, vm: &VirtualMachine) -> PyResult<()> { + let tool = check_valid_tool(tool_id, vm)?; + check_tool_in_use(tool, vm)?; + let normalized = normalize_event_set(event_set, false, vm)?; + let mut state = vm.state.monitoring.lock(); + state.global_events[tool] = normalized; + update_events_mask(vm, &state); + Ok(()) +} + +fn get_local_events(tool_id: i32, code: PyObjectRef, vm: &VirtualMachine) -> PyResult { + if code.downcast_ref::().is_none() { + return Err(vm.new_type_error("code must be a code object")); + } + let tool = check_valid_tool(tool_id, vm)?; + let code_id = code.get_id(); + let state = vm.state.monitoring.lock(); + Ok(state + .local_events + .get(&(tool, code_id)) + .copied() + .unwrap_or(0)) +} + +fn set_local_events( + tool_id: i32, + code: PyObjectRef, + event_set: i32, + vm: &VirtualMachine, +) -> PyResult<()> { + if code.downcast_ref::().is_none() { + return Err(vm.new_type_error("code must be a code object")); + } + let tool = check_valid_tool(tool_id, vm)?; + check_tool_in_use(tool, vm)?; + let normalized = normalize_event_set(event_set, true, vm)?; + let code_id = code.get_id(); + let mut state = vm.state.monitoring.lock(); + if normalized == 0 { + state.local_events.remove(&(tool, code_id)); + } else { + state.local_events.insert((tool, code_id), normalized); + } + update_events_mask(vm, &state); + Ok(()) +} + +fn restart_events(vm: &VirtualMachine) { + let mut state = vm.state.monitoring.lock(); + state.disabled.clear(); +} + +fn all_events(vm: &VirtualMachine) -> PyResult { + // 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) +} + +// Event dispatch + +use core::cell::Cell; + +thread_local! { + /// Re-entrancy guard: prevents monitoring callbacks from triggering + /// additional monitoring events (which would cause infinite recursion). + static FIRING: Cell = const { Cell::new(false) }; + + /// Tracks whether a RERAISE event has been fired since the last + /// EXCEPTION_HANDLED. Used to suppress duplicate RERAISE from + /// cleanup handlers that chain through multiple exception table entries. + static RERAISE_PENDING: Cell = const { Cell::new(false) }; +} + +/// Fire an event for all tools that have the event bit set. +/// `cb_extra` contains the callback arguments after the code object. +fn fire( + vm: &VirtualMachine, + event: u32, + code: &PyRef, + offset: u32, + cb_extra: &[PyObjectRef], +) -> PyResult<()> { + // Prevent recursive event firing + if FIRING.with(|f| f.get()) { + return Ok(()); + } + + let event_id = event.trailing_zeros() as usize; + let code_id = code.get_id(); + + // C_RETURN and C_RAISE are implicitly enabled when CALL is set. + let check_bit = if event & EVENT_C_RETURN_MASK != 0 { + event | EVENT_CALL + } else { + event + }; + + // Collect callbacks and snapshot the DISABLE sentinel under a single lock. + let (callbacks, disable_sentinel): (Vec<(usize, PyObjectRef)>, Option) = { + let state = vm.state.monitoring.lock(); + let mut cbs = Vec::new(); + for tool in 0..TOOL_LIMIT { + let global = state.global_events[tool]; + let local = state + .local_events + .get(&(tool, code_id)) + .copied() + .unwrap_or(0); + if ((global | local) & check_bit) == 0 { + continue; + } + if state.disabled.contains(&(code_id, offset as usize, tool)) { + continue; + } + if let Some(cb) = state.callbacks.get(&(tool, event_id)) { + cbs.push((tool, cb.clone())); + } + } + (cbs, state.disable.clone()) + }; + + if callbacks.is_empty() { + return Ok(()); + } + + let mut args_vec = Vec::with_capacity(1 + cb_extra.len()); + args_vec.push(code.clone().into()); + args_vec.extend_from_slice(cb_extra); + let args = FuncArgs::from(args_vec); + + FIRING.with(|f| f.set(true)); + let result = (|| { + for (tool, cb) in callbacks { + let result = cb.call(args.clone(), vm)?; + if disable_sentinel.as_ref().is_some_and(|d| result.is(d)) { + // Only local events (event_id < LOCAL_EVENTS_COUNT) can be disabled. + // Non-local events (RAISE, EXCEPTION_HANDLED, PY_UNWIND, etc.) + // cannot be disabled per code object. + if event_id >= LOCAL_EVENTS_COUNT { + return Err(vm.new_value_error(format!( + "cannot disable {} events", + EVENT_NAMES[event_id] + ))); + } + let mut state = vm.state.monitoring.lock(); + state.disabled.insert((code_id, offset as usize, tool)); + } + } + Ok(()) + })(); + FIRING.with(|f| f.set(false)); + result +} + +// Public dispatch functions (called from frame.rs) + +pub fn fire_py_start(vm: &VirtualMachine, code: &PyRef, offset: u32) -> PyResult<()> { + fire( + vm, + EVENT_PY_START, + code, + offset, + &[vm.ctx.new_int(offset).into()], + ) +} + +pub fn fire_py_resume(vm: &VirtualMachine, code: &PyRef, offset: u32) -> PyResult<()> { + fire( + vm, + EVENT_PY_RESUME, + code, + offset, + &[vm.ctx.new_int(offset).into()], + ) +} + +pub fn fire_py_return( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + retval: &PyObjectRef, +) -> PyResult<()> { + fire( + vm, + EVENT_PY_RETURN, + code, + offset, + &[vm.ctx.new_int(offset).into(), retval.clone()], + ) +} + +pub fn fire_py_yield( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + retval: &PyObjectRef, +) -> PyResult<()> { + fire( + vm, + EVENT_PY_YIELD, + code, + offset, + &[vm.ctx.new_int(offset).into(), retval.clone()], + ) +} + +pub fn fire_call( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + callable: &PyObjectRef, + arg0: PyObjectRef, +) -> PyResult<()> { + fire( + vm, + EVENT_CALL, + code, + offset, + &[vm.ctx.new_int(offset).into(), callable.clone(), arg0], + ) +} + +pub fn fire_c_return( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + callable: &PyObjectRef, + arg0: PyObjectRef, +) -> PyResult<()> { + fire( + vm, + EVENT_C_RETURN, + code, + offset, + &[vm.ctx.new_int(offset).into(), callable.clone(), arg0], + ) +} + +pub fn fire_c_raise( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + callable: &PyObjectRef, + arg0: PyObjectRef, +) -> PyResult<()> { + fire( + vm, + EVENT_C_RAISE, + code, + offset, + &[vm.ctx.new_int(offset).into(), callable.clone(), arg0], + ) +} + +pub fn fire_line( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + line: u32, +) -> PyResult<()> { + fire(vm, EVENT_LINE, code, offset, &[vm.ctx.new_int(line).into()]) +} + +pub fn fire_instruction(vm: &VirtualMachine, code: &PyRef, offset: u32) -> PyResult<()> { + fire( + vm, + EVENT_INSTRUCTION, + code, + offset, + &[vm.ctx.new_int(offset).into()], + ) +} + +pub fn fire_raise( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + exception: &PyObjectRef, +) -> PyResult<()> { + fire( + vm, + EVENT_RAISE, + code, + offset, + &[vm.ctx.new_int(offset).into(), exception.clone()], + ) +} + +/// Only fires if no RERAISE has been fired since the last EXCEPTION_HANDLED, +/// preventing duplicate events from chained cleanup handlers. +pub fn fire_reraise( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + exception: &PyObjectRef, +) -> PyResult<()> { + if RERAISE_PENDING.with(|f| f.get()) { + return Ok(()); + } + RERAISE_PENDING.with(|f| f.set(true)); + fire( + vm, + EVENT_RERAISE, + code, + offset, + &[vm.ctx.new_int(offset).into(), exception.clone()], + ) +} + +pub fn fire_exception_handled( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + exception: &PyObjectRef, +) -> PyResult<()> { + RERAISE_PENDING.with(|f| f.set(false)); + fire( + vm, + EVENT_EXCEPTION_HANDLED, + code, + offset, + &[vm.ctx.new_int(offset).into(), exception.clone()], + ) +} + +pub fn fire_py_unwind( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + exception: &PyObjectRef, +) -> PyResult<()> { + RERAISE_PENDING.with(|f| f.set(false)); + fire( + vm, + EVENT_PY_UNWIND, + code, + offset, + &[vm.ctx.new_int(offset).into(), exception.clone()], + ) +} + +pub fn fire_py_throw( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + exception: &PyObjectRef, +) -> PyResult<()> { + fire( + vm, + EVENT_PY_THROW, + code, + offset, + &[vm.ctx.new_int(offset).into(), exception.clone()], + ) +} + +#[allow(dead_code)] +pub fn fire_stop_iteration( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + exception: &PyObjectRef, +) -> PyResult<()> { + fire( + vm, + EVENT_STOP_ITERATION, + code, + offset, + &[vm.ctx.new_int(offset).into(), exception.clone()], + ) +} + +pub fn fire_jump( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + destination: u32, +) -> PyResult<()> { + fire( + vm, + EVENT_JUMP, + code, + offset, + &[ + vm.ctx.new_int(offset).into(), + vm.ctx.new_int(destination).into(), + ], + ) +} + +pub fn fire_branch_left( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + destination: u32, +) -> PyResult<()> { + fire( + vm, + EVENT_BRANCH_LEFT, + code, + offset, + &[ + vm.ctx.new_int(offset).into(), + vm.ctx.new_int(destination).into(), + ], + ) +} + +pub fn fire_branch_right( + vm: &VirtualMachine, + code: &PyRef, + offset: u32, + destination: u32, +) -> PyResult<()> { + fire( + vm, + EVENT_BRANCH_RIGHT, + code, + offset, + &[ + vm.ctx.new_int(offset).into(), + vm.ctx.new_int(destination).into(), + ], + ) +} + +#[pymodule(sub)] +pub(super) mod sys_monitoring { + use super::*; + + #[pyclass(no_attr, module = "sys.monitoring", name = "_Sentinel")] + #[derive(Debug, PyPayload)] + pub(super) struct MonitoringSentinel; + + #[pyclass] + impl MonitoringSentinel {} + + #[pyattr(name = "DEBUGGER_ID")] + const DEBUGGER_ID: u8 = 0; + #[pyattr(name = "COVERAGE_ID")] + const COVERAGE_ID: u8 = 1; + #[pyattr(name = "PROFILER_ID")] + const PROFILER_ID: u8 = 2; + #[pyattr(name = "OPTIMIZER_ID")] + const OPTIMIZER_ID: u8 = 5; + + #[pyattr(once, name = "DISABLE")] + fn disable(vm: &VirtualMachine) -> PyObjectRef { + super::get_disable(vm) + } + + #[pyattr(once, name = "MISSING")] + fn missing(vm: &VirtualMachine) -> PyObjectRef { + super::get_missing(vm) + } + + #[pyattr(once)] + fn events(vm: &VirtualMachine) -> PyRef { + let events = PyNamespace::default().into_ref(&vm.ctx); + for (event_id, event_name) in EVENT_NAMES.iter().enumerate() { + events + .as_object() + .set_attr(*event_name, vm.ctx.new_int(1u32 << event_id), vm) + .expect("setting sys.monitoring.events attribute should not fail"); + } + events + .as_object() + .set_attr("NO_EVENTS", vm.ctx.new_int(0), vm) + .expect("setting sys.monitoring.events.NO_EVENTS should not fail"); + events + } + + #[pyfunction] + fn use_tool_id(tool_id: i32, name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<()> { + super::use_tool_id(tool_id, name.as_str(), vm) + } + + #[pyfunction] + fn clear_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> { + super::clear_tool_id(tool_id, vm) + } + + #[pyfunction] + fn free_tool_id(tool_id: i32, vm: &VirtualMachine) -> PyResult<()> { + super::free_tool_id(tool_id, vm) + } + + #[pyfunction] + fn get_tool(tool_id: i32, vm: &VirtualMachine) -> PyResult> { + super::get_tool(tool_id, vm) + } + + #[pyfunction] + fn register_callback( + tool_id: i32, + event: i32, + func: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + super::register_callback(tool_id, event, func, vm) + } + + #[pyfunction] + fn get_events(tool_id: i32, vm: &VirtualMachine) -> PyResult { + super::get_events(tool_id, vm) + } + + #[pyfunction] + fn set_events(tool_id: i32, event_set: i32, vm: &VirtualMachine) -> PyResult<()> { + super::set_events(tool_id, event_set, vm) + } + + #[pyfunction] + fn get_local_events(tool_id: i32, code: PyObjectRef, vm: &VirtualMachine) -> PyResult { + super::get_local_events(tool_id, code, vm) + } + + #[pyfunction] + fn set_local_events( + tool_id: i32, + code: PyObjectRef, + event_set: i32, + vm: &VirtualMachine, + ) -> PyResult<()> { + super::set_local_events(tool_id, code, event_set, vm) + } + + #[pyfunction] + fn restart_events(vm: &VirtualMachine) { + super::restart_events(vm) + } + + #[pyfunction] + fn _all_events(vm: &VirtualMachine) -> PyResult { + super::all_events(vm) + } +} diff --git a/crates/vm/src/vm/interpreter.rs b/crates/vm/src/vm/interpreter.rs index f6d3ee4bc..9f6f4de3d 100644 --- a/crates/vm/src/vm/interpreter.rs +++ b/crates/vm/src/vm/interpreter.rs @@ -124,6 +124,8 @@ where thread_handles: parking_lot::Mutex::new(Vec::new()), #[cfg(feature = "threading")] shutdown_handles: parking_lot::Mutex::new(Vec::new()), + monitoring: PyMutex::default(), + monitoring_events: AtomicCell::new(0), }); // Create VM with the global state diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 9802832ce..3d1fa467d 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -155,6 +155,10 @@ pub struct PyGlobalState { /// Registry for non-daemon threads that need to be joined at shutdown #[cfg(feature = "threading")] pub shutdown_handles: parking_lot::Mutex>, + /// sys.monitoring state (tool names, events, callbacks) + pub monitoring: PyMutex, + /// Fast-path mask: OR of all tools' events. 0 means no monitoring overhead. + pub monitoring_events: stdlib::sys::monitoring::MonitoringEventsMask, } pub fn process_hash_secret_seed() -> u32 {