Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Lib/test/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ def test_locals_clear_locals(self):
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):
Expand Down Expand Up @@ -279,7 +278,6 @@ class ReprTest(unittest.TestCase):
Tests for repr(frame).
"""

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_repr(self):
def outer():
x = 5
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,6 @@ def func():
)
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):
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_super.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ def f():
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
Expand Down
27 changes: 14 additions & 13 deletions crates/vm/src/builtins/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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"));
}
};

Expand Down
14 changes: 7 additions & 7 deletions crates/vm/src/builtins/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand All @@ -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()) {
Expand Down
6 changes: 4 additions & 2 deletions crates/vm/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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(());
Expand Down
140 changes: 111 additions & 29 deletions crates/vm/src/stdlib/sys/monitoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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};

Expand Down Expand Up @@ -270,34 +327,40 @@ 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);
}
}
}
}
i += 1 + caches;
}
}

// All opcodes are now base opcodes.

if events == 0 {
*monitoring_data = None;
return;
Expand All @@ -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);

// 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);
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 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;
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
Loading