## 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"