Match settrace events, f_lineno jumps, and type dict lookup (#8758)

* Keep type dict keys and report current f_lasti

Snapshot the MRO during lookup so non-string keys can run __eq__,
skip subclass updates when set_bases is reentered, make module
__dict__ a readonly member, and forbid deleting exception __dict__.
f_lasti is the current opcode; line events skip NO_LOCATION units.

Assisted-by: Grok:grok-4.6

* Fire settrace call events from RESUME

PY_START/PY_RESUME become a 'call' event only at the first RESUME
of a frame activation, so lasti is the resume unit and f_trace_lines
is honored. Unskip passing TraceTestCase methods.

Assisted-by: Grok:grok-4.6

* Record what_event so f_lineno jumps match line traces

Store the current monitoring event while a trace callback
runs, reject jumps from call/return/exception, unwind tracer
errors in the traced frame, and fire call on PY_THROW.
Unskip JumpTestCase except the infinite-loop and five remaining xfails.

Assisted-by: Grok:grok-4.6

* Fire remaining settrace events from opcodes and jumps

Keep GET_ANEXT's iterator in mark_stacks, dispatch non-call
events through f_trace, fire PY_YIELD/PY_RETURN from the
opcodes, emit same-line LINE on backward jumps, and raise at
flattened call sites. Unskip the rest of test_sys_settrace.

Assisted-by: Grok:grok-4.6

* Fix trampoline raise panic and unxfail passing traces

Do not decode inline-cache units as opcodes when a flattened
call raises. Remove expectedFailure from test_trace and
test_regrtest coverage tests that now pass.

Assisted-by: Grok:grok-4.6

* Fire profile returns without a per-frame f_trace

setprofile must see PY_RETURN/PY_YIELD even when f_trace is unset.
Drop EXPECTED_FAILURE from the pdb doctest and unxfail
test_unfinished_generator.

Assisted-by: Grok:grok-4.6
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-21 18:11:32 +09:00
1 parent 617c4be5d9
commit 53a906c916
16 files changed
+476 -307

No files matched your search

-4
View File
@@ -3432,7 +3432,6 @@ class ClassPropertiesAndMethods(unittest.TestCase):
l = [A() for x in range(100)]
del l
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_set_dict(self):
# Testing __dict__ assignment...
class C(object): pass
@@ -5288,7 +5287,6 @@ class AAAPTypesLongInitTest(unittest.TestCase):
class MiscTests(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; a class namespace drops keys that are not strings, so MyKey.__eq__ is never reached and __bases__ stays put
def test_type_lookup_mro_reference(self):
# Issue #14199: _PyType_Lookup() has to keep a strong reference to
# the type MRO because it may be modified during the lookup, if
@@ -5924,7 +5922,6 @@ class MroTest(unittest.TestCase):
with self.assertRaises(TypeError):
B1.__bases__ += ()
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_tp_subclasses_cycle_in_update_slots(self):
"""
type_set_bases must check for reentrancy upon finishing its job
@@ -5961,7 +5958,6 @@ class MroTest(unittest.TestCase):
self.assertEqual(B1.__bases__, (C,))
self.assertEqual(C.__subclasses__(), [B1])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_tp_subclasses_cycle_error_return_path(self):
"""
The same as test_tp_subclasses_cycle_in_update_slots, but tests
+1 -1
View File
@@ -2129,7 +2129,7 @@ if not hasattr(sys, 'gettrace') or not sys.gettrace():
... 'list', # list source from example 3
... 'continue', # stop debugging
... ''])
>>> try: runner.run(test) # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
>>> try: runner.run(test)
... finally: sys.stdin = real_stdin
... # doctest: +NORMALIZE_WHITESPACE
> <doctest foo-bar@baz[1]>(3)g()
-1
View File
@@ -1226,7 +1226,6 @@ class ArgsTestCase(BaseTestCase):
regex = ('10 slowest tests:\n')
self.check_line(output, regex)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '^lines +cov% +module +\\(path\\)\\n(?: *[0-9]+ *[0-9]{1,2}\\.[0-9]% *[^ ]+ +\\([^)]+\\)+)+' not found in 'Warning: collecting coverage without -j is imprecise. Configure --with-pydebug and run -m test -T -j for best results.\nUsing random seed: 2780369491\n0:00:00 Run 1 test sequentially in a single process\n0:00:00 [1/1] test_regrtest_coverage\n0:00:00 [1/1] test_regrtest_coverage passed\n\n== Tests result: SUCCESS ==\n\n1 test OK.\n\nTotal duration: 102 ms\nTotal tests: run=1\nTotal test files: run=1/1\nResult: SUCCESS\n'
def test_coverage(self):
# test --coverage
test = self.create_test('coverage')
-1
View File
@@ -260,7 +260,6 @@ class ProfileHookTestCase(TestCaseBase):
(1, 'return', g_ident),
])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_unfinished_generator(self):
def f():
for i in range(2):
+3 -168
View File
File diff suppressed because it is too large. Load diff
-10
View File
@@ -130,7 +130,6 @@ class TestLineCounts(unittest.TestCase):
self.tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
self.my_py_filename = fix_ext_py(__file__)
@unittest.expectedFailure # TODO: RUSTPYTHON; + ('crates/pylib/Lib/test/test_trace.py', 48): 1}
def test_traced_func_linear(self):
result = self.tracer.runfunc(traced_func_linear, 2, 5)
self.assertEqual(result, 7)
@@ -143,7 +142,6 @@ class TestLineCounts(unittest.TestCase):
self.assertEqual(self.tracer.results().counts, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; + ('crates/pylib/Lib/test/test_trace.py', 54): 1}
def test_traced_func_loop(self):
self.tracer.runfunc(traced_func_loop, 2, 3)
@@ -156,7 +154,6 @@ class TestLineCounts(unittest.TestCase):
}
self.assertEqual(self.tracer.results().counts, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; + ('crates/pylib/Lib/test/tracedmodules/testmod.py', 3): 1}
def test_traced_func_importing(self):
self.tracer.runfunc(traced_func_importing, 2, 5)
@@ -169,7 +166,6 @@ class TestLineCounts(unittest.TestCase):
self.assertEqual(self.tracer.results().counts, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; + ('crates/pylib/Lib/test/test_trace.py', 76): 10}
def test_trace_func_generator(self):
self.tracer.runfunc(traced_func_calling_generator)
@@ -185,7 +181,6 @@ class TestLineCounts(unittest.TestCase):
}
self.assertEqual(self.tracer.results().counts, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; + ('crates/pylib/Lib/test/test_trace.py', 87): 1}
def test_trace_list_comprehension(self):
self.tracer.runfunc(traced_caller_list_comprehension)
@@ -199,7 +194,6 @@ class TestLineCounts(unittest.TestCase):
}
self.assertEqual(self.tracer.results().counts, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; Diff is 996 characters long. Set self.maxDiff to None to see it.
def test_traced_decorated_function(self):
self.tracer.runfunc(traced_decorated_function)
@@ -219,7 +213,6 @@ class TestLineCounts(unittest.TestCase):
}
self.assertEqual(self.tracer.results().counts, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; + {('crates/pylib/Lib/test/test_trace.py', 108): 1}
def test_linear_methods(self):
# XXX todo: later add 'static_method_linear' and 'class_method_linear'
# here, once issue1764286 is resolved
@@ -243,7 +236,6 @@ class TestRunExecCounts(unittest.TestCase):
self.my_py_filename = fix_ext_py(__file__)
self.addCleanup(sys.settrace, sys.gettrace())
@unittest.expectedFailure # TODO: RUSTPYTHON; KeyError: ('crates/pylib/Lib/test/test_trace.py', 51)
def test_exec_counts(self):
self.tracer = Trace(count=1, trace=0, countfuncs=0, countcallers=0)
code = r'''traced_func_loop(2, 5)'''
@@ -545,7 +537,6 @@ class TestCommandLine(unittest.TestCase):
PYTHONIOENCODING='utf-8')
self.assertIn(direct_stdout.strip(), trace_stdout)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'lines cov% module (path)' not found in ''
def test_count_and_summary(self):
filename = f'{TESTFN}.py'
coverfilename = f'{TESTFN}.cover'
@@ -581,7 +572,6 @@ class TestTrace(unittest.TestCase):
self.tracer = Trace(count=0, trace=1)
self.filemod = my_file_and_modname()
@unittest.expectedFailure # TODO: RUSTPYTHON; IndexError: list index out of range
def test_no_source_file(self):
filename = "<unknown>"
co = traced_func_linear.__code__
+69 -29
View File
@@ -349,18 +349,18 @@ pub(crate) mod stack_analysis {
}
}
_ => {
// Default: use stack_effect
let effect: StackEffect = opcode.stack_effect_info(oparg);
let popped = effect.popped() as i64;
let pushed = effect.pushed() as i64;
let mut ns = next_stack;
for _ in 0..popped {
ns = pop_value(ns);
// PyCompile_OpcodeStackEffect: apply the net delta so
// overlapping in/out (GET_ANEXT: aiter -- aiter, awaitable)
// keep the original kind of the surviving entries.
let mut delta = opcode.stack_effect(oparg);
while delta < 0 {
next_stack = pop_value(next_stack);
delta += 1;
}
for _ in 0..pushed {
ns = push_value(ns, Kind::Object as i64);
while delta > 0 {
next_stack = push_value(next_stack, Kind::Object as i64);
delta -= 1;
}
next_stack = ns;
if next_i < stacks.len() {
stacks[next_i] = next_stack;
}
@@ -484,34 +484,21 @@ impl FrameObject {
#[pygetset]
fn f_lasti(&self) -> u32 {
// Return byte offset (each instruction is 2 bytes) for compatibility.
// For materialized frames, read live lasti from the source iframe on
// the TLS chain so f_lasti reflects the current execution position.
// Byte offset of the current opcode. lasti is stored as the next
// instruction index (see FrameObject::run), so the executing unit
// is lasti-1.
let live = self.find_live_source_iframe();
let val = if !live.is_null() {
unsafe { (*live).lasti.load(Relaxed) }
} else {
self.lasti()
};
val * 2
if val == 0 { 0 } else { (val - 1) * 2 }
}
/// Current line, or -1 when the linetable has no line for lasti.
pub fn lineno(&self) -> i32 {
let lasti_bytes = self.f_lasti() as i32;
// If lasti is 0, execution hasn't started yet - use first line number.
// Read the live iframe so a materialized copy that still has lasti==0
// does not hide the current line while the frame is running.
if lasti_bytes == 0 {
return self
.iframe()
.code()
.first_line_number
.map_or(1, |n| n.get() as i32);
}
// lasti is stored as the next instruction index (see FrameObject::run),
// so the executing opcode is at lasti-1 / lasti_bytes-2.
self.f_code().addr2line(lasti_bytes - 2)
self.f_code().addr2line(self.f_lasti() as i32)
}
#[pygetset]
@@ -536,6 +523,47 @@ impl FrameObject {
}
};
let what_event = vm.what_event.get();
if what_event < 0 {
return Err(
vm.new_value_error("f_lineno can only be set in a trace function".to_owned())
);
}
{
use crate::stdlib::sys::monitoring as mon;
match what_event {
mon::WHAT_PY_RESUME
| mon::WHAT_JUMP
| mon::WHAT_BRANCH
| mon::WHAT_BRANCH_LEFT
| mon::WHAT_BRANCH_RIGHT
| mon::WHAT_LINE
| mon::WHAT_PY_YIELD => {}
mon::WHAT_PY_START => {
return Err(vm.new_value_error(
"can't jump from the 'call' trace event of a new frame".to_owned(),
));
}
mon::WHAT_CALL | mon::WHAT_C_RETURN => {
return Err(vm.new_value_error("can't jump during a call".to_owned()));
}
mon::WHAT_PY_RETURN
| mon::WHAT_PY_UNWIND
| mon::WHAT_PY_THROW
| mon::WHAT_RAISE
| mon::WHAT_C_RAISE
| mon::WHAT_INSTRUCTION
| mon::WHAT_EXCEPTION_HANDLED => {
return Err(
vm.new_value_error("can only jump from a 'line' trace event".to_owned())
);
}
_ => {
return Err(vm.new_system_error("unexpected event type".to_owned()));
}
}
}
let first_line = self
.iframe()
.code()
@@ -575,7 +603,7 @@ impl FrameObject {
self.lasti() as usize
};
let start_idx = current_lasti.saturating_sub(1);
let start_stack = if start_idx < stacks.len() {
let mut start_stack = if start_idx < stacks.len() {
stacks[start_idx]
} else {
OVERFLOWED
@@ -611,6 +639,18 @@ impl FrameObject {
return Err(vm.new_value_error(msg.to_owned()));
}
// Yield leaves the yielded value on the modeled stack; the eval
// loop has already popped it for a suspended generator.
let is_suspended = live.is_null()
&& current_lasti > 0
&& matches!(
FrameOwner::from_i8(self.iframe().owner.load(Relaxed)),
FrameOwner::Generator
);
if is_suspended {
start_stack = pop_value(start_stack);
}
// Count how many entries to pop
let mut pop_count = 0usize;
{
+7
View File
@@ -291,6 +291,13 @@ impl PyModule {
Self::new().into_ref_with_type(vm, cls).map(Into::into)
}
#[pymember]
fn __dict__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult {
zelf.dict()
.map(Into::into)
.ok_or_else(|| vm.new_attribute_error("module has no __dict__"))
}
#[pymethod]
fn __dir__(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
// First check if __dict__ attribute exists and is actually a dictionary
+49 -30
View File
@@ -1459,7 +1459,10 @@ impl PyType {
/// Raw MRO walk without cache.
fn find_name_in_mro_uncached(&self, name: &'static PyStrInterned) -> Option<PyObjectRef> {
for cls in self.mro.read().iter() {
// Keep a strong snapshot of the MRO because tp_mro can be replaced
// during dict lookup, e.g. when comparing to non-string keys.
let mro: Vec<PyTypeRef> = self.mro.read().iter().cloned().collect();
for cls in &mro {
if let Some(value) = cls.attributes.get(name) {
return Some(value);
}
@@ -1767,8 +1770,7 @@ impl PyType {
}
};
// Register this type as a subclass of the given bases
let register_subclasses = |bases: &[PyTypeRef]| {
let add_as_subclass = |bases: &[PyTypeRef]| {
let weakref_type = super::PyWeak::static_type();
for base in bases {
base.subclasses.write().push(
@@ -1779,19 +1781,8 @@ impl PyType {
}
};
let result = Self::with_type_lock(vm, || {
for base in bases.iter() {
if is_subtype_with_mro(&base.mro.read(), base, zelf)
|| (!base.mro.read().is_empty() && type_is_subtype_base_chain(base, zelf, vm))
{
return Err(vm.new_type_error("a __bases__ item causes an inheritance cycle"));
}
}
// Remove this class from the old bases' subclass lists, pruning
// dead entries along the way. Upgraded refs are retired so the
// last strong reference is never dropped under the lock.
for base in zelf.bases.read().iter() {
let remove_as_subclass = |bases: &[PyTypeRef], retired: &mut Vec<PyObjectRef>| {
for base in bases {
let mut subclasses = base.subclasses.write();
let mut kept = Vec::with_capacity(subclasses.len());
for weak in subclasses.drain(..) {
@@ -1809,9 +1800,19 @@ impl PyType {
}
*subclasses = kept;
}
};
let old_bases = core::mem::replace(&mut *zelf.bases.write(), bases);
let old_base = unsafe { zelf.base.swap(Some(new_base)) };
let result = Self::with_type_lock(vm, || {
for base in bases.iter() {
if is_subtype_with_mro(&base.mro.read(), base, zelf)
|| (!base.mro.read().is_empty() && type_is_subtype_base_chain(base, zelf, vm))
{
return Err(vm.new_type_error("a __bases__ item causes an inheritance cycle"));
}
}
let old_bases = core::mem::replace(&mut *zelf.bases.write(), bases.clone());
let old_base = unsafe { zelf.base.swap(Some(new_base.clone())) };
// Recursively update the mros of this class and all subclasses,
// recording the previous mros so a failure can be rolled back.
@@ -1851,13 +1852,20 @@ impl PyType {
retired.extend(failed_mro.into_iter().map(Into::into));
retired.push(cls.into());
}
let failed_bases = core::mem::replace(&mut *zelf.bases.write(), old_bases);
if let Some(failed_base) = unsafe { zelf.base.swap(old_base) } {
keep_alive(failed_base, &mut retired);
// Take no action if tp_bases was replaced through reentrance.
if core::ptr::eq(&**zelf.bases.read(), &*bases) {
let failed_bases = core::mem::replace(&mut *zelf.bases.write(), old_bases);
if let Some(failed_base) = unsafe { zelf.base.swap(old_base) } {
keep_alive(failed_base, &mut retired);
}
retired.push(failed_bases.into_untyped().into());
zelf.modified_inner();
} else {
retired.push(old_bases.into_untyped().into());
if let Some(old_base) = old_base {
keep_alive(old_base, &mut retired);
}
}
register_subclasses(&zelf.bases.read());
retired.push(failed_bases.into_untyped().into());
zelf.modified_inner();
return Err(err);
}
// Retire the replaced mros as well; dropping them here would
@@ -1866,16 +1874,18 @@ impl PyType {
retired.extend(old_mro.into_iter().map(Into::into));
retired.push(cls.into());
}
// Take no action if tp_bases was replaced through reentrance.
if core::ptr::eq(&**zelf.bases.read(), &*bases) {
remove_as_subclass(&old_bases, &mut retired);
add_as_subclass(&bases);
zelf.update_all_slots(&vm.ctx);
}
retired.push(old_bases.into_untyped().into());
if let Some(old_base) = old_base {
keep_alive(old_base, &mut retired);
}
// Invalidate inline caches and rebuild every slot for this type and
// all descendants so slots whose methods left the MRO are reset.
zelf.update_all_slots(&vm.ctx);
register_subclasses(&zelf.bases.read());
crate::stdlib::_testinternalcapi::note_set_bases();
Ok(())
});
@@ -2650,6 +2660,15 @@ impl Constructor for PyType {
)
.map_err(|e| vm.new_type_error(e))?;
// tp_dict keeps non-string keys so lookup can hash-compare them.
if let Some(tp_dict) = typ.attributes.as_dict() {
for (key, value) in &*dict {
if key.downcast_ref::<PyStr>().is_none() {
tp_dict.set_item(&*key, value, vm)?;
}
}
}
// Fill __classcell__ before a custom mro() runs so methods that
// close over __class__ can execute during type creation.
if let Some(cell) = typ.attributes.get(identifier!(vm, __classcell__)) {
+10
View File
@@ -648,6 +648,16 @@ impl PyBaseException {
flags(BASETYPE, HAS_DICT)
)]
impl PyBaseException {
#[pygetset]
fn __dict__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<crate::builtins::PyDictRef> {
crate::builtins::object::object_get_dict(zelf, vm)
}
#[pygetset(setter)]
fn set___dict__(zelf: PyObjectRef, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
crate::builtins::object::object_generic_set_dict(zelf, value, vm)
}
#[pygetset]
pub fn args(&self) -> PyTupleRef {
self.args.read().clone()
+229 -32
View File
@@ -2498,6 +2498,7 @@ impl Py<FrameObject> {
prev_line: &iframe.prev_line,
monitoring_mask: 0,
flatten: Flatten::Nothing,
call_traced: false,
};
f(exec)
}
@@ -2583,6 +2584,7 @@ impl Py<FrameObject> {
prev_line: &iframe.prev_line,
monitoring_mask: 0,
flatten: Flatten::Nothing,
call_traced: false,
};
exec.yield_from_target().map(PyObject::to_owned)
}
@@ -2659,8 +2661,8 @@ pub(crate) fn trampoline_handle_exception(
) -> FrameResult {
let mut exec = exec_iframe(iframe, Flatten::Nothing, vm);
// lasti points past the CallPyExactArgs instruction (+ cache entries).
// The exception occurred at the previous instruction (the call site).
// lasti points past the call opcode and its inline caches. Do not
// decode those cache units: their bytes are not valid opcodes.
let idx = (exec.lasti() as usize).saturating_sub(1);
// Add traceback entry at the call site.
@@ -2670,10 +2672,25 @@ pub(crate) fn trampoline_handle_exception(
exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
}
exec.fire_exception_trace(exception, vm)?;
let exception = {
let mon_events = vm.state.monitoring_events.load();
if mon_events & monitoring::EVENT_RAISE != 0 {
let offset = idx as u32 * 2;
let exc_obj: PyObjectRef = exception.clone().into();
match monitoring::fire_raise(vm, exec.code, offset, &exc_obj) {
Ok(()) => exception.clone(),
Err(monitor_exc) => monitor_exc,
}
} else {
exception.clone()
}
};
exec.unwind_blocks(
vm,
UnwindReason::Raising {
exception: exception.clone(),
exception,
offset: idx as u32,
},
)
@@ -2722,6 +2739,7 @@ fn exec_iframe<'a>(
prev_line: &iframe.prev_line,
monitoring_mask: 0,
flatten,
call_traced: false,
}
}
@@ -2914,6 +2932,8 @@ pub(crate) struct ExecutingFrame<'a> {
/// What this frame may hand back to the trampoline, if it is running
/// under one at all.
flatten: Flatten,
/// PY_START/PY_RESUME Call already fired for this activation.
call_traced: bool,
}
/// How much of what a frame does the trampoline can take over.
@@ -3293,6 +3313,35 @@ impl ExecutingFrame<'_> {
.is_some_and(|c| c.trace.lock().is_some())
}
/// PY_START / PY_RESUME → PyTrace_CALL. Fired from the first RESUME of
/// this activation so lasti is already the resume unit
/// (COPY_FREE_VARS / RETURN_GENERATOR that precede it are not a 'call',
/// and later RESUMEs in a SEND loop are not a new call either).
fn trace_call_from_resume(&mut self, vm: &VirtualMachine, resume_type: u32) -> PyResult<()> {
if self.call_traced {
return Ok(());
}
self.call_traced = true;
if !vm.use_tracing.get() {
return Ok(());
}
// RESUME oparg 0 is PY_START; nonzero is PY_RESUME.
let what = if resume_type == 0 {
monitoring::WHAT_PY_START
} else {
monitoring::WHAT_PY_RESUME
};
let trace_result = vm.trace_event_what(crate::protocol::TraceEvent::Call, what, None)?;
if let Some(local_trace) = trace_result {
let was_unset = self.iframe().cold().trace.lock().is_none();
*self.iframe().cold().trace.lock() = Some(local_trace);
if was_unset {
self.iframe().sync_prev_line_from_lasti();
}
}
Ok(())
}
/// Access the frame's trace_opcodes lock.
#[inline]
fn trace_opcodes_is_set(&self) -> bool {
@@ -3301,6 +3350,14 @@ impl ExecutingFrame<'_> {
.is_some_and(|c| *c.trace_opcodes.lock())
}
/// f_trace_lines, defaulting to true when cold data is not allocated.
#[inline]
fn trace_lines_is_set(&self) -> bool {
self.iframe()
.cold_opt()
.is_none_or(|c| *c.trace_lines.lock())
}
/// Get pending_stack_pops from the frame.
#[inline]
fn pending_stack_pops(&self) -> u32 {
@@ -3373,6 +3430,45 @@ impl ExecutingFrame<'_> {
.expect("cell slot is not a PyCell")
}
/// Apply a pending `f_lineno` jump's stack pops, if any.
fn apply_pending_lineno_jump(&mut self, vm: &VirtualMachine) {
let pops = self.pending_stack_pops();
if pops > 0 {
let from_stack = self.pending_unwind_from_stack();
self.unwind_stack_for_lineno(pops as usize, from_stack, vm);
self.set_pending_stack_pops(0);
}
}
/// `_YIELD_VALUE_EVENT`: fire PY_YIELD while the value is still on the
/// stack. If the tracer assigned `f_lineno`, continue at the new lasti
/// instead of suspending.
fn yield_value_event(&mut self, vm: &VirtualMachine) -> PyResult<bool> {
let lasti_before = self.lasti();
if vm.use_tracing.get() {
let value = self.top_value().to_owned();
vm.trace_event_what(
crate::protocol::TraceEvent::Return,
monitoring::WHAT_PY_YIELD,
Some(value),
)?;
if self.lasti() != lasti_before {
self.apply_pending_lineno_jump(vm);
return Ok(true);
}
}
if self.monitoring_mask & monitoring::EVENT_PY_YIELD != 0 {
let value = self.top_value().to_owned();
let offset = (self.lasti() - 1) * 2;
monitoring::fire_py_yield(vm, self.code, offset, &value)?;
if self.lasti() != lasti_before {
self.apply_pending_lineno_jump(vm);
return Ok(true);
}
}
Ok(false)
}
/// Perform deferred stack unwinding after set_f_lineno.
///
/// set_f_lineno cannot pop the value stack directly because the execution
@@ -3471,35 +3567,69 @@ impl ExecutingFrame<'_> {
// Only fire if this frame has a per-frame trace function set
// (frames entered before sys.settrace() have trace=None).
// Skip RESUME – it should not generate user-visible line events.
// Skip NO_LOCATION units (addr2line == -1); the locations table
// fills those with a dummy line, which would emit a 'line'
// event whose f_lineno is None.
if tracing
&& self.trace_is_set(vm)
&& self.trace_lines_is_set()
&& !matches!(
self.code.instructions.read_op(idx),
Instruction::Resume { .. } | Instruction::InstrumentedResume
)
&& let Some((loc, _)) = self.code.locations.get(idx)
&& loc.line.get() as u32 != self.prev_line.get()
{
self.prev_line.set(loc.line.get() as u32);
vm.trace_event(crate::protocol::TraceEvent::Line, None)?;
// The trace callback may have toggled tracing (e.g. via
// sys.settrace(None)); refresh before the opcode-trace check
// below reuses this flag.
tracing = vm.use_tracing.get();
// Trace callback may have changed lasti via set_f_lineno.
// Re-read and restart the loop from the new position.
if lasti_cell.load(Relaxed) != (idx as u32 + 1) {
// set_f_lineno defers stack unwinding because we hold
// the state mutex. Perform it now.
let pops = self.pending_stack_pops();
if pops > 0 {
let from_stack = self.pending_unwind_from_stack();
self.unwind_stack_for_lineno(pops as usize, from_stack, vm);
self.set_pending_stack_pops(0);
let line = self.code.addr2line(idx as i32 * 2);
if line >= 0 && line as u32 != self.prev_line.get() {
self.prev_line.set(line as u32);
match vm.trace_event(crate::protocol::TraceEvent::Line, None) {
Ok(_) => {}
Err(exception) => {
if let Some((loc, _end_loc)) = self.code.locations.get(idx) {
let next = exception.__traceback__();
let new_traceback = PyTraceback::new(
next,
self.frame_object(vm),
idx as u32 * 2,
loc.line,
);
exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx)));
}
match self.unwind_blocks(
vm,
UnwindReason::Raising {
exception,
offset: idx as u32,
},
) {
Ok(None) => {
arg_state.reset();
idx = lasti_cell.load(Relaxed) as usize;
continue;
}
Ok(Some(value)) => break Ok(value),
Err(e) => break Err(e),
}
}
}
// The trace callback may have toggled tracing (e.g. via
// sys.settrace(None)); refresh before the opcode-trace check
// below reuses this flag.
tracing = vm.use_tracing.get();
// Trace callback may have changed lasti via set_f_lineno.
// Re-read and restart the loop from the new position.
if lasti_cell.load(Relaxed) != (idx as u32 + 1) {
// set_f_lineno defers stack unwinding because we hold
// the state mutex. Perform it now.
let pops = self.pending_stack_pops();
if pops > 0 {
let from_stack = self.pending_unwind_from_stack();
self.unwind_stack_for_lineno(pops as usize, from_stack, vm);
self.set_pending_stack_pops(0);
}
arg_state.reset();
idx = lasti_cell.load(Relaxed) as usize;
continue;
}
arg_state.reset();
idx = lasti_cell.load(Relaxed) as usize;
continue;
}
}
// One aligned acquire load fetches opcode and arg together; two
@@ -3970,6 +4100,15 @@ impl ExecutingFrame<'_> {
// Fire PY_THROW and RAISE events before raising the exception.
// If a monitoring callback fails, its exception replaces the original.
if vm.use_tracing.get() {
let what = monitoring::WHAT_PY_THROW;
if let Some(local_trace) =
vm.trace_event_what(crate::protocol::TraceEvent::Call, what, None)?
{
*self.iframe().cold().trace.lock() = Some(local_trace);
}
self.fire_exception_trace(&exception, vm)?;
}
let exception = {
let mon_events = vm.state.monitoring_events.load();
let exception = if mon_events & monitoring::EVENT_PY_THROW != 0 {
@@ -4722,15 +4861,15 @@ impl ExecutingFrame<'_> {
.instructions
.replace_op(instr_idx, Instruction::JumpBackwardNoJit);
}
self.jump_relative_backward(u32::from(arg), 1);
self.jump_relative_backward_and_trace_line(u32::from(arg), 1, vm)?;
Ok(None)
}
Instruction::JumpBackwardJit | Instruction::JumpBackwardNoJit => {
self.jump_relative_backward(u32::from(arg), 1);
self.jump_relative_backward_and_trace_line(u32::from(arg), 1, vm)?;
Ok(None)
}
Instruction::JumpBackwardNoInterrupt { .. } => {
self.jump_relative_backward(u32::from(arg), 0);
self.jump_relative_backward_and_trace_line(u32::from(arg), 0, vm)?;
Ok(None)
}
Instruction::ListAppend { i } => {
@@ -5395,11 +5534,20 @@ impl ExecutingFrame<'_> {
.store(global_ver, atomic::Ordering::Release);
// Re-execute this instruction (it may now be INSTRUMENTED_RESUME)
self.update_lasti(|i| *i -= 1);
} else {
self.trace_call_from_resume(vm, u32::from(arg))?;
}
Ok(None)
}
Instruction::ReturnValue => {
let value = self.pop_value();
if vm.use_tracing.get() {
vm.trace_event_what(
crate::protocol::TraceEvent::Return,
monitoring::WHAT_PY_RETURN,
Some(value.clone()),
)?;
}
self.unwind_blocks(vm, UnwindReason::Returning { value })
}
Instruction::SetAdd { i } => {
@@ -5643,6 +5791,11 @@ impl ExecutingFrame<'_> {
.all(|sr| !sr.is_borrowed()),
"borrowed refs on stack at yield point"
);
// _YIELD_VALUE_EVENT: if f_lineno jumped, DISPATCH to the
// new instruction instead of suspending.
if self.yield_value_event(vm)? {
return Ok(None);
}
Ok(Some(ExecutionResult::Yield(self.pop_value())))
}
Instruction::Send { .. } => {
@@ -7856,10 +8009,18 @@ impl ExecutingFrame<'_> {
} else if self.monitoring_mask & monitoring::EVENT_PY_RESUME != 0 {
monitoring::fire_py_resume(vm, self.code, offset)?;
}
self.trace_call_from_resume(vm, resume_type)?;
Ok(None)
}
Instruction::InstrumentedReturnValue => {
let value = self.pop_value();
if vm.use_tracing.get() {
vm.trace_event_what(
crate::protocol::TraceEvent::Return,
monitoring::WHAT_PY_RETURN,
Some(value.clone()),
)?;
}
if self.monitoring_mask & monitoring::EVENT_PY_RETURN != 0 {
let offset = (self.lasti() - 1) * 2;
monitoring::fire_py_return(vm, self.code, offset, &value)?;
@@ -7876,12 +8037,10 @@ impl ExecutingFrame<'_> {
.all(|sr| !sr.is_borrowed()),
"borrowed refs on stack at yield point"
);
let value = self.pop_value();
if self.monitoring_mask & monitoring::EVENT_PY_YIELD != 0 {
let offset = (self.lasti() - 1) * 2;
monitoring::fire_py_yield(vm, self.code, offset, &value)?;
if self.yield_value_event(vm)? {
return Ok(None);
}
Ok(Some(ExecutionResult::Yield(value)))
Ok(Some(ExecutionResult::Yield(self.pop_value())))
}
Instruction::InstrumentedCall => {
let args = self.collect_positional_args(u32::from(arg));
@@ -7951,12 +8110,14 @@ impl ExecutingFrame<'_> {
}
Instruction::InstrumentedJumpBackward => {
let src_offset = (self.lasti() - 1) * 2;
let from_idx = self.lasti().saturating_sub(1) as usize;
let target_idx = self.lasti() + 1 - u32::from(arg);
let target = bytecode::Label::from_u32(target_idx);
self.jump(target);
if self.monitoring_mask & monitoring::EVENT_JUMP != 0 {
monitoring::fire_jump(vm, self.code, src_offset, target.as_u32() * 2)?;
}
self.trace_backward_same_line(from_idx, vm)?;
Ok(None)
}
Instruction::InstrumentedForIter => {
@@ -9054,6 +9215,42 @@ impl ExecutingFrame<'_> {
self.update_lasti(|i| *i = target);
}
/// JUMP_BACKWARD plus the settrace LINE event for a backward edge that
/// stays on the same source line (`sys_trace_jump_func`).
fn jump_relative_backward_and_trace_line(
&mut self,
delta: u32,
caches: u32,
vm: &VirtualMachine,
) -> PyResult<()> {
let from_idx = self.lasti().saturating_sub(1) as usize;
self.jump_relative_backward(delta, caches);
self.trace_backward_same_line(from_idx, vm)
}
/// sys.settrace generates line events for all backward edges, even if on
/// the same line.
fn trace_backward_same_line(&mut self, from_idx: usize, vm: &VirtualMachine) -> PyResult<()> {
if !(vm.use_tracing.get() && self.trace_is_set(vm) && self.trace_lines_is_set()) {
return Ok(());
}
let to_idx = self.lasti() as usize;
let from_line = self.code.addr2line(from_idx as i32 * 2);
let to_line = self.code.addr2line(to_idx as i32 * 2);
if to_line >= 0 && to_line == from_line {
self.prev_line.set(to_line as u32);
// lasti currently names the destination instruction; f_lineno
// reads lasti-1 (the in-flight opcode), so point past dest for
// the duration of the callback.
let dest = self.lasti();
self.update_lasti(|i| *i = dest + 1);
let result = vm.trace_event(crate::protocol::TraceEvent::Line, None);
self.update_lasti(|i| *i = dest);
result?;
}
Ok(())
}
/// Step over the `NOT_TAKEN` marker on a conditional jump's fall-through edge.
///
/// The compiler plants `NOT_TAKEN` after every conditional jump purely as the
+60 -11
View File
@@ -176,6 +176,22 @@ impl TraceEvent {
pub(crate) const fn is_opcode_event(self) -> bool {
matches!(self, Self::Opcode)
}
/// Default `what_event` for this legacy event.
#[must_use]
const fn default_what(self) -> i32 {
use crate::stdlib::sys::monitoring as mon;
match self {
Self::Call => mon::WHAT_PY_START,
Self::Return => mon::WHAT_PY_RETURN,
Self::Exception => mon::WHAT_RAISE,
Self::Line => mon::WHAT_LINE,
Self::Opcode => mon::WHAT_INSTRUCTION,
Self::CCall => mon::WHAT_CALL,
Self::CReturn => mon::WHAT_C_RETURN,
Self::CException => mon::WHAT_C_RAISE,
}
}
}
impl core::fmt::Display for TraceEvent {
@@ -208,9 +224,25 @@ impl VirtualMachine {
&self,
event: TraceEvent,
arg: Option<PyObjectRef>,
) -> PyResult<Option<PyObjectRef>> {
self.trace_event_what(event, event.default_what(), arg)
}
/// Like [`Self::trace_event`], but records `what` as `tstate->what_event`
/// for the duration of the callback (so `f_lineno` assignment can tell
/// a 'line' event from a 'call'/'return'/'exception').
#[inline]
pub(crate) fn trace_event_what(
&self,
event: TraceEvent,
what: i32,
arg: Option<PyObjectRef>,
) -> PyResult<Option<PyObjectRef>> {
if self.use_tracing.get() && !self.tracing_is_suppressed() {
self._trace_event_inner(event, arg)
let old = self.what_event.replace(what);
let result = self._trace_event_inner(event, arg);
self.what_event.set(old);
result
} else {
Ok(None)
}
@@ -244,32 +276,49 @@ impl VirtualMachine {
return Ok(None);
}
let frame: PyObjectRef = frame_ref.into();
let event = self.ctx.new_str(event.to_string()).into();
let args = vec![frame, event, arg.unwrap_or_else(|| self.ctx.none())];
// trace_trampoline: CALL uses the global callback; every other
// event uses the per-frame f_trace (and is a no-op if that is unset).
let callback = if event == TraceEvent::Call {
if self.is_none(&trace_func) {
None
} else {
Some(trace_func)
}
} else {
frame_ref
.iframe()
.cold_opt()
.and_then(|c| c.trace.lock().clone())
};
let frame: PyObjectRef = frame_ref.to_owned().into();
let event_str: PyObjectRef = self.ctx.new_str(event.to_string()).into();
let args = vec![frame, event_str, arg.unwrap_or_else(|| self.ctx.none())];
let mut trace_result = None;
// temporarily disable tracing, during the call to the
// tracing function itself.
if is_trace_event && !self.is_none(&trace_func) {
if is_trace_event && let Some(callback) = callback {
self.use_tracing.set(false);
self.enter_tracing();
let res = trace_func.call(args.clone(), self);
let res = callback.call(args.clone(), self);
self.leave_tracing();
self.use_tracing.set(true);
match res {
Ok(result) => {
if !self.is_none(&result) {
*frame_ref.iframe().cold().trace.lock() = Some(result.clone());
trace_result = Some(result);
}
}
Err(e) => {
// trace_trampoline behavior: clear per-frame f_trace
// and propagate the error.
if let Some(frame_ref) = self.current_frame() {
*frame_ref.iframe().cold().trace.lock() = None;
}
// trace_trampoline: disable the global tracer and clear
// this frame's f_trace, then propagate.
*self.trace_func.borrow_mut() = self.ctx.none();
*frame_ref.iframe().cold().trace.lock() = None;
let profile_is_none = self.is_none(&self.profile_func.borrow());
self.use_tracing.set(!profile_is_none);
return Err(e);
}
}
+6
View File
@@ -1276,6 +1276,12 @@ pub mod sys {
fn settrace(function: PyObjectRef, vm: &VirtualMachine) {
vm.trace_func.replace(function);
update_use_tracing(vm);
// The rest of the current line already started before tracing was
// enabled; sync prev_line so leftover opcodes on this line do not
// emit a spurious 'line' event.
if let Some(frame) = vm.current_frame() {
frame.iframe().sync_prev_line_from_lasti();
}
}
#[pyfunction]
+25
View File
@@ -58,6 +58,29 @@ pub(crate) const EVENT_STOP_ITERATION: u32 = MonitoringEvents::STOP_ITERATION.bi
pub(crate) const EVENT_PY_THROW: u32 = MonitoringEvents::PY_THROW.bits();
const EVENT_BRANCH: u32 = MonitoringEvents::BRANCH.bits();
pub(crate) const EVENT_RERAISE: u32 = MonitoringEvents::RERAISE.bits();
/// `tstate->what_event` values, the event currently being monitored.
pub(crate) const WHAT_PY_START: i32 = 0;
pub(crate) const WHAT_PY_RESUME: i32 = 1;
pub(crate) const WHAT_PY_RETURN: i32 = 2;
pub(crate) const WHAT_PY_YIELD: i32 = 3;
pub(crate) const WHAT_CALL: i32 = 4;
pub(crate) const WHAT_LINE: i32 = 5;
pub(crate) const WHAT_INSTRUCTION: i32 = 6;
pub(crate) const WHAT_JUMP: i32 = 7;
pub(crate) const WHAT_BRANCH_LEFT: i32 = 8;
pub(crate) const WHAT_BRANCH_RIGHT: i32 = 9;
#[allow(dead_code)]
pub(crate) const WHAT_STOP_ITERATION: i32 = 10;
pub(crate) const WHAT_RAISE: i32 = 11;
pub(crate) const WHAT_EXCEPTION_HANDLED: i32 = 12;
pub(crate) const WHAT_PY_UNWIND: i32 = 13;
pub(crate) const WHAT_PY_THROW: i32 = 14;
#[allow(dead_code)]
pub(crate) const WHAT_RERAISE: i32 = 15;
pub(crate) const WHAT_C_RETURN: i32 = 16;
pub(crate) const WHAT_C_RAISE: i32 = 17;
pub(crate) const WHAT_BRANCH: i32 = 18;
const EVENT_C_RETURN_MASK: u32 = EVENT_C_RETURN | EVENT_C_RAISE;
const EVENT_NAMES: [&str; EVENTS_COUNT] = [
@@ -883,6 +906,7 @@ fn fire(
FIRING.with(|f| f.set(true));
vm.enter_tracing();
let old_what = vm.what_event.replace(event_id as i32);
let result = (|| {
for (tool, cb) in callbacks {
let result = cb.call(args.clone(), vm)?;
@@ -905,6 +929,7 @@ fn fire(
}
Ok(())
})();
vm.what_event.set(old_what);
vm.leave_tracing();
FIRING.with(|f| f.set(false));
result
+16 -20
View File
@@ -95,6 +95,8 @@ pub struct VirtualMachine {
pub profile_func: RefCell<PyObjectRef>,
pub trace_func: RefCell<PyObjectRef>,
pub use_tracing: Cell<bool>,
/// Event currently being monitored, or -1 when not in a callback.
pub(crate) what_event: Cell<i32>,
tracing_depth: Cell<usize>,
pub recursion_limit: Cell<usize>,
pub(crate) signal_handlers: OnceCell<SignalHandlers>,
@@ -1200,6 +1202,7 @@ impl VirtualMachine {
profile_func,
trace_func,
use_tracing: Cell::new(false),
what_event: Cell::new(-1),
tracing_depth: Cell::new(0),
recursion_limit: Cell::new(if cfg!(debug_assertions) { 256 } else { 1000 }),
signal_handlers,
@@ -2919,35 +2922,28 @@ impl VirtualMachine {
) -> PyResult<R> {
use crate::protocol::TraceEvent;
// Fire 'call' trace event. current_frame() now returns the callee.
let trace_result = self.trace_event(TraceEvent::Call, None)?;
if let Some(local_trace) = trace_result {
let was_unset = frame.iframe().cold().trace.lock().is_none();
*frame.iframe().cold().trace.lock() = Some(local_trace);
if was_unset {
// For a fresh frame this is a no-op (lasti is still 0 here,
// before the frame body below has run). For a generator
// resumed mid-body, `lasti` already reflects the suspended
// position, so `prev_line` -- stale from before tracing was
// installed -- must be synced again or the very next
// instruction fires a spurious 'line' event.
frame.iframe().sync_prev_line_from_lasti();
}
}
// 'call' is PY_START / PY_RESUME, fired from RESUME once lasti is
// the resume unit. Wrapping the body would report lasti=0 (and
// trace RETURN_GENERATOR on async-def construction).
let result = f(frame);
// Fire 'return' event if frame is being traced or profiled.
// PY_UNWIND fires PyTrace_RETURN with arg=None — so we fire for
// both Ok and Err, matching `call_trace_protected` behavior.
if self.use_tracing.get()
// PY_RETURN / PY_YIELD are fired from RETURN_VALUE / YIELD_VALUE.
// PY_UNWIND fires PyTrace_RETURN with arg=None when the exception
// leaves this frame.
if result.is_err()
&& self.use_tracing.get()
&& (!self.is_none(&self.profile_func.borrow())
|| frame
.iframe()
.cold_opt()
.is_some_and(|c| c.trace.lock().is_some()))
{
let ret_result = self.trace_event(TraceEvent::Return, None);
let ret_result = self.trace_event_what(
TraceEvent::Return,
crate::stdlib::sys::monitoring::WHAT_PY_UNWIND,
None,
);
// call_trace_protected: if trace function raises, its error
// replaces the original exception.
ret_result?;
+1
View File
@@ -1399,6 +1399,7 @@ impl VirtualMachine {
profile_func: RefCell::new(global_profile.unwrap_or_else(|| self.ctx.none())),
trace_func: RefCell::new(global_trace.unwrap_or_else(|| self.ctx.none())),
use_tracing: Cell::new(use_tracing),
what_event: Cell::new(-1),
tracing_depth: Cell::new(0),
recursion_limit: self.recursion_limit.clone(),
signal_handlers: core::cell::OnceCell::new(),