Skip to content

fixes finally being skipped when except T as e re-raises (cpp backend)#25775

Open
puffball1567 wants to merge 1 commit intonim-lang:develfrom
puffball1567:fix-handler-raise-finally-v22
Open

fixes finally being skipped when except T as e re-raises (cpp backend)#25775
puffball1567 wants to merge 1 commit intonim-lang:develfrom
puffball1567:fix-handler-raise-finally-v22

Conversation

@puffball1567
Copy link
Copy Markdown

@puffball1567 puffball1567 commented Apr 28, 2026

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:

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.

@puffball1567 puffball1567 force-pushed the fix-handler-raise-finally-v22 branch 2 times, most recently from 5d72b24 to f0edfc3 Compare April 28, 2026 11:14
@puffball1567 puffball1567 changed the base branch from version-2-2 to devel April 28, 2026 11:14
[backport]

When a Nim `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`:

  try:
    try:
      raise newException(CatchableError, "orig")
    except CatchableError as e:
      raise newException(CatchableError, "re:" & e.msg)
    finally:
      echo "finally"   # never runs on cpp backend (arc/orc)
  except CatchableError as outer:
    echo "outer: ", outer.msg

Expected output: `finally / outer: re:orig`.
Actual output on `nim cpp --mm:arc` (and `--mm:orc`): just
`outer: re:orig` — the `finally` line is missing.

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 dropped
  entirely.

  The bug is specific to memory managers that use destructor injection
  (arc/orc).  Under refc, no destructor wrapper is injected, so the
  topmost entry is the user's try and the existing check fires
  correctly.

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 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.

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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant