From 445d46dd9eb6cc20e87994a86e2a342a712a0df7 Mon Sep 17 00:00:00 2001 From: Corey Leavitt Date: Sun, 9 Aug 2026 03:24:02 -0600 Subject: [PATCH] fixes #26092; restore enclosing cast block state when a nested cast block exits (#26093) `unapplyBlockContext` reset `inEnforcedGcSafe` and `inEnforcedNoSideEffects` to false whenever a `{.cast(gcsafe).}` or `{.cast(noSideEffect).}` block ended. When such a block is nested inside another block of the same cast, the inner block's exit switched enforcement back on for the rest of the enclosing block, so statements lexically inside the outer cast were rejected. The fix saves both flags in `PragmaBlockContext` when the block context is created and restores the saved values on exit. That matches how every other piece of block state there (`locked`, `exc`, `tags`, `forbids`) is already handled; these two bools were the only ones reset to a constant instead of restored. No change for non-nested blocks: entering from the non-enforced state saves false, so exit still clears the flag. A statement after the outer block is still rejected as before. Test covers nested `cast(gcsafe)` and nested `cast(noSideEffect)`. `tests/effects` passes unchanged (49/49, same as stock). (cherry picked from commit f4e8e04cd06dc32021d324534b6a55600ee77748) --- compiler/sempass2.nim | 8 +++++-- tests/effects/tnestedcastblocks.nim | 33 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tests/effects/tnestedcastblocks.nim diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim index 7586e82c75..c772787331 100644 --- a/compiler/sempass2.nim +++ b/compiler/sempass2.nim @@ -1281,6 +1281,7 @@ type PragmaBlockContext = object oldLocked: int enforcedGcSafety, enforceNoSideEffects: bool + oldInEnforcedGcSafe, oldInEnforcedNoSideEffects: bool oldExc, oldTags, oldForbids: int exc, tags, forbids: PNode excSource, tagsSource, forbidsSource: PNode @@ -1290,6 +1291,8 @@ proc createBlockContext(tracked: PEffects): PragmaBlockContext = if tracked.forbids != nil: oldForbidsLen = tracked.forbids.len result = PragmaBlockContext(oldLocked: tracked.locked.len, enforcedGcSafety: false, enforceNoSideEffects: false, + oldInEnforcedGcSafe: tracked.inEnforcedGcSafe, + oldInEnforcedNoSideEffects: tracked.inEnforcedNoSideEffects, oldExc: tracked.exc.len, oldTags: tracked.tags.len, oldForbids: oldForbidsLen) @@ -1298,8 +1301,9 @@ proc applyBlockContext(tracked: PEffects, bc: PragmaBlockContext) = if bc.enforceNoSideEffects: tracked.inEnforcedNoSideEffects = true proc unapplyBlockContext(tracked: PEffects; bc: PragmaBlockContext) = - if bc.enforcedGcSafety: tracked.inEnforcedGcSafe = false - if bc.enforceNoSideEffects: tracked.inEnforcedNoSideEffects = false + if bc.enforcedGcSafety: tracked.inEnforcedGcSafe = bc.oldInEnforcedGcSafe + if bc.enforceNoSideEffects: + tracked.inEnforcedNoSideEffects = bc.oldInEnforcedNoSideEffects setLen(tracked.locked, bc.oldLocked) if bc.exc != nil: # beware that 'raises: []' is very different from not saying diff --git a/tests/effects/tnestedcastblocks.nim b/tests/effects/tnestedcastblocks.nim new file mode 100644 index 0000000000..67fd87b481 --- /dev/null +++ b/tests/effects/tnestedcastblocks.nim @@ -0,0 +1,33 @@ +discard """ + output: '''4 +3''' +""" +# bug #26092: exiting a nested cast block used to cancel the enclosing +# block's cast for the statements after it. + +type Obj = ref object + x: int +let g: Obj = Obj(x: 1) +var fp: proc(): int {.nimcall.} = proc(): int {.nimcall.} = 2 + +proc consumer(): int {.gcsafe.} = + {.cast(gcsafe).}: + let a = g.x + var b: int + {.cast(gcsafe).}: + b = g.x + let c = fp() + a + b + c +echo consumer() + +var counter = 0 +proc bump() = inc counter + +proc pure(): int {.noSideEffect.} = + {.cast(noSideEffect).}: + bump() + {.cast(noSideEffect).}: + bump() + bump() + counter +echo pure()