From bb58632f3e7317503cf22aa39490be038802b041 Mon Sep 17 00:00:00 2001 From: Araq Date: Wed, 8 Jul 2026 11:34:41 +0200 Subject: [PATCH] do not forget about early expansions of templates and macros --- compiler/ast2nif.nim | 20 +++++++++++++++++++- compiler/modulegraphs.nim | 5 +++++ compiler/pipelines.nim | 5 ++++- compiler/sem.nim | 6 ++++-- compiler/semdata.nim | 8 +++++++- compiler/semexprs.nim | 6 ++++-- 6 files changed, 43 insertions(+), 7 deletions(-) diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index 19870441d3..f8a37b9625 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -894,6 +894,7 @@ var reexpModTag = registerTag("reexpmod") var offerTag = registerTag("offer") var typeOfferTag = registerTag("toffer") var modulesrcTag = registerTag("modulesrc") +var expansionTag = registerTag("expansion") # `(unusedid )` — the module's first FREE itemId after the frontend # (`.s.bif`) or the lower stage (`.t.bif`). The backend seeds its per-module # sym/type counters here so freshly-minted backend ids (closure envs, RTTI @@ -934,6 +935,7 @@ proc registerNifAstTags*() = offerTag = registerTag("offer") typeOfferTag = registerTag("toffer") modulesrcTag = registerTag("modulesrc") + expansionTag = registerTag("expansion") proc writeNode(w: var Writer; dest: var IcBuilder; n: PNode; forAst = false) = if n == nil: @@ -1559,7 +1561,8 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode; genericParamsCount: int]] = @[]; typeOffers: seq[tuple[generic: PSym; inst: PType]] = @[]; resolvedImportDeps: seq[FileIndex] = @[]; - firstUnusedId: int32 = 0) = + firstUnusedId: int32 = 0; + expansions: seq[(PSym, TLineInfo)] = @[]) = var w = Writer(infos: newLineInfoWriter(config), currentModule: thisModule) w.deps = newIcBuilder(64) var content = newIcBuilder(300) @@ -1637,6 +1640,17 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode; w.deps.addStrLit toFullPath(config, FileIndex(thisModule)) w.deps.addParRi + # Template/macro expansions leave no trace in the sem'checked AST, so record + # each as `(expansion )`: a `Symbol` use of the expanded + # routine carrying the ORIGINAL call-site line info. The loader skips the tag + # (processTopLevel), but `idetools` scans every `Symbol` token in the buffer, + # so this restores "find usages / goto-def" for templates and macros. + for (sym, info) in expansions: + if sym == nil: continue + w.deps.addParLe expansionTag, NoLineInfo + w.deps.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), trLineInfo(w, info) + w.deps.addParRi + # Generic TYPE-instance OFFERS: the `tyGenericInst` types this module created # (e.g. `HashArray[8192, Gwei]`). Non-IC keeps ONE such instance in the global # `typeInstCache`, so a structural bound computed at the first instantiation @@ -3262,6 +3276,10 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag] # self-identification record for the standalone include-graph scanner; # not needed by the loader, just skip past it. skip cur + elif tagIs(cur, "expansion"): + # template/macro expansion usage record for tooling (`idetools` scans it + # as a `Symbol` use); the loader itself needs nothing from it. + skip cur elif tagIs(cur, "implementation"): cont = false elif LoadFullAst in flags or tagIs(cur, toNifTag(nkLetSection)) or diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim index 78a5f42ab3..f9cd3b6f37 100644 --- a/compiler/modulegraphs.nim +++ b/compiler/modulegraphs.nim @@ -177,6 +177,11 @@ type procGlobals*: seq[PNode] nifReplayActions*: Table[int32, seq[PNode]] # module position -> replay actions for NIF + nifExpansions*: Table[int32, seq[(PSym, TLineInfo)]] + # module position -> (template/macro sym, call-site info) for every expansion + # in that module. Templates/macros leave no trace in the sem'checked AST, so + # this side-channel (written into the `.bif`, see ast2nif) is what lets + # `nim track --usages`/`--def` find them. Populated by `rememberExpansion`. cachedMods: IntSet hookClosure: IntSet # modules whose serialized hooks were already registered diff --git a/compiler/pipelines.nim b/compiler/pipelines.nim index aed7944b1c..894e7cc49d 100644 --- a/compiler/pipelines.nim +++ b/compiler/pipelines.nim @@ -317,9 +317,12 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator # the backend seeds its id minting ABOVE this so closure envs / RTTI hooks # never share a `toId` with a frontend sym/type. See ast2nif `(unusedid)`. let firstUnusedId = max(idgen.symId, idgen.typeId) + var expansions: seq[(PSym, TLineInfo)] = @[] + discard graph.nifExpansions.take(module.position.int32, expansions) writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog, replayActions, implDeps, reexportedModuleSyms(graph, module), - genericOffers, typeOffers, resolvedImportDeps, firstUnusedId) + genericOffers, typeOffers, resolvedImportDeps, firstUnusedId, + expansions) # The module's REAL direct imports (incl. macro-generated) for `nim ic`'s # graph re-derivation; see ast2nif.writeSemDeps / semdata.addImportFileDep. var semDepPaths: seq[string] = @[] diff --git a/compiler/sem.nim b/compiler/sem.nim index 7f777a7add..ab6542f9d6 100644 --- a/compiler/sem.nim +++ b/compiler/sem.nim @@ -576,10 +576,12 @@ const proc semMacroExpr(c: PContext, n, nOrig: PNode, sym: PSym, flags: TExprFlags = {}; expectedType: PType = nil): PNode = - rememberExpansion(c, nOrig.info, sym) + let info = getCallLineInfo(n) + # the callee identifier's position is the usage site tooling expects (matches + # `markUsed` below), not the whole-call `nOrig.info`. + rememberExpansion(c, info, sym) pushInfoContext(c.config, nOrig.info, sym.detailedInfo) - let info = getCallLineInfo(n) markUsed(c, info, sym) onUse(info, sym) if sym == c.p.owner: diff --git a/compiler/semdata.nim b/compiler/semdata.nim index 5cdcc18be4..21421fbdb8 100644 --- a/compiler/semdata.nim +++ b/compiler/semdata.nim @@ -668,7 +668,13 @@ proc rememberExpansion*(c: PContext; info: TLineInfo; expandedSym: PSym) = ## ("find all usages of this template" would not work). We need special ## logic to remember macro/template expansions. This is done here and ## delegated to the "NIF" file mechanism. - discard "XXX To implement" + ## + ## We only bother when a NIF file is actually going to be written (IC / `nim m`, + ## `--compress`, or a running suggestion engine); a plain `nim c` throws the + ## record away, so recording it would be pure overhead. + if info.fileIndex == InvalidFileIdx: return + if c.config.cmd == cmdM or optCompress in c.config.globalOptions or c.config.ideActive: + c.graph.nifExpansions.mgetOrPut(c.module.position.int32, @[]).add (expandedSym, info) const errVarForOutParamNeededX = "for a 'var' type a variable needs to be passed; but '$1' is immutable" diff --git a/compiler/semexprs.nim b/compiler/semexprs.nim index ef3c58afce..07564aa171 100644 --- a/compiler/semexprs.nim +++ b/compiler/semexprs.nim @@ -26,13 +26,15 @@ const proc semTemplateExpr(c: PContext, n: PNode, s: PSym, flags: TExprFlags = {}; expectedType: PType = nil): PNode = - rememberExpansion(c, n.info, s) + let info = getCallLineInfo(n) + # `info` (the callee identifier's position, not the whole call node) is what + # tooling wants to see as the usage site — matches `markUsed` below. + rememberExpansion(c, info, s) # IC: this expands `s`'s body into the current module's sem, so the module # depends on that body — record a NeedsImpl (strong) edge to `s`'s module. # The iface cookie hashes only signatures now, so a template body edit moves # only the impl cookie, and just the modules that expanded it re-sem. recordIcImplDep(c.graph, s) - let info = getCallLineInfo(n) markUsed(c, info, s) onUse(info, s) # Note: This is n.info on purpose. It prevents template from creating an info