mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-01 04:59:05 +00:00
Fixes #23615 ## Root cause The leak does not require async at all -- this minimal closure iterator leaks the exception and its stacktrace seq under ARC/ORC: ```nim iterator it(): int {.closure.} = try: yield 1 # try spanning a yield => closureiters transform raise newException(ValueError, "x") except ValueError: # typed except => generated `of` check discard yield 2 ``` A bare `except:` does not leak; a *typed* `except` does: 1. `collectExceptState` in `compiler/closureiters.nim` generates the except-branch type check as `of(getCurrentException(), T)`, using the raw generic magic sym from `getSysMagic("of", mOf)`. 2. `injectdestructors` skips call arguments whose *formal* parameter type is `isCompileTimeOnly`, and the raw generic `of` sym's formal params are `tyGenericParam` so both arguments of the generated `of` call are never processed. 3. `getCurrentException()` increfs `currException` via `=copy` into its result. Since the arc pass never wraps that owned temp in a destroy (`--expandArc` shows the condition left untouched, while a user-written `if f() of ValueError` in the same iterator gets a `:tmpD` + `=destroy`), the caught exception's refcount stays +1 forever. Every `try: await x() except SomeError` in async code has this shape, so each caught async exception leaked once. ## Fix The state-machine wrapper already stores the active exception in the `:curExc` env field before jumping to the except landing state, and `currException == :curExc` on every path into that state. The generated condition now references the env field via `ctx.newCurExcAccess()` instead of calling `getCurrentException()` again -- no ownership transfer, no temp to destroy, one fewer runtime call. Note: the underlying `injectdestructors` behavior (skipping args of calls whose formal params are raw `tyGenericParam`, e.g. from `getSysMagic`) is a separate latent gap that could affect other compiler-generated code; it is intentionally left untouched here. ## Valgrind, before and after Exact code and command from the issue, on Linux (Valgrind 3.19): ``` nim c -d:danger --mm:orc --debugger:native --threads:off -d:useMalloc bug.nim valgrind --leak-check=full --show-leak-kinds=all ./bug ``` Before (devel): ``` ==14663== HEAP SUMMARY: ==14663== in use at exit: 136 bytes in 2 blocks ==14663== total heap usage: 22 allocs, 20 frees, 116,466 bytes allocated ==14663== ==14663== 56 bytes in 1 blocks are indirectly lost in loss record 1 of 2 ==14663== at 0x488A1C4: realloc (vg_replace_malloc.c:1437) ==14663== by 0x10C663: prepareSeqAddUninit (seqs_v2.nim:212) ==14663== by 0x10CF6F: raiseExceptionEx (excpt.nim:538) ==14663== by 0x11661F: amain::amainX20X28AsyncX29_(Future<void>) (bug.nim:10) ==14663== ... ==14663== ==14663== 136 (80 direct, 56 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2 ==14663== at 0x48850C8: malloc (vg_replace_malloc.c:381) ==14663== by 0x10C8E3: nimNewObj (arc.nim:122) ==14663== by 0x115403: err::errX20X28AsyncX29_(Future<void>) (asyncmacro.nim:274) ==14663== by 0x116027: err::errNimAsyncContinue(Future<void>, ClosureIt<void>) (asyncmacro.nim:44) ==14663== by 0x1163D3: bug::err (bug.nim:3) ==14663== ... ==14663== ==14663== LEAK SUMMARY: ==14663== definitely lost: 80 bytes in 1 blocks ==14663== indirectly lost: 56 bytes in 1 blocks ==14663== possibly lost: 0 bytes in 0 blocks ==14663== still reachable: 0 bytes in 0 blocks ==14663== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) ``` After (this PR): ``` ==14675== HEAP SUMMARY: ==14675== in use at exit: 0 bytes in 0 blocks ==14675== total heap usage: 22 allocs, 22 frees, 116,466 bytes allocated ==14675== ==14675== All heap blocks were freed -- no leaks are possible ==14675== ==14675== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ``` ## Testing - New `tests/async/t23615.nim` (modeled on `t23212.nim`: `valgrind: true` + alloc-stats assertion) covers both the pure closure-iterator form and the async form from the issue, with the caught exception looped 50x so the leak blows well past the slack threshold. It passes with this PR and fails against devel. - Testament categories `async`, `arc`, `iter`, `exception` all pass with the patched compiler (323 tests). - Behavior is unchanged on a sanity program covering multi-branch dispatch, `as e` binding, nested try, and re-raise across yields: output is byte-identical to devel; the patched build just frees 2 more blocks per caught exception.
Nim Compiler
- This directory contains the Nim compiler written in Nim.
- Note that this code has been translated from a bootstrapping version written in Pascal.
- So the code is not a poster child of good Nim code.
See Internals of the Nim Compiler for more information.