IC: per-module backend codegen stage (Phase 2b, B1)

--icBackendStage:cg --icBackendModule:<suffix> generates C for a single module
and writes only its .c.nif (no merge, no .c render, no cc/link -- separate
stages). The whole program is still loaded so types resolve, but only the
target module is code-generated; findPendingModule routes every demand into it
(emit-everywhere into the current module), so a definition gets its canonical
owner-suffixed C name regardless of which module's process emits it -- cross-
process duplicates then collide by exact name, ready for the merge stage to keep
one and prototype the rest.

Validated: cg of the main module of a 2-module project recreates its .c.nif with
the demanded closure (greet/add named by their owner suffix); a leaf module whose
procs only callers use yields an (correct) empty .c.nif. Whole-program backend
path unchanged (dispatch guarded on icBackendStage), koch ic thallo green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Araq
2026-06-13 21:20:14 +02:00
parent 7ea5aeedfd
commit 9916d09db3
2 changed files with 79 additions and 0 deletions

View File

@@ -69,6 +69,14 @@ proc getCFile*(m: BModule): AbsoluteFile
proc findPendingModule(m: BModule, s: PSym): BModule =
# TODO fixme
if m.config.cmd == cmdNifC and m.config.icBackendStage == "cg":
# Per-module backend codegen: only module M (`m`) is emitted in this
# process, so every demanded definition — whether a normal proc owned by
# another (here unwritten) module or a minted instance/hook — is emitted
# into M's TU. Definitions owned elsewhere are emitted again by their own
# module's cg process; the merge stage keeps one per C name and turns the
# rest into prototypes (which already live in the unmarked protos section).
return m
if m.config.symbolFiles == v2Sf or optCompress in m.config.globalOptions:
let ms = s.itemId.module #getModule(s)
result = m.g.mods[ms]

View File

@@ -505,9 +505,80 @@ proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
for t in g.icPreserveTypeInfos[moduleId]:
discard genTypeInfo(g.config, bmod, t, unknownLineInfo)
proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
## Per-module backend codegen (`--icBackendStage:cg --icBackendModule:<suffix>`):
## generate C for the single module named by `icBackendModule` and write only
## its `.c.nif` artifact (no merge, no `.c` render, no cc/link — those are
## separate nifmake rules).
##
## The whole program is still LOADED (so every type/symbol resolves), but only
## the target module is code-generated; `findPendingModule` routes every demand
## into it (emit-everywhere). Loading only the target's import closure is a
## later optimization — correctness first.
resetForBackend(g)
var isKnownFile = false
let systemFileIdx = registerNifSuffix(g.config, "sysma2dyk", isKnownFile)
g.config.m.systemFileIdx = systemFileIdx
var precompSys = moduleFromNifFile(g, systemFileIdx, {LoadFullAst, AlwaysLoadInterface})
g.systemModule = precompSys.module
var nifFiles: seq[string] = @[toNifFilename(g.config, systemFileIdx)]
let modules = loadModuleDependencies(g, mainFileIdx, nifFiles)
flushMethodReplays(g)
if modules.len == 0:
rawMessage(g.config, errGenerated,
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
return
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)
for m in modules:
discard setupNifBackendModule(g, m.module)
if precompSys.module != nil:
discard setupNifBackendModule(g, precompSys.module)
# Locate the target module by its NIF suffix.
let targetSuffix = g.config.icBackendModule
var target = PrecompiledModule(module: nil)
for m in modules:
if cachedModuleSuffix(g.config, FileIndex m.module.position) == targetSuffix:
target = m
break
if target.module == nil and precompSys.module != nil and
cachedModuleSuffix(g.config, FileIndex precompSys.module.position) == targetSuffix:
target = precompSys
if target.module == nil:
rawMessage(g.config, errGenerated,
"per-module codegen: module not found for suffix: " & targetSuffix)
return
generateCodeForModule(g, target)
# The main module also owns the whole-program method dispatchers + NimMain.
if sfMainModule in target.module.flags:
emitMethodDispatchers(g)
let bl = BModuleList(g.backend)
let tb = bl.mods[target.module.position]
if tb != nil:
finishModule(g, tb)
# Writes only the target's `.c.nif` (every other loaded module's TU is empty,
# so `cgenWriteModules` emits no artifact for it). cc/link are NOT run here.
cgenWriteModules(g.backend, g.config)
proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
## Main entry point for NIF-based C code generation.
## Traverses the module dependency graph and generates C code.
if g.config.icBackendStage == "cg":
generateCgStage(g, mainFileIdx)
return
elif g.config.icBackendStage.len > 0:
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")