Files
hak/t/try-01.hak

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"