cleanups: the .t.nif files are not negotiable, follows Nimony's hexer split design

This commit is contained in:
Araq
2026-06-21 08:52:57 +02:00
parent abd948296a
commit 46b0555326
6 changed files with 112 additions and 488 deletions

View File

@@ -198,8 +198,8 @@ type
depSuffixes: HashSet[string] # module suffixes already emitted as `(import ...)` deps
emittedBackendTypes: HashSet[int32] # backend-local type items already def'd this module
emittedBackendSyms: HashSet[int32] # backend-local sym items already def'd this module
lowering: bool # serializing the `lower` stage's `.t.nif` (per-entry self-contained)
emittedFieldSyms: HashSet[ItemId] # lowering: derived env-field syms already def'd this entry
lowering: bool # serializing the `lower` stage's whole-module `.t.nif`
emittedFieldSyms: HashSet[ItemId] # lowering: derived env-field syms already def'd
inTypeReclist: int # >0 while writing a type's OWN reclist: fields must be SELF-CONTAINED
# defs (the type can be seek-loaded in isolation), not entry-deduped uses
@@ -718,7 +718,6 @@ var implTag = registerTag("implementation")
var reexpModTag = registerTag("reexpmod")
var offerTag = registerTag("offer")
var typeOfferTag = registerTag("toffer")
var loweredTag = registerTag("lowered")
proc registerNifAstTags*() =
## (Re)registers ast2nif's NIF tags explicitly. The top-level `registerTag`
@@ -751,7 +750,6 @@ proc registerNifAstTags*() =
reexpModTag = registerTag("reexpmod")
offerTag = registerTag("offer")
typeOfferTag = registerTag("toffer")
loweredTag = registerTag("lowered")
proc writeNode(w: var Writer; dest: var TokenBuf; n: PNode; forAst = false) =
if n == nil:
@@ -1518,71 +1516,6 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
writeImplCookie(config, thisModule, dest, ifaceHex)
writeEdgesFile(config, thisModule, implDeps)
proc collectLoweredLocals(w: var Writer; n: PNode) =
## Record every transform-created local (a `Complete`, non-`@bk` sym owned by
## the module being serialized) in `w.locals` so it is written SUFFIX-LESS as
## an inline def. Pre-existing entities (params, result, callees) were sealed
## before serialization, so they are skipped here and emit as index SymUses.
if n == nil: return
if n.kind == nkSym and n.sym != nil:
let s = n.sym
if not s.itemId.isBackendMinted and s.itemId.module == w.currentModule and
s.state == Complete:
w.locals.incl s.itemId
for i in 0 ..< n.safeLen:
collectLoweredLocals(w, n[i])
proc serializeLoweredBodies*(config: ConfigRef; ownerModule: int32;
entries: openArray[tuple[name: string; body: PNode]];
hooks: openArray[LogEntry];
outfile: string) =
## Write the `lower` backend stage's `.t.nif`. Three sections, all covered by
## the file's embedded index (which the `cg` loader registers as the module's
## SECOND index/stream — see loadLoweredBodies):
## - `(repdestroy/repcopy/... key sym)`: the type-bound ops the lower stage
## lifted while transforming (closure-env `=destroy` etc.). `cg`'s
## `registerLoadedHooks` re-attaches them so `injectDestructorCalls` (kept in
## cg) resolves them via `getAttachedOp`'s key fallback.
## - the hook ROUTINES as full `(sd)`: NEW in the lower stage (no `.s.nif`
## signature), so serialize sig + transformed body whole; cg loads them via
## the `.t.nif` index when the HookEntry's SymUse resolves, then demand-emits.
## - owned routines' transformed bodies as `(lowered "<nifname>" <body>)`: only
## the body (signature comes from the `.s.nif`); applyLoweredBodies sets it on
## the existing sym.
## A `@bk` closure-env type/sym referenced anywhere is emitted inline AND
## indexed, so it resolves through the embedded index regardless of section.
var w = Writer(infos: LineInfoWriter(config: config), currentModule: ownerModule)
w.inProc = 1 # we are serializing routine *bodies*
w.lowering = true
var dest = createTokenBuf(256)
createStmtList(dest, NoLineInfo)
for op in hooks:
writeOp(w, dest, op)
var emittedHooks = initHashSet[int32]()
for op in hooks:
if op.sym != nil and op.sym.kindImpl in routineKinds and
not emittedHooks.containsOrIncl(op.sym.itemId.item):
w.emittedBackendTypes.clear()
w.emittedBackendSyms.clear()
w.emittedFieldSyms.clear()
w.locals.clear()
writeSymDef(w, dest, op.sym)
for e in entries:
# Each `(lowered ...)` entry is loaded independently, so it must be
# SELF-CONTAINED: reset the per-Writer backend-local (@bk) dedup so every
# entry re-emits the def of any closure-env type/sym it references.
w.emittedBackendTypes.clear()
w.emittedBackendSyms.clear()
w.emittedFieldSyms.clear()
w.locals.clear()
collectLoweredLocals(w, e.body)
dest.addParLe loweredTag, NoLineInfo
dest.addStrLit e.name
writeNode(w, dest, e.body)
dest.addParRi
dest.addParRi()
writeFile(dest, outfile)
# --------------------------- Loader (lazy!) -----------------------------------------------
proc nodeKind(n: Cursor): TNodeKind {.inline.} =
@@ -1644,15 +1577,6 @@ type
suffix: string
contentStart: int # stream offset of the module body, so a full-AST load can
# rewind after lazy symbol loads moved the cursor
# The module's `.t.nif` (the `lower` stage's transformed bodies): a SECOND
# embedded-index + stream consulted when the main `.s.nif` index misses. The
# transformed bodies' backend-minted (`@bk`) entities (closure-env types/syms,
# temporaries) live ONLY here; they are named with this module's suffix, so
# `createTypeStub`/`loadSymStub` look them up here on a `.s.nif` miss and load
# them from `tStream` (see applyLoweredBodies). "Just use NIF's embedded index."
hasTIndex: bool
tStream: nifstreams.Stream
tIndex: Table[string, NifIndexEntry]
DecodeContext* = object
infos: LineInfoWriter
@@ -1688,8 +1612,8 @@ proc loadedState(c: DecodeContext): ItemState {.inline.} =
if c.infos.config.cmd == cmdNifC: Complete else: Sealed
proc cursorFromIndexEntry(c: var DecodeContext; module: FileIndex; entry: NifIndexEntry;
buf: var TokenBuf; fromT = false): Cursor =
let s = if fromT: addr c.mods[module].tStream else: addr c.mods[module].stream
buf: var TokenBuf): Cursor =
let s = addr c.mods[module].stream
s.r.jumpTo entry.offset
# A seek-load is self-contained: its tokens must decode their relative line
# info against `entry.info` ALONE. The stream's `parents` stack can be left at
@@ -1847,13 +1771,7 @@ proc createTypeStub(c: var DecodeContext; t: SymId): PType =
let ii = addr c.mods[modFi].index
var offs = ii[].getOrDefault(name)
if offs.offset == 0:
# A backend-minted (`@bk`) type produced by the `lower` stage lives in this
# module's `.t.nif`, not its `.s.nif` index. Resolve it through the `.t.nif`
# embedded index (loadType picks `tStream` for a tIndex-only name).
if c.mods[modFi].hasTIndex:
offs = c.mods[modFi].tIndex.getOrDefault(name)
if offs.offset == 0:
raiseAssert "symbol has no offset: " & name
raiseAssert "symbol has no offset: " & name
result = PType(itemId: id, uniqueId: id, kind: TTypeKind(k), state: Partial)
c.types[name] = (result, offs)
@@ -1949,15 +1867,6 @@ proc loadSymStub(c: var DecodeContext; t: SymId; thisModule: string;
let id = if isBk: backendItemId(module.int32, val[]) else: itemId(module.int32, val[])
var offs = c.mods[module].index.getOrDefault(symAsStr)
if offs.offset == 0 and c.mods[module].hasTIndex:
# A sym produced by the `lower` stage lives in this module's `.t.nif`, not
# its `.s.nif` index. This covers BOTH backend-minted (`@bk`) entities
# (closure `:env` param, `:tmp`/`res` temporaries) AND module-homed derived
# closure-env FIELDS (e.g. a captured `i`): the latter have a normal
# module suffix but are added to the env object only in the backend, so
# they are indexed solely in `.t.nif`. Resolve through that embedded index
# (loadSym picks `tStream` for a tIndex-only name).
offs = c.mods[module].tIndex.getOrDefault(symAsStr)
if offs.offset == 0:
# Only module/package self-syms are never written as `(sd)` entries, so a
# missing index offset means this is such a sym — typically the OWNER of an
@@ -2093,11 +2002,7 @@ proc loadType*(c: var DecodeContext; t: PType) =
else:
typeToNifSym(t, c.infos.config)
let modFi = t.itemId.module.FileIndex
# A name resolved through the `.t.nif` (tIndex) — a `lower`-stage closure-env
# type — must seek in `tStream`, not the `.s.nif` stream.
let fromT = c.mods[modFi].hasTIndex and not c.mods[modFi].index.hasKey(typeName) and
c.mods[modFi].tIndex.hasKey(typeName)
var n = cursorFromIndexEntry(c, modFi, c.types[typeName][1], buf, fromT = fromT)
var n = cursorFromIndexEntry(c, modFi, c.types[typeName][1], buf)
var localSyms = initTable[string, PSym]()
loadTypeFromCursor(c, n, t, localSyms)
@@ -2199,10 +2104,7 @@ proc loadSym*(c: var DecodeContext; s: PSym) =
var buf = createTokenBuf(30)
let symsModule = s.itemId.module.FileIndex
let nifname = globalName(s, c.infos.config)
# A `@bk` sym resolved through the `.t.nif` (tIndex) seeks in `tStream`.
let fromT = c.mods[symsModule].hasTIndex and not c.mods[symsModule].index.hasKey(nifname) and
c.mods[symsModule].tIndex.hasKey(nifname)
var n = cursorFromIndexEntry(c, symsModule, c.syms[nifname][1], buf, fromT = fromT)
var n = cursorFromIndexEntry(c, symsModule, c.syms[nifname][1], buf)
expect n, ParLe
if n.tagId != sdefTag:
@@ -2220,110 +2122,22 @@ proc loadSym*(c: var DecodeContext; s: PSym) =
inc n
loadSymFromCursor(c, s, n, c.mods[symsModule].suffix, localSyms)
proc sealLoadedBackendEntities*(c: var DecodeContext) =
## Before the `lower` stage serializes its transformed bodies, mark every
## index-loaded sym/type `Sealed`. The backend loads them `Complete` (mutable
## for the transform); without this, `writeNode`'s `shouldWriteSymDef` would
## emit a duplicate `(sd)`/`(td)` def for a param/result/existing-local/owner
## type — and the `cg` body-loader would then bind the body's `result` to a
## FRESH sym instead of the one in `prc.ast[resultPos]` (the #6/#7 "result
## cannot be captured" class). Sealed ⟹ SymUse ⟹ resolved via `cg`'s module
## index. The transform-CREATED entities are absent from `c.syms`/`c.types`
## (or are backend-minted), so they stay `Complete`/`@bk` and still get the
## inline defs the loader's local-sym pre-scan needs.
for _, v in c.syms:
if v[0] != nil and v[0].state == Complete: v[0].state = Sealed
for _, v in c.types:
if v[0] != nil and v[0].state == Complete: v[0].state = Sealed
proc sealLoadedRoutines*(c: var DecodeContext) =
## Whole-module-lowering variant of `sealLoadedBackendEntities`: seal ONLY the
## Before `writeLoweredModule` re-serializes the lowered module, seal ONLY the
## module's ROUTINE syms. A `.t.nif` written by `writeLoweredModule` is the
## SOLE source the `cg` stage loads (there is no `.s.nif` fallback for its
## bodies), so unlike the per-`(lowered)`-entry path, every type, global, param
## and local must still emit a REAL def in it — only cross-routine references
## may be `SymUse`s (each routine's def is emitted once, at module scope, by
## the explicit stub loop). Types/globals stay `Complete` so `writeType`/
## `writeGlobals` emit them; routines become `Sealed` so a body referencing
## another routine writes a `SymUse` resolved through the module index.
## bodies), so every type, global, param and local must still emit a REAL def
## in it — only cross-routine references may be `SymUse`s (each routine's def is
## emitted once, at module scope, by the explicit stub loop). Types/globals stay
## `Complete` so `writeType`/`writeGlobals` emit them; routines become `Sealed`
## so a body referencing another routine writes a `SymUse` resolved through the
## module index.
for _, v in c.syms:
if v[0] != nil and v[0].state == Complete and v[0].kindImpl in routineKinds:
v[0].state = Sealed
proc resolveHookSym*(c: var DecodeContext; symId: nifstreams.SymId): PSym
proc repTagToOp(tagId: TagId): (bool, TTypeAttachedOp) =
## Map a `(rep…)` hook tag to its attached-op kind (and whether it IS one).
if tagId == repDestroyTag: (true, attachedDestructor)
elif tagId == repCopyTag: (true, attachedAsgn)
elif tagId == repWasMovedTag: (true, attachedWasMoved)
elif tagId == repDupTag: (true, attachedDup)
elif tagId == repSinkTag: (true, attachedSink)
elif tagId == repTraceTag: (true, attachedTrace)
elif tagId == repDeepCopyTag: (true, attachedDeepCopy)
else: (false, attachedDestructor)
proc loadLoweredBodies*(c: var DecodeContext; module: FileIndex; suffix: string;
infile: string; loadBodies = true):
tuple[bodies: seq[tuple[name: string; body: PNode]]; hooks: seq[LogEntry]] =
## Reconstruct the `lower` stage's `.t.nif`. Registers its embedded index as the
## module's SECOND index/stream (`tIndex`/`tStream`) so every backend-minted
## (`@bk`) closure-env entity resolves through NIF's own index — `createTypeStub`/
## `loadSymStub` fall through to it on a `.s.nif` miss, `loadType`/`loadSym` seek
## `tStream`. Returns the owned routines' transformed bodies (the `(lowered …)`
## entries) and the lifted type-bound ops (`(rep… key sym)`); the hook ROUTINES'
## `(sd)` defs are loaded lazily through the index when their op's sym resolves.
result = (@[], @[])
if not fileExists(infile): return
var tstream = nifstreams.open(infile)
let tindex = readEmbeddedIndex(tstream) # leaves the cursor at the content start
let contentStart = offset(tstream.r)
c.mods[module].tStream = tstream
c.mods[module].tIndex = tindex
c.mods[module].hasTIndex = true
# Parse the WHOLE content up front: the per-body loads below lazily seek
# `tStream` for `@bk` entities, which would otherwise clobber a live walk cursor.
var buf = createTokenBuf(256)
tstream.r.jumpTo contentStart
nifcursors.parse(tstream, buf, NoLineInfo)
var n = beginRead(buf)
if n.kind != ParLe: return
inc n # into (stmts -> flags dot
inc n # -> type dot
inc n # -> first entry or ParRi
while n.kind == ParLe:
if n.tagId == loweredTag:
if not loadBodies:
# A dependency: register its `.t.nif` (tIndex + hooks) so the consumer
# can destroy the dep's closures, but don't reconstruct its bodies (only
# the dep's OWN cg emits them).
skip n
continue
inc n # -> StringLit name
let name = pool.strings[n.litId]
inc n # -> body tree
var localSyms = initTable[string, PSym]()
var scanCursor = n
extractLocalSymsFromTree(c, scanCursor, suffix, localSyms)
let body = loadNode(c, n, suffix, localSyms)
result.bodies.add (name, body)
skipParRi n # close (lowered ...)
else:
let (isHook, op) = repTagToOp(n.tagId)
if isHook:
inc n # -> StringLit key
let key = pool.strings[n.litId]
inc n # -> Symbol sym
let sym = resolveHookSym(c, n.symId)
inc n
if sym != nil:
result.hooks.add LogEntry(kind: HookEntry, op: op, module: module.int,
key: key, sym: sym)
skipParRi n
else:
skip n # a hook routine's `(sd)` def — loaded lazily via the index
template withNode(c: var DecodeContext; n: var Cursor; result: PNode; kind: TNodeKind; body: untyped) =
let info = c.infos.oldLineInfo(n.info)
inc n
@@ -2420,8 +2234,7 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string;
# dereferences a nil field type. Detect the unindexed case and FILL
# the sym from the inline def instead.
let m = moduleId(c, sn.module)
let indexed = c.mods[m].index.hasKey(symName) or
(c.mods[m].hasTIndex and c.mods[m].tIndex.hasKey(symName))
let indexed = c.mods[m].index.hasKey(symName)
if indexed:
sym = c.loadSymStub(name.symId, thisModule, localSyms)
skip n # skip the entire sdef for indexed symbols
@@ -2598,9 +2411,9 @@ proc moduleSymbolStubs*(c: var DecodeContext; module: FileIndex): seq[PSym] =
proc toNifFilename*(conf: ConfigRef; f: FileIndex): string =
let suffix = moduleSuffix(conf, f)
# The `cg`/`emit` backend stages load the lowered whole-module NIF (transformed
# bodies + lifted sigs baked in) when `-d:icWholeLowered` is on; the `lower`
# stage and the frontend (`cmdM`) still read the semchecked `.s.nif`.
if isDefined(conf, "icWholeLowered") and conf.cmd == cmdNifC and
# bodies + lifted sigs baked in); the `lower` stage and the frontend (`cmdM`)
# read the semchecked `.s.nif`.
if conf.cmd == cmdNifC and
(conf.icBackendStage == "cg" or conf.icBackendStage == "emit"):
let t = toGeneratedFile(conf, AbsoluteFile(suffix), ".t.nif").string
if fileExists(t):
@@ -2627,11 +2440,6 @@ proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: boo
# Try the format without module suffix
let localKey = sn.name & "." & $sn.count & "."
offs = c.mods[module].index.getOrDefault(localKey)
if offs.offset == 0 and c.mods[module].hasTIndex:
# A `lower`-stage entity (an `@bk` hook routine, OR a module-homed derived
# closure-env field added only in the backend) lives in the module's
# `.t.nif`, not its `.s.nif`: resolve it through the second (tIndex) index.
offs = c.mods[module].tIndex.getOrDefault(symAsStr)
if offs.offset == 0:
return nil
if not alsoConsiderPrivate and offs.vis == Hidden:

View File

@@ -358,52 +358,6 @@ proc readCnifHeads*(f: string): CnifHeads =
endRead(c)
result.valid = sawMeta and version == CnifVersion
proc writeLoweredArtifact*(outfile: string; entries: openArray[string]) =
## The `.t.nif` "lowered" artifact: one `(lowered "<nifname>" <body>)` per
## routine the module OWNS, written by the per-module `lower` backend stage
## for the `cg` stage to read instead of re-deriving the transformed body
## (re-derivation in each parallel `cg` process is what makes a closure
## `:env`'s identity diverge across modules — see transf.transformBody).
##
## SKELETON: every body is the empty-marker `.` ("transformed body == sem
## body"), so `cg` falls back to its own `transformBody` and output stays
## byte-identical. The real transformed body fills this slot in a later step;
## the `.` then means "unchanged by lowering" (the dedup Araq sketched).
var b = nifbuilder.open(outfile)
b.withTree "stmts":
for name in entries:
b.withTree "lowered":
b.addStrLit name
b.addEmpty()
b.close()
proc readLoweredArtifact*(f: string): seq[string] =
## The routine NIF names recorded in a `.t.nif`. (Bodies are not returned
## yet: the skeleton records only empty-markers; reading proves the artifact
## round-trips and that the `cg` rule depends on it.)
result = @[]
if not fileExists(f): return
var pool = newPool()
var tags = newTagPool()
let stmtsTag = tags.registerTag("stmts")
let loweredTag = tags.registerTag("lowered")
var buf = parseFromFile(f, 1000, pool, tags)
var c = beginRead(buf)
if c.kind != TagLit or c.cursorTagId != stmtsTag:
endRead(c)
return
c.loopInto:
if c.kind == TagLit and c.cursorTagId == loweredTag:
c.loopInto:
if c.kind == StrLit:
result.add strVal(c)
inc c
else:
skip c
else:
skip c
endRead(c)
type
CnifLiveness* = object
defs*: int ## proc definitions emitted across all modules

View File

@@ -942,17 +942,13 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
var cnifFiles = newSeq[string](c.nodes.len)
var cFiles = newSeq[string](c.nodes.len)
var tFiles = newSeq[string](c.nodes.len)
# Whole-module lowering writes a PROPER module NIF the cg/emit stages load via
# The `lower` stage writes a PROPER module NIF the cg/emit stages load via
# `toNifFilename` (a `.s.nif` sibling), so its `.t.nif` lives at the suffix base
# (mirroring `semmedFile`), not next to the throwaway `.c`. The side-car path
# keeps the `.c`-relative name (loaded directly by `registerLoweredModule`).
let wholeMode = isDefined(c.config, "icWholeLowered")
# (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] =
if wholeMode: nimcache / node.files[0].modname & ".t.nif"
else: cFiles[i] & ".t.nif"
tFiles[i] = nimcache / node.files[0].modname & ".t.nif"
var b = nifbuilder.open(result)
defer: b.close()
@@ -1019,13 +1015,9 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
inputStr mainNif
for n2 in c.nodes:
inputStr c.semmedFile(n2.files[0])
# Whole-module mode loads dependencies FROM their `.t.nif` (toNifFilename), so
# every module's lowered NIF must precede this cg rule; the side-car only needs
# this module's own `.t.nif`.
if wholeMode:
for j in 0 ..< c.nodes.len: inputStr tFiles[j]
else:
inputStr tFiles[i]
# cg loads dependencies FROM their `.t.nif` (toNifFilename), so every module's
# lowered NIF must precede this cg rule.
for j in 0 ..< c.nodes.len: inputStr tFiles[j]
if node.id == 0:
for j in 0 ..< c.nodes.len:
if c.nodes[j].id != 0:
@@ -1053,10 +1045,9 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
inputStr mainNif
inputStr cnifFiles[i]
inputStr mergeFile
# emit loads modules like cg (for getCFile/type resolution); under whole mode
# those come from the `.t.nif`s, so they must precede this rule.
if wholeMode:
for j in 0 ..< c.nodes.len: inputStr tFiles[j]
# emit loads modules like cg (for getCFile/type resolution); those come from
# the `.t.nif`s, so they must precede this rule.
for j in 0 ..< c.nodes.len: inputStr tFiles[j]
outputStr cFiles[i]
b.endTree()

View File

@@ -245,7 +245,7 @@ proc genOp(c: var Con; t: PType; kind: TTypeAttachedOp; dest, ri: PNode): PNode
let canon = c.graph.canonTypes.getOrDefault(h)
if canon != nil:
op = getAttachedOp(c.graph, canon, kind)
if (op == nil or op.ast.isGenericRoutine) and icLoweredBodies(c.graph.config):
if op == nil or op.ast.isGenericRoutine:
# IC: injectDestructorCalls is demand-driven and runs HERE (cg), not in the
# `lower` stage, so a structural, env-agnostic op the lower stage never had
# reason to serialize — most often a closure PROC type's `=destroy`/`=sink`

View File

@@ -27,7 +27,6 @@ import ast, options, lineinfos, modulegraphs, cgendata, cgen,
cnif
from cgmeth import generateIfMethodDispatchers
from transf import transformBody
from lambdalifting import getEnvParam, addHiddenParam, paramName
import ic / replayer
proc loadModuleDependencies(g: ModuleGraph; mainFileIdx: FileIndex;
@@ -324,110 +323,17 @@ proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
cachedModuleSuffix(g.config, FileIndex precompSys.module.position) == suffix:
return precompSys
proc findHiddenEnvParam(n: PNode; owner: PSym): PSym =
## Locate the hidden env param (`:envP`) that belongs to `owner` in its loaded
## transformed body, so it can be re-welded into `owner`'s signature. Matching
## by `owner` is essential: a body can ALSO reference a callee's `:envP` (a
## closure call passes the callee env), so the first `:envP` in DFS order is
## not necessarily this proc's own.
if n == nil: return nil
if n.kind == nkSym:
if n.sym != nil and n.sym.kind == skParam and n.sym.name.s == paramName and
n.sym.owner == owner:
return n.sym
else:
for i in 0 ..< n.safeLen:
let r = findHiddenEnvParam(n[i], owner)
if r != nil: return r
return nil
proc registerLoweredModule(g: ModuleGraph; m: PSym; applyBodies: bool) =
## Load module `m`'s `<m>.t.nif` and register its `lower`-stage output: its
## embedded index (so its closure-env `@bk` entities resolve) and its lifted
## type-bound ops (so any cg that DESTROYS one of `m`'s closures finds the env
## `=destroy` via getAttachedOp). For the cg TARGET (`applyBodies`), also set
## each owned routine's `transformedBody` so cg's `transformBody` short-circuits
## (transf.nim:1383) instead of re-deriving. `injectDestructorCalls` stays in cg.
let modPos = m.position
let bmod = BModuleList(g.backend).mods[modPos]
if bmod == nil: return
let artifact = getCFile(bmod).string & ".t.nif"
if not fileExists(artifact): return
let suffix = cachedModuleSuffix(g.config, FileIndex modPos)
let (bodies, hooks) = loadLoweredBodies(ast.program, FileIndex modPos, suffix,
artifact, loadBodies = applyBodies)
registerLoadedHooks(g, hooks)
if not applyBodies: return
var byName = initTable[string, PSym]()
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
# Owned routines AND nested closure routines (the `:anonymous` procs the
# lower stage emits as their own entries) — both are index-resolvable syms
# of this module whose transformed body the lower stage authored.
if s.kind in routineKinds and s.itemId.module == modPos:
byName[globalName(s, g.config)] = s
for (name, body) in bodies:
let s = byName.getOrDefault(name)
# `.s.nif` wins: only fill from `.t.nif` if sem did not already transform it.
if s != nil and body != nil and s.transformedBody == nil:
s.transformedBody = body
# Lambda-lift in the lower stage gave this proc a hidden `:envP` env param
# (a captured-var closure env), but cg loaded the PRE-lift signature from
# `.s.nif`. The transformed body references that `@bk` `:envP`; re-weld it
# into the proc's params so genProc assigns it a loc (else "param not
# init"). `transformBody` short-circuits on the cached body, skipping the
# lift that normally adds it. This applies to BOTH a true `ccClosure` proc
# (env arrives via the closure ABI `ClE_0`, needs `tfCapturesEnv`) and a
# plain nested `nimcall` proc that merely captures (env is a regular last
# param). Match the env param by owner — a body can also reference a
# callee's `:envP`.
if s.typ != nil and getEnvParam(s) == nil:
let ep = findHiddenEnvParam(body, s)
if ep != nil:
# From-source, `ast[paramsPos]` and `typ.n` are the SAME node, but a
# NIF-loaded routine has two distinct param nodes. genProc reads
# `typ.n`, so unify them first — else addHiddenParam appends to
# `ast[paramsPos]` and the env param never reaches genProc's loc setup.
if s.typ.n != nil:
s.ast[paramsPos] = s.typ.n
addHiddenParam(s, ep)
# The lower stage's lambda-lift converts EVERY captured nested proc to a
# closure (collectNestedClosureBodies only emits `ccClosure` entries),
# and the serialized call sites use the closure ABI. cg loaded the
# pre-lift signature, which for a proc only ever CALLED (never used as a
# value) is still `nimcall`. Re-apply the lift's `ccClosure` +
# `tfCapturesEnv` so closureSetup maps the env param to `ClE_0` and the
# calls match.
s.typ.callConv = ccClosure
incl(s.typ, {tfCapturesEnv})
proc applyLoweredBodies(g: ModuleGraph; modules: seq[PrecompiledModule];
precompSys: PrecompiledModule; target: PrecompiledModule) =
## Register every loaded module's `.t.nif` (env entities + lifted hooks),
## applying transformed bodies only for the cg target.
if not icLoweredBodies(g.config): return # Stage 0 (lazy): nothing to apply
# Whole-module mode: the cg stage already loaded each module FROM its `.t.nif`
# (toNifFilename), so transformed bodies arrive via loadSymFromCursor and lifted
# hooks via moduleFromNifFile's registerLoadedHooks — no side-car to apply.
if isDefined(g.config, "icWholeLowered"): return
if precompSys.module != nil:
registerLoweredModule(g, precompSys.module, applyBodies = false)
for m in modules:
if m.module != nil:
registerLoweredModule(g, m.module,
applyBodies = (m.module.position == target.module.position))
proc collectNestedClosureBodies(g: ModuleGraph; idgen: IdGenerator; n: PNode;
owner: PSym; seen: var IntSet;
entries: var seq[tuple[name: string; body: PNode]]) =
proc setNestedClosureBodies(g: ModuleGraph; idgen: IdGenerator; n: PNode;
owner: PSym; seen: var IntSet) =
## A closure routine nested in `owner` (the `:anonymous` proc lambda-lifting
## minted, plus any deeper nesting) gets its captured-var→env rewrite produced
## as part of the OWNER's `transformBody`, but only the owner's body is emitted
## as a `(lowered)` entry. The nested proc itself IS index-resolvable (it has a
## `.s.nif` sdef from sem, with its PRE-lift body), so cg loads that and
## re-derives — and the capture mapping is gone (it accesses `x` directly
## instead of `ClE_0->x0`). Walk the transformed body and emit each nested
## closure routine's transformed body as its OWN `(lowered)` entry so
## applyLoweredBodies installs it and cg reuses it verbatim.
## as part of the OWNER's `transformBody`. The nested proc is a module-indexed
## sym whose `.s.nif` sdef carries its PRE-lift body, so without help the whole
## module re-serializer would write that pre-lift body and cg would lose the
## capture mapping (it accesses `x` directly instead of `ClE_0->x0`). Walk the
## owner's transformed body and cache each nested closure's transformed body on
## its sym so `writeSymDef` serializes the lifted body into the routine's
## 2-way-body slot.
if n == nil: return
if n.kind == nkSym:
let s = n.sym
@@ -437,11 +343,10 @@ proc collectNestedClosureBodies(g: ModuleGraph; idgen: IdGenerator; n: PNode;
s.typ != nil and s.typ.callConv == ccClosure:
if s.transformedBody == nil:
s.transformedBody = transformBody(g, idgen, s, {})
entries.add (globalName(s, g.config), s.transformedBody)
collectNestedClosureBodies(g, idgen, s.transformedBody, s, seen, entries)
setNestedClosureBodies(g, idgen, s.transformedBody, s, seen)
else:
for i in 0 ..< n.safeLen:
collectNestedClosureBodies(g, idgen, n[i], owner, seen, entries)
setNestedClosureBodies(g, idgen, n[i], owner, seen)
proc reownFromTwin(n: PNode; twin, s: PSym) =
## Re-own to `s` every entity the frontend attributed to `s`'s forward-decl
@@ -499,102 +404,72 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
rawMessage(g.config, errGenerated,
"per-module lowering: no backend module for suffix: " & g.config.icBackendModule)
return
let artifact = getCFile(tb).string & ".t.nif"
if icLoweredBodies(g.config):
# STAGE 1 (DEFAULT; `-d:icNoLowerBodies` opts out): transform every owned routine
# ONCE in this single process's id space and serialize the results, so `cg`
# reads them instead of re-deriving (the single-writer-per-owner that keeps
# closure-`:env` identity stable). `transformBody` with flags {} mirrors the
# cg call (cgen.nim:1409); we keep only its return value (it clears
# `transformedBody` for non-cached procs). `injectDestructorCalls` is NOT run
# — it stays in `cg` on the loaded body.
# Whole-module mode (`-d:icWholeLowered`): re-serialize the ENTIRE module as a
# proper indexed NIF (`writeLoweredModule`) with transformed bodies baked into
# the routine sdefs, instead of the per-routine `(lowered)` side-car. cg loads
# it through the normal module loader, so nested procs (incl. async state
# machines) arrive as real defs with their lifted bodies — no re-weld, no
# `(lowered)` entries. `entries` stays unused; we only need `transformedBody`
# set on each routine sym so `writeSymDef` serializes it.
let wholeMode = isDefined(g.config, "icWholeLowered")
var entries: seq[tuple[name: string; body: PNode]] = @[]
# `transformBody`/lambda-lifting LIFTS the closure env's type-bound ops
# (`=destroy` etc.) into `g.opsLog`; snapshot its length so we can serialize
# exactly the ops THIS stage created (not those loaded from `.s.nif`).
let opsLogStart = g.opsLog.len
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
if ownsRuntimeRoutine(s, modPos):
# `.s.nif` wins: a routine already transformed during sem (CT eval /
# macro / VM transform) carries its lowered body in the `.s.nif` slot —
# don't re-transform it here, just leave its `.t.nif` entry empty.
if s.transformedBody != nil: continue
# A routine serialized as a forward-decl + impl pair (the writeSymDef
# "separate forward declaration and implementation" design) loads as TWO
# syms; the impl `s` we transform here can carry body entities (`result`,
# locals, nested routines) owned by its fwd-decl TWIN, not by `s`.
# lambda-lifting compares owners by reference → `illegalCapture` rejects a
# twin-owned `result` and the lifting pass can't find twin-owned locals in
# `s`'s env. Pervasive on chronos `{.async.}` methods. Re-own them to `s`,
# matching the single-sym non-IC case. Backend-only (the lowered body is a
# `.t.nif` artifact), so frontend effect/exception inference is untouched.
if s.ast != nil and s.ast.len > resultPos and
s.ast[resultPos].kind == nkSym and s.ast[resultPos].sym.owner != s:
reownFromTwin(s.ast, s.ast[resultPos].sym.owner, s)
let tbody = transformBody(g, tb.idgen, s, {})
if wholeMode:
# Retain the transformed body on the sym so `writeSymDef` serializes it
# in the routine's `(sd)` 2-way-body slot.
s.transformedBody = tbody
else:
entries.add (globalName(s, g.config), tbody)
# Set `transformedBody` on nested ccClosure routines too (so a
# module-indexed nested closure gets its lifted body); in side-car mode
# this also appends their `(lowered)` entries.
var seenNested = initIntSet()
collectNestedClosureBodies(g, tb.idgen, tbody, s, seenNested, entries)
# Collect the hooks this stage lifted, and transform each hook ROUTINE's body
# too (it is itself lowered into NIFC). The hooks' `(sd)` + transformed body go
# into the `.t.nif`; `cg` re-attaches them so `injectDestructorCalls` resolves
# the loaded env's `=destroy`. Iterate to a fixpoint: a hook body can lift
# further hooks (a field's `=destroy`).
var hooks: seq[LogEntry] = @[]
var i = opsLogStart
while i < g.opsLog.len:
let e = g.opsLog[i]
if e.kind == HookEntry and e.sym != nil and e.sym.kind in routineKinds and
e.sym.transformedBody == nil:
hooks.add e
# Transform the hook routine's body and cache it on the sym so
# `writeSymDef` serializes it in the hook's `(sd)` transformed-body slot
# (`transformBody {}` returns the body but does not cache it).
e.sym.transformedBody = transformBody(g, tb.idgen, e.sym, {})
inc i
if wholeMode:
# Re-serialize the whole module to its suffix-based `.t.nif` (the path
# `toNifFilename` resolves for the cg/emit stages). `writeLoweredModule`
# seals routines itself.
let suffix = cachedModuleSuffix(g.config, FileIndex modPos)
let wholeArtifact = toGeneratedFile(g.config, AbsoluteFile(suffix), ".t.nif").string
writeLoweredModule(ast.program, g.config, target, hooks, wholeArtifact)
if isDefined(g.config, "icDceCheck"):
stderr.writeLine "[icLowerWhole] " & extractFilename(wholeArtifact) & " " &
$hooks.len & " hooks"
else:
# Seal the index-loaded entities so their references in the bodies serialize
# as SymUses (resolved via the module index in cg), not duplicate defs.
sealLoadedBackendEntities(ast.program)
serializeLoweredBodies(g.config, modPos.int32, entries, hooks, artifact)
if isDefined(g.config, "icDceCheck"):
stderr.writeLine "[icLower] " & extractFilename(artifact) & " " &
$entries.len & " routines transformed, " & $hooks.len & " hooks"
else:
# DEFAULT (Stage 0, byte-neutral): record one empty-marker per owned routine.
# `cg` derives the transformed body itself, so output is unchanged; this only
# exercises the artifact + scheduling the transform-move builds on.
var names: seq[string] = @[]
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
if ownsRuntimeRoutine(s, modPos):
names.add globalName(s, g.config)
writeLoweredArtifact(artifact, names)
# Transform every owned routine ONCE in this single process's id space and
# re-serialize the ENTIRE module as a proper indexed NIF (`writeLoweredModule`)
# with the transformed bodies baked into the routine `(sd)` entries. `cg` loads
# it through the normal module loader, so nested procs (incl. async state
# machines) arrive as real defs with their lifted bodies — no re-derivation.
# This single-writer-per-owner is what keeps closure-`:env` identity stable
# across the parallel `cg` processes (re-derivation per process was the root of
# the `:env` identity drift). `transformBody` with flags {} mirrors the cg call
# (cgen.nim); `injectDestructorCalls` is NOT run here — it stays in `cg` on the
# loaded body.
#
# `transformBody`/lambda-lifting LIFTS the closure env's type-bound ops
# (`=destroy` etc.) into `g.opsLog`; snapshot its length so we serialize exactly
# the ops THIS stage created (not those loaded from `.s.nif`).
let opsLogStart = g.opsLog.len
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
if ownsRuntimeRoutine(s, modPos):
# `.s.nif` wins: a routine already transformed during sem (CT eval / macro /
# VM transform) carries its lowered body in the `.s.nif` slot — don't
# re-transform it here.
if s.transformedBody != nil: continue
# A routine serialized as a forward-decl + impl pair (writeSymDef's
# "separate forward declaration and implementation") loads as TWO syms; the
# impl `s` we transform here can carry body entities (`result`, locals,
# nested routines) owned by its fwd-decl TWIN, not by `s`. lambda-lifting
# compares owners by reference → `illegalCapture` rejects a twin-owned
# `result` and the lifting pass can't find twin-owned locals in `s`'s env.
# Pervasive on chronos `{.async.}` methods. Re-own them to `s`, matching the
# single-sym non-IC case. Backend-only, so frontend effect/exception
# inference is untouched.
if s.ast != nil and s.ast.len > resultPos and
s.ast[resultPos].kind == nkSym and s.ast[resultPos].sym.owner != s:
reownFromTwin(s.ast, s.ast[resultPos].sym.owner, s)
# Retain the transformed body on the sym so `writeSymDef` serializes it in
# the routine's `(sd)` 2-way-body slot.
s.transformedBody = transformBody(g, tb.idgen, s, {})
# Cache the lifted body on nested ccClosure routines too, so a module-indexed
# nested closure serializes its lifted (capture-rewritten) body.
var seenNested = initIntSet()
setNestedClosureBodies(g, tb.idgen, s.transformedBody, s, seenNested)
# Collect the hooks this stage lifted, and transform each hook ROUTINE's body
# too (it is itself lowered into NIFC). The hooks' `(sd)` + transformed body go
# into the `.t.nif`; `cg` re-attaches them so `injectDestructorCalls` resolves
# the loaded env's `=destroy`. Iterate to a fixpoint: a hook body can lift
# further hooks (a field's `=destroy`).
var hooks: seq[LogEntry] = @[]
var i = opsLogStart
while i < g.opsLog.len:
let e = g.opsLog[i]
if e.kind == HookEntry and e.sym != nil and e.sym.kind in routineKinds and
e.sym.transformedBody == nil:
hooks.add e
# Transform the hook routine's body and cache it on the sym so `writeSymDef`
# serializes it in the hook's `(sd)` transformed-body slot (`transformBody
# {}` returns the body but does not cache it).
e.sym.transformedBody = transformBody(g, tb.idgen, e.sym, {})
inc i
# Re-serialize the whole module to its suffix-based `.t.nif` (the path
# `toNifFilename` resolves for the cg/emit stages). `writeLoweredModule` seals
# routines itself.
let suffix = cachedModuleSuffix(g.config, FileIndex modPos)
let wholeArtifact = toGeneratedFile(g.config, AbsoluteFile(suffix), ".t.nif").string
writeLoweredModule(ast.program, g.config, target, hooks, wholeArtifact)
if isDefined(g.config, "icDceCheck"):
stderr.writeLine "[icLower] " & extractFilename(wholeArtifact) & " " &
$hooks.len & " hooks"
proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
## Per-module backend codegen (`--icBackendStage:cg --icBackendModule:<suffix>`):
@@ -638,7 +513,10 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
"per-module codegen: module not found for suffix: " & g.config.icBackendModule)
return
applyLoweredBodies(g, modules, precompSys, target)
# The `lower` stage already wrote each module's transformed bodies + lifted
# hooks into its `.t.nif`, which the loaders above read directly (toNifFilename
# resolves the `.t.nif`); transformed bodies arrive via loadSymFromCursor and
# lifted hooks via moduleFromNifFile's registerLoadedHooks. Nothing to apply.
generateCodeForModule(g, target)
let bl = BModuleList(g.backend)
# The main module also owns the whole-program method dispatchers + NimMain.
@@ -780,8 +658,8 @@ proc generateLinkStage(g: ModuleGraph; mainFileIdx: FileIndex) =
# `strutils.escape`) — so its body must be at link or that reference is
# undefined. Link every emitted `.c` the merge decision says OWNS a LIVE symbol;
# a node that owns nothing live (a Windows-only winsock node on Linux) is
# correctly skipped. Whole-module only: the side-car default is unchanged.
if isDefined(g.config, "icWholeLowered"):
# correctly skipped.
block:
let nimcache = getNimcacheDir(g.config).string
let decision = readMergeDecision(nimcache / MergeDecisionFile)
if not decision.broken:

View File

@@ -29,7 +29,7 @@ const
nimEnableCovariance* = defined(nimEnableCovariance)
icFormatVersion* = "20"
icFormatVersion* = "21"
## Version of the IC cache format (the sem-NIF module layout written by
## ast2nif.nim plus the iface/impl/edges side files). Bump it whenever
## that layout changes: `commandIc` wipes a nimcache whose `ic.version`
@@ -786,13 +786,6 @@ template quitOrRaise*(conf: ConfigRef, msg = "") =
else:
quit(msg) # quits with QuitFailure
proc icLoweredBodies*(conf: ConfigRef): bool {.inline.} =
## Whether the `nim ic` backend uses the EAGER per-module `lower` stage
## (transformBody serialized to `.t.nif`, cg reuses it) instead of the lazy
## Stage-0 path (cg re-derives every transformed body). This is now the
## DEFAULT; `-d:icNoLowerBodies` opts back into the lazy path for A/B testing.
not isDefined(conf, "icNoLowerBodies")
proc importantComments*(conf: ConfigRef): bool {.inline.} = conf.cmd in cmdDocLike + {cmdIdeTools}
proc usesWriteBarrier*(conf: ConfigRef): bool {.inline.} = conf.selectedGC >= gcRefc
proc usesSso*(conf: ConfigRef): bool {.inline.} = conf.selectedStrings == stringSso