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:
Jeong, YunWon authored and GitHub committed 2026-09-19 10:24:57 +09:00
1 parent faa5a60638
commit b2c48d3ab4
7 files changed
+135 -54

No files matched your search

-2
View File
@@ -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
-1
View File
@@ -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):
-1
View File
@@ -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
+14 -13
View File
@@ -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"));
}
};
+7 -7
View File
@@ -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()) {
+4 -2
View File
@@ -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(());
+110 -28
View File
@@ -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