Skip to content

Commit c759664

Browse files
committed
Fix CI test failures: StopIteration wrapper, dead code validation, monitoring LINE events
- Add set_no_location() for StopIteration wrapper instructions (SetupCleanup, PopBlock, CallIntrinsic1, Reraise) to prevent spurious LINE monitoring events - Push WhileLoop fblock in compile_while for constant=false dead code, validate break/continue in dead code paths - Fix StopIterationError intrinsic message selection using CodeFlags (GENERATOR/COROUTINE) and return Ok instead of Err - Add build_no_location_mask() to parse linetable and identify NO_LOCATION instructions - Use NO_LOCATION mask in instrument_code to prevent instrumenting NO_LOCATION instructions - Reset prev_line in gen_throw before run(vm) to match CPython's eval loop reinitialization - Remove expectedFailure markers for now-passing tests
1 parent 15e2b2e commit c759664

File tree

7 files changed

+154
-29
lines changed

7 files changed

+154
-29
lines changed

Lib/test/test_contextlib.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,6 @@ def woohoo():
9999
raise ZeroDivisionError()
100100
self.assertEqual(state, [1, 42, 999])
101101

102-
# TODO: RUSTPYTHON
103-
@unittest.expectedFailure
104102
def test_contextmanager_traceback(self):
105103
@contextmanager
106104
def f():

Lib/test/test_contextlib_async.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@ async def woohoo():
252252
raise ZeroDivisionError(999)
253253
self.assertEqual(state, [1, 42, 999])
254254

255-
@unittest.expectedFailure # TODO: RUSTPYTHON
256255
async def test_contextmanager_except_stopiter(self):
257256
@asynccontextmanager
258257
async def woohoo():

