From 715173f4bed8775a8094a6279ec76f317e7bbfc0 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 9 Sep 2026 09:58:13 +0200 Subject: [PATCH] fixes #26041 (#26185) --- compiler/closureiters.nim | 88 ++++++++++++++++++++++++++++++++----- tests/iter/t26041.nim | 92 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 tests/iter/t26041.nim diff --git a/compiler/closureiters.nim b/compiler/closureiters.nim index d71b62d555..f62a1a6742 100644 --- a/compiler/closureiters.nim +++ b/compiler/closureiters.nim @@ -139,7 +139,8 @@ import ast, msgs, idents, - renderer, magicsys, lowerings, lambdalifting, modulegraphs, lineinfos, trees + renderer, magicsys, lowerings, lambdalifting, modulegraphs, lineinfos, trees, + types import std/tables @@ -1422,20 +1423,82 @@ proc isClosureIterLocal(c: Ctx, s: PSym): bool = s.kind in {skResult, skVar, skLet, skForVar, skTemp} and sfGlobal notin s.flags and s.owner == c.fn and s != c.externExcSym +proc outlivesSuspension(c: Ctx, s: PSym): bool = + ## Is `s`'s lifetime observable after the iterator has suspended? Only if it + ## owns memory: there is a `=destroy` that belongs at the end of its scope, + ## and under refc a stack slot the collector has to keep seeing. Everything + ## else is unobservable once its last read is gone, which is what lets + ## #23787 keep it on the stack. + s.typ != nil and (hasDestructor(s.typ) or containsGarbageCollectedRef(s.typ)) + +proc extendLifetimes(c: var Ctx, n: PNode, live: var seq[PSym]) = + ## We claim ARC/ORC destroy by scope, not by last usage. `detectCapturedVars` + ## can only see the *states*, so all it can offer is the last-usage criterion, + ## and the splitting transformation makes every state its own scope: a local + ## whose scope outlives the `yield` would be destroyed *at* the `yield` + ## (bug #26041). Scopes only exist before the split, so the promise has to be + ## kept here, on the unsplit body, by lifting whatever is still alive when a + ## `yield` is reached. The scopes below mirror the ones `injectdestructors` + ## opens, since that pass decides where the `=destroy` calls actually land. + template inNewScope(body: PNode) = + let oldLen = live.len + extendLifetimes(c, body, live) + live.setLen oldLen + + case n.kind + of nkSkip: + discard + of nkYieldStmt: + for s in live: + if c.outlivesSuspension(s): c.captureVar(s) + of nkAddr, nkHiddenAddr: + # bug #25596; the very fact that the address is taken can make the local + # outlive its last read, and we cannot see where the pointer ends up. + let s = getRoot(n) + if s != nil and c.isClosureIterLocal(s): c.captureVar(s) + for i in 0..