From f4e8e04cd06dc32021d324534b6a55600ee77748 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). --- 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 35102d9e6b..2d1a42cd86 100644 --- a/compiler/sempass2.nim +++ b/compiler/sempass2.nim @@ -1345,6 +1345,7 @@ type PragmaBlockContext = object oldLocked: int enforcedGcSafety, enforceNoSideEffects: bool + oldInEnforcedGcSafe, oldInEnforcedNoSideEffects: bool oldExc, oldTags, oldForbids: int exc, tags, forbids: PNode excSource, tagsSource, forbidsSource: PNode @@ -1354,6 +1355,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) @@ -1362,8 +1365,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()