crates/codegen/src/compile.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3878,6 +3878,7 @@ impl Compiler {
38783878
delta: handler_block
38793879
}
38803880
);
3881+
self.set_no_location();
38813882
self.push_fblock(FBlockType::StopIteration, handler_block, handler_block)?;
38823883
Some(handler_block)
38833884
} else {
@@ -3917,6 +3918,7 @@ impl Compiler {
39173918
// Close StopIteration handler and emit handler code
39183919
if let Some(handler_block) = stop_iteration_block {
39193920
emit!(self, PseudoInstruction::PopBlock);
3921+
self.set_no_location();
39203922
self.pop_fblock(FBlockType::StopIteration);
39213923
self.switch_to_block(handler_block);
39223924
emit!(
@@ -3925,7 +3927,9 @@ impl Compiler {
39253927
func: oparg::IntrinsicFunction1::StopIterationError
39263928
}
39273929
);
3930+
self.set_no_location();
39283931
emit!(self, Instruction::Reraise { depth: 1u32 });
3932+
self.set_no_location();
39293933
}
39303934

39313935
// Exit scope and create function object
@@ -5280,9 +5284,13 @@ impl Compiler {
52805284
// then compile orelse
52815285
if constant == Some(false) {
52825286
self.emit_nop();
5287+
let while_block = self.new_block();
5288+
let after_block = self.new_block();
5289+
self.push_fblock(FBlockType::WhileLoop, while_block, after_block)?;
52835290
self.do_not_emit_bytecode += 1;
52845291
self.compile_statements(body)?;
52855292
self.do_not_emit_bytecode -= 1;
5293+
self.pop_fblock(FBlockType::WhileLoop);
52865294
self.compile_statements(orelse)?;
52875295
self.leave_conditional_block();
52885296
return Ok(());
@@ -8755,6 +8763,14 @@ impl Compiler {
87558763
});
87568764
}
87578765

8766+
/// Mark the last emitted instruction as having no source location.
8767+
/// Prevents it from triggering LINE events in sys.monitoring.
8768+
fn set_no_location(&mut self) {
8769+
if let Some(last) = self.current_block().instructions.last_mut() {
8770+
last.lineno_override = Some(-1);
8771+
}
8772+
}
8773+
87588774
fn emit_no_arg<I: Into<AnyInstruction>>(&mut self, ins: I) {
87598775
self._emit(ins, OpArg::NULL, BlockIdx::NULL)
87608776
}
@@ -8990,6 +9006,31 @@ impl Compiler {
89909006
is_break: bool,
89919007
) -> CompileResult<()> {
89929008
if self.do_not_emit_bytecode > 0 {
9009+
// Still validate that we're inside a loop even in dead code
9010+
let code = self.current_code_info();
9011+
let mut found_loop = false;
9012+
for i in (0..code.fblock.len()).rev() {
9013+
match code.fblock[i].fb_type {
9014+
FBlockType::WhileLoop | FBlockType::ForLoop => {
9015+
found_loop = true;
9016+
break;
9017+
}
9018+
FBlockType::ExceptionGroupHandler => {
9019+
return Err(self.error_ranged(
9020+
CodegenErrorType::BreakContinueReturnInExceptStar,
9021+
range,
9022+
));
9023+
}
9024+
_ => {}
9025+
}
9026+
}
9027+
if !found_loop {
9028+
if is_break {
9029+
return Err(self.error_ranged(CodegenErrorType::InvalidBreak, range));
9030+
} else {
9031+
return Err(self.error_ranged(CodegenErrorType::InvalidContinue, range));
9032+
}
9033+
}
89939034
return Ok(());
89949035
}
89959036

crates/codegen/src/ir.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -457,10 +457,9 @@ impl CodeInfo {
457457

458458
let cache_count = info.cache_entries as usize;
459459
let (extras, lo_arg) = info.arg.split();
460-
locations.extend(core::iter::repeat_n(
461-
(info.location, info.end_location),
462-
info.arg.instr_size() + cache_count,
463-
));
460+
let loc_pair = (info.location, info.end_location);
461+
locations
462+
.extend(core::iter::repeat_n(loc_pair, info.arg.instr_size() + cache_count));
464463
// Collect linetable locations with lineno_override support
465464
let lt_loc = LineTableLocation {
466465
line: info

crates/compiler-core/src/bytecode.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,72 @@ pub fn decode_exception_table(table: &[u8]) -> Vec<ExceptionTableEntry> {
135135
entries
136136
}
137137

138+
/// Parse linetable to build a boolean mask indicating which code units
139+
/// have NO_LOCATION (line == -1). Returns a Vec<bool> of length `num_units`.
140+
pub fn build_no_location_mask(linetable: &[u8], num_units: usize) -> Vec<bool> {
141+
let mut mask = Vec::new();
142+
mask.resize(num_units, false);
143+
let mut pos = 0;
144+
let mut unit_idx = 0;
145+
146+
while pos < linetable.len() && unit_idx < num_units {
147+
let header = linetable[pos];
148+
pos += 1;
149+
let code = (header >> 3) & 0xf;
150+
let length = ((header & 7) + 1) as usize;
151+
152+
let is_no_location = code == PyCodeLocationInfoKind::None as u8;
153+
154+
// Skip payload bytes based on location kind
155+
match code {
156+
0..=9 => pos += 1, // Short forms: 1 byte payload
157+
10..=12 => pos += 2, // OneLine forms: 2 bytes payload
158+
13 => {
159+
// NoColumns: signed varint (line delta)
160+
while pos < linetable.len() {
161+
let b = linetable[pos];
162+
pos += 1;
163+
if b & 0x40 == 0 {
164+
break;
165+
}
166+
}
167+
}
168+
14 => {
169+
// Long form: signed varint (line delta) + 3 unsigned varints
170+
// line_delta
171+
while pos < linetable.len() {
172+
let b = linetable[pos];
173+
pos += 1;
174+
if b & 0x40 == 0 {
175+
break;
176+
}
177+
}
178+
// end_line_delta, col+1, end_col+1
179+
for _ in 0..3 {
180+
while pos < linetable.len() {
181+
let b = linetable[pos];
182+
pos += 1;
183+
if b & 0x40 == 0 {
184+
break;
185+
}
186+
}
187+
}
188+
}
189+
15 => {} // None: no payload
190+
_ => {}
191+
}
192+
193+
for _ in 0..length {
194+
if unit_idx < num_units {
195+
mask[unit_idx] = is_no_location;
196+
unit_idx += 1;
197+
}
198+
}
199+
}
200+
201+
mask
202+
}
203+
138204
/// CPython 3.11+ linetable location info codes
139205
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
140206
#[repr(u8)]

crates/vm/src/frame.rs

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,12 +1755,6 @@ impl ExecutingFrame<'_> {
17551755
exc_tb: PyObjectRef,
17561756
) -> PyResult<ExecutionResult> {
17571757
self.monitoring_mask = vm.state.monitoring_events.load();
1758-
// Reset prev_line so that LINE monitoring events fire even if
1759-
// the exception handler is on the same line as the yield point.
1760-
// In CPython, _Py_call_instrumentation_line has a special case
1761-
// for RESUME: it fires LINE even when prev_line == current_line.
1762-
// Since gen_throw bypasses RESUME, we reset prev_line instead.
1763-
*self.prev_line = 0;
17641758
if let Some(jen) = self.yield_from_target() {
17651759
// Check if the exception is GeneratorExit (type or instance).
17661760
// For GeneratorExit, close the sub-iterator instead of throwing.
@@ -1796,7 +1790,10 @@ impl ExecutingFrame<'_> {
17961790
self.push_value(vm.ctx.none());
17971791
vm.contextualize_exception(&err);
17981792
return match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) {
1799-
Ok(None) => self.run(vm),
1793+
Ok(None) => {
1794+
*self.prev_line = 0;
1795+
self.run(vm)
1796+
}
18001797
Ok(Some(result)) => Ok(result),
18011798
Err(exception) => Err(exception),
18021799
};
@@ -1838,7 +1835,10 @@ impl ExecutingFrame<'_> {
18381835
self.push_value(vm.ctx.none());
18391836
vm.contextualize_exception(&err);
18401837
match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) {
1841-
Ok(None) => self.run(vm),
1838+
Ok(None) => {
1839+
*self.prev_line = 0;
1840+
self.run(vm)
1841+
}
18421842
Ok(Some(result)) => Ok(result),
18431843
Err(exception) => Err(exception),
18441844
}
@@ -1906,7 +1906,13 @@ impl ExecutingFrame<'_> {
19061906
self.push_value(vm.ctx.none());
19071907

19081908
match self.unwind_blocks(vm, UnwindReason::Raising { exception }) {
1909-
Ok(None) => self.run(vm),
1909+
Ok(None) => {
1910+
// Reset prev_line so that the first instruction in the handler
1911+
// fires a LINE event. In CPython, gen_send_ex re-enters the
1912+
// eval loop which reinitializes its local prev_instr tracker.
1913+
*self.prev_line = 0;
1914+
self.run(vm)
1915+
}
19101916
Ok(Some(result)) => Ok(result),
19111917
Err(exception) => {
19121918
// Fire PY_UNWIND: exception escapes the generator frame.
@@ -9440,20 +9446,25 @@ impl ExecutingFrame<'_> {
94409446
Ok(vm.ctx.new_tuple(list.borrow_vec().to_vec()).into())
94419447
}
94429448
bytecode::IntrinsicFunction1::StopIterationError => {
9443-
// Convert StopIteration to RuntimeError
9444-
// Used to ensure async generators don't raise StopIteration directly
9445-
// _PyGen_FetchStopIterationValue
9446-
// Use fast_isinstance to handle subclasses of StopIteration
9449+
// Convert StopIteration to RuntimeError (PEP 479)
9450+
// Returns the exception object; RERAISE will re-raise it
94479451
if arg.fast_isinstance(vm.ctx.exceptions.stop_iteration) {
9448-
Err(vm.new_runtime_error("coroutine raised StopIteration"))
9452+
let flags = &self.code.flags;
9453+
let msg = if flags.contains(
9454+
bytecode::CodeFlags::COROUTINE | bytecode::CodeFlags::GENERATOR,
9455+
) {
9456+
"async generator raised StopIteration"
9457+
} else if flags.contains(bytecode::CodeFlags::COROUTINE) {
9458+
"coroutine raised StopIteration"
9459+
} else {
9460+
"generator raised StopIteration"
9461+
};
9462+
let err = vm.new_runtime_error(msg);
9463+
err.set___cause__(arg.downcast().ok());
9464+
Ok(err.into())
94499465
} else {
9450-
// If not StopIteration, just re-raise the original exception
9451-
Err(arg.downcast().unwrap_or_else(|obj| {
9452-
vm.new_runtime_error(format!(
9453-
"unexpected exception type: {:?}",
9454-
obj.class()
9455-
))
9456-
}))
9466+
// Not StopIteration, pass through for RERAISE
9467+
Ok(arg)
94579468
}
94589469
}
94599470
bytecode::IntrinsicFunction1::AsyncGenWrap => {

crates/vm/src/stdlib/sys/monitoring.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,9 @@ pub fn instrument_code(code: &PyCode, events: u32) {
368368
// is_line_start[i] = true if position i should have INSTRUMENTED_LINE
369369
let mut is_line_start = vec![false; len];
370370

371+
// Build NO_LOCATION mask from linetable
372+
let no_loc_mask = bytecode::build_no_location_mask(&code.code.linetable, len);
373+
371374
// First pass: mark positions where the source line changes
372375
let mut prev_line: Option<u32> = None;
373376
for (i, unit) in code
@@ -395,6 +398,10 @@ pub fn instrument_code(code: &PyCode, events: u32) {
395398
) {
396399
continue;
397400
}
401+
// Skip NO_LOCATION instructions
402+
if no_loc_mask.get(i).copied().unwrap_or(false) {
403+
continue;
404+
}
398405
if let Some((loc, _)) = code.code.locations.get(i) {
399406
let line = loc.line.get() as u32;
400407
let is_new = prev_line != Some(line);
@@ -445,6 +452,7 @@ pub fn instrument_code(code: &PyCode, events: u32) {
445452
if let Some(target_idx) = target
446453
&& target_idx < len
447454
&& !is_line_start[target_idx]
455+
&& !no_loc_mask.get(target_idx).copied().unwrap_or(false)
448456
{
449457
let target_op = code.code.instructions[target_idx].op;
450458
let target_base = target_op.to_base().map_or(target_op, |b| b);
@@ -465,7 +473,10 @@ pub fn instrument_code(code: &PyCode, events: u32) {
465473
// Third pass: mark exception handler targets as line starts.
466474
for entry in bytecode::decode_exception_table(&code.code.exceptiontable) {
467475
let target_idx = entry.target as usize;
468-
if target_idx < len && !is_line_start[target_idx] {
476+
if target_idx < len
477+
&& !is_line_start[target_idx]
478+
&& !no_loc_mask.get(target_idx).copied().unwrap_or(false)
479+
{
469480
let target_op = code.code.instructions[target_idx].op;
470481
let target_base = target_op.to_base().map_or(target_op, |b| b);
471482
if !matches!(target_base, Instruction::PopIter)

0 commit comments

Comments
 (0)