diff --git a/compiler/sem.nim b/compiler/sem.nim index b89f9a1948..81d1902ba2 100644 --- a/compiler/sem.nim +++ b/compiler/sem.nim @@ -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: diff --git a/compiler/semdata.nim b/compiler/semdata.nim index 4424ea540e..8e3156db97 100644 --- a/compiler/semdata.nim +++ b/compiler/semdata.nim @@ -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) diff --git a/tests/template/toverload_over_untyped.nim b/tests/template/toverload_over_untyped.nim index 56db22e082..ff76663915 100644 --- a/tests/template/toverload_over_untyped.nim +++ b/tests/template/toverload_over_untyped.nim @@ -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