more unnecessary workarounds removed

This commit is contained in:
Araq
2026-06-14 13:02:23 +02:00
parent 2b44f599fd
commit b60a03a2be
6 changed files with 15 additions and 461 deletions

View File

@@ -1980,14 +1980,6 @@ proc genAsgn(p: BProc, e: PNode, fastAsgn: bool) =
loadInto(p, le, ri, a)
proc genStmts(p: BProc, t: PNode) =
if p.config.cmd == cmdNifC and t.kind == nkSym and
t.sym.kind in {skProc, skFunc, skConverter, skIterator} and
not icDceLive(p.module, t.sym):
# Under IC a module's top-level routine definitions reappear as bare
# symbol statements in the loaded statement list and were generated
# eagerly. Skip the ones dce.nim proved unreachable; anything a live
# body references is still generated on demand via `genProc`.
return
var a: TLoc = default(TLoc)
let isPush = p.config.hasHint(hintExtendedContext)

View File

@@ -904,16 +904,6 @@ proc initLocExprSingleUse(p: BProc, e: PNode): TLoc =
result.flags.incl lfSingleUse
expr(p, e, result)
proc icDceLive(m: BModule; sym: PSym): bool =
## Under `nim nifc` the eagerly emitted top-level routine listing is
## filtered through dce.nim's liveness result. Symbols generated on
## demand (`genProc` from a use site) never consult this.
let g = m.g.graph
if not g.icDceEnabled or sym.itemId.isBackendMinted:
result = true
else:
result = globalName(sym, m.config) in g.icLiveNames
include ccgcalls, "ccgstmts.nim"
proc initFrame(p: BProc, procname, filename: Rope): Rope =
@@ -1719,13 +1709,6 @@ proc isActivated(prc: PSym): bool = prc.typ != nil
proc genProc(m: BModule, prc: PSym) =
if sfBorrow in prc.flags or not isActivated(prc): return
if m.config.cmd == cmdNifC and m.g.graph.icDceEnabled and
sfImportc notin prc.flags and not icDceLive(m, prc):
# Stage-2 readiness check: in the current single-process backend, demand
# always wins over the liveness analysis (we generate the proc anyway).
# But per-module codegen will have to trust the analysis, so a proc that
# is demanded yet not marked live is an analysis bug — report it.
m.g.graph.icDceMisses.incl globalName(prc, m.config)
if sfForward in prc.flags:
addForwardedProc(m, prc)
fillProcLoc(m, prc.ast[namePos])
@@ -2897,9 +2880,6 @@ proc cgenWriteModules*(backend: RootRef, config: ConfigRef) =
if cl.broken: stripCnifMarks(codes[i])
else: renderMarkedC(codes[i], cl.live, dropped)
registerModuleCode(mods[i], cfs[i], rendered)
g.graph.icCDefs = cl.defs
g.graph.icCLiveDefs = cl.liveDefs
g.graph.icCDropped = dropped
else:
for m in cgenModules(g):
m.writeModule()

View File

