From 88c23f8519e49a8a16fe93ea2e040c7b1f600b04 Mon Sep 17 00:00:00 2001 From: Araq Date: Thu, 18 Jun 2026 10:56:32 +0200 Subject: [PATCH] IC: big steps forward --- compiler/ast2nif.nim | 108 +++++++++++++++++++++++++++++++++----- compiler/lowerings.nim | 55 +++++++++++++++++++ compiler/modulegraphs.nim | 44 +++++++++++----- compiler/options.nim | 2 +- compiler/pipelines.nim | 63 ++++++++++++---------- compiler/semtypinst.nim | 6 ++- compiler/transf.nim | 16 +++++- 7 files changed, 238 insertions(+), 56 deletions(-) diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index ec35d34918..270f1c048e 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -26,6 +26,13 @@ import typekeys import ic / [enum2nif] const SysModuleSuffix* = "@sys" +const BackendLocalMarker* = "@bk" + ## Suffix marker for a PROCESS-LOCAL backend-minted entity (a closure `:env` + ## type/obj/field/hidden-param minted while the VM compiles a routine body to + ## run a macro). Such entities have no stable cross-process identity, so each + ## module that references one emits its OWN module-local def named + ## `…@bk` and the loader homes it to the reading module with + ## a `backendItemId` (disjoint from real ids). See transf.transformBody. ## Reserved module-suffix sentinel for module-less magic singleton types — the ## `nil` type is created via `newSysType` with the graph idgen, whose `module` ## can be `-1` (e.g. during VM const-eval before a real module is current), so @@ -189,6 +196,9 @@ type #writtenSyms: seq[PSym] # symbols written in this module, to be unloaded later writtenPackages: HashSet[string] depSuffixes: HashSet[string] # module suffixes already emitted as `(import ...)` deps + emittedBackendTypes: HashSet[int32] # backend-local type items already def'd this module + emittedBackendSyms: HashSet[int32] # backend-local sym items already def'd this module + proc isLocalSym(sym: PSym): bool {.inline.} = ## Every symbol is emitted as a *global* (module-suffixed) name so that its @@ -217,7 +227,17 @@ const proc toNifSymName(w: var Writer; sym: PSym): string = ## Generate NIF name for a symbol: local names are `ident.disamb`, ## global names are `ident.disamb.moduleSuffix` - assert not sym.itemId.isBackendMinted + if sym.itemId.isBackendMinted: + # Process-local backend sym (closure env field / hidden `:env` param minted + # during a VM transform): re-home to the current module with the `@bk` + # marker so each referencing module self-contains it. See transformBody. + result = sym.name.s + result.add '.' + result.addInt sym.disamb + result.add '.' + result.add modname(w.currentModule, w.infos.config) + result.add BackendLocalMarker + return result = sym.name.s if sym.kindImpl == skPackage: result.add PkgMarker @@ -239,6 +259,11 @@ proc globalName*(sym: PSym; config: ConfigRef): string = result.addInt sym.disamb result.add '.' result.add modname(sym.itemId.module, config) + # A loaded process-local backend sym keeps its `@bk` marker in the NIF name + # (the index/`c.syms` tables are keyed by it); mirror toNifSymName so name-based + # lookups via globalName don't miss (KeyError `:env.N.` without the marker). + if sym.itemId.isBackendMinted: + result.add BackendLocalMarker type ParsedSymName* = object @@ -312,9 +337,24 @@ proc writeLoc(w: var Writer; dest: var TokenBuf; loc: TLoc) = writeFlags(dest, loc.flags) # TLocFlags dest.addStrLit loc.snippet +proc nifTypeName(w: Writer; typ: PType): string = + ## NIF name of a type as written by THIS module. A process-local backend env + ## type is re-homed to the current module with the `@bk` marker (see + ## BackendLocalMarker); everything else uses the canonical `typeToNifSym`. + if typ.uniqueId.isBackendMinted: + result = "`t" + result.addInt ord(typ.kind) + result.add '.' + result.addInt typ.uniqueId.item + result.add '.' + result.add modname(w.currentModule, w.infos.config) + result.add BackendLocalMarker + else: + result = typeToNifSym(typ, w.infos.config) + proc writeTypeDef(w: var Writer; dest: var TokenBuf; typ: PType) = dest.buildTree tdefTag: - dest.addSymDef pool.syms.getOrIncl(typeToNifSym(typ, w.infos.config)), NoLineInfo + dest.addSymDef pool.syms.getOrIncl(nifTypeName(w, typ)), NoLineInfo dest.addDotToken # always private for the index generator #dest.addIdent toNifTag(typ.kind) @@ -352,6 +392,15 @@ proc writeTypeDef(w: var Writer; dest: var TokenBuf; typ: PType) = proc writeType(w: var Writer; dest: var TokenBuf; typ: PType) = if typ == nil: dest.addDotToken() + elif typ.uniqueId.isBackendMinted: + # Process-local closure env (see transf.transformBody): emit a MODULE-LOCAL + # `@bk` def the first time it is reached in this module, reference it after. + # Per-Writer dedup (NOT the shared `state`), since every referencing module + # must emit its own copy. + if not w.emittedBackendTypes.containsOrIncl(typ.uniqueId.item): + writeTypeDef(w, dest, typ) + else: + dest.addSymUse pool.syms.getOrIncl(nifTypeName(w, typ)), NoLineInfo elif typ.uniqueId.module == w.currentModule and typ.state == Complete: # Ownership for serialization is decided by `uniqueId`, not `itemId`: the NIF # name (`typeToNifSym`) and the loader (`createTypeStub`) both key off @@ -363,7 +412,7 @@ proc writeType(w: var Writer; dest: var TokenBuf; typ: PType) = typ.state = Sealed writeTypeDef(w, dest, typ) else: - dest.addSymUse pool.syms.getOrIncl(typeToNifSym(typ, w.infos.config)), NoLineInfo + dest.addSymUse pool.syms.getOrIncl(nifTypeName(w, typ)), NoLineInfo proc writeBool(dest: var TokenBuf; b: bool) = dest.buildTree (if b: "true" else: "false"): @@ -471,6 +520,13 @@ proc shouldWriteSymDef(w: var Writer; sym: PSym): bool {.inline.} = proc writeSym(w: var Writer; dest: var TokenBuf; sym: PSym) = if sym == nil: dest.addDotToken() + elif sym.itemId.isBackendMinted: + # Process-local backend sym (closure env field / hidden `:env` param): emit a + # MODULE-LOCAL `@bk` def the first time, reference it after. Per-Writer dedup. + if not w.emittedBackendSyms.containsOrIncl(sym.itemId.item): + writeSymDef(w, dest, sym) + else: + dest.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), NoLineInfo elif shouldWriteSymDef(w, sym): sym.state = Sealed writeSymDef(w, dest, sym) @@ -495,8 +551,16 @@ proc writeSymNode(w: var Writer; dest: var TokenBuf; n: PNode; sym: PSym) = var nodeTyp = n.typField if nodeTyp == nil and nfLazyType in n.flags: nodeTyp = sym.typImpl - if shouldWriteSymDef(w, sym): - sym.state = Sealed + # Backend-minted syms (process-local closure `:env` param/fields) are emitted + # as MODULE-LOCAL `@bk` defs the first time reached this module (per-Writer + # dedup shared with `writeSym`), regardless of module: their itemId.module is + # the systemModule of `vmTransfIdgen`, so `shouldWriteSymDef` (which gates on + # currentModule) would otherwise only ever emit a SymUse → dangling def. + let wantDef = + if sym.itemId.isBackendMinted: not w.emittedBackendSyms.containsOrIncl(sym.itemId.item) + else: shouldWriteSymDef(w, sym) + if wantDef: + if not sym.itemId.isBackendMinted: sym.state = Sealed if nodeTyp != n.sym.typImpl: dest.buildTree hiddenTypeTag, trLineInfo(w, n.info): writeType(w, dest, nodeTyp) @@ -1617,7 +1681,10 @@ proc tryCreateTypeStub(c: var DecodeContext; t: SymId): PType = let suffix = name.substr(i) if suffix == SysModuleSuffix: return reconstructSysType(c, name, k, itemVal) - let id = itemId(moduleId(c, suffix).int32, itemVal) + let isBk = suffix.endsWith(BackendLocalMarker) + let realSuffix = if isBk: suffix[0 ..< suffix.len - BackendLocalMarker.len] else: suffix + let modIdx = moduleId(c, realSuffix).int32 + let id = if isBk: backendItemId(modIdx, itemVal) else: itemId(modIdx, itemVal) let ii = addr c.mods[id.module.FileIndex].index let offs = ii[].getOrDefault(name) if offs.offset == 0: @@ -1644,7 +1711,10 @@ proc createTypeStub(c: var DecodeContext; t: SymId): PType = let suffix = name.substr(i) if suffix == SysModuleSuffix: return reconstructSysType(c, name, k, itemVal) - let id = itemId(moduleId(c, suffix).int32, itemVal) + let isBk = suffix.endsWith(BackendLocalMarker) + let realSuffix = if isBk: suffix[0 ..< suffix.len - BackendLocalMarker.len] else: suffix + let modIdx = moduleId(c, realSuffix).int32 + let id = if isBk: backendItemId(modIdx, itemVal) else: itemId(modIdx, itemVal) let ii = addr c.mods[id.module.FileIndex].index let offs = ii[].getOrDefault(name) if offs.offset == 0: @@ -1732,10 +1802,16 @@ proc loadSymStub(c: var DecodeContext; t: SymId; thisModule: string; # Global symbol - look up in index for lazy loading result = c.syms.getOrDefault(symAsStr)[0] if result == nil: - let module = moduleId(c, sn.module) + # A process-local backend sym (closure env field / `:env` param) is named + # `…@bk`: home it to that module with a backendItemId so it + # stays disjoint from the loader's real per-module id space (see toNifSymName). + let isBk = sn.module.endsWith(BackendLocalMarker) + let realMod = if isBk: sn.module[0 ..< sn.module.len - BackendLocalMarker.len] + else: sn.module + let module = moduleId(c, realMod) let val = addr c.mods[module].symCounter inc val[] - let id = itemId(module.int32, val[]) + let id = if isBk: backendItemId(module.int32, val[]) else: itemId(module.int32, val[]) let offs = c.getOffset(module, symAsStr) let (stubKind, stubName) = stubKindAndName(c.cache, sn.name) @@ -2122,11 +2198,14 @@ proc loadSymFromIndexEntry(c: var DecodeContext; module: FileIndex; if result == nil: let symAsStr = nifName let sn = parseSymName(symAsStr) - let symModule = moduleId(c, if sn.module.len > 0: sn.module else: thisModule) + let rawMod = if sn.module.len > 0: sn.module else: thisModule + let isBk = rawMod.endsWith(BackendLocalMarker) + let realMod = if isBk: rawMod[0 ..< rawMod.len - BackendLocalMarker.len] else: rawMod + let symModule = moduleId(c, realMod) let val = addr c.mods[symModule].symCounter inc val[] - let id = itemId(symModule.int32, val[]) + let id = if isBk: backendItemId(symModule.int32, val[]) else: itemId(symModule.int32, val[]) let (stubKind, stubName) = stubKindAndName(c.cache, sn.name) result = PSym(itemId: id, kindImpl: stubKind, name: stubName, disamb: sn.count.int32, state: Partial) c.syms[symAsStr] = (result, entry) @@ -2199,7 +2278,10 @@ proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: boo let sn = parseSymName(symAsStr) if sn.module.len == 0: return nil # Local symbols shouldn't be hooks - let module = moduleId(c, sn.module) + let isBk = sn.module.endsWith(BackendLocalMarker) + let realMod = if isBk: sn.module[0 ..< sn.module.len - BackendLocalMarker.len] + else: sn.module + let module = moduleId(c, realMod) # Look up the symbol in the module's index # Try both formats: with module suffix (e.g., "foo.0.modulename") and without (e.g., "foo.0.") # NIF spec allows local symbols to be stored without module suffix @@ -2215,7 +2297,7 @@ proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: boo # Create a stub symbol let val = addr c.mods[module].symCounter inc val[] - let id = itemId(int32(module), val[]) + let id = if isBk: backendItemId(int32(module), val[]) else: itemId(int32(module), val[]) result = PSym(itemId: id, kindImpl: skProc, name: c.cache.getIdent(sn.name), disamb: sn.count.int32, state: Partial) c.syms[symAsStr] = (result, offs) diff --git a/compiler/lowerings.nim b/compiler/lowerings.nim index 7c7756b0a9..c0a51fd619 100644 --- a/compiler/lowerings.nim +++ b/compiler/lowerings.nim @@ -210,7 +210,62 @@ proc lookupInRecord(n: PNode, id: ItemId): PSym = if matchesDerivedFieldId(n.sym.itemId, id): result = n.sym else: discard +proc lookupCapturedField(n: PNode, s: PSym): PSym = + ## Find an env field that `addField` would have produced for the captured + ## local `s`. Used as a fallback when the derived-itemId match fails because + ## `s` is a macro-generated gensym whose process-local id diverges from the + ## loaded env field's (see `addField`). `addField` always names a field + ## `s.name & $field.position`, so that pair uniquely identifies the field for a + ## local of this name without relying on the (unstable) item id. + result = nil + case n.kind + of nkRecList: + for i in 0..= 1 and n[0].kind == nkStrLit and - n[0].strVal in ["put", "inc", "add", "incl"]: - g.transitiveReplayActions.add n + # Record this transitively-loaded module so the sem driver applies its + # VM-level load effects (macro-cache replay + `{.compileTime.}` global init) + # exactly as for a direct import — see `pendingNifInit`. A throwaway module + # symbol (same shape as moduleFromNifFile's) gives the drain an idgen/info + # context; it is not registered, so a later direct import still loads fully. + if g.config.cmd == cmdM: + let m = PSym(kindImpl: skModule, itemId: itemId(int32(fileIdx), 0'i32), + name: getIdent(g.cache, splitFile(toFullPath(g.config, fileIdx)).name), + infoImpl: newLineInfo(fileIdx, 1, 1), positionImpl: int(fileIdx)) + setOwner(m, getPackage(g.config, g.cache, fileIdx)) + g.pendingNifInit.add (m, precomp.topLevel) # Rebuild generic TYPE- and PROC-instance offers across the WHOLE closure, # not just direct imports (`moduleFromNifFile`). An instance is frozen at # the FIRST module to create it (in a scope where its body's symbols @@ -1086,6 +1100,10 @@ when not defined(nimKochBootstrap): # walks the closure in nifbackend.loadModuleDependencies.) if g.config.cmd == cmdM: loadTransitiveHooks(g, result.deps) + # Record the directly-loaded module for the same VM-level load effects as its + # transitive deps (`pendingNifInit`). AFTER loadTransitiveHooks so the drain + # applies deps before the dependent (macro-cache order). + g.pendingNifInit.add (m, result.topLevel) proc configComplete*(g: ModuleGraph) = #rememberStartupConfig(g.startupPackedConfig, g.config) diff --git a/compiler/options.nim b/compiler/options.nim index 5879d85abe..02f840c07e 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -29,7 +29,7 @@ const nimEnableCovariance* = defined(nimEnableCovariance) - icFormatVersion* = "9" + icFormatVersion* = "10" ## Version of the IC cache format (the sem-NIF module layout written by ## ast2nif.nim plus the iface/impl/edges side files). Bump it whenever ## that layout changes: `commandIc` wipes a nimcache whose `ic.version` diff --git a/compiler/pipelines.nim b/compiler/pipelines.nim index e4c39757d6..f2cf63c8f5 100644 --- a/compiler/pipelines.nim +++ b/compiler/pipelines.nim @@ -352,6 +352,31 @@ proc initLoadedCompileTimeGlobals(graph: ModuleGraph; module: PSym; topLevel: PN sect.add s.ast setupCompileTimeVar(module, idgen, graph, sect) +proc finalizeLoadedModules(graph: ModuleGraph) = + ## Apply the VM-level load effects of every module just loaded from a NIF — + ## direct import OR dep-of-a-dep, both collected in `graph.pendingNifInit` by the + ## loader (modulegraphs.moduleFromNifFile / loadTransitiveHooks). This is the ONE + ## place that knows what loading a module does to global VM state, so a + ## transitively-reached module (which never passes through this proc's caller) + ## gets identical treatment. Modules are in dependency order (deps before + ## dependents), which is the correct macro-cache replay order. + ## 1. macro-cache replay: std/macrocache put/inc/add/incl recorded in the + ## module's top level (pragma replay actions are a backend concern, skipped). + ## 2. eager `{.compileTime.}` global init (see initLoadedCompileTimeGlobals). + ## To add a new per-load effect, extend this proc — do not add a parallel buffer. + if graph.pendingNifInit.len == 0: return + for (m, topLevel) in graph.pendingNifInit: + if topLevel == nil: continue + var replayList = newNodeI(nkStmtList, m.info) + for n in topLevel: + if n.kind == nkReplayAction and n.len >= 1 and n[0].kind == nkStrLit and + n[0].strVal in ["put", "inc", "add", "incl"]: + replayList.add n + if replayList.len > 0: + replayStateChanges(m, graph, replayList) + initLoadedCompileTimeGlobals(graph, m, topLevel) + graph.pendingNifInit.setLen 0 + proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymFlags; fromModule: PSym = nil): PSym = var flags = flags if fileIdx == graph.config.projectMainIdx2: flags.incl sfMainModule @@ -414,33 +439,12 @@ proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymF if sfSystemModule in flags: graph.systemModule = result partialInitModule(result, graph, fileIdx, AbsoluteFile(toFullPath(graph.config, fileIdx))) - # Replay the module's recorded state changes: macro-cache operations - # (std/macrocache puts/incs/adds/incls) plus a few pragmas. The loader - # parsed them into `precomp.topLevel` (mixed with other top-level nodes), - # so filter to the replay actions. A loaded module's `ast` is never - # rebuilt, so this used to be skipped (`result.ast == nil`) and a - # NIF-loaded module's macro cache was lost — e.g. nim-serialization's - # flavor registration became invisible to dependents (`DefaultFlavor: - # automatic serialization is not enabled`). - var replayList = newNodeI(nkStmtList, result.info) - for n in precomp.topLevel: - # Only macro-cache ops (put/inc/add/incl). The pragma replay actions - # (compile/link/passc/hint/...) are a backend/link concern handled by - # the nifc closure, and re-emitting a loaded module's hints/warnings on - # every import would be wrong — so they are deliberately skipped here. - if n.kind == nkReplayAction and n.len >= 1 and n[0].kind == nkStrLit and - n[0].strVal in ["put", "inc", "add", "incl"]: - replayList.add n - # Plus the macro-cache actions of the module's transitive import closure - # (collected by the moduleFromNifFile call above via loadTransitiveHooks), - # so a flavor/type registered in an indirectly-imported module is visible. - for n in graph.transitiveReplayActions: replayList.add n - graph.transitiveReplayActions.setLen 0 - if replayList.len > 0: - replayStateChanges(result, graph, replayList) - # Fill the VM slots of the module's `{.compileTime.}` globals now (sem - # would have, but a NIF-loaded module is never semchecked). - initLoadedCompileTimeGlobals(graph, result, precomp.topLevel) + # Apply the VM-level load effects of this module AND every dep it pulled in + # (moduleFromNifFile recorded them all in graph.pendingNifInit): macro-cache + # replay (else a NIF-loaded module's macro cache is lost — e.g. + # nim-serialization flavor registration) and eager `{.compileTime.}` global + # init. Uniform for direct and transitive deps — see finalizeLoadedModules. + finalizeLoadedModules(graph) return result # Return early, don't process from source let path = toFullPath(graph.config, fileIdx) let filename = AbsoluteFile path @@ -537,6 +541,11 @@ proc compilePipelineProject*(graph: ModuleGraph; projectFileIdx = InvalidFileIdx localError(graph.config, unknownLineInfo, "nim m requires precompiled NIF for system module (expected: " & nifPath & ")") return + # Apply system's (and its deps') load effects now: the main module is + # compiled from source and never re-enters the moduleFromNifFile drain for + # system, so without this its macro-cache / CT globals would wait until the + # first NIF import is processed. See finalizeLoadedModules. + finalizeLoadedModules(graph) discard graph.compilePipelineModule(projectFile, {sfMainModule}) else: graph.compilePipelineSystemModule() diff --git a/compiler/semtypinst.nim b/compiler/semtypinst.nim index 49b4f781d6..1823db0e2d 100644 --- a/compiler/semtypinst.nim +++ b/compiler/semtypinst.nim @@ -480,7 +480,11 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType = else: header = instCopyType(cl, t) - result = newType(tyGenericInst, cl.c.idgen, t.genericHead.owner, son = header.genericHead) + # The instantiating module owns the instance (and announces it as an offer): + # the generic body's module (`t.genericHead.owner`) has no business owning a + # type that references instantiation-site types — that is the IC parent->child + # heap leak the write-barrier surfaces. + result = newType(tyGenericInst, cl.c.idgen, cl.c.module, son = header.genericHead) result.flags = header.flags # be careful not to propagate unnecessary flags here (don't use rawAddSon) # ugh need another pass for deeply recursive generic types (e.g. PActor) diff --git a/compiler/transf.nim b/compiler/transf.nim index f131c862f0..6ee5aa7b92 100644 --- a/compiler/transf.nim +++ b/compiler/transf.nim @@ -1386,7 +1386,21 @@ proc transformBody*(g: ModuleGraph; idgen: IdGenerator; prc: PSym; flags: Transf result = getBody(g, prc) else: prc.transformedBody = newNode(nkEmpty) # protects from recursion - var c = openTransf(g, prc.getModule, "", idgen, flags) + # Lambda-lifting a routine body while the VM compiles it (to run a macro + # under `nim ic`) mints a closure `:env` (type + obj + fields + hidden param) + # that the lift welds into the routine's serialized signature. Such an env is + # a PROCESS-LOCAL artifact (its item number is per-process-sequential), so a + # reference to it must never carry a stable cross-module identity — otherwise + # a consumer resolves it against a canonical NIF built by a different process + # that has no matching def ('symbol has no offset', e.g. Nimbus t17.275). + # Lift in the backend (process-local) id space; ast2nif then emits these as + # module-local `@bk` defs (mirrors setAttachedOp's inVMTransform handling). + var liftIdgen = idgen + if g.inVMTransform > 0 and g.config.cmd == cmdM: + if g.vmTransfIdgen == nil: + g.vmTransfIdgen = idGeneratorForBackend(g.systemModule) + liftIdgen = g.vmTransfIdgen + var c = openTransf(g, prc.getModule, "", liftIdgen, flags) result = liftLambdas(g, prc, getBody(g, prc), c.tooEarly, c.idgen, flags) result = processTransf(c, result, prc) liftDefer(c, result)