diff --git a/compiler/ccgtypes.nim b/compiler/ccgtypes.nim index 25f5eaa52e..eb82c854c7 100644 --- a/compiler/ccgtypes.nim +++ b/compiler/ccgtypes.nim @@ -92,14 +92,11 @@ proc sharedInstanceCName(m: BModule; s: PSym): string = (s.disamb and InstanceDisambBit) != 0'i32 and s.typ != nil and s.typ.callConv != ccInline and not m.hcrOn and {sfImportc, sfExportc, sfCodegenDecl} * s.flags == {}: - let candidate = s.name.s.mangle & "_i" & $s.disamb - let compat = typeToString(s.typ) - let existing = m.g.graph.icSharedSigs.getOrDefault(candidate) - if existing.len == 0: - m.g.graph.icSharedSigs[candidate] = compat - result = candidate - elif existing == compat: - result = candidate + # The content-derived `disamb` is unique per process (collision-probed in + # `setInstanceDisamb`), so the mint-site-independent `_i` name is + # safe to use directly; identical instances across modules collide on it + # exactly and the merge stage keeps one. + result = s.name.s.mangle & "_i" & $s.disamb proc isSharedInstanceCName(m: BModule; s: PSym): bool = m.config.cmd == cmdNifC and s.kind in routineKinds and @@ -2030,12 +2027,6 @@ proc genTypeInfoV2(m: BModule; t: PType; info: TLineInfo): Rope = # definition into the owner module's *unwritten* backend module (discarded in # this process) and emit only an extern here, leaving the symbol undefined. let perModuleCg = m.config.cmd == cmdNifC and m.config.icBackendStage == "cg" - if not perModuleCg and m.config.cmd == cmdNifC and result in m.g.graph.icCachedDataDefs: - # already defined inside a reused TU from the previous run - cgsym(m, "TNimTypeV2") - declareNimType(m, "TNimTypeV2", result, owner) - m.g.typeInfoMarkerV2[sig] = (str: result, owner: owner) - return prefixTI(result) if not perModuleCg and owner != m.module.position and myModuleOpenForCodegen(m, FileIndex owner): # make sure the type info is created in the owner module discard genTypeInfoV2(m.g.mods[owner], origType, info) @@ -2124,16 +2115,6 @@ proc genTypeInfoV1(m: BModule; t: PType; info: TLineInfo): Rope = else: template dbgNti(branch: string) = discard - if m.config.cmd == cmdNifC and result in m.g.graph.icCachedDataDefs: - dbgNti "extern:cachedDataDefs" - # already defined inside a reused TU from the previous run - cgsym(m, "TNimType") - cgsym(m, "TNimNode") - declareNimType(m, "TNimType", result, t.skipTypes(typedescPtrs).itemId.module) - m.g.typeInfoMarker[sig] = (str: result, - owner: t.skipTypes(typedescPtrs).itemId.module) - return prefixTI(result) - let old = m.g.graph.emittedTypeInfo.getOrDefault($result) if old != FileIndex(0): dbgNti "extern:emittedTypeInfo" diff --git a/compiler/cgen.nim b/compiler/cgen.nim index f7a55fb980..a1b36c2db1 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -93,15 +93,6 @@ proc findPendingModule(m: BModule, s: PSym): BModule = var ms = getModule(s) result = m.g.mods[ms.position] -proc isReusedTU(m: BModule): bool = - ## Whether this module's cached translation unit is reused — either as a - ## loaded backend module or purely at the file level (a module the - ## backend never loaded whose BModule demand-driven codegen created). - m.config.cmd == cmdNifC and - (m.module.position in m.g.graph.icReusedModules or - (m.g.graph.icFileReusedCnames.len > 0 and - getCFile(m).string in m.g.graph.icFileReusedCnames)) - proc icNifName(m: BModule; s: PSym): string = ## The serialized NIF name of `s`, recorded next to its C name in the cnif ## artifact so a later run can re-demand the definition when a reused TU @@ -121,23 +112,6 @@ proc icNifName(m: BModule; t: PType): string = else: result = "" -proc redirectToLiveModule(m: BModule, q: BModule): BModule = - ## A module whose cached translation unit is reused never generates code, - ## so a definition that `findPendingModule` routes into it must be emitted - ## elsewhere: into the demanding module, or — when the demander is itself - ## reused (demands raised while wiring up a reused module's init call) — - ## into the main module, which is always regenerated. - result = q - if q != nil and m.config.cmd == cmdNifC and isReusedTU(q): - if not isReusedTU(m): - result = m - else: - result = nil - for cand in m.g.mods: - if cand != nil and sfMainModule in cand.module.flags: - result = cand - break - if result == nil: result = m proc emitsBodyInThisModule(m: BModule, prc: PSym): bool = ## Per-module backend codegen is concerned with ONE module: it emits the @@ -1414,29 +1388,6 @@ proc genProcLvl3*(m: BModule, prc: PSym) = # Any demand before that point yields a prototype. genProcPrototype(m, prc) return - # inline procs are emitted into every using TU; they are never shared - # across translation units, so cached/cross-TU dedup must not touch - # them. Dispatchers always (re)define in main, never from the cache. - let key = stripCnifMarks(prc.loc.snippet) - if (prc.typ == nil or prc.typ.callConv != ccInline) and - sfDispatcher notin prc.flags: - if key in m.g.graph.icCachedCDefs: - # already defined inside a reused TU from the previous run - genProcPrototype(m, prc) - return - # one definition program-wide: the first claimant's TU embeds it, - # everyone else declares it. The claim records the TU as well: with - # redirects the same symbol can be demanded into several TUs. Home - # emissions must claim too — a hook's demand routing goes through the - # type-owner module (`findPendingModule` walks `s.owner`) while its - # eager emission uses the announcing module's TU; when the owner TU - # is reused, the very same symbol reaches this point through both - # paths and only the registry serializes them. - let claim = (sym: prc.itemId, tu: m.module.position) - if m.g.graph.icSharedDefOwner.hasKeyOrPut(key, claim) and - m.g.graph.icSharedDefOwner[key] != claim: - genProcPrototype(m, prc) - return if prc.itemId.module != m.module.position and not isBackendMinted(prc.itemId) and (prc.typ == nil or prc.typ.callConv != ccInline) and @@ -1581,12 +1532,11 @@ proc genProcLvl3*(m: BModule, prc: PSym) = sfDispatcher notin prc.flags: # A unique program-wide definition: external linkage, so exactly one # translation unit may embed its body and everyone else declares it. - # In the whole-program backend `icSharedDefOwner` (first claimant wins) - # enforces this in process; in the per-module backend each module's `cg` - # process emits the body (emit-everywhere), so this flag tells the merge - # stage which definitions to assign a single owner and prototype in the - # rest. The complement — inline procs and method dispatchers — is emitted - # into every using TU (`static`/main-only) and must never be deduplicated. + # Each module's `cg` process emits the body (emit-everywhere); this flag + # tells the merge stage which definitions to assign a single owner and + # prototype in the rest. The complement — inline procs and method + # dispatchers — is emitted into every using TU (`static`/main-only) and + # must never be deduplicated. defFlags.add 'u' if not hasCnifMarks(prc.loc.snippet): # The C name was not minted through `fillBackendName` (e.g. set by an @@ -1710,7 +1660,7 @@ proc genProcLvl2(m: BModule, prc: PSym) = genProcLvl3(m, prcCopy) else: let m2 = if m.config.symbolFiles != disabledSf: m - else: redirectToLiveModule(m, findPendingModule(m, prc)) + else: findPendingModule(m, prc) fillProcLoc(m2, prc.ast[namePos]) #elif {sfExportc, sfImportc} * prc.flags == {}: # # reset name to restore consistency in case of hashing collisions: @@ -1720,7 +1670,7 @@ proc genProcLvl2(m: BModule, prc: PSym) = genProcPrototype(m, prc) genProcLvl3(m, prc) elif sfImportc notin prc.flags: - var q = redirectToLiveModule(m, findPendingModule(m, prc)) + var q = findPendingModule(m, prc) fillProcLoc(q, prc.ast[namePos]) # generate a getProc call to initialize the pointer for this # externally-to-the-current-module defined proc, also important @@ -1750,23 +1700,8 @@ proc requestConstImpl(p: BProc, sym: PSym) = if genConstSetup(p, sym): let m = p.module # declare implementation: - var q = findPendingModule(m, sym) - var defineIt = true - if m.config.cmd == cmdNifC: - if stripCnifMarks(sym.loc.snippet) in m.g.graph.icCachedDataDefs: - # already defined inside a reused TU from the previous run - defineIt = false - else: - let q2 = redirectToLiveModule(m, q) - if q2 != q: - # redirected definition: one TU program-wide embeds it - q = q2 - let key = stripCnifMarks(sym.loc.snippet) - let claim = (sym: sym.itemId, tu: q2.module.position) - if m.g.graph.icSharedDefOwner.hasKeyOrPut(key, claim) and - m.g.graph.icSharedDefOwner[key] != claim: - defineIt = false - if defineIt and q != nil and not containsOrIncl(q.declaredThings, sym.id): + let q = findPendingModule(m, sym) + if q != nil and not containsOrIncl(q.declaredThings, sym.id): assert q.initProc.module == q genConstDefinition(q, p, sym) # declare header: @@ -1802,17 +1737,6 @@ proc requestProcDef*(m: BModule, prc: PSym) = ## code had referenced it. genProc(m, prc) -proc requestAnyDef*(m: BModule, sym: PSym) = - ## Demand entry for the def-retention check: a definition that lived in - ## this TU in the previous run and that a reused TU still references must - ## keep being emitted. Routines and constants can be re-demanded; symbols - ## of any other kind never get here (the check un-reuses the referencing - ## TU instead). - case sym.kind - of routineKinds: genProc(m, sym) - of skConst: requestConstImpl(m.initProc, sym) - else: discard - proc genVarPrototype(m: BModule, n: PNode) = #assert(sfGlobal in sym.flags) let sym = n.sym @@ -2269,25 +2193,6 @@ proc registerModuleToMain(g: BModuleList; m: BModule) = else: g.otherModsInit.addCallStmt(init) -proc registerReusedInit*(g: BModuleList; moduleBase: string; - initRequired, datInitRequired: bool) = - ## init/datInit registration for a translation unit that is reused purely - ## from its cached files (the module is not even loaded); the names are - ## reconstructed from the module's mangled base name recorded in the - ## artifact's meta head. - if datInitRequired: - let datInit = moduleBase & "DatInit000" - g.mainModProcs.addDeclWithVisibility(Private): - g.mainModProcs.addProcHeader(ccNimCall, datInit, CVoid, cProcParams()) - g.mainModProcs.finishProcHeaderAsProto() - g.mainDatInit.addCallStmt(datInit) - if initRequired: - let init = moduleBase & "Init000" - g.mainModProcs.addDeclWithVisibility(Private): - g.mainModProcs.addProcHeader(ccNimCall, init, CVoid, cProcParams()) - g.mainModProcs.finishProcHeaderAsProto() - g.otherModsInit.addCallStmt(init) - proc registerReusedModuleToMain*(g: BModuleList; m: BModule; initRequired, datInitRequired: bool) = ## `registerModuleToMain` for a module whose cached translation unit is @@ -2945,32 +2850,12 @@ proc genForwardedProcs(g: BModuleList) = # Note: ``genProcLvl2`` may add to ``forwardedProcs`` while g.forwardedProcs.len > 0: let prc = g.forwardedProcs.pop() - var m = g.mods[prc.itemId.module] - if isReusedTU(m): - # the home TU is reused; emit through the main module instead - for cand in g.mods: - if cand != nil and sfMainModule in cand.module.flags: - m = cand - break + let m = g.mods[prc.itemId.module] if sfForward in prc.flags: internalError(m.config, prc.info, "still forwarded: " & prc.name.s) genProcLvl2(m, prc) -proc reuseCachedModule(g: BModuleList; m: BModule) = - ## The module's cached `.c`/`.o`/artifact are reused: register the cached - ## object file for the link, keep the cached artifact in the liveness - ## inputs and replay the module's init registration from the artifact's - ## meta head. - let cfile = getCFile(m) - var cf = Cfile(nimname: m.module.name.s, cname: cfile, - obj: completeCfilePath(m.config, toObjFile(m.config, cfile)), - flags: {CfileFlag.Cached}) - addFileToCompile(m.config, cf) - g.graph.icCnifFiles.add cfile.string & ".nif" - let meta = g.graph.icReusedMeta.getOrDefault(m.module.position) - registerReusedModuleToMain(g, m, meta.initRequired, meta.datInitRequired) - proc cgenWriteModules*(backend: RootRef, config: ConfigRef) = let g = BModuleList(backend) g.config = config @@ -2980,18 +2865,6 @@ proc cgenWriteModules*(backend: RootRef, config: ConfigRef) = # order anyway) genForwardedProcs(g) - # translation units reused purely from cached files (modules the backend - # never loaded): link their objects, keep their artifacts in the liveness - # inputs, replay their init registration. NB: these TUs rarely have init - # code (they were demand-only in the producing run as well). - for fr in g.graph.icFileReused: - var cf = Cfile(nimname: splitFile(fr.cname).name, cname: AbsoluteFile fr.cname, - obj: completeCfilePath(config, toObjFile(config, AbsoluteFile fr.cname)), - flags: {CfileFlag.Cached}) - addFileToCompile(config, cf) - g.graph.icCnifFiles.add fr.cname & ".nif" - registerReusedInit(g, fr.moduleBase, fr.initRequired, fr.datInitRequired) - if config.cmd == cmdNifC and not isDefined(config, "icNoCDce"): # Two-phase write: produce every module's marked text and artifact # first, then compute global liveness over the artifacts and render @@ -3002,11 +2875,6 @@ proc cgenWriteModules*(backend: RootRef, config: ConfigRef) = var cfs: seq[Cfile] = @[] var codes: seq[string] = @[] for m in cgenModules(g): - if m.module.position in g.graph.icReusedModules: - reuseCachedModule(g, m) - continue - if isReusedTU(m): - continue # file-level reused: registered before this loop already let cfile = getCFile(m) var cf = Cfile(nimname: m.module.name.s, cname: cfile, obj: completeCfilePath(m.config, toObjFile(m.config, cfile)), flags: {}) @@ -3026,11 +2894,6 @@ proc cgenWriteModules*(backend: RootRef, config: ConfigRef) = g.graph.icCDropped = dropped else: for m in cgenModules(g): - if config.cmd == cmdNifC and m.module.position in g.graph.icReusedModules: - reuseCachedModule(g, m) - elif isReusedTU(m): - discard # file-level reused: registered before this loop already - else: - m.writeModule() + m.writeModule() writeMapping(config, g.mapping) if g.generatedHeader != nil: writeHeader(g.generatedHeader) diff --git a/compiler/cnif.nim b/compiler/cnif.nim index 85f53affb7..0caebd8682 100644 --- a/compiler/cnif.nim +++ b/compiler/cnif.nim @@ -486,8 +486,7 @@ proc computeMergeDecision*(files: openArray[string]): MergeDecision = ## Each `cg` process emits the body of every definition it demands ## (emit-everywhere), so the same externally-linked definition appears in ## several artifacts. A `'u'` flag on the `(cdef ...)` marks those that need - ## exactly one owner (the whole-program backend's `icSharedDefOwner` - ## invariant, here recomputed across processes); the owner is the + ## exactly one owner, assigned here across processes: the owner is the ## lexicographically smallest artifact that emits it — a pure function of the ## claimant set, hence stable across rebuilds. Definitions without `'u'` ## (inline procs, dispatchers) are `static`/main-only and emitted into every diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim index 2a68ca74e7..b262acb2c3 100644 --- a/compiler/modulegraphs.nim +++ b/compiler/modulegraphs.nim @@ -78,37 +78,9 @@ type # `setInstanceDisamb` icCnifFiles*: seq[string] # `.c.nif` artifacts written by this run icCDefs*, icCLiveDefs*, icCDropped*: int # render-time DCE stats - icSharedSigs*: Table[string, string] # shared instance C name -> signature - # (collision guard for the 30-bit hash) - icSharedDefOwner*: Table[string, tuple[sym: ItemId, tu: int]] - # shared instance C name -> the symbol and - # the TU (module position) embedding the - # single program-wide definition - icReusedModules*: IntSet # module positions whose cached `.c`/`.o` - # is reused: codegen is skipped for them - icCachedCDefs*: HashSet[string] # C names of proc definitions inside - # reused TUs (from their artifacts' cdef heads) - icCachedDataDefs*: HashSet[string] # C names of data definitions (consts, - # globals, RTTI) inside reused TUs - icReusedMeta*: Table[int, tuple[initRequired, datInitRequired: bool]] - icFileReused*: seq[tuple[cname, moduleBase: string; - initRequired, datInitRequired: bool]] - # TUs reused purely from cached files: modules the backend never - # loads (reached only through system or demand-driven codegen) - icFileReusedCnames*: HashSet[string] # their .c paths, so demand-created - # BModules for them never write anything pendingMethodReplays*: seq[PSym] # method registrations loaded under # `nim nifc`, bucketed only after every # module is loaded (`flushMethodReplays`) - icPreserveDefs*: Table[int, seq[PSym]] # module position -> symbols whose - # definitions the TU must keep emitting: - # they were in its previous artifact and a - # reused TU still references them (the - # backend def-migration check) - icPreserveTypeInfos*: Table[int, seq[PType]] # the same for RTTI data - # definitions, which are type-driven: the - # type whose `genTypeInfo` the TU must - # re-demand icImplDeps*: IntSet # NeedsImpl edge tracking under `nim m`: # module ids (FileIndex) whose routine BODIES # this compilation consumed at compile time. @@ -826,10 +798,7 @@ proc getModule*(g: ModuleGraph; fileIdx: FileIndex): PSym = result = nil proc moduleOpenForCodegen*(g: ModuleGraph; m: FileIndex): bool {.inline.} = - ## A module whose cached translation unit is reused does not accept new - ## definitions: anything that would be emitted into it must be emitted - ## into the demanding module instead. - result = m.int notin g.icReusedModules + result = true proc recordIcImplDep*(g: ModuleGraph; s: PSym) = ## NeedsImpl edge tracking, see `icImplDeps`. Called from the compile-time diff --git a/compiler/nifbackend.nim b/compiler/nifbackend.nim index 66fb72b707..62a873393f 100644 --- a/compiler/nifbackend.nim +++ b/compiler/nifbackend.nim @@ -86,338 +86,6 @@ proc setupNifBackendModule(g: ModuleGraph; module: PSym): BModule = g.backend = cgendata.newModuleList(g) result = cgen.newModule(BModuleList(g.backend), module, g.config, idGeneratorForBackend(module)) -proc enforceDefRetention(g: ModuleGraph; mainPos: int; - reusedHeads: var Table[int, CnifHeads]; - fileCandidates: var seq[tuple[cname: string, heads: CnifHeads]]; - staleArtifacts: seq[string]; - loadedArtifacts: seq[tuple[pos: int, artifact: string]]; - icDebug: bool) = - ## The backend def-migration check ("previously defined, still referenced"). - ## - ## A definition can live in a TU other than its symbol's home module: - ## redirected defs, shared `_i` instances, demand-generated hooks. - ## When that TU regenerates and the demand chain that put the definition - ## there does not re-arise (its demanders sit in reused TUs now), the - ## definition vanishes while the reused TUs still call it — a guaranteed - ## link error that only a cold rebuild heals. - ## - ## For every definition in a regenerating TU's *previous* artifact that - ## some reused TU references (its `cref` head) and that no reused TU - ## defines, the recorded NIF name is resolved against the current sem - ## state and the symbol is re-demanded into the TU the definition lived - ## in (`icPreserveDefs`, consumed by `generateCodeForModule`). When the - ## symbol no longer exists — only possible with an incoherent cache, - ## e.g. mixed compiler generations or renamed RTTI — the referencing TUs - ## lose their reuse instead and regenerate; their old definitions then - ## become sources for the same check, hence the fixpoint loop. - if mainPos < 0: return - # Sources: the previous artifacts of every TU that regenerates this run. - # `ownTU` marks sources whose module runs full codegen this run (loaded - # backend modules): only those re-emit their global variables and their - # live top-level listing by themselves. Definitions in stale unclaimed - # artifacts have no TU of their own; the main module (always regenerated) - # hosts their re-demands. - var sources: seq[tuple[target: int, ownTU: bool, heads: CnifHeads]] = @[] - for la in loadedArtifacts: - if la.pos notin g.icReusedModules: - let h = readCnifHeads(la.artifact) - if h.valid: sources.add (la.pos, true, h) - for a in staleArtifacts: - let h = readCnifHeads(a) - if h.valid: sources.add (mainPos, false, h) - if sources.len == 0: return - - while true: - # what the surviving reused TUs define and reference - var cachedDefs = initHashSet[string]() - template addDefs(h: CnifHeads) = - for d in h.cdefs: cachedDefs.incl d.cname - for d in h.cdata: cachedDefs.incl d.cname - for h in reusedHeads.values: addDefs(h) - for fc in fileCandidates: addDefs(fc.heads) - # referencing TUs per name: loaded modules by position, file-level - # TUs by -(index+1) - var refdBy = initTable[string, seq[int]]() - for pos, h in reusedHeads.pairs: - for r in h.crefs: refdBy.mgetOrPut(r, @[]).add pos - for i, fc in fileCandidates.pairs: - for r in fc.heads.crefs: refdBy.mgetOrPut(r, @[]).add -(i+1) - - # modules whose TU runs full codegen this run: definitions they still - # own are re-emitted with them (globals through the serialized var - # sections unconditionally, live routines through the top-level listing) - var regenSuffixes = initHashSet[string]() - for src in sources.items: - if src.ownTU: - let base = extractFilename(src.heads.semmedNif) - if base.len > 4: regenSuffixes.incl base[0..^5] # strip ".nif" - - # Every definition of a regenerating TU's previous artifact must stay - # defined ("previously defined => still defined"): references to it can - # be invisible (compilerproc/exportc names are not cnif-marked), so the - # preserve decision must NOT depend on the cref index — that one only - # targets the un-reuse fallback. Demand-side dedup (`declaredThings`, - # the cached/claim shortcuts) makes redundant re-demands cheap. - clear g.icPreserveDefs - clear g.icPreserveTypeInfos - var unreuse = initHashSet[int]() - for src in sources.items: - template check(defseq) = - for d in defseq: - if d.cname notin cachedDefs: - if d.nifname.startsWith("`t"): - # an RTTI definition: re-demand is type-driven (`genTypeInfo`), - # there is no symbol to resolve - let typ = resolveGlobalType(ast.program, d.nifname) - if typ != nil: - g.icPreserveTypeInfos.mgetOrPut(src.target, @[]).add typ - if icDebug: - stderr.writeLine "[icRetain] preserve typeinfo " & d.cname - continue - elif d.cname in refdBy: - # type vanished: un-reuse the TUs that still reference it - for tu in refdBy[d.cname]: unreuse.incl tu - if icDebug: - stderr.writeLine "[icRetain] cannot re-demand typeinfo " & - d.cname & "; un-reusing referencing TUs" - continue - else: - continue - var sym: PSym = nil - if d.nifname.len > 0: - sym = resolveGlobalSym(ast.program, d.nifname) - # NB: reading `sym.kind` forces the lazy stub, so the kind is real - var action = 0 # un-reuse any visibly referencing TUs - if sym != nil: - if sfDispatcher in sym.flags: - # method dispatchers are synthesized from the whole - # program's method set and emitted into the main TU on - # every run; re-demanding the serialized sym would emit - # its (empty) serialized body - action = 1 - elif sym.kind in routineKinds or sym.kind == skConst: - # routine and const definitions exist only by demand (the - # serialized top level holds just the eager init statements, - # not a proc listing!), and reused TUs never demand — so - # every definition of the previous artifact whose symbol - # still exists is re-demanded explicitly - action = 2 - elif sym.kind in {skVar, skLet} and - parseSymName(d.nifname).module in regenSuffixes: - action = 1 # globals are re-emitted with their module's - # eager top-level statements - case action - of 1: discard - of 2: - g.icPreserveDefs.mgetOrPut(src.target, @[]).add sym - if icDebug: - stderr.writeLine "[icRetain] preserve " & d.cname - else: - if d.cname in refdBy: - for tu in refdBy[d.cname]: unreuse.incl tu - if icDebug: - stderr.writeLine "[icRetain] cannot re-demand " & d.cname & - " (nif: " & d.nifname & "); un-reusing referencing TUs" - # no visible references and no symbol to re-demand: a deleted - # definition; invisible (unmarked) references would stem from - # exportc-style names whose callers re-sem on deletion anyway - check(src.heads.cdefs) - check(src.heads.cdata) - if unreuse.len == 0: break - # un-reused TUs regenerate; their previous definitions become sources - # for the next round - var dropFile: seq[int] = @[] - for tu in unreuse.items: - if tu >= 0: - if tu in reusedHeads: - g.icReusedModules.excl tu - g.icReusedMeta.del tu - sources.add (tu, true, reusedHeads[tu]) - reusedHeads.del tu - else: - dropFile.add -(tu+1) - sort dropFile - for j in countdown(dropFile.high, 0): - # an un-reused file-level TU has no backend module: only demand-driven - # codegen recreates its content, so its definitions are re-demanded - # through the main module (ownTU = false) - sources.add (mainPos, false, fileCandidates[dropFile[j]].heads) - fileCandidates.delete dropFile[j] - -proc computeModuleReuse(g: ModuleGraph; modules: seq[PrecompiledModule]; - precompSys: PrecompiledModule; - nifDeps: Table[string, seq[string]]) = - ## Decides which modules' cached translation units can be reused: codegen - ## is skipped for them and their `.c`/`.o`/artifact files are used as is. - ## - ## A module is reusable when the newest semmed NIF in its transitive - ## import closure is older than its `.c.nif` artifact — so neither the - ## module itself nor anything that can influence its generated C (type - ## layouts of dependencies in particular) has changed — and the cached - ## artifact, `.c` and `.o` files are all present. The main module is - ## always regenerated: it carries NimMain's init-call list and the method - ## dispatchers, which depend on the whole program. - ## - ## A regenerated module may still demand entities that live in a reused - ## TU: definitions already inside the cached TU become prototypes (see - ## `genProcLvl3`/`genTypeInfo*` and the artifact's cdef/cdata heads), - ## fresh demands are redirected into the demanding TU - ## (`redirectToLiveModule`). - if not g.icDceEnabled or isDefined(g.config, "icNoReuse") or - g.config.hcrOn or g.config.symbolFiles != disabledSf: - return - let icDebug = isDefined(g.config, "icTimings") - # newest mtime in every NIF file's transitive import closure, via - # fixpoint iteration (the import graph can contain cycles). The implicit - # system import is not part of the NIF import lists, so system counts as - # a dependency of every module. - let systemNif = toNifFilename(g.config, g.config.m.systemFileIdx) - var maxTime = initTable[string, Time]() - for f in nifDeps.keys: - maxTime[f] = getLastModificationTime(f) - var changed = true - while changed: - changed = false - for f, deps in nifDeps: - var newest = maxTime[f] - if systemNif in maxTime and maxTime[systemNif] > newest and f != systemNif: - newest = maxTime[systemNif] - for d in deps: - if d in maxTime and maxTime[d] > newest: newest = maxTime[d] - if newest > maxTime[f]: - maxTime[f] = newest - changed = true - - # Fine-grained reuse gate, mirroring the m-step's cookie gating: a TU's - # generated C depends on its own semmed NIF, on every direct import's - # *interface* (type layouts, signatures, const values, inline-semantics - # bodies — all hashed into the `.iface.nif` cookie, whose hash chains the - # direct deps' cookies and is therefore transitively sensitive), on the - # implicit system import, and on the *implementations* of the modules - # whose routine bodies the TU physically embeds (redirected defs, shared - # instances, hooks — recorded in the artifact's cdeps head, gated on - # `.impl.nif`). The cookie sidecars are written OnlyIfChanged by the - # m-step, so a body-only edit in a leaf module leaves every dependent's - # iface input untouched and only the edited module's TU regenerates. - # When a sidecar is missing (`-d:icNoIfaceGate` m-runs, foreign caches) - # the gate falls back to the transitive NIF-mtime closure; `-d:icCoarseReuse` - # forces that fallback. - let coarseReuse = isDefined(g.config, "icCoarseReuse") - var inputTimes = initTable[string, Time]() - proc sidecarOf(nifFile, kind: string): string = - nifFile[0..^5] & "." & kind & ".nif" # "/.nif" -> "/..nif" - proc staleVsArtifact(nifFile: string; artTime: Time; heads: CnifHeads): string = - ## Empty when every input is older than the artifact, else the reason. - if nifFile notin inputTimes: - inputTimes[nifFile] = getLastModificationTime(nifFile) - if inputTimes[nifFile] > artTime: - return "semmed NIF newer than artifact" - var missingSidecar = coarseReuse - if not missingSidecar: - block fine: - var inputs = @[sidecarOf(systemNif, "iface")] - for dep in nifDeps.getOrDefault(nifFile): - inputs.add sidecarOf(dep, "iface") - for s in heads.cdeps: - inputs.add getNimcacheDir(g.config).string / s & ".impl.nif" - for inp in inputs: - if inp notin inputTimes: - if fileExists(inp): - inputTimes[inp] = getLastModificationTime(inp) - else: - missingSidecar = true - break fine - if inputTimes[inp] > artTime: - return "cookie newer than artifact: " & inp - return "" - if maxTime.getOrDefault(nifFile, artTime) > artTime: - return "dependency closure newer than artifact (coarse)" - return "" - - let bl = BModuleList(g.backend) - var handledArtifacts = initHashSet[string]() - var loadedArtifacts: seq[tuple[pos: int, artifact: string]] = @[] - var reusedHeads = initTable[int, CnifHeads]() # loaded reused modules - var mainPos = -1 - for i in 0..modules.len: - let pm = if i < modules.len: modules[i] else: precompSys - if pm.module == nil: continue - let pos = pm.module.position - let bmod = bl.mods[pos] - if bmod == nil: continue - let artifact = getCFile(bmod).string & ".nif" - # claimed by a loaded module — regenerated or reused, but never - # eligible for the file-level reuse below - handledArtifacts.incl artifact - loadedArtifacts.add (pos, artifact) - if sfMainModule in pm.module.flags: - mainPos = pos - continue - let nifFile = toNifFilename(g.config, FileIndex pos) - template reject(reason: string) = - if icDebug: - stderr.writeLine "[icReuse] regen " & cachedModuleSuffix(g.config, FileIndex pos) & - ": " & reason - continue - if nifFile notin maxTime: reject("not in dce closure: " & nifFile) - let cfile = getCFile(bmod) - let obj = completeCfilePath(g.config, toObjFile(g.config, cfile)) - if not fileExists(artifact): reject("no artifact " & artifact) - if not fileExists(cfile.string): reject("no C file") - if not fileExists(obj.string): reject("no object file") - let heads = readCnifHeads(artifact) - if not heads.valid: reject("artifact has no meta head") - let staleReason = staleVsArtifact(nifFile, getLastModificationTime(artifact), heads) - if staleReason.len > 0: reject(staleReason) - g.icReusedModules.incl pos - g.icReusedMeta[pos] = (heads.initRequired, heads.datInitRequired) - reusedHeads[pos] = heads - - # Translation units of modules the backend module list does not even - # contain (reached only through system's imports or demand-driven - # codegen): their artifacts are self-describing, so they can be reused - # purely at the file level. When one of them is stale, its import - # closure is stale too, so every TU that could reference it regenerates - # and demand recreates the definitions. - var fileCandidates: seq[tuple[cname: string, heads: CnifHeads]] = @[] - var staleArtifacts: seq[string] = @[] - for artifact in walkFiles(getNimcacheDir(g.config).string / "*.c.nif"): - if artifact in handledArtifacts: continue - let heads = readCnifHeads(artifact) - if not heads.valid or heads.semmedNif.len == 0 or heads.moduleBase.len == 0: - continue - if heads.semmedNif notin maxTime: - # not part of this program anymore (e.g. a removed module), but its - # definitions may still be referenced by reused TUs - staleArtifacts.add artifact - continue - let cname = artifact[0..^5] # strip ".nif" - let obj = completeCfilePath(g.config, toObjFile(g.config, AbsoluteFile cname)) - if not (fileExists(cname) and fileExists(obj.string)) or - staleVsArtifact(heads.semmedNif, - getLastModificationTime(artifact), heads).len > 0: - staleArtifacts.add artifact - continue - fileCandidates.add (cname, heads) - - if not isDefined(g.config, "icNoRetain"): - enforceDefRetention(g, mainPos, reusedHeads, fileCandidates, staleArtifacts, - loadedArtifacts, icDebug) - - # The cached-definition sets and the file-level reuse list reflect the - # TUs that survived the retention check. - for heads in reusedHeads.values: - for d in heads.cdefs: g.icCachedCDefs.incl d.cname - for d in heads.cdata: g.icCachedDataDefs.incl d.cname - for fc in fileCandidates: - g.icFileReused.add (fc.cname, fc.heads.moduleBase, - fc.heads.initRequired, fc.heads.datInitRequired) - g.icFileReusedCnames.incl fc.cname - for d in fc.heads.cdefs: g.icCachedCDefs.incl d.cname - for d in fc.heads.cdata: g.icCachedDataDefs.incl d.cname - if icDebug and g.icFileReused.len > 0: - stderr.writeLine "[icReuse] file-level reused TUs: " & $g.icFileReused.len - proc isMetaIter(t: PType, closure: RootRef): bool = # openArray/varargs hooks are sem bookkeeping: no real flow ever demands # them, and generating one pollutes the TU's type cache with a struct @@ -561,15 +229,6 @@ proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) = " typ: " & typeToString(op.sym.typ) & " in " & precomp.module.name.s requestProcDef(bmod, op.sym) - # Definitions this TU embedded in the previous run that reused TUs still - # reference must keep being emitted (see `enforceDefRetention`). - if g.icPreserveDefs.hasKey(moduleId): - for sym in g.icPreserveDefs[moduleId]: - requestAnyDef(bmod, sym) - if g.icPreserveTypeInfos.hasKey(moduleId): - for t in g.icPreserveTypeInfos[moduleId]: - discard genTypeInfo(g.config, bmod, t, unknownLineInfo) - proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex): tuple[modules: seq[PrecompiledModule], precompSys: PrecompiledModule, nifFiles: seq[string]] = @@ -775,8 +434,8 @@ proc generateMergeStage(g: ModuleGraph) = ## wrote, computes the global live set and — for each `'u'`-flagged unique ## definition that several `cg` processes emitted (emit-everywhere) — the one ## artifact allowed to embed its body, and writes the decision the `emit` - ## stages consume. This replaces the whole-program backend's in-process - ## `icSharedDefOwner`/DCE coordination with a cross-process rule. + ## stages consume — the cross-process replacement for what used to be + ## in-process first-claimant/DCE coordination. let nimcache = getNimcacheDir(g.config).string var files: seq[string] = @[] for artifact in walkFiles(nimcache / "*.c.nif"): @@ -942,21 +601,9 @@ proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) = if precompSys.module != nil: discard setupNifBackendModule(g, precompSys.module) - # Decide which modules' cached translation units can be reused - computeModuleReuse(g, modules, precompSys, nifDeps) - phaseDone "reuse (" & $g.icReusedModules.len & " modules reused)" - - template generateOrReuse(precomp: PrecompiledModule) = - if precomp.module.position in g.icReusedModules: - # no code generation, but the recorded compile/link directives - # (passl/passc/...) still apply to this build - replayBackendActions(g, precomp.module, precomp.topLevel) - else: - generateCodeForModule(g, precomp) - # System module is generated first if it exists if precompSys.module != nil: - generateOrReuse(precompSys) + generateCodeForModule(g, precompSys) # Track which modules have been processed to avoid duplicates var processed = initIntSet() @@ -966,7 +613,7 @@ proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) = # Generate code for all modules (skip system since it's already processed) for m in modules: if not processed.containsOrIncl(m.module.position): - generateOrReuse(m) + generateCodeForModule(g, m) emitMethodDispatchers(g) phaseDone "cgen" diff --git a/compiler/pipelineutils.nim b/compiler/pipelineutils.nim index 1f2bb0a8dd..7719230e89 100644 --- a/compiler/pipelineutils.nim +++ b/compiler/pipelineutils.nim @@ -23,6 +23,4 @@ proc prepareConfigNotes*(graph: ModuleGraph; module: PSym) = graph.config.notes = graph.config.foreignPackageNotes proc moduleHasChanged*(graph: ModuleGraph; module: PSym): bool {.inline.} = - # under `nim nifc` a module whose cached translation unit is reused - # does not generate code; the set is empty for every other command - result = module.position notin graph.icReusedModules + result = true