@@ -1,263 +0,0 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Dead code analysis over per-module NIF files — a port of Nimony's
## `hexer/dce1.nim`/`dce2.nim` ideas onto the `nifcore` API.
##
## Per module we collect, in a single token walk over its `.nif` file:
## - `roots`: symbols that are alive by construction — anything referenced
## from top-level init code (every module's init proc is always emitted),
## plus flag-based entry points (see below)
## - `uses`: edges `definition -> symbols referenced inside its body`
##
## A global mark&sweep over the union of all modules' graphs then yields the
## set of live symbols.
##
## The NIF files contain *semchecked* (unlowered) AST, so uses that only
## materialize during the backend's lowering passes are invisible to the
## token walk. Those are covered by conservative roots instead:
## - registered hooks and `$enum` procs (the `(rep* "key" sym)` entries):
## `injectdestructors` and magic lowering insert calls to them at codegen
## - `{.compilerproc.}` symbols: requested by name via `cgsym`
## - `{.exportc.}` symbols, methods and dispatchers: external entry points
## resp. reachable through dynamic dispatch only
##
## In the current single-process backend the result is consumed as a skip
## filter for the eagerly generated top-level routine listing
## (`ccgstmts.genStmts`); cgen's demand-driven `genProc` remains in place,
## so an analysis miss can only cost code size, never correctness. The same
## analysis is the building block for per-module incremental codegen later,
## where it has to stand on its own.
import std / [tables, sets, os, assertions]
from std / strutils import rfind
import "../dist/nimony/src/lib" / nifcoreparse
import ast, options, pathutils
import ic / enum2nif
type
DceContext = object
pool: Pool # shared literal pool: same name <=> same SymId everywhere
tags: TagPool # shared tag pool: tag ids fixed by the registrations below
uses: Table[SymId, HashSet[SymId]]
roots: HashSet[SymId]
stmtsTag, sdefTag, implTag, replayTag, importTag, includeTag: TagId
methodKindTag: TagId
hookTags: HashSet[TagId]
routineKindTags: HashSet[TagId]
offers: HashSet[SymId] # generic routine instances defined by the modules
broken: bool # a module failed to parse; the result must not be used
DceStats* = object
instances*: int ## routine instance definitions across all modules
uniqueInstances*: int ## distinct instantiation keys (name.disamb)
## `instances - uniqueInstances` = definitions a merge step would drop
const
NoSym = SymId(0) # pool ids start at 1
proc symIdAt(c: Cursor): SymId {.inline.} =
# Every symbol in our NIFs is written with its `.disamb.modulesuffix`, so
# the name is always longer than nifcore's 3-byte inline-string cutoff and
# lands in the (shared) pool: pool ids are stable identities across all
# modules' token buffers.
assert not isInlineLit(c), "unexpectedly short NIF symbol name"
SymId(combinedPayload(c) shr 1)
proc recordUse(ctx: var DceContext; sym, owner: SymId) =
if owner == NoSym:
ctx.roots.incl sym
else:
ctx.uses.mgetOrPut(owner, initHashSet[SymId]()).incl sym
proc walkDef(ctx: var DceContext; c: var Cursor; owner: SymId; declarative: bool)
proc walk(ctx: var DceContext; c: var Cursor; owner: SymId; declarative: bool) =
## Generic walk. `owner == NoSym and not declarative` is init-code context:
## symbol uses become roots. With an owner they become `uses` edges. In
## declarative context (the listing after the `(implementation)` marker)
## bare uses record nothing — only definitions found inside contribute.
case c.kind
of TagLit:
if c.cursorTagId == ctx.sdefTag:
walkDef(ctx, c, owner, declarative)
else:
c.loopInto:
walk(ctx, c, owner, declarative)
of Symbol:
if not declarative:
recordUse(ctx, symIdAt(c), owner)
inc c
else:
skip c
proc walkDef(ctx: var DceContext; c: var Cursor; owner: SymId; declarative: bool) =
# Layout (ast2nif.writeSymDef):
# (sd SymbolDef <x|.> (symkind ...) magic flags options offset ...)
# NB: no `return` inside `into` — it would skip the cursor rescoping.
c.into:
if c.kind == SymbolDef:
let self = symIdAt(c)
# An sdef is emitted at the symbol's *first reference*; in use
# positions that reference counts like a plain symbol use.
if not declarative:
recordUse(ctx, self, owner)
inc c
if c.hasMore: skip c # export marker: "x" or dot
var rooted = false
var isRoutine = false
if c.hasMore and c.kind == TagLit: # symbol kind tree
if c.cursorTagId == ctx.methodKindTag:
rooted = true # reachable via dynamic dispatch
isRoutine = c.cursorTagId in ctx.routineKindTags
c.loopInto:
walk(ctx, c, self, false) # guard sym/bitsize for vars
if c.hasMore: skip c # magic: ident or dot
if c.hasMore: # flags: ident or dot
if c.kind == Ident:
let fl = parse(TSymFlag, strVal(c))
if sfExportc in fl or sfCompilerProc in fl or sfDispatcher in fl:
rooted = true
if isRoutine and sfFromGeneric in fl:
ctx.offers.incl self
skip c
if rooted: ctx.roots.incl self
# rest: options, offset, position, lib, type, owner, ast, loc,
# constraint, instantiatedFrom — all walked as the definition's body
while c.hasMore:
walk(ctx, c, self, false)
else:
# malformed sdef; consume defensively
while c.hasMore:
walk(ctx, c, owner, declarative)
proc rootHookSyms(ctx: var DceContext; c: var Cursor) =
# (repdestroy "typekey" hookSym) and friends
c.loopInto:
if c.kind == Symbol:
ctx.roots.incl symIdAt(c)
inc c
else:
skip c
proc analyzeNifFile(ctx: var DceContext; filename: string;
imports: var seq[string]) =
if not fileExists(filename):
ctx.broken = true
return
var buf = parseFromFile(filename, 1000, ctx.pool, ctx.tags)
var c = beginRead(buf)
if c.kind == TagLit and c.cursorTagId == ctx.stmtsTag:
var declarative = false
c.loopInto:
case c.kind
of TagLit:
let tag = c.cursorTagId
if tag == ctx.implTag:
# marks the start of the declarative listing (routines, type
# sections, consts); everything before it is init code
declarative = true
skip c
elif tag == ctx.importTag:
# (import . . "modsuffix") — the analysis discovers the module
# closure itself; the backend's own module list omits modules that
# are only reached through system or through demand-driven codegen
c.loopInto:
if c.kind == StrLit:
imports.add strVal(c)
inc c
else:
skip c
elif tag == ctx.replayTag or tag == ctx.includeTag:
skip c # compile directives and include info
elif tag in ctx.hookTags:
rootHookSyms(ctx, c)
elif tag == ctx.sdefTag:
# a definition listed at section level (globals before the marker,
# announced hooks after it): a declaration, not a use
walkDef(ctx, c, NoSym, true)
else:
walk(ctx, c, NoSym, declarative)
of Symbol:
inc c # bare re-listing of a written definition
else:
skip c # the stmts wrapper's flag/type dots
else:
ctx.broken = true
endRead(c)
proc markLive(ctx: DceContext): HashSet[SymId] =
result = initHashSet[SymId]()
var work = newSeqOfCap[SymId](ctx.roots.len)
for r in ctx.roots: work.add r
while work.len > 0:
let s = work.pop()
if not result.containsOrIncl(s):
if ctx.uses.hasKey(s):
for dep in ctx.uses[s]:
if dep notin result:
work.add dep
proc computeLiveSymbols*(conf: ConfigRef; seedFiles: openArray[string];
live: var HashSet[string]; stats: var DceStats;
nifDeps: var Table[string, seq[string]]): bool =
## Global liveness over a program's NIF modules: the seeds plus the
## transitive closure of their `(import ...)` entries. On success fills
## `live` with the NIF names (`name.disamb.modsuffix`) of every reachable
## symbol and returns true. Returns false when any module could not be
## analyzed — the caller must then treat everything as live.
## `nifDeps` receives the import graph over NIF file paths — the full
## closure including the modules the backend's own module list omits;
## the artifact-reuse decision needs it for transitive invalidation.
var ctx = DceContext(pool: newPool(), tags: newTagPool())
ctx.stmtsTag = ctx.tags.registerTag("stmts")
ctx.sdefTag = ctx.tags.registerTag("sd")
ctx.implTag = ctx.tags.registerTag("implementation")
ctx.replayTag = ctx.tags.registerTag("replay")
ctx.importTag = ctx.tags.registerTag("import")
ctx.includeTag = ctx.tags.registerTag("include")
ctx.methodKindTag = ctx.tags.registerTag("method")
for t in ["repdestroy", "repcopy", "repwasmoved", "repdup", "repsink",
"reptrace", "repdeepcopy", "repenumtostr"]:
ctx.hookTags.incl ctx.tags.registerTag(t)
for t in ["proc", "func", "iterator", "converter", "method"]:
ctx.routineKindTags.incl ctx.tags.registerTag(t)
var queue = newSeq[string](seedFiles.len)
for i in 0..<seedFiles.len: queue[i] = seedFiles[i]
var seen = initHashSet[string]()
var i = 0
while i < queue.len:
let f = queue[i]
inc i
if seen.containsOrIncl(f): continue
var imports: seq[string] = @[]
analyzeNifFile(ctx, f, imports)
if ctx.broken: return false
if conf != nil:
var depFiles = newSeqOfCap[string](imports.len)
for suffix in imports:
let depFile = toGeneratedFile(conf, AbsoluteFile(suffix), ".nif").string
depFiles.add depFile
queue.add depFile
nifDeps[f] = depFiles
let liveIds = markLive(ctx)
live = initHashSet[string](liveIds.len)
for s in liveIds:
live.incl ctx.pool.syms[s]
# Instance duplication stats: with content-derived instance disambs the
# NIF name minus the module suffix is the instantiation key, so the same
# instantiation made by several modules counts as one unique instance.
stats = DceStats(instances: ctx.offers.len)
var uniq = initHashSet[string]()
for s in ctx.offers:
let name = ctx.pool.syms[s]
let suffixStart = rfind(name, '.')
uniq.incl(if suffixStart >= 0: name[0..<suffixStart] else: name)
stats.uniqueInstances = uniq.len
result = true

