mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Keep specialized CALL under Return/Unwind events (#8735)
* Keep specialized CALL under Return/Unwind events Instrument only opcodes whose EVENT_FOR_OPCODE is active so CALL_ALLOC_AND_ENTER_INIT survives Return+Unwind. LINE and INSTRUCTION still restore every specialized opcode. FOR_ITER RAISE/STOP_ITERATION consult the global event mask. Assisted-by: Grok:grok-4.6 * Fix frame repr, f_lineno delete, and class cell timing frame.__repr__ matches the C format. Deleting f_lineno raises AttributeError. __classcell__ is filled before a custom mro() so methods that close over __class__ can run during type creation. Assisted-by: Grok:grok-4.6
This commit is contained in:
1 parent
faa5a60638
commit
b2c48d3ab4
7 files changed
+135
-54
No files matched your search
Vendored
-2
@@ -217,7 +217,6 @@ class FrameAttrsTest(unittest.TestCase):
|
||||
self.assertEqual(outer.f_locals, {})
|
||||
self.assertEqual(inner.f_locals, {})
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: can't delete f_lineno attribute
|
||||
def test_f_lineno_del_segfault(self):
|
||||
f, _, _ = self.make_frames()
|
||||
with self.assertRaises(AttributeError):
|
||||
@@ -279,7 +278,6 @@ class ReprTest(unittest.TestCase):
|
||||
Tests for repr(frame).
|
||||
"""
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_repr(self):
|
||||
def outer():
|
||||
x = 5
|
||||
|
||||
Vendored
-1
@@ -1054,7 +1054,6 @@ class ExceptionMonitoringTest(CheckEvents):
|
||||
)
|
||||
self.assertEqual(events[0], ("throw", IndexError))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; CALL_ALLOC_AND_ENTER_INIT specialization
|
||||
@unittest.skipUnless(_testinternalcapi, "requires _testinternalcapi")
|
||||
@requires_specialization_ft
|
||||
def test_no_unwind_for_shim_frame(self):
|
||||
|
||||
Vendored
-1
@@ -187,7 +187,6 @@ class TestSuper(unittest.TestCase):
|
||||
B = type("B", (), test_namespace)
|
||||
self.assertIs(B.f(), B)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test___class___mro(self):
|
||||
# See issue #23722
|
||||
test_class = None
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
*/
|
||||
|
||||
use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyIntRef, PyStrRef};
|
||||
use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyIntRef};
|
||||
use crate::{
|
||||
Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
||||
AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
||||
class::PyClassImpl,
|
||||
frame::{FrameObject, FrameObjectRef, FrameOwner},
|
||||
function::PySetterValue,
|
||||
@@ -433,15 +433,16 @@ pub(crate) fn init(context: &'static Context) {
|
||||
}
|
||||
|
||||
impl Representable for FrameObject {
|
||||
#[inline]
|
||||
fn repr(_zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
|
||||
const REPR: &str = "<frame object at .. >";
|
||||
Ok(vm.ctx.intern_str(REPR).to_owned())
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn repr_str(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
|
||||
unreachable!("use repr instead")
|
||||
fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
|
||||
let code = zelf.iframe().code();
|
||||
let file_repr = code.source_path().to_owned().as_object().repr(vm)?;
|
||||
let lineno = zelf.f_lineno();
|
||||
let name = code.code.obj_name.as_wtf8();
|
||||
let ptr = zelf as *const Py<Self> as usize;
|
||||
Ok(format!(
|
||||
"<frame at {ptr:#x}, file {}, line {lineno}, code {name}>",
|
||||
file_repr.as_wtf8(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +465,7 @@ impl FrameObject {
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(flags(DISALLOW_INSTANTIATION), with(Py))]
|
||||
#[pyclass(flags(DISALLOW_INSTANTIATION), with(Py, Representable))]
|
||||
impl FrameObject {
|
||||
#[pygetset]
|
||||
fn f_globals(&self) -> PyDictRef {
|
||||
@@ -539,7 +540,7 @@ impl FrameObject {
|
||||
.map_err(|_| vm.new_value_error("lineno must be an integer"))?
|
||||
}
|
||||
PySetterValue::Delete => {
|
||||
return Err(vm.new_type_error("can't delete f_lineno attribute"));
|
||||
return Err(vm.new_attribute_error("cannot delete attribute"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2662,13 +2662,8 @@ impl Constructor for PyType {
|
||||
)
|
||||
.map_err(|e| vm.new_type_error(e))?;
|
||||
|
||||
if custom_mro {
|
||||
mro_internal(&typ, vm)?;
|
||||
typ.init_slots(&vm.ctx);
|
||||
}
|
||||
|
||||
// Consume __classcell__ before slot members of the same name
|
||||
// overwrite the compiler-provided cell.
|
||||
// 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__)) {
|
||||
let cell = PyCellRef::try_from_object(vm, cell.clone()).map_err(|_| {
|
||||
vm.new_type_error(format!(
|
||||
@@ -2694,6 +2689,11 @@ impl Constructor for PyType {
|
||||
typ.attributes.remove(identifier!(vm, __classdictcell__));
|
||||
}
|
||||
|
||||
if custom_mro {
|
||||
mro_internal(&typ, vm)?;
|
||||
typ.init_slots(&vm.ctx);
|
||||
}
|
||||
|
||||
if let Some(ref slots) = heaptype_slots {
|
||||
let class_name = typ.name().to_string();
|
||||
for (offset, member) in (base_member_count..).zip(slots.as_slice().iter()) {
|
||||
|
||||
@@ -7208,7 +7208,9 @@ impl ExecutingFrame<'_> {
|
||||
Ok(PyIterReturn::StopIteration(value)) => {
|
||||
// FOR_ITER_GEN returns through END_FOR, which
|
||||
// fires STOP_ITERATION rather than RAISE.
|
||||
if self.monitoring_mask & monitoring::EVENT_STOP_ITERATION != 0 {
|
||||
if vm.state.monitoring_events.load() & monitoring::EVENT_STOP_ITERATION
|
||||
!= 0
|
||||
{
|
||||
let offset = (self.lasti() - 1) * 2;
|
||||
let val = vm.unwrap_or_none(value.clone());
|
||||
monitoring::fire_stop_iteration(vm, self.code, offset, &val)?;
|
||||
@@ -8522,7 +8524,7 @@ impl ExecutingFrame<'_> {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let need_raise = self.monitoring_mask & monitoring::EVENT_RAISE != 0;
|
||||
let need_raise = vm.state.monitoring_events.load() & monitoring::EVENT_RAISE != 0;
|
||||
let need_trace = vm.use_tracing.get() && self.trace_is_set(vm);
|
||||
if !need_raise && !need_trace {
|
||||
return Ok(());
|
||||
|
||||
@@ -226,6 +226,62 @@ fn normalize_event_set(event_set: i32, local: bool, vm: &VirtualMachine) -> PyRe
|
||||
Ok(event_set)
|
||||
}
|
||||
|
||||
/// Event that causes this opcode to be rewritten to INSTRUMENTED_*.
|
||||
/// RESUME uses `oparg`: 0 → PY_START, nonzero → PY_RESUME.
|
||||
fn event_for_opcode(op: rustpython_compiler_core::bytecode::Instruction, oparg: u8) -> Option<u32> {
|
||||
use rustpython_compiler_core::bytecode::Opcode;
|
||||
match op.deoptimize().as_opcode() {
|
||||
Opcode::ReturnValue => Some(EVENT_PY_RETURN),
|
||||
Opcode::Call | Opcode::CallKw | Opcode::CallFunctionEx | Opcode::LoadSuperAttr => {
|
||||
Some(EVENT_CALL)
|
||||
}
|
||||
Opcode::YieldValue => Some(EVENT_PY_YIELD),
|
||||
Opcode::JumpForward | Opcode::JumpBackward => Some(EVENT_JUMP),
|
||||
Opcode::PopJumpIfFalse
|
||||
| Opcode::PopJumpIfTrue
|
||||
| Opcode::PopJumpIfNone
|
||||
| Opcode::PopJumpIfNotNone => Some(EVENT_BRANCH_RIGHT),
|
||||
Opcode::ForIter => Some(EVENT_BRANCH_LEFT),
|
||||
Opcode::PopIter => Some(EVENT_BRANCH_RIGHT),
|
||||
Opcode::EndFor | Opcode::EndSend => Some(EVENT_STOP_ITERATION),
|
||||
Opcode::NotTaken => Some(EVENT_BRANCH_LEFT),
|
||||
Opcode::EndAsyncFor => Some(EVENT_BRANCH_RIGHT),
|
||||
Opcode::Resume => {
|
||||
if oparg != 0 {
|
||||
Some(EVENT_PY_RESUME)
|
||||
} else {
|
||||
Some(EVENT_PY_START)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn opcode_event_is_active(
|
||||
op: rustpython_compiler_core::bytecode::Instruction,
|
||||
oparg: u8,
|
||||
events: u32,
|
||||
) -> bool {
|
||||
event_for_opcode(op, oparg).is_some_and(|ev| events & ev != 0)
|
||||
}
|
||||
|
||||
/// Walk real instructions, skipping specialization CACHE payloads.
|
||||
/// Cache slots may contain pointer bits that are not valid opcodes.
|
||||
fn for_each_instruction(
|
||||
code: &PyCode,
|
||||
mut f: impl FnMut(usize, rustpython_compiler_core::bytecode::Instruction, u8),
|
||||
) {
|
||||
let len = code.code.instructions.len();
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
let op = code.code.instructions.read_op(i);
|
||||
let oparg = code.code.instructions.read_arg(i).as_u8();
|
||||
let caches = op.deoptimize().cache_entries();
|
||||
f(i, op, oparg);
|
||||
i += 1 + caches;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrite a code object's bytecode in-place with layered instrumentation.
|
||||
///
|
||||
/// Three layers (outermost first):
|
||||
@@ -234,6 +290,7 @@ fn normalize_event_set(event_set: i32, local: bool, vm: &VirtualMachine) -> PyRe
|
||||
/// 3. Regular INSTRUMENTED_* — direct 1:1 opcode swap (no side-table needed)
|
||||
///
|
||||
/// De-instrumentation peels layers in reverse order.
|
||||
/// Specialized opcodes are restored to base only when their EVENT_FOR_OPCODE is active.
|
||||
pub(crate) fn instrument_code(code: &PyCode, events: u32) {
|
||||
use rustpython_compiler_core::bytecode::{self, Instruction};
|
||||
|
||||
@@ -270,25 +327,33 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Remove regular INSTRUMENTED_* and specialized opcodes → restore base opcodes.
|
||||
// Also clear all CACHE entries so specialization starts fresh.
|
||||
// Phase 3: Restore INSTRUMENTED_* whose event is no longer active.
|
||||
// Restore specialized opcodes to base only when that family will be instrumented.
|
||||
{
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
let op = code.code.instructions[i].op;
|
||||
let op = code.code.instructions.read_op(i);
|
||||
let oparg = code.code.instructions.read_arg(i).as_u8();
|
||||
let base_op = op.deoptimize();
|
||||
if u8::from(base_op) != u8::from(op) {
|
||||
let caches = base_op.cache_entries();
|
||||
// LINE/INSTRUCTION wrap every instruction, so specialized
|
||||
// CACHE payloads must be cleared first.
|
||||
let want_instrumented = events & (EVENT_LINE | EVENT_INSTRUCTION) != 0
|
||||
|| opcode_event_is_active(base_op, oparg, events);
|
||||
let restore_base = if want_instrumented {
|
||||
!op.is_instrumented() && u8::from(base_op) != u8::from(op)
|
||||
} else {
|
||||
op.is_instrumented()
|
||||
};
|
||||
if restore_base {
|
||||
unsafe {
|
||||
code.code.instructions.replace_op(i, base_op);
|
||||
}
|
||||
}
|
||||
let caches = base_op.cache_entries();
|
||||
// Zero all CACHE entries (the op+arg bytes may have been overwritten
|
||||
// by specialization with arbitrary data like pointers).
|
||||
for c in 1..=caches {
|
||||
if i + c < len {
|
||||
unsafe {
|
||||
code.code.instructions.write_cache_u16(i + c, 0);
|
||||
for c in 1..=caches {
|
||||
if i + c < len {
|
||||
unsafe {
|
||||
code.code.instructions.write_cache_u16(i + c, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,8 +361,6 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) {
|
||||
}
|
||||
}
|
||||
|
||||
// All opcodes are now base opcodes.
|
||||
|
||||
if events == 0 {
|
||||
*monitoring_data = None;
|
||||
return;
|
||||
@@ -318,24 +381,43 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) {
|
||||
data.per_instruction_opcodes.resize(len, 0);
|
||||
|
||||
// Find _co_firsttraceable: index of first RESUME instruction
|
||||
let first_traceable = code
|
||||
.code
|
||||
.instructions
|
||||
.iter()
|
||||
.position(|u| matches!(u.op, Instruction::Resume { .. } | Instruction::ResumeCheck))
|
||||
.unwrap_or(0);
|
||||
let mut first_traceable = None;
|
||||
for_each_instruction(code, |i, op, _| {
|
||||
if first_traceable.is_none()
|
||||
&& matches!(
|
||||
op,
|
||||
Instruction::Resume { .. }
|
||||
| Instruction::ResumeCheck
|
||||
| Instruction::InstrumentedResume
|
||||
)
|
||||
{
|
||||
first_traceable = Some(i);
|
||||
}
|
||||
});
|
||||
let first_traceable = first_traceable.unwrap_or(0);
|
||||
|
||||
// Phase 4: Place regular INSTRUMENTED_* opcodes
|
||||
for i in 0..len {
|
||||
let op = code.code.instructions[i].op;
|
||||
if let Some(instrumented) = op.to_instrumented() {
|
||||
unsafe {
|
||||
code.code.instructions.replace_op(i, instrumented);
|
||||
// Phase 4: Place regular INSTRUMENTED_* opcodes whose event is active.
|
||||
// Walk by cache_entries so specialized CACHE payloads are not decoded as opcodes.
|
||||
{
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
let op = code.code.instructions.read_op(i);
|
||||
let oparg = code.code.instructions.read_arg(i).as_u8();
|
||||
let caches = op.deoptimize().cache_entries();
|
||||
if (events & (EVENT_LINE | EVENT_INSTRUCTION) != 0
|
||||
|| opcode_event_is_active(op, oparg, events))
|
||||
&& let Some(instrumented) = op.to_instrumented()
|
||||
{
|
||||
unsafe {
|
||||
code.code.instructions.replace_op(i, instrumented);
|
||||
}
|
||||
}
|
||||
i += 1 + caches;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Place INSTRUMENTED_INSTRUCTION (if EVENT_INSTRUCTION is active)
|
||||
// LINE/INSTRUCTION restore specialized opcodes first, so CACHE slots are valid.
|
||||
if events & EVENT_INSTRUCTION != 0 {
|
||||
for i in first_traceable..len {
|
||||
let op = code.code.instructions[i].op;
|
||||
@@ -482,7 +564,7 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) {
|
||||
&& !is_line_start[target_idx]
|
||||
&& !no_loc_mask.get(target_idx).copied().unwrap_or(false)
|
||||
{
|
||||
let target_op = code.code.instructions[target_idx].op;
|
||||
let target_op = code.code.instructions.read_op(target_idx);
|
||||
let target_base = target_op.to_base().unwrap_or(target_op);
|
||||
if !matches!(target_base, Instruction::PopIter)
|
||||
&& let Some((loc, _)) = line_locations.get(target_idx)
|
||||
@@ -502,7 +584,7 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) {
|
||||
.skip(first_traceable)
|
||||
{
|
||||
if marked {
|
||||
let op = code.code.instructions[i].op;
|
||||
let op = code.code.instructions.read_op(i);
|
||||
data.line_opcodes[i] = u8::from(op);
|
||||
unsafe {
|
||||
code.code
|
||||
|
||||
Reference in new issue
Block a user