diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index 8e25f07bcb..677fd6ec6c 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -206,7 +206,7 @@ proc blockLeaveActions(p: BProc, howManyTrys, howManyExcepts: int, isReturnStmt # Called by return and break stmts. # Deals with issues faced when jumping out of try/except/finally stmts. - var stack = newSeq[tuple[fin: PNode, inExcept: bool, label: Natural]](0) + var stack = newSeq[tuple[fin: PNode, inExcept: bool, isHidden: bool, label: Natural]](0) inc p.withinBlockLeaveActions for i in 1..howManyTrys: @@ -769,12 +769,26 @@ proc raiseExitCleanup(p: BProc, destroy: string) = [p.nestedTryStmts[^1].label, destroy]) proc finallyActions(p: BProc) = - if p.config.exc != excGoto and p.nestedTryStmts.len > 0 and p.nestedTryStmts[^1].inExcept: - # if the current try stmt have a finally block, - # we must execute it before reraising - let finallyBlock = p.nestedTryStmts[^1].fin - if finallyBlock != nil: - genSimpleBlock(p, finallyBlock[0]) + if p.config.exc != excGoto: + # Walk past compiler-injected `nkHiddenTryStmt` wrappers (e.g. ARC's + # destructor try/finally that wraps `except T as e:` bodies) to reach + # the user's actual try. We must NOT walk past a real user try whose + # body we are currently in, because a raise from there will be caught + # by that try's own except branches rather than escaping outward. + # + # If after skipping wrappers the next entry is a user try in its + # except branch (inExcept=true), inline its finally body before the + # raise propagates — without this, the C++ sibling-catch rule would + # cause the user's catch(...)/finally pair to be bypassed and the + # finally would be silently dropped. + for i in countdown(p.nestedTryStmts.high, 0): + if p.nestedTryStmts[i].isHidden: + continue + if p.nestedTryStmts[i].inExcept: + let finallyBlock = p.nestedTryStmts[i].fin + if finallyBlock != nil: + genSimpleBlock(p, finallyBlock[0]) + return proc raiseInstr(p: BProc; result: var Rope) = if p.config.exc == excGoto: @@ -1080,7 +1094,7 @@ proc genTryCpp(p: BProc, t: PNode, d: var TLoc) = lineCg(p, cpsLocals, "std::exception_ptr T$1_;$n", [etmp]) let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, false, 0.Natural)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, 0.Natural)) if t.kind == nkHiddenTryStmt: lineCg(p, cpsStmts, "try {$n", []) @@ -1240,7 +1254,7 @@ proc genTryCppOld(p: BProc, t: PNode, d: var TLoc) = genLineDir(p, t) cgsym(p.module, "popCurrentExceptionEx") let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, false, 0.Natural)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, 0.Natural)) startBlock(p, "try {$n") expr(p, t[0], d) endBlock(p) @@ -1309,7 +1323,7 @@ proc genTryGoto(p: BProc; t: PNode; d: var TLoc) = let lab = p.labels let hasExcept = t[1].kind == nkExceptBranch if hasExcept: inc p.withinTryWithExcept - p.nestedTryStmts.add((fin, false, Natural lab)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, Natural lab)) p.flags.incl nimErrorFlagAccessed @@ -1461,7 +1475,7 @@ proc genTrySetjmp(p: BProc, t: PNode, d: var TLoc) = linefmt(p, cpsStmts, "$1.status = setjmp($1.context);$n", [safePoint]) lineCg(p, cpsStmts, "if ($1.status == 0) {$n", [safePoint]) let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, quirkyExceptions, 0.Natural)) + p.nestedTryStmts.add((fin, quirkyExceptions, t.kind == nkHiddenTryStmt, 0.Natural)) expr(p, t[0], d) if not quirkyExceptions: linefmt(p, cpsStmts, "#popSafePoint();$n", []) diff --git a/compiler/cgendata.nim b/compiler/cgendata.nim index 7bc273df30..30dfef350e 100644 --- a/compiler/cgendata.nim +++ b/compiler/cgendata.nim @@ -75,10 +75,13 @@ type flags*: set[TCProcFlag] lastLineInfo*: TLineInfo # to avoid generating excessive 'nimln' statements currLineInfo*: TLineInfo # AST codegen will make this superfluous - nestedTryStmts*: seq[tuple[fin: PNode, inExcept: bool, label: Natural]] + nestedTryStmts*: seq[tuple[fin: PNode, inExcept: bool, isHidden: bool, label: Natural]] # in how many nested try statements we are # (the vars must be volatile then) - # bool is true when are in the except part of a try block + # `inExcept` is true when we are in the except part of a try block. + # `isHidden` is true for compiler-injected `nkHiddenTryStmt` wrappers + # (e.g. ARC's destructor try/finally around `except T as e:` bodies); + # finallyActions walks past such wrappers to reach the user's try. finallySafePoints*: seq[Rope] # For correctly cleaning up exceptions when # using return in finally statements labels*: Natural # for generating unique labels in the C proc diff --git a/tests/exception/tcpp_handler_raise_finally.nim b/tests/exception/tcpp_handler_raise_finally.nim new file mode 100644 index 0000000000..880903d5a9 --- /dev/null +++ b/tests/exception/tcpp_handler_raise_finally.nim @@ -0,0 +1,61 @@ +discard """ + targets: "cpp" + matrix: "--mm:arc; --mm:orc; --mm:refc" + output: ''' +inner: orig +finally +outer: re:orig +inner-typeless: orig +finally-typeless +outer-typeless: re-tl:orig +no-catch-finally +caught-propagated: prop +''' +""" + +# When an `except` handler raises a new exception, the enclosing `finally` +# block must still run before the new exception propagates to the outer +# try. +# +# The C++ backend previously emitted the finally's `catch (...)` as a +# sibling of the user-written catches. C++ does not allow sibling catches +# to catch each other's throws, so a handler-raised exception bypassed the +# finally entirely. The fix wraps the inner try/catch sequence in an +# outer try, so any escaping exception (whether from the body or from a +# handler) is captured before the finally runs. + +block typed_except: + try: + try: + raise newException(CatchableError, "orig") + except CatchableError as e: + echo "inner: ", e.msg + raise newException(CatchableError, "re:" & e.msg) + finally: + echo "finally" + except CatchableError as outer: + echo "outer: ", outer.msg + +block typeless_except: + try: + try: + raise newException(CatchableError, "orig") + except: + let e = getCurrentException() + echo "inner-typeless: ", e.msg + raise newException(CatchableError, "re-tl:" & e.msg) + finally: + echo "finally-typeless" + except CatchableError as outer: + echo "outer-typeless: ", outer.msg + +# try/finally without an except: the body's exception must still propagate +# after the finally runs. +block no_catch_finally: + try: + try: + raise newException(CatchableError, "prop") + finally: + echo "no-catch-finally" + except CatchableError as e: + echo "caught-propagated: ", e.msg diff --git a/tests/exception/tcpp_imported_exc.nim b/tests/exception/tcpp_imported_exc.nim index 0c7846956b..8ee008bd96 100644 --- a/tests/exception/tcpp_imported_exc.nim +++ b/tests/exception/tcpp_imported_exc.nim @@ -1,5 +1,5 @@ discard """ -matrix: "--mm:refc" +matrix: "--mm:refc; --mm:orc" targets: "cpp" output: ''' caught as std::exception