View File

@@ -455,7 +455,12 @@ proc mainCommand*(graph: ModuleGraph) =
of cmdUnknown, cmdNone, cmdIdeTools:
rawMessage(conf, errGenerated, "invalid command: " & conf.command)
if conf.errorCounter == 0 and conf.cmd notin {cmdTcc, cmdDump, cmdNop}:
if conf.errorCounter == 0 and conf.cmd notin {cmdTcc, cmdDump, cmdNop} and
not (conf.cmd == cmdNifC and conf.icBackendStage.len > 0):
# Per-module backend stages (cg/emit/merge/link) run as many parallel child
# processes; each would print a `[SuccessX]` summary line that misleadingly
# reports `out: <the whole compiler>` for a step that only wrote one
# `.c.nif`/`.c`. The driving `nim ic` (and koch) reports the real result.
if optProfileVM in conf.globalOptions:
echo conf.dump(conf.vmProfileData)
genSuccessX(conf)

View File

@@ -68,16 +68,10 @@ type
enumToStringProcs*: Table[ItemId, PSym]
loadedEnumToStringProcs: Table[string, PSym]
emittedTypeInfo*: Table[string, FileIndex]
icLiveNames*: HashSet[string] # NIF names of reachable symbols (dce.nim);
# filters the top-level listing under `nim nifc`
icDceEnabled*: bool
icDceMisses*: HashSet[string] # demand-generated but not marked live:
# analysis bugs that per-module codegen would hit
instDisambs: Table[(int, int32), ItemId] # (name id, content disamb) ->
# instance, for collision probing in
# `setInstanceDisamb`
icCnifFiles*: seq[string] # `.c.nif` artifacts written by this run
icCDefs*, icCLiveDefs*, icCDropped*: int # render-time DCE stats
pendingMethodReplays*: seq[PSym] # method registrations loaded under
# `nim nifc`, bucketed only after every
# module is loaded (`flushMethodReplays`)

