diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index 53d42a595..ad7eda566 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -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 diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index e59fce573..bb375606a 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -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): diff --git a/Lib/test/test_super.py b/Lib/test/test_super.py index 88e41780c..193c8b7d7 100644 --- a/Lib/test/test_super.py +++ b/Lib/test/test_super.py @@ -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 diff --git a/crates/vm/src/builtins/frame.rs b/crates/vm/src/builtins/frame.rs index 3bc259af0..f684c21e8 100644 --- a/crates/vm/src/builtins/frame.rs +++ b/crates/vm/src/builtins/frame.rs @@ -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, vm: &VirtualMachine) -> PyResult { - const REPR: &str = ""; - Ok(vm.ctx.intern_str(REPR).to_owned()) - } - - #[cold] - fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { - unreachable!("use repr instead") + fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { + 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 as usize; + Ok(format!( + "", + 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")); } }; diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index cf46d0730..e3f1b9d09 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -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()) { diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 949bbf5e9..5c1219b02 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -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(()); diff --git a/crates/vm/src/stdlib/sys/monitoring.rs b/crates/vm/src/stdlib/sys/monitoring.rs index a4be3ba5a..b0083c0b3 100644 --- a/crates/vm/src/stdlib/sys/monitoring.rs +++ b/crates/vm/src/stdlib/sys/monitoring.rs @@ -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 { + 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