mirror of
https://github.com/nim-lang/Nim.git
synced 2026-05-25 06:18:16 +00:00
## Bug
When an `except T as e:` handler in the cpp backend raises a new
exception, the enclosing `finally` block is silently dropped under
`--mm:arc` and `--mm:orc`:
```nim
proc main() =
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
main()
```
Expected output:
```
inner: orig
finally
outer: re:orig
```
Actual output on `nim cpp --mm:arc` (and `--mm:orc`):
```
inner: orig
outer: re:orig
```
The `finally` line is missing. The bug is specific to memory managers
that use destructor injection (arc/orc); under `--mm:refc` the original
code path works correctly because no destructor wrapper is injected.
## Root cause
When the body of `except T as e:` is processed under ARC/ORC, the
destructor injection pass injects a compiler-generated `nkHiddenTryStmt`
wrapper around the handler body to call `=destroy` on `e` when it goes
out of scope. That wrapper sits at the top of `p.nestedTryStmts` with
`inExcept = false`.
`finallyActions` (which inlines the user-finally body before a raise
propagates) only inspected the topmost entry of `nestedTryStmts`.
Because the wrapper has `inExcept = false`, the check short-circuited
and the user's finally was never inlined.
After the raise, C++'s rule that sibling catch clauses do not catch each
other's throws means the surrounding `catch(...)/finally` emitted by
`genTryCpp` never runs either, so the user's finally is silently
dropped.
## Fix
- Add an `isHidden` flag to `nestedTryStmts` entries, set to `t.kind ==
nkHiddenTryStmt` so compiler-injected try wrappers can be distinguished
from user-written ones.
- In `finallyActions`, walk past `isHidden` wrappers but stop at the
first user try. If that user try is in its except branch with a finally,
inline the finally body before the raise; otherwise leave the raise
untouched (the raise will be caught by that user try's own except
branches and the inner finally will run via normal unwinding, which is
what already happens correctly under refc).
Walking past wrappers fixes the `as e` case under arc/orc. Stopping at
user trys preserves the existing correct behaviour for nested
try/except/finally constructs (e.g. `tests/exception/tfinally.nim`'s
`nested_finally`), which would otherwise see the outer finally inlined
too eagerly when an inner raise is processed.
## Tests
Adds `tests/exception/tcpp_handler_raise_finally.nim` covering:
- `except T as e:` re-raise + outer finally
- typeless `except:` re-raise + outer finally
- try/finally without except (exception propagation through finally)
The test runs on `--mm:arc`, `--mm:orc`, and `--mm:refc`.
Locally verified on both `devel` and `version-2-2`:
- `tests/exception/` — 42 PASS, 0 FAIL, 3 SKIP
- `tests/destructor/` — all PASS
- `tests/cpp/` — all PASS (single unrelated failure: `tasync_cpp.nim`
needs the `jester` package)
- `megatest` — PASS for both `--mm:arc` and `--mm:refc`, including the
previously regressing `tfinally.nim`'s `nested_finally`
## Backport
Tagged `[backport]` in the commit message for inclusion in
`version-2-2`.
---------
Co-authored-by: puffball1567 <17452514+puffball1567@users.noreply.github.com>
62 lines
1.7 KiB
Nim
62 lines
1.7 KiB
Nim
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
|