Skip to content

Commit 03a5cb1

Browse files
fix: an error that crosses except or finally keeps the line it was raised at
1 parent 61ca366 commit 03a5cb1

10 files changed

Lines changed: 59 additions & 15 deletions

File tree

‎src/parser/control.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,8 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
540540
break;
541541
}
542542

543+
// The header runs code of its own, so an error in its type reports its line.
544+
self.mark_stmt();
543545
let mut as_name: Option<String> = None;
544546
if matches!(self.peek(), Some(TokenType::Colon)) {
545547
had_bare = true;

‎src/parser/stmt.rs‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@ enum TupleElem {
2323

2424
impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
2525

26-
/* Statement dispatch, returns true if a value is left on the stack for caller to PopTop. */
27-
pub(super) fn stmt(&mut self) -> bool {
28-
// Record ip->source offset before any emit so `resolve()` can map ip to source.
26+
/* Records ip->source offset before any emit, so `resolve()` maps what follows to the next token. */
27+
pub(super) fn mark_stmt(&mut self) {
2928
let ip = self.chunk.instructions.len() as u32;
3029
let pos = self.tokens.peek().map(|t| t.start as u32).unwrap_or(self.last_end as u32);
3130
self.chunk.stmt_pos.push((ip, pos));
31+
}
32+
33+
/* Statement dispatch, returns true if a value is left on the stack for caller to PopTop. */
34+
pub(super) fn stmt(&mut self) -> bool {
35+
self.mark_stmt();
3236

3337
match self.peek() {
3438
Some(TokenType::If) => {

‎src/value/frames.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ pub enum Unwind {
7272
Return(Val),
7373
// break/continue, run `remaining` more cleanups, then resume at `target`.
7474
Goto { target: usize, remaining: u16 },
75-
Reraise(VmErr),
75+
// An exception passing through, with the byte offset it was raised at.
76+
Reraise(VmErr, Option<u32>),
7677
}
7778

7879
/* Saved stack/iter/with/unwind depths for unwinding to a handler. Stored on the active Coroutine so `try`/`except` survives yields. */

‎src/vm/dispatch.rs‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ impl<'a> VM<'a> {
228228
self.call_stack.clear();
229229
match self.next_cleanup_handler(exc_base) {
230230
Some(h) => {
231-
self.unwind_stack.push(Unwind::Reraise(e));
231+
self.unwind_stack.push(Unwind::Reraise(e, None));
232232
ip = h;
233233
continue;
234234
}
@@ -260,20 +260,22 @@ impl<'a> VM<'a> {
260260
let msg_val = alloc(&mut self.heap, HeapObj::Str(e.message()))?;
261261
alloc(&mut self.heap, HeapObj::ExcInstance(msg, alloc::vec![msg_val]))?
262262
};
263-
self.error_byte_pos = None;
263+
// Kept so raising the exception again reports where it was first raised.
264+
let at = self.error_byte_pos.take();
264265
// Drop reasons from finally bodies this exception unwinds past.
265266
self.unwind_stack.truncate(frame.unwind_depth);
266267
match frame.kind {
267268
BlockKind::Except => {
268269
// Record the handled exc so a bare `raise` in the handler can re-raise it.
269270
self.handling_exc = Some(exc);
271+
self.handling_pos = at;
270272
self.push(exc);
271273
ip = frame.handler_ip;
272274
}
273275
// finally/with run their cleanup, then re-raise via EndFinally.
274276
BlockKind::Finally => {
275277
self.pending.exc_val = Some(exc);
276-
self.unwind_stack.push(Unwind::Reraise(e));
278+
self.unwind_stack.push(Unwind::Reraise(e, at));
277279
ip = frame.handler_ip;
278280
}
279281
}
@@ -699,7 +701,7 @@ impl<'a> VM<'a> {
699701
let cm = self.with_stack.pop().ok_or(cold_runtime("WithExit without matching WithEnter"))?;
700702
let (exit_fn, class) = self.with_dunder(cm, "__exit__")?;
701703
// Reraise selects `__exit__(type, exc, None)`, other exits pass three Nones.
702-
let (exc_type, exc) = if matches!(self.unwind_stack.last(), Some(Unwind::Reraise(_))) {
704+
let (exc_type, exc) = if matches!(self.unwind_stack.last(), Some(Unwind::Reraise(..))) {
703705
let exc = self.pending.exc_val.unwrap_or(Val::none());
704706
let exc_name = self.exc_type_name(exc);
705707
(self.heap.alloc(HeapObj::Type(exc_name))?, exc)
@@ -716,7 +718,7 @@ impl<'a> VM<'a> {
716718
OpCode::WithJudge => {
717719
let r = self.pop()?;
718720
// A truthy `__exit__` suppresses the exception, but never a cancel.
719-
if !self.cancelling && matches!(self.unwind_stack.last(), Some(Unwind::Reraise(_))) && self.truthy(r) {
721+
if !self.cancelling && matches!(self.unwind_stack.last(), Some(Unwind::Reraise(..))) && self.truthy(r) {
720722
// Suppress, turn the re-raise into a normal exit and drop the exc identity.
721723
if let Some(top) = self.unwind_stack.last_mut() { *top = Unwind::Normal; }
722724
self.pending.exc_val = None;
@@ -742,7 +744,10 @@ impl<'a> VM<'a> {
742744
*ip = target;
743745
}
744746
}
745-
Some(Unwind::Reraise(e)) => return Err(e),
747+
Some(Unwind::Reraise(e, at)) => {
748+
self.error_byte_pos = at;
749+
return Err(e);
750+
}
746751
}
747752
}
748753
// break/continue across N finally/with blocks, the following Jump completes the transfer.

‎src/vm/mod.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ pub struct VM<'a> {
120120
pub(crate) unwind_stack: Vec<types::Unwind>,
121121
/* Exception currently being handled in an except block, a bare `raise` re-raises it. */
122122
pub(crate) handling_exc: Option<Val>,
123+
// Where the handled exception was raised, so re-raising it reports that line.
124+
pub(crate) handling_pos: Option<u32>,
123125
pub(crate) functions: Vec<&'a (Vec<String>, SSAChunk, u16, u16)>,
124126
// (chunk_ptr, global fn ids), linear scan over a tiny list avoids HashMap monomorphization.
125127
pub(crate) fn_index: Vec<(*const SSAChunk, Vec<u32>)>,
@@ -269,6 +271,7 @@ impl<'a> VM<'a> {
269271
exception_stack: Vec::new(),
270272
unwind_stack: Vec::new(),
271273
handling_exc: None,
274+
handling_pos: None,
272275
error_byte_pos: None,
273276
module_table: HashMap::default(),
274277
fn_module: Vec::new(),
@@ -527,6 +530,7 @@ impl<'a> VM<'a> {
527530
self.pending_sync_frames.clear();
528531
self.executing_coros.clear();
529532
self.handling_exc = None;
533+
self.handling_pos = None;
530534
self.cancelling = false;
531535
self.resume_raise = None;
532536
self.yielded = false;

‎src/vm/opcodes/stack.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,16 @@ impl<'a> VM<'a> {
163163
};
164164
let name = self.exc_type_name(exc);
165165
self.pending.exc_val = Some(exc);
166+
self.error_byte_pos = self.handling_pos;
166167
return Err(VmErr::Raised(name));
167168
}
168169
// RaiseFrom emits both `expr` then `from expr`, the topmost value is the cause, but the exception to raise is the LHS.
169170
if op == OpCode::RaiseFrom { let _cause = self.pop()?; }
170171
let mut exc = self.pop()?;
172+
// The exception being handled raised again keeps the line it was first raised at.
173+
if Some(exc) == self.handling_exc {
174+
self.error_byte_pos = self.handling_pos;
175+
}
171176
// A class deriving from an exception raises an instance of itself made with no arguments.
172177
if matches!(self.heap.try_get(exc), Some(HeapObj::Class(..))) && self.exc_base(exc).is_some() {
173178
self.push(exc);

‎src/vm/snapshot.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ codec!(enum Unwind, put_unwind, get_unwind {
249249
0 Normal,
250250
1 Return(v: val),
251251
2 Goto { target: usz, remaining: u16 },
252-
3 Reraise(e: (put_vm_err, get_vm_err)),
252+
3 Reraise(e: (put_vm_err, get_vm_err), at: opt_u32),
253253
});
254254

255255
codec!(enum WaitKind, put_wait_kind, get_wait_kind {
@@ -504,6 +504,7 @@ vm_state! {
504504
exception_stack: [put_exc_frame, get_exc_frame],
505505
unwind_stack: [put_unwind, get_unwind],
506506
handling_exc: opt_val,
507+
handling_pos: opt_u32,
507508
pending_sync_frames: [put_sync_frame, get_sync_frame],
508509
pending_exec_exc_base: opt_usz,
509510
pending: (put_pending, get_pending),

‎tests/cases/vm.json‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5199,5 +5199,15 @@
51995199
{"src": "x = 3.25\nprint(id(x) == id(x), id(None) == id(None), id(True) != id(1), id(0) != id(False))", "output": ["True True True True"]},
52005200
{"src": "class C:\n pass\nobjs = [C() for _ in range(5)]\nprint(len({id(o) for o in objs}), all((id(p) == id(q)) == (p is q) for p in objs for q in objs))", "output": ["5 True"]},
52015201
{"src": "n = float('nan')\nm = float('nan')\nprint(id(n) == id(n), id(n) != id(m), (id(n) == id(m)) == (n is m))", "output": ["True True True"]},
5202-
{"src": "big = 10 ** 30\nneg = -10 ** 30\nprint(id(big) != id(neg), (id(big) == id(big)), id(big) >= 0, id(neg) >= 0)", "output": ["True True True True"]}
5202+
{"src": "big = 10 ** 30\nneg = -10 ** 30\nprint(id(big) != id(neg), (id(big) == id(big)), id(big) >= 0, id(neg) >= 0)", "output": ["True True True True"]},
5203+
{"src": "try:\n x = 1 / 0\nexcept NoExiste:\n pass\n", "output": [], "error": "NameError: name 'NoExiste' is not defined", "error_line": 3},
5204+
{"src": "try:\n x = 1 / 0\nfinally:\n pass\n", "output": [], "error": "ZeroDivisionError: division by zero", "error_line": 2},
5205+
{"src": "try:\n x = 1 / 0\nexcept KeyError:\n pass\n", "output": [], "error": "ZeroDivisionError: division by zero", "error_line": 2},
5206+
{"src": "try:\n x = 1 / 0\nexcept ZeroDivisionError:\n raise\n", "output": [], "error": "ZeroDivisionError", "error_line": 2},
5207+
{"src": "try:\n x = 1 / 0\nexcept ZeroDivisionError as e:\n raise e\n", "output": [], "error": "ZeroDivisionError: division by zero", "error_line": 2},
5208+
{"src": "try:\n x = 1 / 0\nfinally:\n int('x')\n", "output": [], "error": "ValueError", "error_line": 4},
5209+
{"src": "try:\n x = 0\nexcept ValueError:\n pass\nelse:\n y = 1 / x\n", "output": [], "error": "ZeroDivisionError: division by zero", "error_line": 6},
5210+
{"src": "try:\n x = 1 / 0\nexcept ValueError:\n pass\nexcept Nope as e:\n pass\n", "output": [], "error": "NameError: name 'Nope' is not defined", "error_line": 5},
5211+
{"src": "class CM:\n def __enter__(self):\n return self\n def __exit__(self, t, v, tb):\n return False\nwith CM():\n x = 1 / 0\n", "output": [], "error": "ZeroDivisionError: division by zero", "error_line": 7},
5212+
{"src": "def f():\n return 1 / 0\ntry:\n f()\nfinally:\n pass\n", "output": [], "error": "ZeroDivisionError: division by zero", "error_line": 2}
52035213
]

‎tests/snapshot.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ mod test {
4242
// Present means send() reaches a scheduler, this corpus only needs it accepted.
4343
#[serde(default)]
4444
sends: Option<Vec<(String, String)>>,
45+
// The line the vm suite checks an error at, which a replay leaves alone.
46+
#[serde(default, rename = "error_line")]
47+
_error_line: Option<usize>,
4548
}
4649

4750
fn accept_send(_group: &str, _body: &str) -> bool {

‎tests/vm.rs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ mod test {
2424
// Present installs a scheduler hook, the (group, body) pairs send() handed over.
2525
#[serde(default)]
2626
sends: Option<Vec<(String, String)>>,
27+
// The line the error points at, counted from 1.
28+
#[serde(default)]
29+
error_line: Option<usize>,
2730
}
2831

2932
std::thread_local! {
@@ -127,9 +130,15 @@ mod test {
127130
}
128131
}
129132
Err(e) => match &case.error {
130-
Some(expected) => if !e.to_string().contains(expected.as_str()) {
131-
failures.push(format!("ERR {:?}\n got '{}'\n want '{}'", case.src, e, expected));
132-
},
133+
Some(expected) => {
134+
if !e.to_string().contains(expected.as_str()) {
135+
failures.push(format!("ERR {:?}\n got '{}'\n want '{}'", case.src, e, expected));
136+
}
137+
let line = vm.error_pos().map(|at| case.src[..at.min(case.src.len())].matches('\n').count() + 1);
138+
if let Some(want) = case.error_line && line != Some(want) {
139+
failures.push(format!("LINE {:?}\n got {:?}\n want {}", case.src, line, want));
140+
}
141+
}
133142
None => failures.push(format!("RAISED {:?}: {}", case.src, e)),
134143
}
135144
}

0 commit comments

Comments
 (0)