203 lines
6.6 KiB
Plaintext
203 lines
6.6 KiB
Plaintext
## Two related defects in how 'return' interacts with try/catch. Both are
|
|
## fixed; this checks both, because each hid the other while writing the test.
|
|
##
|
|
## 1. compile_return() emitted TRY_EXIT/CLASS_EXIT before the return value was
|
|
## compiled, so the value expression ran with every enclosing handler in the
|
|
## function already popped - not just the innermost one. The unwinding now
|
|
## comes from emit_return(), after the value is on the operand stack.
|
|
## Only the syntactic form used to matter: try { x := (f) } caught what f
|
|
## threw while try { return (f) } did not, so both forms appear below.
|
|
##
|
|
## 2. A 'return', 'break' or 'continue' inside a catch body emitted a TRY_EXIT
|
|
## for the very try whose handler it sat in - but 'throw' has already
|
|
## unwound that frame, so the second pop drove the exception stack below its
|
|
## base. HAK_EXSTACK_IS_EMPTY tests 'exsp <= st', so an underflowed stack
|
|
## reads as empty from then on and EVERY later try in the process reported
|
|
## 'exception not handled'. The catch body is now compiled with its try
|
|
## block marked in_catch, which suppresses the instruction.
|
|
##
|
|
## That corruption was never visible in the function that caused it, so the
|
|
## assertions that actually guard it are the ones exercising a try AFTER a
|
|
## catch that returned, broke or continued.
|
|
|
|
fun chk(ok msg) {
|
|
if ok { printf "OK: %s\n" msg } \
|
|
else { printf "ERROR: %s\n" msg }
|
|
}
|
|
|
|
fun boom() { throw 99 }
|
|
|
|
## ------------------------------------------------------------------
|
|
## 1. a throw inside a return value
|
|
## ------------------------------------------------------------------
|
|
|
|
fun c1() {
|
|
try { return (boom) } catch (e) { return (+ e 1) }
|
|
}
|
|
chk (= (c1) 100) "a throw in a return value is caught by the enclosing try"
|
|
|
|
## the catch need not return for the handler to run
|
|
seen := 0
|
|
fun c2() {
|
|
try { return (boom) } catch (e) { seen := e }
|
|
return 7
|
|
}
|
|
chk (= (c2) 7) "execution resumes after the try when the catch falls through"
|
|
chk (= seen 99) "the catch body ran"
|
|
|
|
fun c3() {
|
|
try { return (boom) } catch (e) { }
|
|
return 8
|
|
}
|
|
chk (= (c3) 8) "an empty catch body still handles the throw"
|
|
|
|
## nesting: the innermost handler wins and the outer one stays out of it.
|
|
## the old code unwound every enclosing try, so neither fired.
|
|
inner := 0
|
|
outer := 0
|
|
fun c4() {
|
|
try {
|
|
try { return (boom) } catch (e) { inner := (+ inner 1) }
|
|
} catch (e2) { outer := (+ outer 1) }
|
|
return 9
|
|
}
|
|
chk (= (c4) 9) "a nested try returns normally after handling"
|
|
chk (= inner 1) "the innermost try catches"
|
|
chk (= outer 0) "the outer try is left out of it"
|
|
|
|
## across a call boundary the callee's own handler wins; the old code let the
|
|
## throw escape to the caller's handler instead
|
|
g_seen := 0
|
|
h_seen := 0
|
|
fun g() { try { return (boom) } catch (e) { g_seen := 1 ; return -1 } }
|
|
fun h() { try { return (g) } catch (e) { h_seen := 1 ; return -2 } }
|
|
chk (= (h) -1) "the callee handles its own throw"
|
|
chk (= g_seen 1) "the callee's catch ran"
|
|
chk (= h_seen 0) "the caller's catch did not"
|
|
|
|
## the form that always worked must keep working
|
|
fun c5() {
|
|
| v |
|
|
v := 0
|
|
try { v := (boom) } catch (e) { v := e }
|
|
return v
|
|
}
|
|
chk (= (c5) 99) "a throw in an assignment is still caught"
|
|
|
|
## ------------------------------------------------------------------
|
|
## 2. leaving a catch body by return, break or continue
|
|
##
|
|
## each case is followed by a fresh try: that is where an underflowed
|
|
## exception stack shows up, never in the function that caused it.
|
|
## ------------------------------------------------------------------
|
|
|
|
## a try that must still work after the cases above, which all returned from
|
|
## inside a catch
|
|
fun still1() {
|
|
| v |
|
|
v := 0
|
|
try { v := (boom) } catch (e) { v := 21 }
|
|
return v
|
|
}
|
|
chk (= (still1) 21) "a try still works after earlier catch bodies returned"
|
|
|
|
## break out of a loop from inside a catch
|
|
bj := 0
|
|
while (< bj 9) {
|
|
bj := (+ bj 1)
|
|
try { y := (boom) } catch (e) { break }
|
|
}
|
|
chk (= bj 1) "break inside a catch leaves the loop"
|
|
|
|
fun still2() {
|
|
| v |
|
|
v := 0
|
|
try { v := (boom) } catch (e) { v := 22 }
|
|
return v
|
|
}
|
|
chk (= (still2) 22) "a try still works after a break from inside a catch"
|
|
|
|
## continue to the next iteration from inside a catch
|
|
ci := 0
|
|
ch := 0
|
|
while (< ci 3) {
|
|
ci := (+ ci 1)
|
|
try { z := (boom) } catch (e) { ch := (+ ch 1) ; continue }
|
|
ch := 100
|
|
}
|
|
chk (= ci 3) "continue inside a catch keeps iterating"
|
|
chk (= ch 3) "continue inside a catch skips the rest of the loop body"
|
|
|
|
fun still3() {
|
|
| v |
|
|
v := 0
|
|
try { v := (boom) } catch (e) { v := 23 }
|
|
return v
|
|
}
|
|
chk (= (still3) 23) "a try still works after a continue from inside a catch"
|
|
|
|
## the in_catch mark must suppress only the block whose handler we are in. the
|
|
## return below sits in the inner catch, so the inner frame is already gone,
|
|
## but the outer try is still live and has to be unwound on the way out. a
|
|
## frame left behind there would misdirect the next throw instead.
|
|
fun nest2() {
|
|
try {
|
|
try { w := (boom) } catch (e) { return 11 }
|
|
} catch (e2) { }
|
|
return 12
|
|
}
|
|
chk (= (nest2) 11) "a return from an inner catch still unwinds the outer try"
|
|
|
|
fun still4() {
|
|
| v |
|
|
v := 0
|
|
try { v := (boom) } catch (e) { v := 24 }
|
|
return v
|
|
}
|
|
chk (= (still4) 24) "a try still works after returning from a nested catch"
|
|
|
|
## ------------------------------------------------------------------
|
|
## 3. break and continue in a try BODY still unwind
|
|
##
|
|
## these share emit_ctlblk_unwind with return, and here the frame is live and
|
|
## must be popped. a leaked frame per iteration would exhaust the exception
|
|
## stack long before these loops finish.
|
|
## ------------------------------------------------------------------
|
|
|
|
i := 0
|
|
n := 0
|
|
while (< i 2000) {
|
|
i := (+ i 1)
|
|
try { if (= (mod i 2) 0) { continue } ; n := (+ n 1) } catch (e) { n := -1 }
|
|
}
|
|
chk (= n 1000) "continue inside a try body unwinds without leaking a handler"
|
|
|
|
i := 0
|
|
while (< i 2000) {
|
|
i := (+ i 1)
|
|
try { if (= i 7) { break } } catch (e) { }
|
|
}
|
|
chk (= i 7) "break inside a try body unwinds and leaves the loop"
|
|
|
|
## ------------------------------------------------------------------
|
|
## 4. a handler stays live across a whole call chain
|
|
## ------------------------------------------------------------------
|
|
|
|
hits := 0
|
|
caught := 0
|
|
fun rec(k) {
|
|
if (<= k 0) { throw 55 }
|
|
try { return (rec (- k 1)) } catch (e) { hits := (+ hits 1) ; caught := e ; return e }
|
|
}
|
|
chk (= (rec 20) 55) "recursion with a try per frame propagates the thrown value"
|
|
chk (= caught 55) "the thrown value arrives intact"
|
|
chk (= hits 1) "only the frame nearest the throw handles it"
|
|
|
|
fun still5() {
|
|
| v |
|
|
v := 0
|
|
try { v := (boom) } catch (e) { v := 25 }
|
|
return v
|
|
}
|
|
chk (= (still5) 25) "a try still works after deep try-per-frame recursion"
|