View File

@@ -23,7 +23,7 @@ when defined(nimPreviewSlimSystem):
import std/assertions
import ast, options, lineinfos, modulegraphs, cgendata, cgen,
pathutils, extccomp, msgs, modulepaths, idents, types, ast2nif, typekeys, dce,
pathutils, extccomp, msgs, modulepaths, idents, types, ast2nif, typekeys,
cnif
from cgmeth import generateIfMethodDispatchers
import ic / replayer
@@ -92,29 +92,6 @@ proc isMetaIter(t: PType, closure: RootRef): bool =
# descriptor for what must remain a (ptr, len) parameter expansion
t.kind in tyMetaTypes + {tyTyped, tyUntyped, tyNone, tyVarargs, tyOpenArray}
proc eagerHookCandidate(sym: PSym): bool =
## Announced hooks that can actually be code-generated: generic hook
## announcements and meta-typed ones (`varargs[typed]` etc.) are replay
## information for sem, not code.
let typ = sym.typ
if typ == nil or containsGenericType(typ): return false
if typ.n == nil: return false
for i in 1..<typ.n.len:
let pt = typ.n[i].typ
if pt == nil: return false
if iterOverType(pt, isMetaIter, nil): return false
# a `=dup` of an imported type returns it by value; for "lying" importc
# typedefs like `jmp_buf` (declared as `object`, really a C array) that
# signature does not compile. Demand-driven codegen never demands such
# sem-bookkeeping hooks (under refc nothing dups a `C_JmpBuf`), and no
# working artifact can call one — its prototype would be the same
# invalid C — so they are safe to skip.
let ret = typ.returnType
if ret != nil:
let r = ret.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct})
if r.sym != nil and sfImportc in r.sym.flags: return false
true
proc finishModule(g: ModuleGraph; bmod: BModule) =
# Finalize the module (this adds it to modulesClosed)
# Create an empty stmt list as the init body - genInitCode in writeModule will set it up properly
@@ -223,20 +200,6 @@ proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
# a concrete, non-generic, runtime routine with a real body, owned here
requestProcDef(bmod, s)
# The hooks and `$enum` procs this module announces are liveness roots:
# a cached TU from a previous run may call them without any demand
# arising in this run (the demanding instance body sits inside a reused
# TU). Demand them unconditionally so a regenerated TU never *loses*
# definitions that cached TUs link against.
if g.icDceEnabled and not isDefined(g.config, "icNoReuse"):
for op in precomp.logOps:
if op.kind in {HookEntry, EnumToStrEntry} and op.sym != nil and
eagerHookCandidate(op.sym):
when defined(icDbg):
stderr.writeLine "[icHook] " & $op.kind & " " & op.sym.name.s &
" typ: " & typeToString(op.sym.typ) & " in " & precomp.module.name.s
requestProcDef(bmod, op.sym)
proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex):
tuple[modules: seq[PrecompiledModule], precompSys: PrecompiledModule,
nifFiles: seq[string]] =
@@ -386,17 +349,14 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
rawMessage(g.config, errGenerated,
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
return
# No whole-program DCE here, exactly as for a non-main target: `icDceEnabled`
# stays false so each module emits the routines it owns and the MERGE stage
# recomputes the one program-wide live set across all `.c.nif`s. Running
# `computeLiveSymbols` over all ~260 NIFs in the main `cg` cost ~900 MB for a
# result the merge stage throws away — pure redundancy now that the funnel is
# gone (the main module no longer emits its transitive closure's bodies).
# No whole-program DCE here: each module emits the routines it owns and the
# MERGE stage recomputes the one program-wide live set across all `.c.nif`s.
# Running a whole-program liveness pass over all ~260 NIFs in the main `cg`
# would cost ~900 MB for a result the merge stage throws away.
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
else:
# No whole-program load, hence no whole-program DCE: `icDceEnabled` stays
# false, so `icDceLive` keeps every top-level routine and the target emits
# its full demanded closure. The merge stage drops what is globally dead.
# No whole-program load, hence no whole-program DCE: the target emits its
# full demanded closure and the merge stage drops what is globally dead.
(modules, precompSys, target) = loadDepClosure(g, g.config.icBackendModule)
if target.module == nil:
rawMessage(g.config, errGenerated,
@@ -552,120 +512,6 @@ proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
elif g.config.icBackendStage == "link":
generateLinkStage(g, mainFileIdx)
return
elif g.config.icBackendStage.len > 0:
else:
rawMessage(g.config, errGenerated,
"per-module backend stage not implemented yet: " & g.config.icBackendStage)
return
# Phase timing, enabled with `-d:icTimings` on the nifc command line.
let icTimings = isDefined(g.config, "icTimings")
var phaseStart = epochTime()
template phaseDone(name: string) =
if icTimings:
let now = epochTime()
stderr.writeLine "[icTime] " & name & ": " &
formatFloat(now - phaseStart, ffDecimal, 2) & "s"
phaseStart = now
# Reset backend state
resetForBackend(g)
var isKnownFile = false
let systemFileIdx = registerNifSuffix(g.config, "sysma2dyk", isKnownFile)
g.config.m.systemFileIdx = systemFileIdx
#msgs.fileInfoIdx(g.config,
# g.config.libpath / RelativeFile"system.nim")
# Load system module first - it's always needed and contains essential hooks
var precompSys = PrecompiledModule(module: nil)
precompSys = moduleFromNifFile(g, systemFileIdx, {LoadFullAst, AlwaysLoadInterface})
g.systemModule = precompSys.module
# Load all modules in dependency order using stack traversal
# This must happen BEFORE any code generation so that hooks are loaded into loadedOps
var nifFiles: seq[string] = @[toNifFilename(g.config, systemFileIdx)]
let modules = loadModuleDependencies(g, mainFileIdx, nifFiles)
# build the method dispatch buckets now that every module is loaded
flushMethodReplays(g)
phaseDone "load (" & $ (modules.len + 1) & " modules)"
if modules.len == 0:
rawMessage(g.config, errGenerated,
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
return
# Compute the global live set so that the top-level routine listing can be
# filtered (see `ccgstmts.genStmts`). On analysis failure everything stays
# alive — demand-driven `genProc` makes this a size optimization only.
var dceStats = DceStats()
var nifDeps = initTable[string, seq[string]]()
if not isDefined(g.config, "icNoDce"):
g.icDceEnabled = computeLiveSymbols(g.config, nifFiles, g.icLiveNames,
dceStats, nifDeps)
phaseDone "dce"
# Set up backend modules for all modules that need code generation
for m in modules:
discard setupNifBackendModule(g, m.module)
if precompSys.module != nil:
discard setupNifBackendModule(g, precompSys.module)
# System module is generated first if it exists
if precompSys.module != nil:
generateCodeForModule(g, precompSys)
# Track which modules have been processed to avoid duplicates
var processed = initIntSet()
if precompSys.module != nil:
processed.incl precompSys.module.position
# Generate code for all modules (skip system since it's already processed)
for m in modules:
if not processed.containsOrIncl(m.module.position):
generateCodeForModule(g, m)
emitMethodDispatchers(g)
phaseDone "cgen"
# during code generation of `main.nim` we can trigger the code generation
# of symbols in different modules so we need to finish these modules
# here later, after the above loop!
# Important: The main module must be finished LAST so that all other modules
# have registered their init procs before genMainProc uses them.
var mainModule: BModule = nil
for m in BModuleList(g.backend).mods:
if m != nil:
assert m.module != nil
if sfMainModule in m.module.flags:
mainModule = m
else:
finishModule g, m
if mainModule != nil:
finishModule g, mainModule
phaseDone "finish"
if g.icDceEnabled and isDefined(g.config, "icDceCheck"):
var misses: seq[string] = @[]
for n in g.icDceMisses: misses.add n
sort misses
for n in misses:
stderr.writeLine "[icDce] MISS (generated on demand, not marked live): " & n
stderr.writeLine "[icDce] live: " & $g.icLiveNames.len & " misses: " & $misses.len &
" modules: " & $nifFiles.len
stderr.writeLine "[icDce] instances: " & $dceStats.instances &
" unique: " & $dceStats.uniqueInstances &
" mergeable: " & $(dceStats.instances - dceStats.uniqueInstances)
# Write C files
cgenWriteModules(g.backend, g.config)
phaseDone "write"
if isDefined(g.config, "icDceCheck") and g.icCnifFiles.len > 0:
stderr.writeLine "[icDceC] cdefs: " & $g.icCDefs & " live: " & $g.icCLiveDefs &
" dropped: " & $g.icCDropped
# Run C compiler
if g.config.cmd != cmdTcc:
extccomp.callCCompiler(g.config)
phaseDone "cc+link"
if not g.config.hcrOn:
extccomp.writeJsonBuildInstructions(g.config, g.cachedFiles)
"the per-module NIF backend requires --icBackendStage:cg|merge|emit|link")