fix: {.cast(uncheckedAssign).} ineffective across yield in closure iterators (#25916)

closureiters.nim splits a stmt list at yield points, moving post-yield
code into a new state body. When that stmt list was inside a pragma
block like `{.cast(uncheckedAssign).}`, the new state's body was created
as a bare nkStmtList without the wrapper.

Fix: track the enclosing pragma block in the transform context, and wrap
newly-created state bodies in a copy of it when the split occurs inside
one. Added an explicit `nkPragmaBlock` case to
`transformClosureIteratorBody` that saves/restores `ctx.enclosingPragma`
around its body.
This commit is contained in:
Ryan McConnell
2026-06-16 13:15:16 -04:00
committed by GitHub
parent f5d9e7a207
commit f8e470eb57
2 changed files with 69 additions and 2 deletions

View File

@@ -167,6 +167,8 @@ type
curExcSym: PSym # Current exception
externExcSym: PSym # Extern exception: what would getCurrentException() return outside of closure iter
enclosingPragmas: seq[PNode] # stack of pragma blocks wrapping stmtlist
states: seq[State] # The resulting states. Label is int literal.
finallyPathStack: seq[FinallyTarget] # Stack of split blocks, whiles and finallies
stateLoopLabel: PSym # Label to break on, when jumping between states.
@@ -983,9 +985,14 @@ proc transformClosureIteratorBody(ctx: var Ctx, n: PNode, gotoOut: PNode): PNode
for j in i + 1..<n.len:
s.add(n[j])
var body = s
for pragma in ctx.enclosingPragmas:
body = newTreeI(nkPragmaBlock, n[i + 1].info,
pragma[0].copyTree, body)
n.sons.setLen(i + 1)
discard ctx.newState(s, true, label)
if ctx.transformClosureIteratorBody(s, gotoOut) != s:
discard ctx.newState(body, true, label)
if ctx.transformClosureIteratorBody(body, gotoOut) != body:
internalError(ctx.g.config, "transformClosureIteratorBody != s")
break
else:
@@ -1123,6 +1130,14 @@ proc transformClosureIteratorBody(ctx: var Ctx, n: PNode, gotoOut: PNode): PNode
finallyBody = ctx.transformClosureIteratorBody(finallyBody, finallyExit)
dec ctx.curFinallyLevel
of nkPragmaBlock:
# Propagate the pragma blocks so that blocks like {.cast(uncheckedAssign).}
# remain effective
ctx.enclosingPragmas.add(n)
n[1] = ctx.transformClosureIteratorBody(n[1], gotoOut)
discard ctx.enclosingPragmas.pop()
result = n
of nkGotoState, nkForStmt:
internalError(ctx.g.config, "closure iter " & $n.kind)

View File

@@ -0,0 +1,52 @@
discard """
action: "run"
"""
# Test: {.cast(uncheckedAssign).} must suppress FieldDiscriminantCheck
# when a yield inside the pragma block causes the closure-iterator
# transform to split the body. The discriminant assignment lands in
# the post-yield state and must inherit the wrapper.
type
MyKind = enum mkOne, mkTwo
MyVariant = object
case kind: MyKind
of mkOne:
x: int
of mkTwo:
y: float
iterator iterUncheckedYield(dest: var MyVariant): int {.closure.} =
{.cast(uncheckedAssign).}:
yield 1
dest.kind = mkTwo
block:
var v = MyVariant(kind: mkOne, x: 42)
var count = 0
for x in iterUncheckedYield(v):
if count == 0:
doAssert x == 1
inc count
# don't break — continue to advance past the yield,
# which runs the discriminant assignment
else:
break
doAssert v.kind == mkTwo
iterator iterNestedPragma(dest: var MyVariant): int {.closure.} =
{.cast(uncheckedAssign).}:
{.cast(gcsafe).}:
yield 1
dest.kind = mkTwo
block:
var v = MyVariant(kind: mkOne, x: 42)
var count = 0
for x in iterNestedPragma(v):
if count == 0:
doAssert x == 1
inc count
else:
break
doAssert v.kind == mkTwo