# # # The Nim Compiler # (c) Copyright 2025 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## Generate a .build.nif file for nifmake from a Nim project. ## This enables incremental and parallel compilation using the `m` switch. import std / [os, tables, sets, times, osproc, algorithm, strtabs, strutils, syncio] import options, msgs, lineinfos, pathutils, condsyms, modulepaths, extccomp, cnif, platform import "../dist/nimony/src/lib" / [nifstreams, bitabs, nifreader, nifbuilder] import icmodnames import icnifcore type FilePair = object nimFile: string modname: string Node = ref object files: seq[FilePair] # main file + includes deps: seq[int] # indices into DepContext.nodes id: int DepContext = object config: ConfigRef nifler: string nodes: seq[Node] processedModules: Table[string, int] # modname -> node index includeStack: seq[string] systemNodeId: int # ID of the system.nim node implicitNodeIds: seq[int] # node IDs of `--import`ed modules (conf.implicitImports); # every ordinary module implicitly imports these, so each # gets a dependency edge on them, exactly like system.nim scanningMain: bool # currently scanning the project main module's deps; # makes `when isMainModule` conditions evaluate true # only there (every other module is imported) proc toPair(c: DepContext; f: string): FilePair = FilePair(nimFile: f, modname: moduleSuffix(f, cast[seq[string]](c.config.searchPaths))) proc depsFile(c: DepContext; f: FilePair): string = getNimcacheDir(c.config).string / f.modname & ".deps.nif" proc parsedFile(c: DepContext; f: FilePair): string = getNimcacheDir(c.config).string / f.modname & ".p.nif" proc semmedFile(c: DepContext; f: FilePair): string = getNimcacheDir(c.config).string / f.modname & ".s.bif" proc ifaceFile(c: DepContext; f: FilePair): string = ## Interface-cookie sidecar written by `nim m` (ast2nif.writeIfaceCookie, ## OnlyIfChanged). Dependents' nim_m rules use it as their input instead of ## the semmed NIF: a body-only change in a dependency then keeps the sidecar ## mtime and nifmake prunes the whole re-sem cascade behind it. getNimcacheDir(c.config).string / f.modname & ".iface.bif" proc implFile(c: DepContext; suffix: string): string = ## Implementation-cookie sidecar (ast2nif.writeImplCookie): flips on ANY ## content change of the module (private bodies included; supersedes the ## iface cookie). Used as the edge for dependents that consumed the ## module's bodies at compile time (NeedsImpl edges). getNimcacheDir(c.config).string / suffix & ".impl.bif" proc edgesFile(c: DepContext; f: FilePair): string = getNimcacheDir(c.config).string / f.modname & ".edges.bif" proc readNeedsImpl(c: DepContext; f: FilePair): seq[string] = ## Reads the module's recorded NeedsImpl edge set (module suffixes whose ## bodies its last sem consumed at compile time). Missing file (never ## compiled yet) -> empty: the rule fires anyway on the first build and the ## recording exists from then on. Recordings are self-correcting with a ## one-run lag: whatever changes a module's consumption set is itself a ## gated input of its rule, so the rule re-fires and re-records. result = @[] if fileExists(c.edgesFile(f)): result = collectBifStrLits(c.edgesFile(f)) proc semDepsFile(c: DepContext; f: FilePair): string = getNimcacheDir(c.config).string / f.modname & ".s.deps.bif" proc readSemDeps(c: DepContext; f: FilePair): seq[string] = ## The module's REAL direct imports (full source paths) as sem resolved them, ## including macro-generated imports the static scanner missed ## (ast2nif.writeSemDeps). Missing file (not yet semmed) -> empty. result = @[] if fileExists(c.semDepsFile(f)): result = collectBifStrLits(c.semDepsFile(f)) proc findNifler(): string = # Look for nifler in common locations let nimDir = getAppDir() result = nimDir / "nifler" if not fileExists(result): result = findExe("nifler") proc findNifmake(): string = # Look for nifmake in common locations # Try relative to nim executable let nimDir = getAppDir() result = nimDir / "nifmake" if not fileExists(result): result = findExe("nifmake") proc runNifler(c: DepContext; nimFile: string): bool = ## Run nifler deps on a file if needed. Returns true on success. ## NOTE: the `setLastModificationTime` coordination below is a known hack; its ## clean removal lands with the Phase 2 frontend/backend split, which redefines ## this pre-scan's role. (A naive switch to keying on the parsed file produced ## a stale warm rebuild, so it's left intact until the restructure.) let pair = c.toPair(nimFile) let depsPath = c.depsFile(pair) # Check if deps file is up-to-date if fileExists(depsPath) and fileExists(nimFile): if getLastModificationTime(depsPath) > getLastModificationTime(nimFile): return true # Already up-to-date # Create output directory if needed createDir(parentDir(depsPath)) # Run nifler deps let cmd = quoteShell(c.nifler) & " deps " & quoteShell(nimFile) & " " & quoteShell(depsPath) let exitCode = execShellCmd(cmd) result = exitCode == 0 if result: # The build graph's `nifler parse --deps` rule outputs BOTH the parsed # file and the deps file. Refreshing the deps file here would MASK that # rule: nifmake's `needsRebuild` takes the freshest output as proof of # "ran since the inputs changed", so the rule never re-fires and the # parsed file goes stale. For an import-cycle group that loses the edit # entirely — a non-representative member's source is not a direct input # of the group's `nim_m` rule; its only build-graph connection is the # (now stale) parsed file. Drop a genuinely stale parsed file so the # nifler rule re-fires on the missing output. let parsedPath = c.parsedFile(pair) if fileExists(parsedPath) and getLastModificationTime(parsedPath) < getLastModificationTime(nimFile): removeFile(parsedPath) # nifler writes OnlyIfChanged: after an edit that leaves the import set # unchanged the deps file keeps its old mtime and would stay older than # the source forever, re-running this scan (and re-deleting the parsed # file) on every warm build. Bump it explicitly: it is the scan's own # up-to-date marker. if getLastModificationTime(depsPath) < getLastModificationTime(nimFile): setLastModificationTime(depsPath, getTime()) proc resolveImport(c: DepContext; origin, toResolve: string): string = ## Resolve an import path using the compiler's normal module lookup rules. var toResolve = toResolve if '$' in toResolve: # string-literal import paths support `$nim`-style substitutions # (see modulepaths.getModuleName) try: toResolve = pathSubs(c.config, toResolve, origin.splitFile().dir) except ValueError: discard result = findModule(c.config, toResolve, origin).string proc resolveInclude(c: DepContext; origin, toResolve: string): string = ## Resolve an include path relative to the including file or the search paths. let originDir = parentDir(origin) result = originDir / toResolve.addFileExt("nim") if fileExists(result): return result for searchPath in c.config.searchPaths: result = searchPath.string / toResolve.addFileExt("nim") if fileExists(result): return result result = "" proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) proc processInclude(c: var DepContext; includePath: string; current: Node; origin: string) = # `origin` = the file the `include` literally appears in (an included file's # own nested includes/imports must resolve relative to IT, not the importing # module's main file). let resolved = resolveInclude(c, origin, includePath) if resolved.len == 0 or not fileExists(resolved): return # Check for recursive includes for s in c.includeStack: if s == resolved: return # Skip recursive include c.includeStack.add resolved current.files.add c.toPair(resolved) traverseDeps(c, c.toPair(resolved), current) discard c.includeStack.pop() proc getsImplicitImports(c: DepContext; nimFile: string): bool = ## Mirror the compiler's `belongsToStdlib` guard (pipelines.nim): `--import:X` ## (conf.implicitImports) is applied only to NON-stdlib modules. The scanner ## must agree, otherwise it edges a stdlib module → X that the compiler never ## actually creates, fabricating a cycle that folds X — and the modules X ## claims to produce — into the system SCC (whose `nim m` is driven from ## system.nim and never reaches them). Stdlib == under conf.libpath. not isRelativeTo(nimFile, c.config.libpath.string) proc processImport(c: var DepContext; importPath: string; current: Node; origin: string) = # `origin` = the file the `import` literally appears in. Crucial for imports # inside `include`d files: e.g. `system.nim` includes `system/excpt.nim`, which # does `import stacktraces` — that must resolve relative to `excpt.nim` # (lib/system/) → `lib/system/stacktraces.nim`, NOT relative to `system.nim` # (lib/) which has no `stacktraces.nim`. Resolving against the main file silently # dropped the `system → stacktraces` edge, so stacktraces was a separate SCC in # the static round and got re-grouped (and recompiled with divergent type ids) # only after the post-sem `.s.deps` revealed the edge. let resolved = resolveImport(c, origin, importPath) if resolved.len == 0 or not fileExists(resolved): return let pair = c.toPair(resolved) let existingIdx = c.processedModules.getOrDefault(pair.modname, -1) if existingIdx == -1: # New module - create node and process it let newNode = Node(files: @[pair], id: c.nodes.len) current.deps.add newNode.id # Every module depends on system.nim if c.systemNodeId >= 0: newNode.deps.add c.systemNodeId # ... and on every `--import`ed module (conf.implicitImports), but only for # the non-stdlib modules the compiler actually applies implicit imports to # (see getsImplicitImports). A `--import`ed module is imported by its own # non-stdlib closure (which also gets these edges), so that cycle folds into # one small strongly-connected component (see computeSCCs) instead of being # smeared across system + stdlib. if getsImplicitImports(c, pair.nimFile): for impId in c.implicitNodeIds: if impId != newNode.id: newNode.deps.add impId c.processedModules[pair.modname] = newNode.id c.nodes.add newNode traverseDeps(c, pair, newNode) else: # Already processed - just add dependency if existingIdx notin current.deps: current.deps.add existingIdx proc skipSubtree(s: var Stream; first: PackedToken) = ## Consume tokens until the ParLe at `first` is balanced. Caller has ## already obtained `first`. if first.kind != ParLe: return var depth = 1 while depth > 0: let t = next(s) if t.kind == ParLe: inc depth elif t.kind == ParRi: dec depth elif t.kind == EofToken: return type CondVal = enum ## Tri-state truth of a `when` condition as the static scanner sees it. ## `cvUnknown` is the crucial state: the scanner can't determine the value ## (an arbitrary call like `compiles`/`tryImport`, an unknown const ident, ## an unresolvable comparison). A dependency scanner must NEVER drop a real ## import, so callers treat `cvUnknown` as "keep the dependency". The bug ## this replaces: everything-unknown collapsed to `true`, and `not true` ## is `false`, so an `else:` branch (emitted as `when (not COND)`) silently ## dropped its imports (e.g. `when tryImport x: ... else: import x`, or ## system's `else: include excpt` hiding `import stacktraces`). cvFalse, cvTrue, cvUnknown proc toCondVal(b: bool): CondVal = (if b: cvTrue else: cvFalse) proc condNot(a: CondVal): CondVal = case a of cvFalse: cvTrue of cvTrue: cvFalse of cvUnknown: cvUnknown proc condAnd(a, b: CondVal): CondVal = if a == cvFalse or b == cvFalse: cvFalse elif a == cvTrue and b == cvTrue: cvTrue else: cvUnknown proc condOr(a, b: CondVal): CondVal = if a == cvTrue or b == cvTrue: cvTrue elif a == cvFalse and b == cvFalse: cvFalse else: cvUnknown proc evalCondIdent(c: DepContext; v: string): CondVal = ## Truth value of a bare identifier appearing in a `when` condition. Unknown ## idents are `cvUnknown` (kept), not `true` — so `when not SOMEIDENT:` no ## longer drops its import. case v of "true": cvTrue of "false": cvFalse of "hasThreadSupport": # system.nim's `hasThreadSupport` is `compileOption("threads") and # not defined(nimscript)`; the conservative `true` would schedule the # threads-only modules (syslocks, threadtypes, sharedlist, locks) # whose NIFs a --threads:off compile never produces — nifmake then # sees missing outputs and re-runs the system rule (and everything # downstream) on every rerun. toCondVal(optThreads in c.config.globalOptions) of "usesDestructors": # system.nim's `usesDestructors = defined(gcDestructors) or # defined(gcHooks)`; guards mmdisp.nim's `include "system/gc"` whose # transitive imports (sharedlist, locks) an orc compile never produces. toCondVal(isDefined(c.config, "gcDestructors") or isDefined(c.config, "gcHooks")) of "isMainModule": # Only the project main module is compiled with `isMainModule` true; an # imported module's `when isMainModule` blocks are dead. The conservative # `true` would schedule main-only imports (e.g. parser.nim's # `tools/grammar_nanny`, a node that gets a cg rule but is never linked, # so the merge stage can pick it as a shared def's owner -> undefined # symbols at link). toCondVal(c.scanningMain) else: cvUnknown proc constIdentValue(c: DepContext; ident: string): string = ## String value of a compile-time platform constant that appears in `when` ## guards, or "" when unknown. Mirrors the compiler's magics so the scanner ## evaluates e.g. `when hostOS == "standalone"` the SAME way the real compile ## does. Without this the comparison is "unknown" → the conservative `true`, ## which is WRONG once negated (`else:` branches emit `not (==)`), so a real ## conditional `include`/`import` is dropped (e.g. system's `else: include ## excpt`, hiding `import stacktraces`). # Must match the compiler's magics EXACTLY, incl. case: `hostOS`/`hostCPU` etc. # fold to the lower-cased platform name (see semfold.nim mHostOS/mHostCPU), and # user code compares against lower-case literals (`when hostOS == "linux"`). case ident of "hostOS": result = toLowerAscii(platform.OS[c.config.target.targetOS].name) of "hostCPU": result = toLowerAscii(platform.CPU[c.config.target.targetCPU].name) of "buildOS": result = toLowerAscii(platform.OS[c.config.target.hostOS].name) of "buildCPU": result = toLowerAscii(platform.CPU[c.config.target.hostCPU].name) else: result = "" proc readOperandValue(c: DepContext; s: var Stream): string = ## Read one operand of an `==`/`!=` infix and return its string value (a string ## literal verbatim, a platform-constant ident resolved, anything else ""), fully ## consuming the operand (subtrees are skipped) so the caller stays in sync. let t = next(s) case t.kind of StringLit: result = pool.strings[t.litId] of Ident: result = constIdentValue(c, pool.strings[t.litId]) of ParLe: result = "" skipSubtree(s, t) else: result = "" proc evalCondCmp(c: DepContext; s: var Stream; isEq: bool): CondVal = ## Evaluate `a == b` / `a != b`. Both operands known → real result; otherwise ## `cvUnknown` (so a negated comparison keeps, not drops, the dependency). let v1 = readOperandValue(c, s) let v2 = readOperandValue(c, s) if v1.len > 0 and v2.len > 0: result = toCondVal((v1 == v2) == isEq) else: result = cvUnknown proc evalCondExpr(c: DepContext; s: var Stream; t: PackedToken): CondVal proc readCond(c: DepContext; s: var Stream): CondVal = ## Read one full condition subtree (its own opener included) and evaluate it. let t = next(s) evalCondExpr(c, s, t) proc evalCondExpr(c: DepContext; s: var Stream; t: PackedToken): CondVal = ## Evaluate the condition whose opening token `t` has ALREADY been read, ## consuming the rest of the expression so the caller stays in sync. ## Recognises `defined(IDENT)`, `not`/`and`/`or`, `==`/`!=` and the literals ## `true`/`false`; everything else (an arbitrary call such as `compiles` / ## `tryImport`, an unknown const) is `cvUnknown`. Both negation-sensitive ## (`not cvUnknown == cvUnknown`) and short-circuit-free: `and`/`or` always ## read both operands so the stream stays in sync regardless of the result. case t.kind of Ident: result = evalCondIdent(c, pool.strings[t.litId]) of ParLe: let tag = pool.tags[t.tagId] # For prefix/infix/call nodes the operator name is the first child; for a # bare `(not ...)`/`(and ...)`/`(or ...)`/`(par ...)` node the tag itself is # the operator and the operands follow directly. var name = tag case tag of "call", "cmd", "callstrlit", "infix", "prefix": let head = next(s) if head.kind == Ident: name = pool.strings[head.litId] else: name = "" else: discard case name of "defined": let arg = next(s) var sym = "" if arg.kind == Ident: sym = pool.strings[arg.litId] result = toCondVal(sym.len > 0 and isDefined(c.config, sym)) of "not": result = condNot(readCond(c, s)) of "and": let a = readCond(c, s) let b = readCond(c, s) result = condAnd(a, b) of "or": let a = readCond(c, s) let b = readCond(c, s) result = condOr(a, b) of "==", "!=": result = evalCondCmp(c, s, name == "==") of "par": # a parenthesised grouping such as `(defined(a) or defined(b))`. result = readCond(c, s) else: result = cvUnknown # Drain whatever remains until the matching ParRi. var depth = 1 while depth > 0: let n = next(s) if n.kind == ParLe: inc depth elif n.kind == ParRi: dec depth elif n.kind == EofToken: return else: result = cvUnknown proc whenMarkerHolds(c: DepContext; s: var Stream): CondVal = ## Caller has just consumed the `(when` ParLe. Read children until the ## matching `)`, AND-ing each evaluated condition. Returns the tri-state ## result; callers keep the dependency unless it is provably `cvFalse`. result = cvTrue while true: let t = next(s) if t.kind == ParRi: return if t.kind == EofToken: return result = condAnd(result, evalCondExpr(c, s, t)) proc parseImportPath(s: var Stream; t: var PackedToken): seq[string] = ## Parse an import path expression and return the list of module paths it ## refers to. Handles plain idents (`foo`), string literals, `std/foo` ## infixes (including nested ones like `std/private/since`) and bracketed ## groups like `std/[bitops, fenv]` which expand to several imports. ## On entry `t` is the first token of the expression; on exit `t` is the ## token immediately following the whole expression. result = @[] case t.kind of Ident: result.add pool.strings[t.litId] t = next(s) of StringLit: result.add pool.strings[t.litId] t = next(s) of ParLe: let tag = pool.tags[t.tagId] if tag == "infix": t = next(s) # skip 'infix' tag var op = "" if t.kind == Ident: op = pool.strings[t.litId] t = next(s) let left = parseImportPath(s, t) let right = parseImportPath(s, t) if op == "as": # `import ../rlp/results as rlp_results`: the alias is not a path # component — treating `as` like `/` produced the garbage path # `../rlp/results/rlp_results`, silently dropping the dependency result = left else: let prefix = if left.len == 1: left[0] else: "" for r in right: if prefix.len > 0: result.add prefix & "/" & r else: result.add r if t.kind == ParRi: t = next(s) # skip closing ')' elif tag == "prefix": # Relative import paths: `import ../dist/checksums/...` parses as # `(prefix ../ dist)` — a path-prefix operator (`../`, `./`) applied to # the first path component. Concatenate operator and operand verbatim; # `findModule` resolves the relative path against the importing module. t = next(s) # skip 'prefix' tag var op = "" if t.kind == Ident: op = pool.strings[t.litId] t = next(s) for r in parseImportPath(s, t): result.add op & r if t.kind == ParRi: t = next(s) # skip closing ')' elif tag == "bracket": t = next(s) # skip 'bracket' tag while t.kind != ParRi and t.kind != EofToken: result.add parseImportPath(s, t) if t.kind == ParRi: t = next(s) # skip closing ')' else: # Unknown subtree: skip it entirely. var depth = 1 t = next(s) while depth > 0 and t.kind != EofToken: if t.kind == ParLe: inc depth elif t.kind == ParRi: dec depth if depth == 0: break t = next(s) if t.kind == ParRi: t = next(s) else: t = next(s) proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) = ## Read a .deps.nif file and process imports/includes let depsPath = c.depsFile(pair) if not fileExists(depsPath): return # `current.id == 0` is the project main (rootNode); restored on exit so the # flag is correct for each parent frame between its child recursions. let prevScanningMain = c.scanningMain c.scanningMain = current.id == 0 defer: c.scanningMain = prevScanningMain var s = nifstreams.open(depsPath) defer: nifstreams.close(s) discard processDirectives(s.r) var t = next(s) if t.kind != ParLe: return # Skip to content (past stmts tag) t = next(s) while t.kind != EofToken: if t.kind == ParLe: let tag = pool.tags[t.tagId] case tag of "import", "fromimport", "importexcept", "include": # Read first child. May be a `(when COND...)` marker — parse and # evaluate; if the condition is statically false, skip the import # entirely. Otherwise advance past the marker and parse the path. t = next(s) var live = true if t.kind == ParLe and pool.tags[t.tagId] == "when": # whenMarkerHolds consumes everything up to and including the # closing `)` of the `(when ...)` subtree. Drop the import only when # the condition is PROVABLY false; a `cvUnknown` condition (e.g. an # `else:` branch guarded by `not `, as in # `when tryImport x: ... else: import x`) keeps the dependency so the # static graph never misses a real import. live = whenMarkerHolds(c, s) != cvFalse t = next(s) if not live: # Drain the rest of this import/include node. var depth = 1 while depth > 0: let n = next(s) if n.kind == ParLe: inc depth elif n.kind == ParRi: dec depth elif n.kind == EofToken: break t = next(s) continue # Process the path expression(s). Each path supports plain idents, # string literals, `std/foo` infixes (possibly nested, e.g. # `std/private/since`) and bracketed groups like `std/[bitops, fenv]` # that expand to several imports. A plain `import a, b, c` lists several # modules as siblings; a `fromimport` has a single path followed by the # imported symbol list, which must not be treated as modules. if tag == "fromimport" or tag == "importexcept": # `from m import syms` / `import m except syms`: the first child is the # module path; the rest is the (in/ex)cluded symbol list, which must not # be treated as modules. Both still create a real dependency on `m`. for importPath in parseImportPath(s, t): if importPath.len > 0: processImport(c, importPath, current, pair.nimFile) else: while t.kind != ParRi and t.kind != EofToken: for importPath in parseImportPath(s, t): if importPath.len > 0: if tag == "include": processInclude(c, importPath, current, pair.nimFile) else: processImport(c, importPath, current, pair.nimFile) # Drain any remaining tokens of this node (e.g. the symbol list of a # `fromimport`), up to and including the node's closing ')'. var depth = 1 while depth > 0 and t.kind != EofToken: if t.kind == ParLe: inc depth elif t.kind == ParRi: dec depth if depth == 0: break t = next(s) else: # Skip unknown node var depth = 1 while depth > 0: t = next(s) if t.kind == ParLe: inc depth elif t.kind == ParRi: dec depth t = next(s) proc collectIncludeNames(depsPath: string; names: var seq[string]) = ## Lightweight scan of a `.deps.nif` prelude: collect the raw path text of ## every entry inside an `(include ...)` node (idents like `semexprs`, string ## literals like `"system/mmdisp"`, and the leaves of `a/b` path infixes). ## Liberal by design — it also picks up entries under a statically-false ## `(when ...)`; that is harmless for the only caller (`includerSbifs`), whose ## over-collection just costs an extra, result-free bif scan downstream. if not fileExists(depsPath): return var s = nifstreams.open(depsPath) defer: nifstreams.close(s) discard processDirectives(s.r) var depth = 0 var includeDepth = 0 # the `depth` at which the current `(include` opened; 0 = not inside one var t = next(s) while t.kind != EofToken: case t.kind of ParLe: inc depth if includeDepth == 0 and pool.tags[t.tagId] == "include": includeDepth = depth of ParRi: if includeDepth != 0 and depth == includeDepth: includeDepth = 0 dec depth of Ident, StringLit: if includeDepth != 0: names.add pool.strings[t.litId] else: discard t = next(s) proc entryStemBase(roots: seq[string]; name: string): (string, string) = ## Resolve include entry `name` to (deps-stem, base-name); ("","") if unfound. for r in roots: let p = r / name.addFileExt("nim") if fileExists(p): return (moduleSuffix(p, []), splitFile(p).name) result = ("", "") proc includerSbifs*(conf: ConfigRef; targetFile: AbsoluteFile): seq[string] = ## For an include file `targetFile`, return the `.s.bif` paths of every module ## that includes it — directly OR transitively (following the include chain ## `module -> incA -> incB -> targetFile`). `nim track` uses this to avoid ## loading and scanning every module bif: an include file has no bif of its ## own, so its type-checked tokens live in the *including* module's bif. Only ## the small `.deps.nif` preludes are read here, never a `.s.bif`. const depsExt = ".deps.nif" let nc = getNimcacheDir(conf).string # Candidate roots for resolving an `(include X)` entry to a real file, so its # module suffix (== its own deps-file stem) can be computed. Include entries # carry any sub-path (`system/mmdisp`), so the file's *directory* roots suffice: # the target's own dir, the project dir, and the search paths cover the # compiler, the stdlib and typical single-tree projects. var roots: seq[string] = @[parentDir(targetFile.string)] if conf.projectPath.string.len > 0: roots.add conf.projectPath.string for sp in conf.searchPaths: roots.add sp.string # One pass over every prelude builds the reverse include graph, keyed by base # file name: `includedBy[b]` = deps stems whose owner directly `include`s a # file named `b`. `stemBase` maps an include-only file's deps stem back to its # own base name, so the walk can climb through nested includes. var includedBy = initTable[string, seq[string]]() var stemBase = initTable[string, string]() for depsPath in walkFiles(nc / "*" & depsExt): let base = extractFilename(depsPath) if base.endsWith(".p" & depsExt): continue # `.p.deps.nif` twin let ownerStem = base[0 ..< base.len - depsExt.len] var names: seq[string] = @[] collectIncludeNames(depsPath, names) for n in names: let (childStem, childBase) = entryStemBase(roots, n) if childBase.len == 0: continue includedBy.mgetOrPut(childBase, @[]).add ownerStem stemBase[childStem] = childBase # this child's stem -> its base name # Walk UP from the target: a deps stem that includes the current base name is # either a module (has a `.s.bif` -> collect it) or itself an include file # (recurse via its own base name). result = @[] var seenBase = initHashSet[string]() var work = @[splitFile(targetFile.string).name] while work.len > 0: let b = work.pop() if seenBase.containsOrIncl(b): continue for stem in includedBy.getOrDefault(b): let sbif = nc / stem & ".s.bif" if fileExists(sbif): if sbif notin result: result.add sbif # module owner else: let ob = stemBase.getOrDefault(stem) # include-only owner: climb higher if ob.len > 0: work.add ob proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) = ## Process a module: run nifler and read deps if not runNifler(c, pair.nimFile): rawMessage(c.config, errGenerated, "nifler failed for: " & pair.nimFile) return readDepsFile(c, pair, current) proc computeSCCs(c: DepContext): seq[seq[int]] = ## Tarjan's strongly-connected-components over the module dependency graph ## (`node.deps`). Each returned component is a list of node indices; a module ## that is not part of any import cycle yields a singleton component. Tarjan ## emits components in reverse-topological order (a component's external ## dependencies come out before it), which is exactly the order `nifmake` ## needs for the per-group `nim m` build rules. type Frame = object v, pi: int let n = c.nodes.len var index = newSeq[int](n) var lowlink = newSeq[int](n) var onStack = newSeq[bool](n) var visited = newSeq[bool](n) var stack: seq[int] = @[] var counter = 0 result = @[] # Iterative Tarjan (explicit work stack) so a deep module-dependency chain # cannot overflow the call stack. for start in 0.. 0: let v = work[^1].v if work[^1].pi == 0: visited[v] = true index[v] = counter lowlink[v] = counter inc counter stack.add v onStack[v] = true if work[^1].pi < c.nodes[v].deps.len: let w = c.nodes[v].deps[work[^1].pi] inc work[^1].pi if not visited[w]: work.add Frame(v: w, pi: 0) elif onStack[w]: lowlink[v] = min(lowlink[v], index[w]) else: if lowlink[v] == index[v]: var comp: seq[int] = @[] while true: let w = stack.pop() onStack[w] = false comp.add w if w == v: break result.add comp work.setLen work.len - 1 if work.len > 0: lowlink[work[^1].v] = min(lowlink[work[^1].v], lowlink[v]) proc computeForwardedArgs(c: DepContext): seq[string] = ## Config/define forwarding shared by the frontend (`nim m`) and backend ## (`nim nifc`) child commands. Depends only on the driver's config, not on ## the dependency graph, so it is computed once per `nim ic` run (and also ## writes the precompiled-config artifact the children replay). ## # Forward the project's configuration to the per-module child processes. # Non-incremental compilation semchecks every module in one process with one # define set (the project's config files apply to the stdlib too); the IC # children compile with the *module* as their project file and would miss # e.g. compiler/nim.cfg's `define:nimPreviewSlimSystem`, so their `when` # bodies — and thus their import sets and NIF contents — would silently # diverge from the dependency graph computed here. Forward every define that # is not part of the compiler's built-in baseline, plus the threads switch. let nimcache = getNimcacheDir(c.config).string result = @[] let baseline = newStringTable(modeStyleInsensitive) initDefines(baseline) for k, v in pairs(c.config.symbols): if not baseline.hasKey(k) or baseline[k] != v: result.add "--define:" & k & (if v == "true": "" else: "=" & v) sort result result.add "--threads:" & (if optThreads in c.config.globalOptions: "on" else: "off") # Forward the memory-management mode too: the children would otherwise # compile with the default GC while the dependency graph here was computed # with the selected one (e.g. under --mm:refc the scanner keeps # system/gc's transitive imports but default-orc children never compile # them — phantom outputs that re-fire the build on every rerun). if c.config.selectedGC != gcUnselected: result.add "--mm:" & $c.config.selectedGC # method dispatch semantics must match across the child processes: # a child compiled without --multimethods:on builds different dispatch # buckets (and rejects calls as ambiguous that multi-dispatch accepts) if optMultiMethods in c.config.globalOptions: result.add "--multimethods:on" # Forward the debug-info switch: the cg children — not the driver — fill the # backend C names, and `--debugger:native` selects the Itanium mangling # scheme (ccgtypes.fillBackendName). A child without it would name routines # with the plain `_u` scheme while a sibling that read the project's # config.nims (`--debugger:native`) used Itanium, so the same symbol's # definition and cross-module references would disagree at link. if optCDebug in c.config.globalOptions: result.add "--debugger:native" # the children compile each MODULE as their own project file, which makes # that module's package the "main package" and unfilters foreign-package # diagnostics — a vendored package's hintAsError/warningAsError promotions # then abort builds the whole-program compilation accepts. Forward the # real project so children filter diagnostics identically. result.add "--icproject:" & c.config.projectFull.string # Precompiled config: every child replays the one artifact produced (in a # separate `nim icconfig` process) and already replayed by the driver itself — # see `icconfig.ensureIcConfig`, run before the driver's own `loadConfigs`. So # `nim ic` is always governed by this single artifact, for speed and so the # driver and its children agree by construction. Forward the path the driver # replayed (`conf.icPreparsedConfig`); `commandIc` has already guaranteed it # exists, else it bailed. result.add "--icPreparsedConfig:" & c.config.icPreparsedConfig proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): string = ## Frontend build file: the nifler (parse) and `nim m` (sem) rules only. The ## driver runs this to a discovery fixpoint; it produces every module's semmed ## NIF plus the cookie/edge sidecars that the backend build file then consumes. ## The backend step lives in its own nifmake run (generateBackendBuildFile) so ## that "which TUs rebuild" stays a pure nifmake mtime decision rather than ## something the driver interleaves with the `.s.deps` discovery loop. This ## split is also the scaffold for the per-module backend: once the backend is ## per-module, its rules slot into the backend file unchanged. let nimcache = getNimcacheDir(c.config).string createDir(nimcache) result = nimcache / c.nodes[0].files[0].modname & ".frontend.build.nif" var b = nifbuilder.open(result) defer: b.close() b.addHeader("nim ic", "nifmake") b.addTree "stmts" # Define nifler command b.addTree "cmd" b.addSymbolDef "nifler" b.addStrLit c.nifler b.addStrLit "parse" b.addStrLit "--deps" b.addTree "input" b.endTree() b.addTree "output" b.endTree() b.endTree() # Define nim m command b.addTree "cmd" b.addSymbolDef "nim_m" b.addStrLit getAppFilename() b.addStrLit "m" b.addStrLit "--nimcache:" & nimcache # Add search paths for p in c.config.searchPaths: b.addStrLit "--path:" & p.string for a in forwardedArgs: b.addStrLit a b.addTree "args" b.endTree() b.withTree "input": b.addIntLit 0 # main parsed file b.endTree() # Build rules for parsing (nifler) var seenFiles = initHashSet[string]() for node in c.nodes: for pair in node.files: let parsed = c.parsedFile(pair) if not seenFiles.containsOrIncl(parsed): b.addTree "do" b.addIdent "nifler" b.addTree "input" b.addStrLit pair.nimFile b.endTree() b.addTree "output" b.addStrLit parsed b.endTree() b.addTree "output" b.addStrLit c.depsFile(pair) b.endTree() b.endTree() # Build rules for semantic checking (nim m). # # Modules are grouped into strongly-connected components: a module that is not # in an import cycle is its own singleton group and compiles in its own # `nim m ` invocation as before. A cycle (A imports B, B imports A) cannot # be ordered for separate per-module compilation, so the whole component is # handed to a single `nim m` invocation: the first member is the project file, # every member is passed via `--icGroup:` so the compiler compiles them # all from source in one process (resolving the recursion in-memory) and writes # a NIF for each. Only dependencies *outside* the component become build-graph # inputs — intra-component edges are produced by this very rule and listing # them would reintroduce the cycle nifmake just rejected. let sccs = computeSCCs(c) var sccOf = newSeq[int](c.nodes.len) for sccId, comp in sccs: for nodeIdx in comp: sccOf[nodeIdx] = sccId for comp in sccs: # Representative (project file for this invocation) = smallest node id, so a # component containing the root (node 0) is driven by the root. var members = comp members.sort() let repPair = c.nodes[members[0]].files[0] let isGroup = members.len > 1 b.addTree "do" b.addIdent "nim_m" b.addTree "args" # The root module (node 0) is the program's real entry point; mark it so # `isMainModule` resolves to true only for it (every module otherwise gets # `sfMainModule` for NIF writing under `nim m`). if members[0] == 0: b.addStrLit "--isMainModule:on" # For a real cycle, tell the compiler which modules form the group so it # compiles them all from source and writes each one's NIF. if isGroup: for m in members: b.addStrLit "--icGroup:" & c.nodes[m].files[0].nimFile b.endTree() # Input 0 (the project file passed to `nim m`): the representative's .nim. b.withTree "input": b.addStrLit repPair.nimFile # All parsed files of every member (nifler outputs this group consumes). for m in members: for f in c.nodes[m].files: b.addTree "input" b.addStrLit c.parsedFile(f) b.endTree() # Depend on the dependencies *outside* this component — on their interface # COOKIE sidecars, not the semmed NIFs themselves: the sidecar's mtime only # moves when the dep's importer-visible surface (or, via hash chaining, any # surface in its import closure) changed, so body-only edits stop the # re-sem cascade right here. Dependencies whose BODIES the last sem of a # member consumed at compile time (the recorded NeedsImpl edge set) are # gated on their IMPL cookie instead, which flips on any content change: # `const x = dep.foo()` then re-sems when foo's body changes. # `-d:icNoIfaceGate` restores the old full-NIF edges. let ifaceGate = not isDefined(c.config, "icNoIfaceGate") var needsImpl = initHashSet[string]() if ifaceGate: # union over the members; restricted to the group's transitive dep # closure: a stale recording naming a module this group no longer # imports cannot be consumed anymore (and honoring it could even create # a build-graph cycle after refactorings). var reachable = initHashSet[string]() var stack: seq[int] = @[] for m in members: for depIdx in c.nodes[m].deps: if sccOf[depIdx] != sccOf[members[0]]: stack.add depIdx var visited = initHashSet[int]() while stack.len > 0: let n = stack.pop() if visited.containsOrIncl(n): continue reachable.incl c.nodes[n].files[0].modname for depIdx in c.nodes[n].deps: stack.add depIdx for m in members: for suffix in readNeedsImpl(c, c.nodes[m].files[0]): if suffix in reachable: needsImpl.incl suffix var seenDep = initHashSet[string]() var directDeps = initHashSet[string]() for m in members: for depIdx in c.nodes[m].deps: if sccOf[depIdx] == sccOf[m]: continue # intra-component edge let depName = c.nodes[depIdx].files[0].modname directDeps.incl depName let depFile = if not ifaceGate: c.semmedFile(c.nodes[depIdx].files[0]) elif depName in needsImpl: c.implFile(depName) else: c.ifaceFile(c.nodes[depIdx].files[0]) if not seenDep.containsOrIncl(depFile): b.addTree "input" b.addStrLit depFile b.endTree() # NeedsImpl on modules that are not direct imports (bodies consumed via # re-exports or transitively, e.g. a macro's private helper two hops # away): additional impl-cookie inputs. if ifaceGate: var extra: seq[string] = @[] for suffix in needsImpl: if suffix notin directDeps: extra.add suffix sort extra for suffix in extra: b.addTree "input" b.addStrLit c.implFile(suffix) b.endTree() # Output: one semmed NIF (plus its cookie/edge sidecars) per member. for m in members: b.addTree "output" b.addStrLit c.semmedFile(c.nodes[m].files[0]) b.endTree() if ifaceGate: b.addTree "output" b.addStrLit c.ifaceFile(c.nodes[m].files[0]) b.endTree() b.addTree "output" b.addStrLit c.implFile(c.nodes[m].files[0].modname) b.endTree() b.addTree "output" b.addStrLit c.edgesFile(c.nodes[m].files[0]) b.endTree() b.endTree() b.endTree() # stmts proc backendCFile(c: DepContext; node: Node): string = ## The `.c` path the backend writes for `node`, computed exactly as ## `cgen.getCFile` does: `mangleModuleName` of the module's cfilename, which ## is the source path for the main module (registered at its source index) and ## the NIF suffix for every dependency (a `fikNifModule` whose `toFullPath` is ## the suffix). Lets nifmake declare a per-module output without loading any ## backend module. let cfilename = if node.id == 0: AbsoluteFile node.files[0].nimFile else: AbsoluteFile node.files[0].modname result = changeFileExt(completeCfilePath(c.config, mangleModuleName(c.config, cfilename).AbsoluteFile), ".nim.c").string proc computeLiveBackendNodes(c: DepContext): seq[bool] = ## Which nodes the backend must code-generate: the closure reachable from the ## program roots (main + `system` + `--import`ed modules) via the REAL, ## post-sem import edges (`.s.deps`). ## ## The static `.deps` scan over-approximates: it cannot evaluate guards like ## `when defined(windows)` or const-aliased ones (`when useWinVersion`, with ## `const useWinVersion = defined(windows) or defined(nimdoc)`), so it keeps ## the dead branch's import. e.g. on Linux `nativesockets`'s static deps list ## `winlean`; the discovery fixpoint only ever *adds* edges, never prunes, so ## `winlean` stays a node and got a full `lower`/`cg`/`emit`/link pipeline. ## That is harmless for sem (an extra `nim m`) but fatal for codegen: ## `winlean`'s `importc, header: "winsock2.h"` decls emit ## `#include "winsock2.h"` into a C file that cannot compile off-Windows. ## Sem's resolved import set (`.s.deps`) is the real program graph — the ## non-IC compiler would never touch `winlean` here — so restrict the backend ## to it. (`.s.deps` is the same data the discovery loop trusts; it is written ## for every sem'd module, including grouped SCC members.) result = newSeq[bool](c.nodes.len) var stack: seq[int] = @[0] # main module if c.systemNodeId >= 0: stack.add c.systemNodeId for impId in c.implicitNodeIds: stack.add impId # every module imports these while stack.len > 0: let ni = stack.pop() if ni < 0 or ni >= c.nodes.len or result[ni]: continue result[ni] = true for p in readSemDeps(c, c.nodes[ni].files[0]): let idx = c.processedModules.getOrDefault(c.toPair(p).modname, -1) if idx >= 0: stack.add idx proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string = ## Per-module backend build file. One `nim_nifc` command template (the actual ## stage/module switches ride in each rule's `(args …)`), then the stages of ## the per-module backend as separate nifmake rules: ## cg(per module) -> merge -> emit(per module) -> link ## Every module's semmed NIF is a leaf input (produced by the frontend run). ## `cg` emits a module's whole demanded closure into its `.c.nif` ## (emit-everywhere); `merge` picks one owner per duplicated definition across ## all `.c.nif`; `emit` renders each module's `.c` (dropping non-owned/dead ## bodies); `link` compiles and links every `.c` in one `callCCompiler`. The ## main module's `cg` depends on every other `.c.nif` because it reads their ## init/datInit meta heads to wire up NimMain, so it must run last. let nimcache = getNimcacheDir(c.config).string createDir(nimcache) result = nimcache / c.nodes[0].files[0].modname & ".backend.build.nif" let mainNif = c.nodes[0].files[0].nimFile # Honor `--out`/`--outdir`: `cmdIc`'s `setOutFile` populated `conf.outFile` # (the user's `--out`, or the default ``), so `absOutFile` is # the final link target — exactly what a whole-program `nim c` would produce. # The `link` child computes its own output from its project name, so the path # is also forwarded to it below. let exeFile = string(c.config.absOutFile) let mergeFile = nimcache / MergeDecisionFile # Per-node output paths. var cnifFiles = newSeq[string](c.nodes.len) var cFiles = newSeq[string](c.nodes.len) var tFiles = newSeq[string](c.nodes.len) # The `lower` stage writes a PROPER module NIF the cg/emit stages load via # `toNifFilename` (a `.s.bif` sibling), so its `.t.bif` lives at the suffix base # (mirroring `semmedFile`), not next to the throwaway `.c`. for i, node in c.nodes: cFiles[i] = backendCFile(c, node) cnifFiles[i] = cFiles[i] & ".nif" tFiles[i] = nimcache / node.files[0].modname & ".t.bif" # Only code-generate modules the real program actually reaches; statically # over-approximated nodes (e.g. `winlean` on Linux) are sem'd but not emitted. let live = computeLiveBackendNodes(c) # Drop a pruned node's stale backend artifacts: the `merge` stage globs # `*.c.nif` off disk (not the build-file inputs) and the `link` stage scans # the loaded closure's `.c`s, so a leftover `.c.nif`/`.c` from a run before # this module became unreachable (a prior over-approximated build, or an edit # that removed its last real importer) would still be merged/compiled — # reintroducing exactly the off-platform `#include` this prune avoids. var prunedStale = false for i in 0 ..< c.nodes.len: if not live[i]: # `fileExists` before remove so we only force a merge recompute (below) # when an artifact was actually present — i.e. a build where this module # WAS emitted, not the steady state where it never is. if fileExists(cnifFiles[i]) or fileExists(cFiles[i]): prunedStale = true removeFile(cnifFiles[i]) removeFile(cFiles[i]) # The merge decision is a pure function of the set of `.c.nif`s present; if we # just removed an over-approximated module's artifacts, a decision computed # while they were present is stale — it can name a now-absent module as a # symbol's owner (`asyncdispatch` owning `NTIdomain` here), leaving that symbol # undefined at link. nifmake will not re-fire `merge` on its own: dropping an # input makes no remaining input newer than the output. Delete the decision so # the (now missing) output forces a recompute against the live `.c.nif` set. if prunedStale: removeFile(mergeFile) var b = nifbuilder.open(result) defer: b.close() b.addHeader("nim ic", "nifmake") b.addTree "stmts" # Command template: `nifc --nimcache … --path … # `. The trailing `(args)` is filled per rule with the stage and # module switches; `(input 0)` is the project file. b.addTree "cmd" b.addSymbolDef "nim_nifc" b.addStrLit getAppFilename() b.addStrLit "nifc" b.addStrLit "--nimcache:" & nimcache for p in c.config.searchPaths: b.addStrLit "--path:" & p.string for a in forwardedArgs: b.addStrLit a b.addTree "args" b.endTree() # The project file is a fixed command ARGUMENT, not a tracked input: backend # stages read NIFs (resolved by suffix), never the `.nim` source, so its # content cannot change any artifact. Passing it as `(input 0)` made its mtime # an input to every rule, so editing the main module's source re-fired the # whole backend. b.addStrLit mainNif b.endTree() template inputStr(s: string) = b.addTree "input" b.addStrLit s b.endTree() template outputStr(s: string) = b.addTree "output" b.addStrLit s b.endTree() # lower: one rule per module. Transforms (eventually) the routines the module # OWNS once, in the owner's id space, into `.t.nif`, so the `cg` stage # reads them instead of re-deriving (which makes a closure `:env`'s identity # diverge across the parallel `cg` processes). Runs per module in parallel. # # Input is this module's OWN semmed NIF and nothing else. A module does NOT # depend on its importers, so listing every semmed NIF (or even the import # closure) was wrong: it made e.g. `strutils`'s rule depend on the `finish` # that imports it. nifmake handles the indirect dependency for free — the # frontend writes `.s.nif`s content-stably, so an interface change to a # dependency re-sems (and re-emits the `.s.nif` of) every transitive importer; # a module whose own `.s.nif` is unchanged genuinely needs no re-lowering. for i, node in c.nodes: if not live[i]: continue b.addTree "do" b.addIdent "nim_nifc" b.withTree "args": b.addStrLit "--icBackendStage:lower" b.addStrLit "--icBackendModule:" & node.files[0].modname inputStr c.semmedFile(node.files[0]) outputStr tFiles[i] b.endTree() # cg: one rule per module. Input is this module's OWN `.t.nif`. cg DOES read # its dependencies' `.t.nif`s at runtime (loadDepClosure), but ordering is # guaranteed by nifmake's depth-barriered scheduler: every `lower` is depth 1 # (its `.s.nif` is a leaf) and every `cg` is depth 2, so all lowering finishes # before any cg starts — no need to list the closure for ordering. For # invalidation, a dependency's change reaches this module through its own # `.t.nif` (own `.s.nif` re-sem -> own `lower`); a foreign body this module # emit-everywhere'd but does not own is dropped by `emit` regardless, so a # stale copy here is harmless. The main module additionally depends on every # other `.c.nif` (it reads their init/datInit metas to wire up NimMain). for i, node in c.nodes: if not live[i]: continue b.addTree "do" b.addIdent "nim_nifc" b.withTree "args": b.addStrLit "--icBackendStage:cg" b.addStrLit "--icBackendModule:" & node.files[0].modname inputStr tFiles[i] if node.id == 0: for j in 0 ..< c.nodes.len: if c.nodes[j].id != 0 and live[j]: inputStr cnifFiles[j] outputStr cnifFiles[i] b.endTree() # merge: read every `.c.nif`, write the ownership/liveness decision. b.addTree "do" b.addIdent "nim_nifc" b.withTree "args": b.addStrLit "--icBackendStage:merge" for i in 0 ..< c.nodes.len: if live[i]: inputStr cnifFiles[i] outputStr mergeFile b.endTree() # emit: render each module's `.c` from its `.c.nif` + the merge decision. for i, node in c.nodes: if not live[i]: continue b.addTree "do" b.addIdent "nim_nifc" b.withTree "args": b.addStrLit "--icBackendStage:emit" b.addStrLit "--icBackendModule:" & node.files[0].modname # Inputs: this module's OWN `.c.nif` and the global merge decision. emit also # loads `.t.nif`s at runtime (getCFile/type resolution), but those are depth 1 # and emit is past the merge barrier, so they always exist — no need to list # them. (emit still re-fires for every module whenever `merge` rewrites the # decision file; making that incremental is a separate concern.) inputStr cnifFiles[i] inputStr mergeFile outputStr cFiles[i] b.endTree() # link: compile + link every emitted `.c` in one process. b.addTree "do" b.addIdent "nim_nifc" b.withTree "args": b.addStrLit "--icBackendStage:link" # The link child is its own `cmdNifC` process whose project is the main # module, so it would default the binary to `/
`. # Forward the resolved target so it writes exactly `exeFile` (`--out`'s # path splits back into outDir+outFile in the child). b.addStrLit "--out:" & exeFile for i in 0 ..< c.nodes.len: if live[i]: inputStr cFiles[i] outputStr exeFile b.endTree() b.endTree() # stmts proc commandIc*(conf: ConfigRef; frontendOnly = false) = ## Main entry point for `nim ic`. With `frontendOnly` (used by `nim track` for ## IDE queries) it runs only Phase 1 — the incremental nifler + `nim m` ## frontend that writes every module's `.s.bif` — and skips the whole-program ## backend (`nim nifc` -> C -> link), which a goto-def / find-usages scan does ## not need. when not defined(nimKochBootstrap): let nifler = findNifler() if nifler.len == 0: rawMessage(conf, errGenerated, "nifler tool not found. Install nimony or add nifler to PATH.") return # Resolve the `.nim` source first, exactly like `wantMainModule`. Without # this, an extensionless project arg (`nim ic path/to/foo`) resolves to a # same-named sibling that already exists — e.g. the ELF a prior `nim c` # left behind — and nifler chokes on the binary (`invalid token \127`, # ELF magic). `addFileExt` only appends when there is no extension. conf.projectFull = addFileExt(conf.projectFull, NimExt) let projectFile = conf.projectFull.string if not fileExists(projectFile): rawMessage(conf, errGenerated, "project file not found: " & projectFile) return # Create nimcache directory; start from a clean one when its format # stamp is absent or outdated (see `icFormatVersion`) let cacheDir = getNimcacheDir(conf).string createDir(cacheDir) let versionFile = cacheDir & "/ic.version" let stamp = if fileExists(versionFile): readFile(versionFile) else: "" if stamp != icFormatVersion: removeDir(cacheDir) createDir(cacheDir) writeFile(versionFile, icFormatVersion) var c = DepContext( config: conf, nifler: nifler, nodes: @[], processedModules: initTable[string, int](), includeStack: @[], systemNodeId: -1 ) # Create root node for main project file let rootPair = c.toPair(projectFile) let rootNode = Node(files: @[rootPair], id: 0) c.nodes.add rootNode c.processedModules[rootPair.modname] = 0 # model the system.nim dependency: let sysPair = toPair(c, (conf.libpath / RelativeFile"system.nim").string) if sysPair.modname != rootPair.modname: let sysNode = Node(files: @[sysPair], id: 1) c.nodes.add sysNode c.systemNodeId = sysNode.id rootNode.deps.add sysNode.id c.processedModules[sysPair.modname] = sysNode.id # Traverse system.nim's own dependency tree. `nim m system.nim` compiles # system's entire import closure from source in one process (none of it # can be precompiled: every module implicitly imports system) and writes # a NIF for each closure member. Every member also gets the implicit # dependency edge on system, so Tarjan folds the whole closure into # system's strongly-connected component and the build file contains a # single rule producing all of those NIFs. Without this traversal each # closure member that is also imported by an ordinary module got its own # `nim m` rule whose output silently OVERWROTE the system-written NIF # with freshly numbered type ids, leaving dangling type references (the # ids are baked into sysma2dyk.nif and into every module semchecked # against the first version) — "symbol has no offset" failures that # depended on nifmake's scheduling. traverseDeps(c, sysPair, sysNode) # Model `--import:X` switches (conf.implicitImports). Every ordinary module # is compiled with these implicitly imported, so each `nim m` child demands # the corresponding NIF. They are invisible to the static import scanner # (they come from config, not from `import` statements) and cannot be # discovered via `.s.deps` either: every module fails identically at import # resolution before recording anything, so there is no bootstrap. Seed them # up front like system.nim — create a node, traverse its closure, and record # its id so `processImport` adds the edge to every other module. (e.g. Nimbus # uses `--import:libbacktrace` together with `-d:nimStackTraceOverride`.) for imp in conf.implicitImports: let resolved = resolveImport(c, rootPair.nimFile, imp) if resolved.len == 0 or not fileExists(resolved): continue let impPair = toPair(c, resolved) if impPair.modname.len > 0 and impPair.modname notin c.processedModules: let impNode = Node(files: @[impPair], id: c.nodes.len) if c.systemNodeId >= 0: impNode.deps.add c.systemNodeId c.nodes.add impNode c.processedModules[impPair.modname] = impNode.id rootNode.deps.add impNode.id c.implicitNodeIds.add impNode.id traverseDeps(c, impPair, impNode) # Process dependencies traverseDeps(c, rootPair, rootNode) # Discovery via `.s.deps`: imports GENERATED by macros (chronicles builds # `import chronicles/textlines` via parseStmt from the chronicles_sinks # define) are invisible to the static scanner. Each `nim m` records the # imports it ACTUALLY resolved (static + macro-generated) into a # `.s.deps.nif` sidecar (ast2nif.writeSemDeps); a child that fails on a # not-yet-built import flushes it before erroring. We re-derive the graph # from those sidecars — adding any module the scanner missed, plus the edge # from its importer — and rerun; nifmake's mtime pruning keeps completed # work. A round that discovers nothing new but still fails is a real error. let forwardedArgs = computeForwardedArgs(c) # The precompiled config drives every `nim m`/`nim nifc` child and the driver # itself (`ensureIcConfig` produced it and `loadConfigs` replayed it). If it # is not on disk something went wrong producing it — children would each # silently fall back to re-parsing the whole config chain — so refuse to # continue without it. if conf.icPreparsedConfig.len == 0 or not fileExists(conf.icPreparsedConfig): rawMessage(conf, errGenerated, "precompiled config missing: " & conf.icPreparsedConfig) return let nifmake = findNifmake() # Build the per-module rules concurrently: nifmake fans out all commands at # each DAG depth via execProcesses (defaults to all cores). Cold builds are # otherwise serial (one child at a time) and leave the machine idle. An # uncapped fan-out across many cores can exhaust RAM on a large project (each # `nim m`/`cg` child holds its own module graph), which nifmake's own `-j:N` # exists to bound. Concurrency is chosen (highest precedence first): # * `-d:icNoParallel` -> serial (readable, non-interleaved child output) # * `-d:icJobs:N` -> cap at N (legacy IC-tuning define) # * `--parallelBuild:N` -> cap at N (the standard Nim build-parallelism # flag; a no-op for `nim c` under IC, so we give # it meaning here — lets Nimbus devs pick their # own value without a `-d:` define) # * otherwise -> uncapped (all cores) let parallel = if isDefined(conf, "icNoParallel"): "" elif isDefined(conf, "icJobs"): " --parallel:" & conf.symbols["icJobs"] elif conf.numberOfProcessors > 0: " --parallel:" & $conf.numberOfProcessors else: " --parallel" # Phase 1 — frontend (nifler + `nim m`), run to a discovery fixpoint. var rounds = 0 var frontendOk = false while true: let buildFile = generateFrontendBuildFile(c, forwardedArgs) rawMessage(conf, hintSuccess, "generated: " & buildFile) if nifmake.len == 0: rawMessage(conf, hintSuccess, "run:" & " nifmake run" & parallel & " " & buildFile) # without nifmake we can only print the manual commands; emit the # backend's too (best effort — discovery cannot run) and stop. An IDE # query (`frontendOnly`) needs no backend, so skip it there. if not frontendOnly: let backendFile = generateBackendBuildFile(c, forwardedArgs) rawMessage(conf, hintSuccess, "generated: " & backendFile) rawMessage(conf, hintSuccess, "run:" & " nifmake run" & parallel & " " & backendFile) return let cmd = quoteShell(nifmake) & " run" & parallel & " " & quoteShell(buildFile) rawMessage(conf, hintExecuting, cmd) let exitCode = execShellCmd(cmd) if exitCode == 0: frontendOk = true break # Re-derive from the post-sem deps of every node compiled so far. Imports # the static scanner missed become new nodes; the importer->import edge # the scanner could not see is added so the discovered module builds # first. (Static-import edges are already present, so `notin deps` skips # the redundant ones.) var discovered = false inc rounds if rounds <= 20: let n0 = c.nodes.len # snapshot: new nodes are traversed as they're added for ni in 0 ..< n0: for p in readSemDeps(c, c.nodes[ni].files[0]): let pair = c.toPair(p) var idx = c.processedModules.getOrDefault(pair.modname, -1) if idx == -1: let newNode = Node(files: @[pair], id: c.nodes.len) if c.systemNodeId >= 0: newNode.deps.add c.systemNodeId if getsImplicitImports(c, pair.nimFile): for impId in c.implicitNodeIds: if impId != newNode.id: newNode.deps.add impId c.processedModules[pair.modname] = newNode.id c.nodes.add newNode idx = newNode.id traverseDeps(c, pair, newNode) discovered = true if idx != ni and idx notin c.nodes[ni].deps: c.nodes[ni].deps.add idx discovered = true if not discovered: rawMessage(conf, errGenerated, "nifmake failed with exit code: " & $exitCode) break # Phase 2 — backend (whole-program `nim nifc`), run once over the now-final # graph. Kept a separate nifmake run so backend rebuilds are decided purely # by nifmake's input mtimes, independent of frontend discovery. # An IDE query (`frontendOnly`) stops after Phase 1: the `.s.bif` it scans # are all produced by the frontend; codegen + link would be wasted work. if frontendOk and not frontendOnly: let backendFile = generateBackendBuildFile(c, forwardedArgs) rawMessage(conf, hintSuccess, "generated: " & backendFile) let cmd = quoteShell(nifmake) & " run" & parallel & " " & quoteShell(backendFile) rawMessage(conf, hintExecuting, cmd) let exitCode = execShellCmd(cmd) if exitCode != 0: rawMessage(conf, errGenerated, "nifmake (backend) failed with exit code: " & $exitCode) else: rawMessage(conf, errGenerated, "nim ic not available in bootstrap build")