IC related bugfixes (#25946)

This commit is contained in:
Andreas Rumpf
2026-07-03 15:52:41 +02:00
committed by GitHub
parent c7ea004ca9
commit 1c61307692
45 changed files with 1453 additions and 131 deletions

View File

@@ -11,8 +11,10 @@
import std / [assertions, tables, sets]
from std / strutils import startsWith, endsWith, contains
from std / os import fileExists, dirExists, walkFiles
from std / syncio import readFile
from std / os import fileExists, dirExists, walkFiles, existsEnv,
commandLineParams, getCurrentProcessId
from std / exitprocs import addExitProc
from std / syncio import readFile, stderr, writeLine
from std / algorithm import sort
import "../dist/checksums/src/checksums" / sha1
import astdef, idents, msgs, options
@@ -27,7 +29,7 @@ import "../dist/nimony/src/lib" / [bitabs, nifstreams, lineinfos,
# uses; the reader reaches pools via `symName(c)`/`strVal(c)` etc.
import "../dist/nimony/src/lib/nifcore" except pool
from "../dist/nimony/src/lib" / bif import load, BifModule
import "../dist/nimony/src/gear2" / modnames
import icmodnames
import "../dist/nimony/src/models" / nifindex_tags
import typekeys
import icnifcore
@@ -288,9 +290,23 @@ proc toNifSymName(w: var Writer; sym: PSym): string =
# Process-local backend sym (closure env field / hidden `:env` param minted
# during a VM transform): re-home to the current module with the `@bk`
# marker so each referencing module self-contains it. See transformBody.
#
# Use `itemId.item` (the writer's dedup identity, see `emittedBackendSyms`)
# as the numeric name component, NOT `disamb`: closure `:env` syms in one
# module are minted from TWO id spaces — the backend lower stage's
# `tb.idgen` and sem's `vmTransfIdgen` (transf.transformBody) — whose
# `disambTable`s each start `:env` at the same low count, so a macro-lowered
# `:env` (e.g. `implementSendProcBody`) and a backend-lowered one
# (`peerTrimmerHeartbeat`) collide on `:env.2.<mod>@bk`. Two distinct syms
# then share a NIF name; the loader's name-keyed index/`c.syms` return the
# first for both, so one proc's `:env` gets the OTHER proc's env type
# (mismatched-pointer C, "has no member colonup_" at link). `itemId.item` is
# unique per `@bk` sym (both are emitted as defs, see writeSym), mirroring
# how `@bk` TYPES already key off `uniqueId.item` (nifTypeName). The loader
# copies this back into `disamb` (sn.count), so `globalName` round-trips.
result = sym.name.s
result.add '.'
result.addInt sym.disamb
result.addInt sym.itemId.item
result.add '.'
result.add modname(w.currentModule, w.infos.config)
result.add BackendLocalMarker
@@ -496,8 +512,19 @@ proc writeTypeDef(w: var Writer; dest: var IcBuilder; typ: PType) =
# name in isolation (cg seeks the `.t.nif`/`.s.nif` index entry), so its
# fields must be DEFS here, not entry-deduped SymUses whose def lives
# elsewhere in the `(lowered)` entry and is never read by the seek.
#
# `emittedFieldSyms` only guards against a field being def'd twice WITHIN one
# reclist, so scope it per-reclist: a generic object and its instances SHARE one
# field PSym (same itemId) yet each instance carries a DISTINCT field type (e.g.
# `MDigest[256].data: array[32,byte]` vs `MDigest[384].data: array[48,byte]`), so
# each reclist needs its OWN typed def. A Writer-global set deduped every instance
# after the first to a typeless `SymUse` stub (nil typ/owner on load → crash in
# destructor lifting). Field NIF names are local (no module suffix, not in the
# global `c.syms`), so def'ing the same field in two reclists never collides.
inc w.inTypeReclist
let savedFieldSyms = move w.emittedFieldSyms
writeNode(w, dest, typ.nImpl)
w.emittedFieldSyms = savedFieldSyms
dec w.inTypeReclist
writeSym(w, dest, typ.ownerFieldImpl)
writeSym(w, dest, typ.symImpl)
@@ -901,6 +928,10 @@ proc writeNode(w: var Writer; dest: var IcBuilder; n: PNode; forAst = false) =
if n == nil:
dest.addDotToken
else:
if nfLazyBody in n.flags and forceLazyBodyHook != nil:
# Materialize a deferred body before serializing so its real flags/typ and
# children are written (never the empty `nfLazyBody` placeholder).
forceLazyBodyHook(n)
case n.kind
of nkNone:
assert n.typField == nil, "nkNone should not have a type"
@@ -1070,10 +1101,18 @@ proc writeToplevelNode(w: var Writer; dest, bottom: var IcBuilder; n: PNode) =
of nkEmpty:
discard "ignore"
of nkTypeSection, nkCommentStmt, nkMixinStmt, nkBindStmt, nkUsingStmt,
nkPragma,
nkProcDef, nkFuncDef, nkMethodDef, nkIteratorDef, nkConverterDef, nkMacroDef, nkTemplateDef:
# We write purely declarative nodes at the bottom of the file
writeNode(w, bottom, n)
of nkPragma:
# Top-level pragmas — chiefly `{.emit.}`, plus the `{.push/pop.}` that guard
# its neighbours — must survive the backend reload so the `cg` stage re-runs
# genPragma/genEmit. The bottom (implementation) section is reloaded lazily
# BY SYMBOL INDEX, which a symbol-less pragma can never be on, so a pragma
# written there is silently dropped on reload (e.g. a module-level `#include`
# vanishes and the generated C fails to compile). The header init section is
# replayed verbatim by `processTopLevel`, so write it there instead.
writeNode(w, dest, n)
of nkConstSection:
writeGlobals(w, bottom, n)
of nkLetSection, nkVarSection:
@@ -1756,8 +1795,18 @@ type
semIndex: Table[string, NifIndexEntry]
semTried: bool # `semBuf`/`semIndex` load attempted (idempotent)
PendingBody = object
## A deferred routine body (bodyPos son). `cursor` points AT the body node in
## the module buffer (kept alive by the cursor's refcounted owner); `localSyms`
## is the snapshot of the enclosing sym def's local symbols so body-local
## references resolve to the SAME PSyms the signature already created.
cursor: Cursor
thisModule: string
localSyms: Table[string, PSym]
DecodeContext* = object
infos: LineInfoWriter
pendingBodies: Table[int, PendingBody] # nodeId(placeholder) -> deferred body
#moduleIds: Table[string, int32]
types: Table[string, (PType, NifIndexEntry)]
syms: Table[string, (PSym, NifIndexEntry)]
@@ -1767,11 +1816,60 @@ type
## Mangled module name of the module being compiled fresh (cmdM). Symbols
## belonging to it that are re-exported by a dependency must NOT be loaded
## as stubs, otherwise they collide with the freshly compiled originals.
symLoads, typeLoads: CountTable[FileIndex]
## Diagnostics (opt-in via env `NIM_IC_LOADSTATS`): per OWNING-module count
## of stub materializations in THIS process. Quantifies the "every backend
## worker deserializes system.bif + a bunch of others" cost — breadth (how
## many syms) attributed to duplication axis (which shared module).
proc createDecodeContext*(config: ConfigRef; cache: IdentCache): DecodeContext =
## Supposed to be a global variable
result = DecodeContext(infos: LineInfoWriter(config: config), cache: cache)
var loadStatsInit {.threadvar.}: int # 0=unknown 1=on 2=off
var statsCtxPtr {.threadvar.}: ptr DecodeContext
var loaderCtx {.threadvar.}: ptr DecodeContext # the live `program`; for lazy-body
# materialization off the len hook
var nodesDecoded {.threadvar.}: int # all PNodes materialized this proc
var astFieldNodes {.threadvar.}: int # subset: routine-body (s.ast) subtrees
proc dumpLoadStatsExit() {.noconv.} =
if statsCtxPtr == nil: return
let c = statsCtxPtr
var merged = initTable[FileIndex, array[2, int]]()
for m, cnt in c.symLoads.pairs: merged.mgetOrPut(m, [0, 0])[0] = cnt
for m, cnt in c.typeLoads.pairs: merged.mgetOrPut(m, [0, 0])[1] = cnt
var order: seq[FileIndex] = @[]
var totS, totT: int = 0
for m, a in merged:
order.add m
totS += a[0]; totT += a[1]
sort(order, proc (a, b: FileIndex): int =
(merged[b][0] + merged[b][1]) - (merged[a][0] + merged[a][1]))
let params = commandLineParams()
let target = if params.len > 0: params[^1] else: "?"
stderr.writeLine "=== IC loadstats pid=" & $getCurrentProcessId() &
" main=" & c.mainModuleSuffix & " target=" & target & " ==="
stderr.writeLine " TOTAL symLoads=" & $totS & " typeLoads=" & $totT &
" modulesTouched=" & $order.len
let pct = if nodesDecoded > 0: 100 * astFieldNodes div nodesDecoded else: 0
stderr.writeLine " PNODES decoded=" & $nodesDecoded & " routineBody=" &
$astFieldNodes & " (" & $pct & "% deferrable via lazy PSym.ast)"
for m in order:
let a = merged[m]
let name = if c.mods.hasKey(m): c.mods[m].suffix else: "?"
stderr.writeLine " " & $(a[0] + a[1]) & "\tsym=" & $a[0] & " typ=" & $a[1] &
"\t" & name
proc recordLoad(c: var DecodeContext; m: FileIndex; isType: bool) =
if loadStatsInit == 0:
loadStatsInit = if existsEnv("NIM_IC_LOADSTATS"): 1 else: 2
if loadStatsInit == 1:
statsCtxPtr = addr c
addExitProc(dumpLoadStatsExit)
if loadStatsInit == 2: return
if isType: c.typeLoads.inc(m) else: c.symLoads.inc(m)
proc nextBackendSymItem*(c: var DecodeContext; module: int32): int32 =
## Allocate the next backend-minted SYM item for `module` from the SAME
## per-module counter the loader uses when it re-homes `@bk` syms loaded from
@@ -2002,6 +2100,42 @@ proc reconstructSysType(c: var DecodeContext; name: string; k: int; itemVal: int
result.alignImpl = int16 c.infos.config.target.ptrSize
c.types[name] = (result, NifIndexEntry())
proc stripBkSuffix(rawMod: string): (bool, string) {.inline.} =
## Split a possibly-`@bk` (BackendLocalMarker) module suffix into
## `(isBackendMinted, realSuffix)`. See `toNifSymName`/`nifTypeName`.
if rawMod.endsWith(BackendLocalMarker):
(true, rawMod[0 ..< rawMod.len - BackendLocalMarker.len])
else:
(false, rawMod)
proc nextSymId(c: var DecodeContext; module: FileIndex; isBk: bool): ItemId =
## Mint the next per-module SYM id from `symCounter`: a `backendItemId` for a
## process-local `@bk` sym, else a plain loader `itemId`. Both draw from the one
## counter so loaded and cg-minted backend syms stay disjoint (see
## `nextBackendSymItem`). Types do NOT use this — they preserve the item parsed
## from their own name (see `tryCreateTypeStub`).
let val = addr c.mods[module].symCounter
inc val[]
result = if isBk: backendItemId(module.int32, val[]) else: itemId(module.int32, val[])
proc mintSymId(c: var DecodeContext; rawMod: string): (FileIndex, ItemId) =
## Resolve a possibly-`@bk` module suffix to its FileIndex and mint a fresh sym
## id for it — the common case where the module is not needed before minting
## (see `stripBkSuffix`/`nextSymId`).
let (isBk, realMod) = stripBkSuffix(rawMod)
let module = moduleId(c, realMod)
result = (module, c.nextSymId(module, isBk))
proc makePartialSymStub(c: var DecodeContext; symAsStr: string; sn: ParsedSymName;
id: ItemId; entry: NifIndexEntry): PSym =
## Create + cache (keyed by the NIF name) a `Partial` global-sym stub, lazily
## filled later by `loadSym` from `entry`. `stubKindAndName` strips NIF-only
## markers (e.g. a package's `PkgMarker`) so the backend mangles the clean name.
let (stubKind, stubName) = stubKindAndName(c.cache, sn.name)
result = PSym(itemId: id, kindImpl: stubKind, name: stubName,
disamb: sn.count.int32, state: Partial)
c.syms[symAsStr] = (result, entry)
proc tryCreateTypeStub(c: var DecodeContext; name: string): PType =
## Like `createTypeStub` but returns nil instead of raising when the type has
## no offset in its module index (used by the best-effort `(offer …)` loader).
@@ -2024,8 +2158,7 @@ proc tryCreateTypeStub(c: var DecodeContext; name: string): PType =
let suffix = name.substr(i)
if suffix == SysModuleSuffix:
return reconstructSysType(c, name, k, itemVal)
let isBk = suffix.endsWith(BackendLocalMarker)
let realSuffix = if isBk: suffix[0 ..< suffix.len - BackendLocalMarker.len] else: suffix
let (isBk, realSuffix) = stripBkSuffix(suffix)
let modIdx = moduleId(c, realSuffix).int32
let id = if isBk: backendItemId(modIdx, itemVal) else: itemId(modIdx, itemVal)
let modFi = id.module.FileIndex
@@ -2037,34 +2170,12 @@ proc tryCreateTypeStub(c: var DecodeContext; name: string): PType =
c.types[name] = (result, c.mods[modFi].index.getOrDefault(name))
proc createTypeStub(c: var DecodeContext; name: string): PType =
## As `tryCreateTypeStub`, but a missing index offset is a hard error (the
## caller demanded a definition that must exist).
assert name.startsWith("`t")
result = c.types.getOrDefault(name)[0]
result = tryCreateTypeStub(c, name)
if result == nil:
var i = len("`t")
var k = 0
while i < name.len and name[i] in {'0'..'9'}:
k = k * 10 + name[i].ord - ord('0')
inc i
if i < name.len and name[i] == '.': inc i
var itemVal = 0'i32
while i < name.len and name[i] in {'0'..'9'}:
itemVal = itemVal * 10'i32 + int32(name[i].ord - ord('0'))
inc i
if i < name.len and name[i] == '.': inc i
let suffix = name.substr(i)
if suffix == SysModuleSuffix:
return reconstructSysType(c, name, k, itemVal)
let isBk = suffix.endsWith(BackendLocalMarker)
let realSuffix = if isBk: suffix[0 ..< suffix.len - BackendLocalMarker.len] else: suffix
let modIdx = moduleId(c, realSuffix).int32
let id = if isBk: backendItemId(modIdx, itemVal) else: itemId(modIdx, itemVal)
let modFi = id.module.FileIndex
if not hasTypeOffset(c, modFi, name):
raiseAssert "symbol has no offset: " & name
result = PType(itemId: id, uniqueId: id, kind: TTypeKind(k), state: Partial)
# `loadType` re-resolves the buffer via `typeCursor`, so the cached entry is a
# don't-care for types — store the primary one if any (else a 0-offset stub).
c.types[name] = (result, c.mods[modFi].index.getOrDefault(name))
raiseAssert "symbol has no offset: " & name
proc extractLocalSymsFromTree(c: var DecodeContext; n: var Cursor; thisModule: string;
localSyms: var Table[string, PSym]) =
@@ -2093,9 +2204,7 @@ proc extractLocalSymsFromTree(c: var DecodeContext; n: var Cursor; thisModule: s
# Local symbol - create stub and immediately load it fully
# since local symbols have no index offsets for lazy loading
let module = moduleId(c, thisModule)
let val = addr c.mods[module].symCounter
inc val[]
let id = itemId(module.int32, val[])
let id = c.nextSymId(module, isBk = false)
# `stubKindAndName` strips NIF-only markers (e.g. a field's `` `f ``) so the
# backend mangles the clean name; `loadSymFromCursor` then fills the real kind.
let (_, stubName) = stubKindAndName(c.cache, sn.name)
@@ -2148,11 +2257,9 @@ proc loadFieldStub(c: var DecodeContext; symAsStr: string; thisModule: string;
let sn = parseSymName(symAsStr)
let (stubKind, stubName) = stubKindAndName(c.cache, sn.name)
let module = moduleId(c, thisModule)
let val = addr c.mods[module].symCounter
inc val[]
# `sn.count` is the field POSITION (see toNifSymName): tuple element access reads
# it directly off this stub, so preserve it. Named-object uses re-navigate by name.
result = PSym(itemId: itemId(module.int32, val[]), kindImpl: stubKind,
result = PSym(itemId: c.nextSymId(module, isBk = false), kindImpl: stubKind,
name: stubName, disamb: sn.count.int32, state: Complete)
result.positionImpl = sn.count.int32
if typ != nil: result.typImpl = typ
@@ -2177,16 +2284,9 @@ proc loadSymStub(c: var DecodeContext; symAsStr: string; thisModule: string;
result = c.syms.getOrDefault(symAsStr)[0]
if result == nil:
# A process-local backend sym (closure env field / `:env` param) is named
# `…<thisModuleSuffix>@bk`: home it to that module with a backendItemId so it
# stays disjoint from the loader's real per-module id space (see toNifSymName).
let isBk = sn.module.endsWith(BackendLocalMarker)
let realMod = if isBk: sn.module[0 ..< sn.module.len - BackendLocalMarker.len]
else: sn.module
let module = moduleId(c, realMod)
let val = addr c.mods[module].symCounter
inc val[]
let id = if isBk: backendItemId(module.int32, val[]) else: itemId(module.int32, val[])
# `…<thisModuleSuffix>@bk`: `mintSymId` homes it to that module with a
# backendItemId so it stays disjoint from the loader's real id space.
let (module, id) = c.mintSymId(sn.module)
let offs = c.mods[module].index.getOrDefault(symAsStr)
if offs.offset == 0:
# Only module/package self-syms are never written as `(sd)` entries, so a
@@ -2199,9 +2299,7 @@ proc loadSymStub(c: var DecodeContext; symAsStr: string; thisModule: string;
infoImpl: newLineInfo(module, 1, 1), state: Complete)
c.syms[symAsStr] = (result, NifIndexEntry())
return result
let (stubKind, stubName) = stubKindAndName(c.cache, sn.name)
result = PSym(itemId: id, kindImpl: stubKind, name: stubName, disamb: sn.count.int32, state: Partial)
c.syms[symAsStr] = (result, offs)
result = c.makePartialSymStub(symAsStr, sn, id, offs)
proc loadSymStub(c: var DecodeContext; n: var Cursor; thisModule: string;
localSyms: var Table[string, PSym]): PSym =
@@ -2306,6 +2404,7 @@ proc loadTypeFromCursor(c: var DecodeContext; n: var Cursor; t: PType; localSyms
proc loadType*(c: var DecodeContext; t: PType) =
if t.state != Partial: return
t.state = c.loadedState
recordLoad(c, t.itemId.module.FileIndex, isType = true)
# A backend-minted (`@bk`) closure-env type produced by the `lower` stage lives
# ONLY in the `.t.nif` and is keyed by its `@bk` name (see nifTypeName), not the
# canonical `typeToNifSym` (which asserts non-`@bk`). Reconstruct that name so a
@@ -2399,7 +2498,10 @@ proc loadSymFromCursor(c: var DecodeContext; s: PSym; n: var Cursor; thisModule:
s.ownerFieldImpl = loadSymStub(c, n, thisModule, localSyms)
# Load the AST for routine symbols and constants
# Constants need their AST for astdef() to return the constant's value
let astNodesBefore = nodesDecoded
s.astImpl = loadNode(c, n, thisModule, localSyms)
if loadStatsInit == 1 and s.kindImpl in routineKinds:
astFieldNodes += nodesDecoded - astNodesBefore
loadLoc c, n, s.locImpl
s.constraintImpl = loadNode(c, n, thisModule, localSyms)
s.instantiatedFromImpl = loadSymStub(c, n, thisModule, localSyms)
@@ -2426,6 +2528,8 @@ proc loadSymFromCursor(c: var DecodeContext; s: PSym; n: var Cursor; thisModule:
proc loadSym*(c: var DecodeContext; s: PSym) =
if s.state != Partial: return
s.state = c.loadedState
if loaderCtx == nil: loaderCtx = addr c
recordLoad(c, s.itemId.module.FileIndex, isType = false)
let symsModule = s.itemId.module.FileIndex
let nifname = globalName(s, c.infos.config)
var n = cursorFromIndexEntry(c, symsModule, c.syms[nifname][1])
@@ -2478,6 +2582,7 @@ template withNode(c: var DecodeContext; n: var Cursor; result: PNode; kind: TNod
proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string;
localSyms: var Table[string, PSym]): PNode =
if loadStatsInit == 1: inc nodesDecoded
result = nil
case n.kind
of Symbol:
@@ -2544,9 +2649,7 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string;
if sym == nil:
# First time seeing this local symbol - create it
let module = moduleId(c, thisModule)
let val = addr c.mods[module].symCounter
inc val[]
let id = itemId(module.int32, val[])
let id = c.nextSymId(module, isBk = false)
# strip NIF-only markers (a field's `` `f ``) so the backend sees the
# clean name; `loadSymFromCursor` below fills the real kind.
let (_, stubName) = stubKindAndName(c.cache, sn.name)
@@ -2587,9 +2690,7 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string;
else:
sym = c.syms.getOrDefault(symName)[0]
if sym == nil:
let val = addr c.mods[m].symCounter
inc val[]
sym = PSym(itemId: itemId(m.int32, val[]), kindImpl: skStub,
sym = PSym(itemId: c.nextSymId(m, isBk = false), kindImpl: skStub,
name: c.cache.getIdent(sn.name), disamb: sn.count.int32,
state: Partial)
c.syms[symName] = (sym, NifIndexEntry())
@@ -2666,6 +2767,27 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string;
of nkNilLit:
c.withNode n, result, kind:
discard
of routineDefs:
# Defer the heavy `bodyPos` son: build the routine-def header eagerly, but
# install a `nfLazyBody` placeholder (carrying the real body kind, so cheap
# `ast[bodyPos].kind != nkEmpty` checks need no load) whose children are
# materialized on demand (see `materializeLazyBody`, driven by the `len`
# hook). An empty body is a single node — not worth deferring.
c.withNode n, result, kind:
var idx = 0
while n.hasMore:
if idx == bodyPos and n.kind == TagLit and
n.nodeKind notin {nkEmpty, nkNone}:
let info = c.infos.oldLineInfo(n.info, cursorPool(n))
let ph = newNodeI(n.nodeKind, info)
ph.flags.incl nfLazyBody
c.pendingBodies[cast[int](ph)] =
PendingBody(cursor: n, thisModule: thisModule, localSyms: localSyms)
result.sons.add ph
skip n
else:
result.sons.add c.loadNode(n, thisModule, localSyms)
inc idx
else:
c.withNode n, result, kind:
while n.hasMore:
@@ -2673,25 +2795,46 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string;
else:
raiseAssert "expected string literal but got " & $n.kind
proc materializeLazyBody*(c: var DecodeContext; node: PNode) =
## Fill a `nfLazyBody` placeholder's children in place (identity-preserving:
## callers already hold `node`). Decodes the deferred body from the stashed
## cursor with the enclosing def's `localSyms` so param/local refs resolve to
## the SAME PSyms the signature created.
node.flags.excl nfLazyBody # clear first: the loadNode below calls `len`
let key = cast[int](node)
var pb = PendingBody()
if not c.pendingBodies.pop(key, pb): return
var cur = pb.cursor
let real = c.loadNode(cur, pb.thisModule, pb.localSyms)
# `real` has the same kind as the placeholder (peeked at defer time); graft its
# decoded content onto the node the callers hold.
node.sons = real.sons
node.typField = real.typField
node.flags = real.flags
forceLazyBodyHook = proc (n: PNode) {.nimcall, raises: [], tags: [], gcsafe.} =
# `len` (the sole caller path) MUST stay effect-free, so this hook is typed
# `raises: []`. The underlying `loadNode` chain infers `raises: [KeyError]`
# (index/sym Table lookups), but materialization only ever runs for a body
# DEFERRED during THIS load — the buffer/index is present by construction, so a
# KeyError here means a corrupt cache: a fatal bug, not a recoverable error.
# Treat it as effect-free (a `Defect`-like invariant) via a scoped cast.
if loaderCtx != nil:
{.cast(raises: []).}:
{.cast(tags: []).}:
{.cast(gcsafe).}:
materializeLazyBody(loaderCtx[], n)
proc loadSymFromIndexEntry(c: var DecodeContext; module: FileIndex;
nifName: string; entry: NifIndexEntry; thisModule: string): PSym =
## Loads a symbol from the NIF index entry using the entry directly.
## Creates a symbol stub without looking up in the index (since the index may be moved out).
result = c.syms.getOrDefault(nifName)[0]
if result == nil:
let symAsStr = nifName
let sn = parseSymName(symAsStr)
let sn = parseSymName(nifName)
let rawMod = if sn.module.len > 0: sn.module else: thisModule
let isBk = rawMod.endsWith(BackendLocalMarker)
let realMod = if isBk: rawMod[0 ..< rawMod.len - BackendLocalMarker.len] else: rawMod
let symModule = moduleId(c, realMod)
let val = addr c.mods[symModule].symCounter
inc val[]
let id = if isBk: backendItemId(symModule.int32, val[]) else: itemId(symModule.int32, val[])
let (stubKind, stubName) = stubKindAndName(c.cache, sn.name)
result = PSym(itemId: id, kindImpl: stubKind, name: stubName, disamb: sn.count.int32, state: Partial)
c.syms[symAsStr] = (result, entry)
let (_, id) = c.mintSymId(rawMod)
result = c.makePartialSymStub(nifName, sn, id, entry)
proc extractBasename(nifName: string): string =
## Extract the base name from a NIF name (ident.disamb.module -> ident)
@@ -2790,9 +2933,7 @@ proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: boo
let sn = parseSymName(symAsStr)
if sn.module.len == 0:
return nil # Local symbols shouldn't be hooks
let isBk = sn.module.endsWith(BackendLocalMarker)
let realMod = if isBk: sn.module[0 ..< sn.module.len - BackendLocalMarker.len]
else: sn.module
let (isBk, realMod) = stripBkSuffix(sn.module)
let module = moduleId(c, realMod)
# Look up the symbol in the module's index
# Try both formats: with module suffix (e.g., "foo.0.modulename") and without (e.g., "foo.0.")
@@ -2806,12 +2947,9 @@ proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: boo
return nil
if not alsoConsiderPrivate and offs.vis == Hidden:
return nil
# Create a stub symbol
let val = addr c.mods[module].symCounter
inc val[]
let id = if isBk: backendItemId(int32(module), val[]) else: itemId(int32(module), val[])
result = PSym(itemId: id, kindImpl: skProc, name: c.cache.getIdent(sn.name),
disamb: sn.count.int32, state: Partial)
# Create a stub symbol (skProc: `resolveSym` only resolves hook/routine syms).
result = PSym(itemId: c.nextSymId(module, isBk), kindImpl: skProc,
name: c.cache.getIdent(sn.name), disamb: sn.count.int32, state: Partial)
c.syms[symAsStr] = (result, offs)
proc resolveHookSym*(c: var DecodeContext; name: string): PSym =
@@ -3115,9 +3253,14 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag]
skip cur
elif tagIs(cur, "implementation"):
cont = false
elif LoadFullAst in flags or tagIs(cur, toNifTag(nkLetSection)) or tagIs(cur, toNifTag(nkVarSection)):
elif LoadFullAst in flags or tagIs(cur, toNifTag(nkLetSection)) or
tagIs(cur, toNifTag(nkVarSection)) or tagIs(cur, toNifTag(nkPragma)):
# Parse the full statement. let/var sections are loaded unconditionally
# (see above) so `{.compileTime.}` globals reach the eager initializer.
# Top-level pragmas are loaded too: a module-level `{.emit.}` (and the
# `{.push/pop.}` around it) must reach the `cg` stage's genPragma/genEmit,
# else e.g. a `#include` is dropped and the generated C won't compile.
# writeToplevelNode routes these into this header section.
let stmtNode = loadNode(c, cur, suffix, localSyms)
if stmtNode != nil:
result.topLevel.sons.add stmtNode

View File

@@ -339,6 +339,14 @@ type
# because openSym experimental switch is disabled
# gives warning instead
nfLazyType # node has a lazy type
nfLazyBody # IC: this node is a placeholder for a routine body (bodyPos son)
# not yet materialized. Reading its children (via `len`/`safeLen`)
# triggers `forceLazyBodyHook`. Process-local, stripped on serialize.
nfBroadcast # this `nkBracket` is a *broadcast* default array: a single son
# standing for `lengthOrd` identical zero copies (see
# `broadcastArrayThreshold`). The flag disambiguates it from an
# ordinary 1-element collection (e.g. a seq value that happens to
# carry an array type), so it must survive copies + serialization.
TNodeFlags* = set[TNodeFlag]
TTypeFlag* = enum # keep below 32 for efficiency reasons (now: 47)
@@ -866,7 +874,8 @@ const
nfFromTemplate, nfDefaultRefsParam,
nfExecuteOnReload, nfLastRead,
nfFirstWrite, nfSkipFieldChecking,
nfDisabledOpenSym, nfLazyType}
nfDisabledOpenSym, nfLazyType,
nfBroadcast}
namePos* = 0
patternPos* = 1 # empty except for term rewriting macros
genericParamsPos* = 2
@@ -903,7 +912,24 @@ const
defaultOffset* = -1
var forceLazyBodyHook*: proc (n: PNode) {.nimcall, raises: [], tags: [], gcsafe.}
## Set by the IC loader (ast2nif). When a node carries `nfLazyBody`, any access
## to its children through `len` materializes the deferred routine body in place.
## `safeLen` delegates to `len`, so it is covered transitively; a lazy body is
## never a leaf kind, so the `{nkNone..nkNilLit}` short-circuit never hides it.
##
## The type MUST be effect-free (`raises: []`/`tags: []`): `len` is a fundamental
## `PNode` accessor that the whole compiler — and every compiler-as-library
## consumer (nimble, nimsuggest, ...) — assumes cannot raise. An unannotated
## `proc` var defaults to `raises: [Exception]`, so the indirect call tainted
## `len`/`safeLen`/`items` with `Exception`, breaking any iterator/`{.raises.}`
## over a `PNode` (e.g. nimble's `extract {.raises: [CatchableError].}`).
## Materialization is a pure in-memory buffer transform; a corrupt buffer is a
## `Defect` (`raiseAssert`), which is outside exception tracking.
proc len*(n: PNode): int {.inline.} =
if nfLazyBody in n.flags and forceLazyBodyHook != nil:
forceLazyBodyHook(n)
result = n.sons.len
proc safeLen*(n: PNode): int {.inline.} =

View File

@@ -3964,6 +3964,13 @@ proc getDefaultValue(p: BProc; typ: PType; info: TLineInfo; result: var Builder)
let elemTyp = skipTypes(t.elementType, abstractRange+{tyOwned}-{tyTypeDesc})
if isOpaqueImportcType(elemTyp):
result.add "{0}"
elif toInt(lengthOrd(p.config, t.indexType)) > broadcastArrayThreshold and
elemTyp.kind in {tyInt..tyUInt64, tyBool, tyChar, tyFloat..tyFloat128,
tyPtr, tyPointer, tyCstring}:
# Large array of a scalar whose default is the zero representation: a single
# C `{0}` zero-fills all `lengthOrd` slots instead of emitting that many
# initializers (keeps huge SSZ-style zero buffers compact in the C output).
result.add "{0}"
else:
var arrInit: StructInitializer
result.addStructInitializer(arrInit, kind = siArray):
@@ -4250,7 +4257,13 @@ proc genBracedInit(p: BProc, n: PNode; isConst: bool; optionalType: PType; resul
var d: TLoc = initLocExpr(p, n)
result.add rdLoc(d)
of tyArray, tyVarargs:
genConstSimpleList(p, n, isConst, result)
if isDefaultBroadcastArray(n, p.config):
# Compact zero/null-default array (see `isDefaultBroadcastArray`): the
# whole thing is the null value of every slot, so a single C `{0}`
# zero-fills all `lengthOrd` elements — no need to materialise them.
result.add "{0}"
else:
genConstSimpleList(p, n, isConst, result)
of tyTuple:
genConstTuple(p, n, isConst, typ, result)
of tyOpenArray:

View File

@@ -819,7 +819,11 @@ proc genRecordFieldsAux(m: BModule; n: PNode,
# don't use fieldType here because we need the
# tyGenericInst for C++ template support
let noInit = sfNoInit in field.flags or (field.typ.sym != nil and sfNoInit in field.typ.sym.flags)
if not noInit and (fieldType.isOrHasImportedCppType() or hasCppCtor(m, field.owner.typ)):
# Under `nim ic`, object fields are local NIF syms restored without an
# `owner`; `rectype` is the owning record type, so fall back to it rather
# than deref a nil `field.owner`.
let ownerTyp = if field.owner != nil: field.owner.typ else: rectype
if not noInit and (fieldType.isOrHasImportedCppType() or hasCppCtor(m, ownerTyp)):
var didGenTemp = false
initializer = genCppInitializer(m, nil, fieldType, didGenTemp)
result.addField(field, sname, typ, isFlexArray, initializer)

View File

@@ -1663,7 +1663,15 @@ proc genProcPrototype(m: BModule, sym: PSym) =
useHeader(m, sym)
if lfNoDecl in sym.loc.flags or sfCppMember * sym.flags != {}: return
if lfDynamicLib in sym.loc.flags:
if sym.itemId.module != m.module.position and
if m.config.cmd == cmdNifC and m.config.icBackendStage == "cg":
# Under IC per-module cg every demander emits the dynlib proc's DEFINITION
# locally (findPendingModule returns `m`, so symInDynamicLib follows this
# call and the merge stage keeps one def per C name). Emitting the
# cross-module `extern` proto here would register `sym.id` in
# `m.declaredThings` and thereby make that `symInDynamicLib` skip, leaving
# the `Dl_*` symbol declared-but-never-defined -> undefined at link.
discard "definition emitted by symInDynamicLib"
elif sym.itemId.module != m.module.position and
not containsOrIncl(m.declaredThings, sym.id):
let vis = if isReloadable(m, sym): StaticProc else: Extern
let name = mangleDynLibProc(sym)

View File

@@ -15,7 +15,7 @@ import options, msgs, lineinfos, pathutils, condsyms,
modulepaths, extccomp, cnif, platform
import "../dist/nimony/src/lib" / [nifstreams, bitabs, nifreader, nifbuilder]
import "../dist/nimony/src/gear2" / modnames
import icmodnames
import icnifcore
type
@@ -956,7 +956,12 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
result = nimcache / c.nodes[0].files[0].modname & ".backend.build.nif"
let mainNif = c.nodes[0].files[0].nimFile
let exeFile = changeFileExt(c.nodes[0].files[0].nimFile, ExeExt)
# Honor `--out`/`--outdir`: `cmdIc`'s `setOutFile` populated `conf.outFile`
# (the user's `--out`, or the default `<project><exeExt>`), 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.
@@ -1117,6 +1122,11 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
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 `<maindir>/<main><exeExt>`.
# 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

View File

@@ -1473,6 +1473,8 @@ proc genFlags*(s: set[TNodeFlag]; dest: var string) =
of nfSkipFieldChecking: dest.add "s0"
of nfDisabledOpenSym: dest.add "d3"
of nfLazyType: dest.add "l1"
of nfLazyBody: discard # process-local placeholder; never serialized
of nfBroadcast: dest.add "v"
proc parse*(t: typedesc[TNodeFlag]; s: string): set[TNodeFlag] =
@@ -1533,6 +1535,7 @@ proc parse*(t: typedesc[TNodeFlag]; s: string): set[TNodeFlag] =
inc i
else: result.incl nfSem
of 't': result.incl nfTransf
of 'v': result.incl nfBroadcast
of 'w': result.incl nfFirstWrite
else: discard
inc i

55
compiler/icmodnames.nim Normal file
View File

@@ -0,0 +1,55 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Nim's OWN module-suffix, replacing nimony's `gear2/modnames.moduleSuffix`.
##
## nimony's version hashes a path made RELATIVE to `getCurrentDir()` (or the
## shortest search-path-relative form), so the produced suffix depends on the
## current working directory AND the searchPath set. Under `nim ic` the
## DISCOVERY pass (`deps.nim`, in the driver process) and the COMPILE pass
## (`nifgen`/`typekeys`, in a child `nim m` process) can run with different CWDs
## or `--path` sets, so the SAME file hashes to two different suffixes: e.g.
## `std/staticos` became `sta5rk8sn1` at discovery but `sta4c0qxk` at compile, so
## every importer waited forever for a `.s.bif` that was actually written under
## the other name — a cold `nim ic` build (of anything pulling in `std/os`, whose
## `oscommon` does `from std/staticos import PathComponent`) never converged.
##
## Hashing the CANONICAL ABSOLUTE path makes the suffix a pure function of the
## file, identical across every process and call site. The base-name prefix +
## base-36 `uhash` layout is kept byte-for-byte compatible with the old scheme so
## nothing but the hashed string changes.
import std/os
import "../dist/nimony/src/lib" / tinyhashes
const
PrefixLen = 3 # keep it short: the suffix ends up in every mangled C name
Base36 = "0123456789abcdefghijklmnopqrstuvwxyz"
proc moduleSuffix*(path: string; searchPaths: openArray[string]): string =
## `searchPaths` is accepted for signature-compatibility with the replaced
## `modnames.moduleSuffix` but is deliberately IGNORED — the suffix must not
## depend on the search-path set or the CWD (see the module doc).
# Absolute inputs (the norm at every call site: `toFullPath`/`projectFull`)
# pass straight through `normalizedPath` with no `getCurrentDir` involvement;
# a stray relative path is made absolute against the CWD only as a fallback.
var f = path
if not isAbsolute(f):
try: f = absolutePath(f)
except CatchableError: discard
f = normalizedPath(f)
let m = splitFile(f).name
var id = uhash(f)
result = newStringOfCap(10)
for i in 0 ..< min(m.len, PrefixLen):
result.add m[i]
# base-36 of the hash, low digit first (order is irrelevant for identity).
while id > 0'u32:
result.add Base36[int(id mod 36'u32)]
id = id div 36'u32

View File

@@ -436,6 +436,9 @@ proc mainCommand*(graph: ModuleGraph) =
# Generate .build.nif for nifmake
setUseIc(true)
wantMainModule(conf)
# Resolve the output binary path (honoring `--out`) up front, like cmdNifC:
# the backend build file derives the link target from `conf.absOutFile`.
setOutFile(conf)
when not defined(nimKochBootstrap):
commandIc(conf)
else:

View File

@@ -906,6 +906,10 @@ proc needsCompilation*(g: ModuleGraph, fileIdx: FileIndex): bool =
proc getBody*(g: ModuleGraph; s: PSym): PNode {.inline.} =
result = s.ast[bodyPos]
if result != nil and nfLazyBody in result.flags and forceLazyBodyHook != nil:
# Sanctioned body-access gate (see astdef.bodyPos): materialize the deferred
# IC body so callers may safely touch `.sons` directly, not only via `len`.
forceLazyBodyHook(result)
assert result != nil
when not defined(nimKochBootstrap):

View File

@@ -24,12 +24,22 @@ when defined(nimPreviewSlimSystem):
import ast, options, lineinfos, modulegraphs, cgendata, cgen,
pathutils, extccomp, msgs, modulepaths, idents, types, ast2nif, typekeys,
cnif
cnif, icmodnames
from cgmeth import generateIfMethodDispatchers
from transf import transformBody
from injectdestructors import injectDestructorCalls
import ic / replayer
proc systemNifSuffix(conf: ConfigRef): string =
## The system module's NIF suffix, derived from `system.nim`'s path EXACTLY as
## the frontend derives it (deps.nim's `toPair` on `libpath/system.nim`), so the
## backend loads the very `.s.bif` the frontend wrote. It must NOT be a constant:
## `moduleSuffix` (icmodnames) now hashes the absolute path, so the system suffix
## is install-dependent (was hardcoded `sysma2dyk`, valid only for the old
## relative-path scheme where `system.nim` always relativized to `system.nim`).
moduleSuffix((conf.libpath / RelativeFile"system.nim").string,
cast[seq[string]](conf.searchPaths))
proc loadModuleDependencies(g: ModuleGraph; mainFileIdx: FileIndex;
nifFiles: var seq[string];
depFlags: set[LoadFlag] = {LoadFullAst}): seq[PrecompiledModule] =
@@ -198,8 +208,16 @@ proc ownsRuntimeRoutine(s: PSym; modPos: int): bool =
{sfForward, sfImportc, sfCompileTime, sfError} * s.flags == {} and
s.typ != nil and not signatureHasMetaType(s.typ) and
s.ast != nil and s.ast.safeLen > bodyPos and
s.ast[genericParamsPos].kind == nkEmpty and
s.ast[bodyPos].kind != nkEmpty
s.ast[genericParamsPos].kind == nkEmpty
# NOTE: an `nkEmpty` body is NOT a disqualifier. A concrete, owned, non-
# forward/-importc/-magic routine whose body folds to nothing is still a real
# definition the owner must emit (`void f(void){}`), exactly as whole-program
# cgen does — else a cross-module caller links to nothing. This bites e.g.
# Nimbus' `extras.incInternalErrors`, a plain `proc` whose sole statement is a
# metrics-counter `.inc()` that the `metrics` library expands to a no-op when
# the importing tool (ncli) builds with `-u:metrics`; the body is then a bare
# `nkEmpty`, but `state_transition_epoch` still calls it. Forward declarations
# (the other empty-body case) carry `sfForward` and are excluded above.
proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
## Generate C code for a single module.
@@ -245,7 +263,7 @@ proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex):
## and only needs each module's `(replay ...)` directives, which load anyway.
resetForBackend(g)
var isKnownFile = false
let systemFileIdx = registerNifSuffix(g.config, "sysma2dyk", isKnownFile)
let systemFileIdx = registerNifSuffix(g.config, systemNifSuffix(g.config), isKnownFile)
g.config.m.systemFileIdx = systemFileIdx
var precompSys = moduleFromNifFile(g, systemFileIdx, {AlwaysLoadInterface})
g.systemModule = precompSys.module
@@ -268,7 +286,7 @@ proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex):
# closure here too — otherwise `findTargetModule` cannot resolve their suffix.
block:
var visited = initHashSet[string]()
visited.incl "sysma2dyk"
visited.incl systemNifSuffix(g.config)
for m in modules:
visited.incl cachedModuleSuffix(g.config, FileIndex m.module.position)
var stack: seq[ModuleSuffix] = @[]
@@ -311,14 +329,14 @@ proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
## dispatchers, runs essentially alone since every other `.c.nif` precedes it).
resetForBackend(g)
var isKnownFile = false
let systemFileIdx = registerNifSuffix(g.config, "sysma2dyk", isKnownFile)
let systemFileIdx = registerNifSuffix(g.config, systemNifSuffix(g.config), isKnownFile)
g.config.m.systemFileIdx = systemFileIdx
let precompSys = moduleFromNifFile(g, systemFileIdx, {AlwaysLoadInterface})
g.systemModule = precompSys.module
var modules: seq[PrecompiledModule] = @[]
var visited = initHashSet[string]()
visited.incl "sysma2dyk"
visited.incl systemNifSuffix(g.config)
# Only the target is codegen'd, so only it needs its full AST; the closure is
# loaded interface-only (demanded bodies come lazily from the kept-open
@@ -730,31 +748,33 @@ proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
let targetIsMain = g.config.icBackendModule.len == 0 or
g.config.icBackendModule == mainSuffix
var modules: seq[PrecompiledModule]
var precompSys: PrecompiledModule
var target: PrecompiledModule
if targetIsMain:
var nifFiles: seq[string]
(modules, precompSys, nifFiles) = loadBackendModules(g, mainFileIdx)
if modules.len == 0:
rawMessage(g.config, errGenerated,
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
return
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
else:
(modules, precompSys, target) = loadDepClosure(g, g.config.icBackendModule)
if target.module == nil:
# emit renders a module's final `.c` PURELY from its own `.c.nif` and the merge
# decision (see `renderCFromArtifact` — text filtering, no AST is touched). It
# used to load the target's whole transitive import closure as BModules solely
# to reach `getCFile(bmod)` for the output path. Under the fire-all-every-edit
# merge barrier (every `emit` re-fires whenever `merge` bumps the decision's
# mtime — deliberate insurance so a decision change re-renders all `.c`
# consistently) that per-process `loadDepClosure` was the bulk of a warm
# rebuild's cost: 240 processes each re-parsing a module closure only to filter
# a handful of `.c.nif`s whose bytes are usually unchanged. Derive the `.c`
# path directly instead — the SAME pure computation `deps.nim.backendCFile`
# uses to DECLARE this stage's output (`getCFile` == that formula) — so an emit
# process loads nothing and the fire-all costs process-startup, not a graph load.
let cfilename =
if targetIsMain: AbsoluteFile toFullPath(g.config, mainFileIdx)
else: AbsoluteFile g.config.icBackendModule
let cfile = changeFileExt(completeCfilePath(g.config,
mangleModuleName(g.config, cfilename).AbsoluteFile), ".nim.c").string
let artifact = cfile & ".nif"
if not fileExists(artifact):
rawMessage(g.config, errGenerated,
"per-module emit: module not found for suffix: " & g.config.icBackendModule)
"per-module emit: missing .c.nif artifact for suffix: " & g.config.icBackendModule)
return
let decision = readMergeDecision(getNimcacheDir(g.config).string / MergeDecisionFile)
if decision.broken:
rawMessage(g.config, errGenerated,
"per-module emit: missing or unparsable merge decision " & MergeDecisionFile)
return
let bmod = BModuleList(g.backend).mods[target.module.position]
let cfile = getCFile(bmod).string
let artifact = cfile & ".nif"
var dropped = 0
let code = renderCFromArtifact(artifact, decision, extractFilename(artifact), dropped)
# Write the `.c` content-stably. `merge` re-runs on any edit and bumps the

View File

@@ -16,7 +16,7 @@ import
import "../dist/nimony/src/lib" / nifbuilder
import "../dist/nimony/src/models" / nifler_tags
import "../dist/nimony/src/gear2" / modnames
import icmodnames
## This was copied from Nifler's bridge.nim. However, this code will evolve
## in a different direction as it needs to translate the semchecked AST which

View File

@@ -29,7 +29,7 @@ const
nimEnableCovariance* = defined(nimEnableCovariance)
icFormatVersion* = "25"
icFormatVersion* = "29"
## 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`

View File

@@ -476,7 +476,12 @@ proc foldArrayAccess(m: PSym, n: PNode; idgen: IdGenerator; g: ModuleGraph): PNo
#localError(g.config, n.info, formatErrorIndexBound(idx, x.len-1) & $n)
of nkBracket:
idx -= toInt64(firstOrd(g.config, x.typ))
if idx >= 0 and idx < x.len: result = x[int(idx)]
if isDefaultBroadcastArray(x, g.config):
# compact default array: any in-bounds index folds to the default element
if idx >= 0 and idx < toInt64(lengthOrd(g.config, x.typ.skipTypes(abstractInst))):
result = copyTree(x[0])
else: result = nil
elif idx >= 0 and idx < x.len: result = x[int(idx)]
else:
result = nil
#localError(g.config, n.info, formatErrorIndexBound(idx, x.len-1) & $n)

View File

@@ -1126,9 +1126,20 @@ proc trackCall(tracked: PEffects; n: PNode) =
# the per-module IC backend emits every owned routine (no DCE) and would
# otherwise feed the magic to codegen. Mirrors the `tfTriggersCompileTime ->
# sfCompileTime` path in `semProcAux`.
#
# GATE TO THE IC STAGES ONLY (`cmdM` sem + `cmdNifC` cg). The magic can reach a
# runtime proc's body via an INLINED TEMPLATE (not a macro/template *owner*, so
# the `insideMeta` walk below can't see it) — e.g. confutils' runtime
# `addConfigFile`/json-serialization's `inputFile` expand a serialization
# template that pastes a `getAst`/`quote` magic inline. Under plain `nim c` such
# a proc still code-generates fine (the magic folds / is demand-pruned), so
# marking it `sfCompileTime` there is a pure regression: "request to generate
# code for .compileTime proc". Only the emit-everything IC backend needs the
# mark, so restrict it to `{cmdM, cmdNifC}` (was `!= cmdNimscript`, which
# wrongly swept in `cmdCompileToC`/JS/`cmdCheck`).
if a.kind == nkSym and a.sym.magic in {mNLen..mNError, mSlurp..mQuoteAst} and
tracked.owner != nil and tracked.owner.kind in routineKinds and
tracked.config.cmd != cmdNimscript and tracked.inNimvmBranch == 0:
tracked.config.cmd in {cmdM, cmdNifC} and tracked.inNimvmBranch == 0:
# ...but NOT under `nim e`: nimscript has no codegen backend to protect, and
# marking a routine `sfCompileTime` makes `semExpr` eagerly fold calls to it
# at sem time (emConst), where module-level globals it reads have no VM slot
@@ -1198,8 +1209,16 @@ proc trackCall(tracked: PEffects; n: PNode) =
else:
if laxEffects notin tracked.c.config.legacyFeatures and a.kind == nkSym and
a.sym.kind in routineKinds:
let (isHook, opKind) = findHookKind(a.sym.name.s)
if (not isHook) or opKind notin {attachedAsgn, attachedSink, attachedDup}:
# A hook reaching here has no effect list yet, i.e. it has not been
# effect-tracked. Propagating from its (still unset) type flags would
# spuriously mark the caller GC-unsafe/side-effecting: e.g. under
# `nim ic` a concrete `=destroy` reached through a generic
# instantiation is not analyzed before the instance body is tracked
# here. Skip all such hooks (generalizes #25940, which special-cased
# `=asgn`/`=sink`/`=dup`); once analyzed they carry an effect list and
# take the branch below.
let (isHook, _) = findHookKind(a.sym.name.s)
if not isHook:
propagateEffects(tracked, n, a.sym)
else:
mergeRaises(tracked, effectList[exceptionEffects], n)

View File

@@ -294,13 +294,22 @@ proc replaceTypeVarsN(cl: var TReplTypeVars, n: PNode; start=0; expectedType: PT
replaceTypeVarsS(cl, n.sym, result.typ)
else:
replaceTypeVarsS(cl, n.sym, replaceTypeVarsT(cl, n.sym.typ))
if result.sym.kind == skField and result.sym.ast != nil and
if result.sym.kind == skField and
(cl.owner == nil or result.sym.owner == cl.owner):
# instantiate default value of object/tuple field
var n = result.sym.ast
cl.c.fitDefaultNode(cl.c, n, result.sym.typ)
result.sym.ast = n
result.sym.typ = n.typ.skipIntLit(cl.c.idgen)
if result.sym.ast != nil:
# instantiate default value of object/tuple field
var n = result.sym.ast
cl.c.fitDefaultNode(cl.c, n, result.sym.typ)
result.sym.ast = n
result.sym.typ = n.typ.skipIntLit(cl.c.idgen)
elif result.typ != nil:
# The field SYM can be SHARED across the branches of an `nkRecWhen` (the
# generic body reuses one `value` PSym, so it carries the LAST branch's
# type), while the resolved field NODE carries the correct branch type.
# Sync the sym to the node so the instantiated field's sym-type and
# node-type agree (else a generic-object instance serializes a field
# whose sym-type diverges from its node-type -> loader/computeSize crash).
result.sym.typ = result.typ
# sym type can be nil if was gensym created by macro, see #24048
if result.sym.typ != nil and result.sym.typ.kind == tyVoid:
# don't add the 'void' field

View File

@@ -13,7 +13,7 @@
import std/[assertions, sets]
import "../dist/nimony/src/lib" / [treemangler]
import "../dist/nimony/src/gear2" / modnames
import icmodnames
import astdef, idents, options, lineinfos, msgs
import ic / [enum2nif]

View File

@@ -633,6 +633,34 @@ proc lengthOrd*(conf: ConfigRef; t: PType): Int128 =
let first = firstOrd(conf, t)
result = last - first + One
const broadcastArrayThreshold* = 32
## `getNullValue` represents the default of an `array[N, T]` with `N` above this
## as a single *broadcast* element — a one-son `nkBracket` standing for `N`
## identical zero copies — instead of materialising `N` zero nodes. This keeps
## huge zeroed arrays (e.g. SSZ byte buffers in nimbus) compact in the IC caches
## (`.s.bif`/`.t.bif`), in the VM, and in the generated C (`{0}` zero-fills).
proc isDefaultBroadcastArray*(n: PNode; conf: ConfigRef): bool =
## True iff `n` is a broadcast default array: a single son standing for
## `lengthOrd` identical zero copies. Identified by the explicit `nfBroadcast`
## marker (set by `getNullValue`), NOT by `len == 1 < lengthOrd` — the latter
## also matches an ordinary 1-element collection that happens to be an
## `nkBracket` carrying an array type, e.g. a `@[a, b, c]` seq value shrunk to
## length 1 by `setLen`/`delete` (its VM node keeps the array-literal type).
result = n != nil and n.kind == nkBracket and nfBroadcast in n.flags
proc expandBroadcastArray*(n: PNode; conf: ConfigRef) =
## Materialise a broadcast default array (see `isDefaultBroadcastArray`) into a
## full `lengthOrd`-son `nkBracket`, each son a copy of the single default
## element. Used by VM ops that index-address, mutate, or measure such a node;
## the common read-only paths leave it compact. Clears `nfBroadcast` since the
## node is now a fully materialised literal.
if isDefaultBroadcastArray(n, conf):
let total = toInt(lengthOrd(conf, n.typ.skipTypes(abstractInst)))
let elem = n[0]
for i in 1 ..< total: n.add copyTree(elem)
n.flags.excl nfBroadcast
# -------------- type equality -----------------------------------------------
type

View File

@@ -702,6 +702,10 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
# A bodge, but this takes in `toOpenArray(rb, rc, rc)` and emits
# nkTupleConstr(x, y, z) into the `regs[ra]`. These can later be used for calculating the slice we have taken.
decodeBC(rkNode)
# Slicing/openArray needs the real length and per-element nodes, so a
# compact default array must be materialised first.
if isDefaultBroadcastArray(regs[ra].node, c.config):
expandBroadcastArray(regs[ra].node, c.config)
let
collection = regs[ra].node
leftInd = regs[rb].intVal
@@ -770,6 +774,15 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
regs[ra].node.intVal = src.strVal[idx].ord
else:
stackTrace(c, tos, pc, formatErrorIndexBound(idx, src.strVal.len-1))
elif isDefaultBroadcastArray(src, c.config):
# `a[i]` on a compact default array yields `default(T)` directly, without
# ever materialising the (potentially huge) array — the point of the
# broadcast form. See `getNullValue`/`isDefaultBroadcastArray`.
let total = toInt(lengthOrd(c.config, src.typ.skipTypes(abstractInst)))
if idx <% total:
regs[ra].node = copyTree(src[0])
else:
stackTrace(c, tos, pc, formatErrorIndexBound(idx, total-1))
elif src.kind notin {nkEmpty..nkFloat128Lit} and idx <% src.len:
regs[ra].node = src[idx]
else:
@@ -781,6 +794,9 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
stackTrace(c, tos, pc, formatErrorIndexBound(regs[rc].intVal, high(int)))
let idx = regs[rc].intVal.int
let src = if regs[rb].kind == rkNode: regs[rb].node else: regs[rb].nodeAddr[]
# Taking the address of an element needs distinct, stable per-slot nodes, so
# a compact default array must be materialised first.
if isDefaultBroadcastArray(src, c.config): expandBroadcastArray(src, c.config)
case src.kind
of nkTupleConstr:
let
@@ -829,6 +845,8 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
let idx = regs[rb].intVal.int
assert regs[ra].kind == rkNode
let arr = regs[ra].node
# Writing a slot materialises a compact default array into a full literal.
if isDefaultBroadcastArray(arr, c.config): expandBroadcastArray(arr, c.config)
case arr.kind
of nkTupleConstr: # refer to `opcSlice`
let
@@ -1031,6 +1049,8 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
case node.kind
of nkTupleConstr: # refer to `of opcSlice`
regs[ra].intVal = node[2].intVal - node[1].intVal + 1 - high
elif isDefaultBroadcastArray(node, c.config):
regs[ra].intVal = toInt(lengthOrd(c.config, node.typ.skipTypes(abstractInst))) - high
else:
# safeArrLen also return string node len
# used when string is passed as openArray in VM

View File

@@ -1921,7 +1921,11 @@ proc genCheckedObjAccessAux(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags
let strType = getSysType(c.graph, n.info, tyString)
var msgReg: TDest = c.getTemp(strType)
let fieldName = $accessExpr[1]
let msg = genFieldDefect(c.config, fieldName, disc.sym)
# Re-navigate the discriminant in the object type: under `nim ic` `disc.sym` is a
# field-use stub with a nil `owner`, which `genFieldDefect` dereferences. Look up the
# canonical discriminant field by name. Byte-neutral for non-IC (returns the same sym).
let dfield = lookupFieldAgain(accessExpr[0].typ, disc.sym)
let msg = genFieldDefect(c.config, fieldName, dfield)
let strLit = newStrNode(msg, accessExpr[1].info)
strLit.typ = strType
c.genLit(strLit, msgReg)
@@ -2025,8 +2029,20 @@ proc getNullValue(c: PCtx; typ: PType, info: TLineInfo; conf: ConfigRef): PNode
getNullValueAux(c, t, t.n, result, conf, currPosition)
of tyArray:
result = newNodeIT(nkBracket, info, t)
for i in 0..<toInt(lengthOrd(conf, t)):
let n = toInt(lengthOrd(conf, t))
if n > 0:
result.add getNullValue(c, elemType(t), info, conf)
# For a large array, keep a single broadcast element (the default of every
# slot is identical) instead of `n` copies; `isDefaultBroadcastArray`
# consumers expand on demand. Small arrays stay fully materialised so the
# well-trodden paths are untouched. See `broadcastArrayThreshold`.
if n <= broadcastArrayThreshold:
for i in 1..<n:
result.add getNullValue(c, elemType(t), info, conf)
else:
# Broadcast form: mark the single-son node so `isDefaultBroadcastArray`
# recognises it unambiguously (see `nfBroadcast`).
result.flags.incl nfBroadcast
of tyTuple:
result = newNodeIT(nkTupleConstr, info, t)
for a in t.kids: