mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
617c4be5d9
commit
53a906c916
16 files changed
+476
-307
No files matched your search
Vendored
-4
@@ -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
@@ -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()
|
||||
|
||||
Vendored
-1
@@ -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')
|
||||
|
||||
Vendored
-1
@@ -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):
|
||||
|
||||
Vendored
+3
-168
File diff suppressed because it is too large.
Load diff
Vendored
-10
@@ -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__
|
||||
|
||||
@@ -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;
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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?;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user