This commit is contained in:
Araq
2026-06-08 07:06:11 +02:00
parent a5bc2490d2
commit 040f2dbfce
3 changed files with 49 additions and 14 deletions

View File

@@ -248,17 +248,22 @@ proc newSymG*(kind: TSymKind, n: PNode, c: PContext): PSym =
localError(c.config, n.info, "cannot use symbol of kind '$1' as a '$2'" %
[result.kind.toHumanStr, kind.toHumanStr])
# bug #25693: a local declared inside a template/macro operand (recorded in
# `shadowDiscardedDefs`) may be realized in a real scope at most once. A
# second realization - e.g. a `typed` argument captured by a `{.dirty.}`
# template and emitted by two `t()` calls - produces two definitions of the
# same symbol, which is a redefinition just like `var x = 0; var x = 1`.
# `shadowDiscardedDefs`) can be captured by a `{.dirty.}` template and
# re-emitted as a definition more than once. The first emission keeps the
# original symbol (so a leaked dirty-template name still resolves); every
# later emission gets a fresh copy, so distinct emissions don't share one
# symbol - which the destructor/liveness analysis would otherwise miscompile.
# Unlike a plain redefinition check this is control-flow agnostic, so the
# common "emit a `typed` body in several mutually-exclusive branches" pattern
# keeps working.
if kind in {skVar, skLet, skForVar} and sfGenSym notin result.flags and
result.id in c.shadowDiscardedDefs:
if containsOrIncl(c.realizedDefs, result.id) and
injectedSymbolRedefinition notin c.config.legacyFeatures:
localError(c.config, n.info,
"redefinition of '$1'; it is injected by a template more than once" %
result.name.s)
if containsOrIncl(c.realizedDefs, result.id):
let fresh = copySym(result, c.idgen)
fresh.ast = result.ast
put(c.p, result, fresh)
c.hasSymRedefs = true
result = fresh
when false:
if sfGenSym in result.flags and result.kind notin {skTemplate, skMacro, skParam}:
# declarative context, so produce a fresh gensym:

View File

@@ -191,11 +191,16 @@ type
semAsgnOpr*: proc (c: PContext; n: PNode; k: TNodeKind): PNode {.nimcall.}
shadowDiscardedDefs*: IntSet
# ids of local symbols that were declared inside a template/macro operand's
# shadow scope and then discarded; such a symbol may be realized in a real
# scope at most once. See bug #25693 and `rememberShadowDefs`.
# shadow scope and then discarded; re-emitting such a symbol as a
# definition gives a fresh copy so distinct emissions don't share a symbol.
# See bug #25693 and `rememberShadowDefs`.
realizedDefs*: IntSet
# ids from `shadowDiscardedDefs` that have already been realized as a
# definition; a second realization is a redefinition.
# ids from `shadowDiscardedDefs` already realized once; the first emission
# keeps the original symbol (so leaked dirty-template names still resolve),
# later emissions get a fresh copy.
hasSymRedefs*: bool
# set once a redefinition mapping has been installed; makes `getGenSym`
# consult the proc-con mapping for non-gensym symbols too.
TBorrowState* = enum
bsNone, bsReturnNotMatch, bsNoDistinct, bsGeneric, bsNotSupported, bsMatch
@@ -288,7 +293,10 @@ proc get*(p: PProcCon; key: PSym): PSym =
result = p.mapping.getOrDefault(key.itemId)
proc getGenSym*(c: PContext; s: PSym): PSym =
if sfGenSym notin s.flags: return s
# `c.hasSymRedefs` additionally routes ordinary (non-gensym) symbols through
# the mapping so a re-emitted definition can redirect them to its fresh copy,
# see bug #25693 and `newSymG`.
if sfGenSym notin s.flags and not c.hasSymRedefs: return s
var it = c.p
while it != nil:
result = get(it, s)

View File

@@ -1,5 +1,6 @@
discard """
output: '''ok
ok
ok'''
"""
@@ -33,6 +34,27 @@ block: # scalar `untyped` parameter
block: t()
echo "ok"
block: # `typed` parameter captured and re-emitted: each emission gets its own
# symbols, otherwise the destructor/liveness pass miscompiles the shared
# local `a` and the program crashes at runtime
template g(b: typed) {.dirty.} =
template t: untyped = b
let q = "a"
g:
var a: seq[int]
try:
f(a, q & "1")
except CatchableError:
discard
try:
f(a, q & "1")
except CatchableError:
discard
block: t()
block: t()
echo "ok"
block: # `varargs[untyped]` parameter takes the same pristine-AST path
template g(b: varargs[untyped]) {.dirty.} =
template t: untyped = b