mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-26 16:41:39 +00:00
IC related bugfixes (#25946)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -87,6 +87,7 @@ tweeter_test.db
|
||||
|
||||
/tests/megatest.nim
|
||||
/tests/ic/*_temp.nim
|
||||
/tests/ic/*_mm/
|
||||
/tests/navigator/*_temp.nim
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.} =
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
55
compiler/icmodnames.nim
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
69
doc/ic.md
69
doc/ic.md
@@ -288,6 +288,75 @@ Validation bar (held on every change): `koch bootic` must reach its byte-identic
|
||||
fixed point, and binary size must not regress (DCE parity), across the
|
||||
external-package CI set.
|
||||
|
||||
Further possible improvements
|
||||
=============================
|
||||
|
||||
A warm-edit profiling pass (2026-07-02, self-compiling the compiler into a
|
||||
dedicated `--nimcache`, editing one private proc body — `internalErrorImpl` — in
|
||||
the hub module `compiler/msgs.nim`) surfaced where a **hub-module** warm rebuild
|
||||
actually spends its time. The result refines the "a body-only edit re-fires one
|
||||
module" claim above: that holds for the *backend*, but the *frontend* can still
|
||||
cascade.
|
||||
|
||||
Measured: no-op `0.05s`; hub body edit `~15s`, split **~13s frontend / ~1.6s
|
||||
backend**. Editing a body in a leaf (few importers) is fast; editing a body in a
|
||||
widely-imported module is not, and the cost is almost entirely frontend re-sem.
|
||||
|
||||
- **Frontend over-invalidation (the dominant hub-edit cost).** Editing *any* body
|
||||
in a module — even a private routine that is only ever *called* — flips that
|
||||
module's whole-module **impl cookie** (`writeImplCookie` hashes the entire
|
||||
serialized module). Every module carrying a **NeedsImpl** edge on it then
|
||||
re-sems, even though the symbol it actually consumed is unchanged (e.g. a
|
||||
dependent that expanded the `internalError` *template* needs the template body,
|
||||
which is untouched; it does **not** need `internalErrorImpl`'s body). In the
|
||||
msgs edit this re-fires **57** `nim m` processes. A `.s.bif` mtime diff *hides*
|
||||
this — `.s.bif` is content-stable, so a re-semmed-but-identical module keeps its
|
||||
timestamp; count actual `nim m` PIDs to see the fan-out.
|
||||
|
||||
The precise fix is **per-symbol NeedsImpl gating**: record which *symbols'*
|
||||
bodies a dependent consumed (the recording site `modulegraphs.recordIcImplDep`
|
||||
already receives the `PSym`; it currently coarsens to `module(s.itemId)`) and
|
||||
gate the dependent
|
||||
on only those. The obstacle is that `nifmake` gates on file mtimes, so
|
||||
per-symbol granularity needs either many cookie files or a bucketing scheme, and
|
||||
"which bodies are compile-time-consumable" is entangled with `getImpl` and the
|
||||
CT call graph (a macro that runs a private helper at CT *does* consume its body).
|
||||
A conservative narrowing — keep template/generic/macro/`sfCompileTime` bodies
|
||||
(plus `getImpl` targets) in the impl cookie but drop ordinary runtime routine
|
||||
bodies — captures the common "edit a private implementation proc" case, at the
|
||||
cost of proving the exclusion is complete.
|
||||
|
||||
- **Serial re-sem chains.** The 57 re-sems above run essentially **one at a time**
|
||||
despite `--parallel`, because the core modules they belong to form a deep import
|
||||
*chain* and `nifmake`'s depth-barriered scheduler runs one depth level at a time
|
||||
(≈1 node per level). This is independent of the invalidation problem: even
|
||||
perfect per-symbol precision leaves a serial tail whenever the re-sem set is a
|
||||
chain. Mitigations live in the scheduler (content-stability already stops the
|
||||
cascade at one level, but does not flatten the chain).
|
||||
|
||||
- **Emit stage need not load the module graph (done).** `generateEmitStage` used
|
||||
to `loadDepClosure`/`loadBackendModules` — materializing a module's whole
|
||||
transitive import closure as `BModule`s — solely to reach `getCFile(bmod)` for
|
||||
the output path. `renderCFromArtifact` is pure text filtering over the `.c.nif`
|
||||
plus the merge decision; it needs none of that. Deriving the `.c` path directly
|
||||
from the suffix (the same pure computation `deps.backendCFile` uses to *declare*
|
||||
the stage's output) lets an `emit` process load nothing. Under the
|
||||
fire-all-every-edit `emit` barrier (see below) this halved backend CPU
|
||||
(user-time `51s → 24s` on the msgs edit); wall-clock barely moved because the
|
||||
frontend dominates, but the reduced CPU/RAM contention matters when an editor is
|
||||
running alongside. `koch ic` stays byte-identical.
|
||||
|
||||
- **Do NOT make the merge decision content-stable.** A tempting frontend to the
|
||||
above: `emit` re-fires for *every* live module whenever `merge` rewrites the
|
||||
decision file's mtime (deliberate — a decision change must re-render every `.c`
|
||||
consistently). Writing the decision `OnlyIfChanged` (with a stamp output so the
|
||||
`merge` rule is not perpetually stale) makes a warm no-op instant, but a real
|
||||
edit then fires `emit` only for the modules whose `.c.nif` changed — and that
|
||||
produces **multiple-definition link errors** even when the decision is
|
||||
byte-identical. Fire-all `emit` is a correctness invariant, not just insurance
|
||||
(see the comment at `generateEmitStage`): partial `emit` leaves inconsistent
|
||||
ownership across the `.c` set. This path was tried and reverted; do not retry.
|
||||
|
||||
Code, logic & debugging
|
||||
========================
|
||||
|
||||
|
||||
6
koch.nim
6
koch.nim
@@ -16,11 +16,11 @@ const
|
||||
ChecksumsStableCommit = "0b8e46379c5bc1bf73d8b3011908389c60fb9b98" # 2.0.1
|
||||
SatStableCommit = "e63eaea8baf00bed8bcd5a29ffd8823abb265b39"
|
||||
|
||||
NimonyStableCommit = "030fb8a132d29bb58b6db4d329647ab4bc512fd2" # unversioned \
|
||||
NimonyStableCommit = "557b865192e25e8e76ce4ccc81c6ea71676e92e7" # unversioned \
|
||||
# Note that Nimony uses Nim as a git submodule but we don't want to install
|
||||
# Nimony's dependency to Nim as we are Nim. So a `git clone` without --recursive
|
||||
# is **required** here.
|
||||
# Commit from 2026-06-27
|
||||
# Commit from 2026-07-01 -- contains a critical feature addition from nifmake for us
|
||||
|
||||
# examples of possible values for fusion: #head, #ea82b54, 1.2.3
|
||||
FusionStableHash = "#562467452b32cb7a97410ea177f083e6d8405734"
|
||||
@@ -619,7 +619,7 @@ proc runIcTestFile(inp: string) =
|
||||
const icSuite = ["thallo", "tconverter", "timp", "tmiscs", "tparseutils",
|
||||
"tcompiletimeglobal", "tsighashstable", "tpureenum", "tgenericoffer",
|
||||
"tconverterreexport", "ttypeoffer", "ttransitiveoffer",
|
||||
"tmodsymref", "tmethupref"]
|
||||
"tmodsymref", "tmethupref", "temit"]
|
||||
|
||||
proc icTest(args: string) =
|
||||
temp("")
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# included from testament.nim
|
||||
|
||||
import important_packages
|
||||
import std/[strformat, strutils]
|
||||
import std/[strformat, strutils, tables]
|
||||
from std/sequtils import filterIt
|
||||
|
||||
const
|
||||
@@ -488,6 +488,236 @@ proc testNimblePackages(r: var TResults; cat: Category; packageFilter: string) =
|
||||
|
||||
# ---------------- IC tests ---------------------------------------------
|
||||
|
||||
# ---- Metamorphic IC tests --------------------------------------------------
|
||||
#
|
||||
# A metamorphic IC test drives a *sequence of edits across several modules*
|
||||
# through `nim ic` in a fixed build directory (same absolute paths throughout,
|
||||
# which is what keeps the cache content-stable) and asserts the invariants the
|
||||
# incremental backend is supposed to guarantee — see doc/ic_ideas.md:
|
||||
#
|
||||
# * clean build == incremental build (a fresh in-place rebuild of the
|
||||
# final sources is byte-identical to
|
||||
# the binary and full cache set the
|
||||
# incremental edits converged to)
|
||||
# * a no-op edit changes no artifact (`noop`)
|
||||
# * a body-only edit touches no interface (`body-edit`: no `*.iface.bif`
|
||||
# cookie changes -> no importer re-sem)
|
||||
# * an interface edit propagates to (`iface-edit`: an `*.iface.bif`
|
||||
# importers cookie changes and >= 2 modules'
|
||||
# `*.s.bif` codegen is rebuilt)
|
||||
#
|
||||
# File format (a `tests/ic/t*.nim` whose body, after the spec header, contains a
|
||||
# line `#? metamorphic`):
|
||||
#
|
||||
# #? metamorphic
|
||||
# #!FILE a.nim
|
||||
# proc greet*(): string = "hi"
|
||||
# #!FILE main.nim # `main.nim` is always the build root
|
||||
# import a
|
||||
# echo greet()
|
||||
# #!STEP expect: hi
|
||||
# #!FILE a.nim # re-emit a module to "edit" it
|
||||
# proc greet*(): string = "hi" # identical content
|
||||
# #!STEP expect: hi; noop
|
||||
#
|
||||
# `#!FILE <name>` blocks (re)write a module in the virtual file system; the
|
||||
# accumulated file set is materialised before each `#!STEP`. A `#!STEP`'s
|
||||
# attributes are `;`-separated, each either `key: value` or a bare flag:
|
||||
# expect: <stdout> noop body-edit iface-edit modules: <n> clean
|
||||
# The last step always also runs the clean==incremental check.
|
||||
|
||||
type MetamorphicError = object of CatchableError
|
||||
resultKind: TResultEnum
|
||||
expected, given: string
|
||||
|
||||
proc mmRaise(kind: TResultEnum, expected, given: string) =
|
||||
var e = newException(MetamorphicError, given)
|
||||
e.resultKind = kind
|
||||
e.expected = expected
|
||||
e.given = given
|
||||
raise e
|
||||
|
||||
proc isMetamorphicIcTest(content: string): bool =
|
||||
for line in content.splitLines:
|
||||
if line.strip == "#? metamorphic": return true
|
||||
|
||||
proc snapshotDir(dir: string): Table[string, string] =
|
||||
## relative path -> raw file contents, for every file under `dir`.
|
||||
result = initTable[string, string]()
|
||||
if dirExists(dir):
|
||||
for it in walkDirRec(dir):
|
||||
result[it.relativePath(dir)] = readFile(it)
|
||||
|
||||
proc changedPaths(prev, cur: Table[string, string]): seq[string] =
|
||||
result = @[]
|
||||
for k, v in cur:
|
||||
if prev.getOrDefault(k) != v: result.add k
|
||||
for k in prev.keys:
|
||||
if k notin cur: result.add k
|
||||
|
||||
proc isProvenance(path: string): bool =
|
||||
## Build-provenance sidecars that legitimately differ between a fresh build and
|
||||
## an edit-accumulated one (they record build history, not codegen). Excluded
|
||||
## only from the cross-build clean==incremental comparison — a *no-op* edit must
|
||||
## still leave even these untouched.
|
||||
path.endsWith(".frontend.build.nif")
|
||||
|
||||
proc stableBinary(path: string): string =
|
||||
## Contents of a linked executable past its header region, for comparing whether
|
||||
## two builds produced the same *code*. Linkers embed build-time-volatile fields
|
||||
## in the header (e.g. the mingw PE `TimeDateStamp` and its derived `CheckSum`),
|
||||
## so two builds seconds apart differ there even with identical codegen. Skipping
|
||||
## a generous fixed window keeps the clean-vs-incremental check about codegen.
|
||||
const headerSkip = 4096
|
||||
var f: File
|
||||
if not open(f, path, fmRead):
|
||||
raise newException(IOError, "cannot open: " & path)
|
||||
defer: close(f)
|
||||
if getFileSize(f) > headerSkip:
|
||||
setFilePos(f, headerSkip)
|
||||
result = readAll(f)
|
||||
|
||||
proc changedModuleCount(changed: seq[string]): int =
|
||||
## distinct modules whose codegen (`*.s.bif`) was rebuilt.
|
||||
var mods: seq[string] = @[]
|
||||
for p in changed:
|
||||
if p.endsWith(".s.bif"):
|
||||
let key = p.extractFilename.split('.')[0]
|
||||
if key notin mods: mods.add key
|
||||
result = mods.len
|
||||
|
||||
proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options: string) =
|
||||
var test = TTest(cat: cat, name: file, options: options,
|
||||
spec: initSpec(file), startTime: epochTime())
|
||||
test.spec.targets = {targetC}
|
||||
inc r.total
|
||||
|
||||
# Absolute paths: `nim ic` runs with `workingDir = buildDir`, so a relative
|
||||
# `--nimcache` would resolve against the build dir, not where we read it back.
|
||||
let buildDir = (file.changeFileExt("") & "_mm").absolutePath
|
||||
let nc = buildDir / "nc"
|
||||
let bin = buildDir / "prog".addFileExt(ExeExt)
|
||||
removeDir(buildDir)
|
||||
createDir(buildDir)
|
||||
|
||||
template compileIc(): untyped =
|
||||
execCmdEx2(compilerPrefix, ["ic", "--hint:Conf:off", "--warnings:off",
|
||||
"--nimcache:" & nc, "--out:" & bin, "main.nim"],
|
||||
workingDir = buildDir)
|
||||
|
||||
# Parse the source into a flat op list: ("file", name, content) | ("step", attrs, "").
|
||||
type OpKind = enum opFile, opStep
|
||||
type Op = object
|
||||
kind: OpKind
|
||||
a, b: string
|
||||
var ops: seq[Op] = @[]
|
||||
block parse:
|
||||
var curName = ""
|
||||
var buf = ""
|
||||
template flushFile() =
|
||||
if curName.len > 0: ops.add Op(kind: opFile, a: curName, b: buf)
|
||||
curName = ""; buf = ""
|
||||
for raw in readFile(file).splitLines:
|
||||
let s = raw.strip
|
||||
if s.startsWith("#!FILE"):
|
||||
flushFile()
|
||||
curName = s["#!FILE".len .. ^1].strip
|
||||
elif s.startsWith("#!STEP"):
|
||||
flushFile()
|
||||
ops.add Op(kind: opStep, a: s["#!STEP".len .. ^1].strip)
|
||||
elif curName.len > 0:
|
||||
buf.add raw; buf.add "\n"
|
||||
let lastStep = block:
|
||||
var n = 0
|
||||
for o in ops:
|
||||
if o.kind == opStep: inc n
|
||||
n
|
||||
|
||||
var vfs = initTable[string, string]()
|
||||
var prevSnap = initTable[string, string]()
|
||||
var prevBin = ""
|
||||
var stepIdx = 0
|
||||
try:
|
||||
for o in ops:
|
||||
if o.kind == opFile:
|
||||
vfs[o.a] = o.b
|
||||
continue
|
||||
inc stepIdx
|
||||
let where = "step " & $stepIdx
|
||||
# Parse step attributes.
|
||||
var attrs = initTable[string, string]()
|
||||
for part in o.a.split(';'):
|
||||
let p = part.strip
|
||||
if p.len == 0: continue
|
||||
let c = p.find(':')
|
||||
if c >= 0: attrs[p[0 ..< c].strip] = p[c+1 .. ^1].strip
|
||||
else: attrs[p] = ""
|
||||
|
||||
for fn, content in vfs: writeFile(buildDir / fn, content)
|
||||
let (_, cout, ccode) = compileIc()
|
||||
if ccode != 0:
|
||||
mmRaise(reBuildFailed, "", where & ": `nim ic` failed:\n" & cout)
|
||||
let (_, rout, rcode) = execCmdEx2(bin.absolutePath, [], workingDir = buildDir)
|
||||
if rcode != 0:
|
||||
mmRaise(reBuildFailed, "", where & ": program exited with " & $rcode & ":\n" & rout)
|
||||
if "expect" in attrs:
|
||||
let want = attrs["expect"].replace("\\n", "\n")
|
||||
if rout.strip == want.strip: discard
|
||||
else: mmRaise(reOutputsDiffer, want, where & " output:\n" & rout.strip)
|
||||
|
||||
let snap = snapshotDir(nc)
|
||||
let binBytes = stableBinary(bin)
|
||||
if stepIdx > 1:
|
||||
let changed = changedPaths(prevSnap, snap)
|
||||
if "noop" in attrs and (changed.len != 0 or binBytes != prevBin):
|
||||
mmRaise(reOutputsDiffer, "no artifact change",
|
||||
where & ": no-op edit changed " & $changed.len & " cache file(s): " & changed.join(", "))
|
||||
if "body-edit" in attrs:
|
||||
for p in changed:
|
||||
if p.endsWith(".iface.bif"):
|
||||
mmRaise(reOutputsDiffer, "no interface change",
|
||||
where & ": body-only edit changed an interface cookie: " & p)
|
||||
if "iface-edit" in attrs:
|
||||
var sawIface = false
|
||||
for p in changed:
|
||||
if p.endsWith(".iface.bif"): sawIface = true
|
||||
if not sawIface:
|
||||
mmRaise(reOutputsDiffer, "interface change", where & ": interface edit changed no `*.iface.bif` cookie")
|
||||
if changedModuleCount(changed) < 2:
|
||||
mmRaise(reOutputsDiffer, "propagation to importer",
|
||||
where & ": interface edit did not propagate (only " & $changedModuleCount(changed) & " module rebuilt)")
|
||||
if "modules" in attrs:
|
||||
let want = parseInt(attrs["modules"])
|
||||
let got = changedModuleCount(changed)
|
||||
if got != want:
|
||||
mmRaise(reOutputsDiffer, $want & " modules rebuilt", where & ": " & $got & " module(s) rebuilt")
|
||||
prevSnap = snap
|
||||
prevBin = binBytes
|
||||
|
||||
if "clean" in attrs or stepIdx == lastStep:
|
||||
removeDir(nc)
|
||||
let (_, cout2, ccode2) = compileIc()
|
||||
if ccode2 != 0:
|
||||
mmRaise(reBuildFailed, "", where & ": clean rebuild failed:\n" & cout2)
|
||||
let cleanSnap = snapshotDir(nc)
|
||||
let cleanBin = stableBinary(bin)
|
||||
if cleanBin != binBytes:
|
||||
mmRaise(reOutputsDiffer, "clean binary == incremental binary",
|
||||
where & ": clean rebuild produced a different binary")
|
||||
var diff: seq[string] = @[]
|
||||
for p in changedPaths(snap, cleanSnap):
|
||||
if not isProvenance(p): diff.add p
|
||||
if diff.len != 0:
|
||||
mmRaise(reOutputsDiffer, "clean cache == incremental cache",
|
||||
where & ": clean rebuild differs in " & $diff.len & " cache file(s): " & diff.join(", "))
|
||||
prevSnap = cleanSnap
|
||||
prevBin = cleanBin
|
||||
finishTest(r, test, targetC, "", "", "", reSuccess)
|
||||
inc r.passed
|
||||
except MetamorphicError:
|
||||
let e = (ref MetamorphicError)(getCurrentException())
|
||||
finishTest(r, test, targetC, "", e.expected, e.given, e.resultKind)
|
||||
|
||||
proc icTests(r: var TResults; testsDir: string, cat: Category, options: string;
|
||||
isNavigatorTest: bool) =
|
||||
template editedTest() =
|
||||
@@ -498,11 +728,18 @@ proc icTests(r: var TResults; testsDir: string, cat: Category, options: string;
|
||||
|
||||
const tempExt = "_temp.nim"
|
||||
for it in walkDirRec(testsDir):
|
||||
# `_mm` directories hold materialised modules + nimcache for metamorphic
|
||||
# tests; never collect their files as tests in their own right.
|
||||
if "_mm" in it: continue
|
||||
if isTestFile(it) and not it.endsWith(tempExt):
|
||||
let content = readFile(it)
|
||||
if isMetamorphicIcTest(content):
|
||||
runMetamorphicIcTest(r, it, cat, options)
|
||||
continue
|
||||
|
||||
let nimcache = nimcacheDir(it, options, targetC)
|
||||
removeDir(nimcache)
|
||||
|
||||
let content = readFile(it)
|
||||
for fragment in content.split("#!EDIT!#"):
|
||||
let file = it.replace(".nim", tempExt)
|
||||
writeFile(file, fragment)
|
||||
|
||||
2
tests/ic/mdefconverter.nim
Normal file
2
tests/ic/mdefconverter.nim
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
converter toBool*(x: int): bool = x != 0
|
||||
14
tests/ic/memit.nim
Normal file
14
tests/ic/memit.nim
Normal file
@@ -0,0 +1,14 @@
|
||||
# Helper for temit.nim: a NON-main module with a module-scope `{.emit.}` that
|
||||
# introduces a C macro consumed by an `{.importc, nodecl.}` const. The IC backend
|
||||
# reload used to drop top-level emit pragmas — writeToplevelNode wrote them to the
|
||||
# by-symbol-index implementation section, where a symbol-less pragma is never
|
||||
# reloaded — so the generated C lost the `#define` and failed to compile with
|
||||
# "use of undeclared identifier". Mirrors lib/pure/concurrency/cpuinfo.nim's
|
||||
# `#include <sys/sysctl.h>` + `CTL_HW`/`HW_NCPU` importc pattern.
|
||||
{.emit: """/*TYPESECTION*/
|
||||
#define NIM_IC_EMIT_ANSWER 42
|
||||
""".}
|
||||
|
||||
let icEmitAnswer {.importc: "NIM_IC_EMIT_ANSWER", nodecl.}: cint
|
||||
|
||||
proc emitAnswer*(): int = int(icEmitAnswer)
|
||||
9
tests/ic/memptycaller.nim
Normal file
9
tests/ic/memptycaller.nim
Normal file
@@ -0,0 +1,9 @@
|
||||
# Calls `emptyOwned` from a DIFFERENT module than the one that owns it, so the
|
||||
# reference at link time must resolve to a definition the owning module emits.
|
||||
|
||||
import memptyowned
|
||||
|
||||
proc callEmpty*(cond: bool) =
|
||||
if cond:
|
||||
emptyOwned()
|
||||
echo "called ", cond
|
||||
12
tests/ic/memptyowned.nim
Normal file
12
tests/ic/memptyowned.nim
Normal file
@@ -0,0 +1,12 @@
|
||||
# Helper for `temptyowned`: exports a concrete proc whose body folds to
|
||||
# nothing (an `nkEmpty` body, like Nimbus' `extras.incInternalErrors` when the
|
||||
# metrics counter's `.inc()` expands to a no-op under `-u:metrics`). The proc is
|
||||
# NOT called within its own module — only `memptycaller` references it — so the
|
||||
# per-module backend's owned-routine seeding is the ONLY thing that can emit it.
|
||||
|
||||
template maybe*(x: untyped) =
|
||||
when false:
|
||||
x
|
||||
|
||||
proc emptyOwned*() =
|
||||
maybe(echo "unreachable")
|
||||
29
tests/ic/minitordera.nim
Normal file
29
tests/ic/minitordera.nim
Normal file
@@ -0,0 +1,29 @@
|
||||
# Helper for tinitorder (not a test itself; no `discard`).
|
||||
#
|
||||
# Imports minitorderb and, in its OWN init, reads the state minitorderb set up.
|
||||
# With the wrong (importer-first) init order `gBState` is still 0 here. It also
|
||||
# allocates a seq in its init so that under `--mm:refc` the GC (set up by the
|
||||
# system module's init) must already be live — i.e. the system module's init has
|
||||
# to be ordered first.
|
||||
|
||||
import minitorderb
|
||||
|
||||
var
|
||||
gASawB = -1
|
||||
gAItems: seq[int]
|
||||
|
||||
proc getASawB*(): int = gASawB
|
||||
proc getACount*(): int = gAItems.len
|
||||
|
||||
proc recordA() =
|
||||
gASawB = getBState()
|
||||
gAItems = @[1, 2, 3]
|
||||
# Allocate (and drop) enough garbage to force a GC cycle DURING module init.
|
||||
# Under refc that runs a conservative stack scan, which needs the main
|
||||
# thread's stack bottom already set — i.e. `initStackBottomWith` must run
|
||||
# before the module inits, not after them.
|
||||
for i in 0 ..< 100_000:
|
||||
let s = @[i, i + 1, i + 2]
|
||||
doAssert s.len == 3
|
||||
|
||||
recordA()
|
||||
15
tests/ic/minitorderb.nim
Normal file
15
tests/ic/minitorderb.nim
Normal file
@@ -0,0 +1,15 @@
|
||||
# Helper for tinitorder (not a test itself; no `discard`).
|
||||
#
|
||||
# An imported module whose INIT sets module-level state at runtime. Under the
|
||||
# per-module backend its init must run BEFORE any importer's init (the imported
|
||||
# module is a dependency → post-order). With the buggy importer-first order this
|
||||
# module's `setupB` runs too late and importers observe `gBState == 0`.
|
||||
|
||||
var gBState: int
|
||||
|
||||
proc getBState*(): int = gBState
|
||||
|
||||
proc setupB() =
|
||||
gBState = 42
|
||||
|
||||
setupB()
|
||||
15
tests/ic/mmethanimal.nim
Normal file
15
tests/ic/mmethanimal.nim
Normal file
@@ -0,0 +1,15 @@
|
||||
# Helper for tmethitanium: the OWNER module of a `{.base.}` method. Its concrete
|
||||
# base body is emitted here; the whole-program dispatcher is synthesized into the
|
||||
# main module. Under `--debugger:native` the backend uses the Itanium mangling
|
||||
# scheme, which encodes the signature instead of the `disamb`, so the base method
|
||||
# and its same-signature dispatcher want the identical clean C name. The
|
||||
# clean-vs-unique tie-break used to depend on a per-MODULE set (`mangledPrcs`),
|
||||
# which the per-module IC backend cannot share — the base mangled clean at this
|
||||
# owner but `speak_u<n>` (an unstable `itemId.item`) at every demander, so it was
|
||||
# defined once and referenced under names nobody defined. See
|
||||
# ccgutils.makeUnique (disamb, not itemId) + ccgtypes.fillBackendName.
|
||||
|
||||
type Animal* = ref object of RootObj
|
||||
|
||||
method speak*(a: Animal): string {.base.} =
|
||||
"generic-animal-sound"
|
||||
14
tests/ic/mmethdog.nim
Normal file
14
tests/ic/mmethdog.nim
Normal file
@@ -0,0 +1,14 @@
|
||||
# Helper for tmethitanium: an override in a DIFFERENT module than the base, plus
|
||||
# a `procCall` super-reference to the base from this non-owner module. That
|
||||
# cross-module reference to the base impl is what diverged from the base's
|
||||
# definition name under the per-module Itanium mangling.
|
||||
|
||||
import mmethanimal
|
||||
|
||||
type Dog* = ref object of Animal
|
||||
|
||||
method speak*(a: Dog): string =
|
||||
"woof"
|
||||
|
||||
proc speakBoth*(a: Dog): string =
|
||||
procCall(speak(Animal(a))) & "/" & speak(a)
|
||||
7
tests/ic/temit.nim
Normal file
7
tests/ic/temit.nim
Normal file
@@ -0,0 +1,7 @@
|
||||
# Regression: a module-scope `{.emit.}` in an imported module must survive the
|
||||
# `nim ic` backend reload (see memit.nim). Before the fix the dropped `#define`
|
||||
# made the generated C fail to compile, so `nim ic` exited non-zero.
|
||||
import memit
|
||||
|
||||
doAssert emitAnswer() == 42
|
||||
echo emitAnswer()
|
||||
18
tests/ic/temptyowned.nim
Normal file
18
tests/ic/temptyowned.nim
Normal file
@@ -0,0 +1,18 @@
|
||||
discard """
|
||||
output: '''called false
|
||||
done'''
|
||||
"""
|
||||
|
||||
# Regression test for the per-module IC backend dropping an owned routine whose
|
||||
# body folds to `nkEmpty`. `memptyowned.emptyOwned` is a real, concrete, owned
|
||||
# proc with an empty body; `memptycaller.callEmpty` references it across a module
|
||||
# boundary. The owning module's `ownsRuntimeRoutine` seeding used to reject any
|
||||
# routine with an `nkEmpty` body, so nobody emitted `emptyOwned` -> undefined
|
||||
# reference at link (mirrors Nimbus `ncli` linking `extras.incInternalErrors`).
|
||||
# Whole-program cgen always emits it as `void emptyOwned(void){}`; the per-module
|
||||
# backend must too.
|
||||
|
||||
import memptycaller
|
||||
|
||||
callEmpty(false)
|
||||
echo "done"
|
||||
23
tests/ic/tinitorder.nim
Normal file
23
tests/ic/tinitorder.nim
Normal file
@@ -0,0 +1,23 @@
|
||||
discard """
|
||||
output: '''42 3'''
|
||||
"""
|
||||
|
||||
# Regression test for per-module-backend module-init ORDERING.
|
||||
#
|
||||
# NimMain must call each module's init in DEPENDENCY (post-order) order: an
|
||||
# imported module's init has to run before its importer's. The whole-program
|
||||
# backend gets this for free (it iterates `modulesClosed`, built in module-finish
|
||||
# order); the per-module backend reconstructs it in `nifbackend`. The earlier
|
||||
# code iterated `bl.mods` by POSITION, which runs importers before their
|
||||
# dependencies (an importer gets a lower file position than the modules it
|
||||
# imports) — and the system module (which runs `initGC` in its init) was not
|
||||
# ordered first at all.
|
||||
#
|
||||
# Module chain: tinitorder -> minitordera -> minitorderb. `minitorderb`'s init
|
||||
# sets a global to 42; `minitordera`'s init reads it (and allocates a seq). With
|
||||
# the buggy order `minitordera` runs first and reads 0 (or, under refc, crashes
|
||||
# allocating before the GC is up). Correct order prints `42 3`.
|
||||
|
||||
import minitordera
|
||||
|
||||
echo getASawB(), " ", getACount()
|
||||
46
tests/ic/tmeta_async.nim
Normal file
46
tests/ic/tmeta_async.nim
Normal file
@@ -0,0 +1,46 @@
|
||||
discard """
|
||||
description: '''metamorphic IC: async/await across an edit (continuations + clean==incremental)'''
|
||||
"""
|
||||
|
||||
# Async is the most cache-fragile area under `nim ic`: the `{.async.}` transform
|
||||
# generates continuation closures and lifts environments, and those lowered
|
||||
# bodies must survive incremental edits and converge to exactly what a clean
|
||||
# build produces. This drives an edit to an async proc body and asserts the
|
||||
# importer is NOT rebuilt (the lifted continuation stays local to its module),
|
||||
# a no-op changes nothing, and the final state is byte-identical to a clean
|
||||
# build. See doc/ic_ideas.md and [[ic-nimbus-test]] (async was the long pole).
|
||||
|
||||
#? metamorphic
|
||||
|
||||
#!FILE worker.nim
|
||||
import std/asyncdispatch
|
||||
proc compute*(x: int): Future[int] {.async.} =
|
||||
await sleepAsync(0)
|
||||
result = x * 2
|
||||
|
||||
#!FILE main.nim
|
||||
import std/asyncdispatch, worker
|
||||
echo waitFor compute(21)
|
||||
|
||||
#!STEP expect: 42
|
||||
|
||||
# --- edit the async proc body. The continuation env lives in `worker`, so only
|
||||
# `worker` rebuilds; `main` (the caller) is left untouched -> modules: 1.
|
||||
# (An async body edit does perturb `worker`'s interface cookie via the
|
||||
# generated env type, so this is not asserted as a pure `body-edit`.)
|
||||
#!FILE worker.nim
|
||||
import std/asyncdispatch
|
||||
proc compute*(x: int): Future[int] {.async.} =
|
||||
await sleepAsync(0)
|
||||
result = x * 3
|
||||
|
||||
#!STEP expect: 63; modules: 1
|
||||
|
||||
# --- re-emit identical content: nothing may change, lifted closures included.
|
||||
#!FILE worker.nim
|
||||
import std/asyncdispatch
|
||||
proc compute*(x: int): Future[int] {.async.} =
|
||||
await sleepAsync(0)
|
||||
result = x * 3
|
||||
|
||||
#!STEP expect: 63; noop
|
||||
64
tests/ic/tmeta_exprcase.nim
Normal file
64
tests/ic/tmeta_exprcase.nim
Normal file
@@ -0,0 +1,64 @@
|
||||
discard """
|
||||
description: '''metamorphic IC: expression-based `let x = case ...` idiom'''
|
||||
"""
|
||||
|
||||
# nimbus-eth2 leans on expression-style code (`let x = case ...` rather than
|
||||
# statement assignment). This exercises that construct through the incremental
|
||||
# path: a body edit that adds a `case` branch stays local to the module, while a
|
||||
# return-type change (interface edit) propagates to the importer even though the
|
||||
# importer's source is byte-identical. See doc/ic_ideas.md.
|
||||
|
||||
#? metamorphic
|
||||
|
||||
#!FILE classify.nim
|
||||
proc classify*(n: int): string =
|
||||
let kind = case n
|
||||
of 0: "zero"
|
||||
of 1, 2, 3: "small"
|
||||
else: "big"
|
||||
result = kind & "(" & $n & ")"
|
||||
|
||||
#!FILE main.nim
|
||||
import classify
|
||||
echo classify(0), " ", classify(2), " ", classify(99)
|
||||
|
||||
#!STEP expect: zero(0) small(2) big(99)
|
||||
|
||||
# --- body edit: add a `case` branch and tweak a label. The signature is
|
||||
# unchanged, so no interface cookie changes and only `classify` rebuilds.
|
||||
#!FILE classify.nim
|
||||
proc classify*(n: int): string =
|
||||
let kind = case n
|
||||
of 0: "ZERO"
|
||||
of 1, 2, 3: "small"
|
||||
of 4, 5, 6: "medium"
|
||||
else: "big"
|
||||
result = kind & "(" & $n & ")"
|
||||
|
||||
#!STEP expect: ZERO(0) small(2) big(99); body-edit; modules: 1
|
||||
|
||||
# --- re-emit identical content: nothing may change.
|
||||
#!FILE classify.nim
|
||||
proc classify*(n: int): string =
|
||||
let kind = case n
|
||||
of 0: "ZERO"
|
||||
of 1, 2, 3: "small"
|
||||
of 4, 5, 6: "medium"
|
||||
else: "big"
|
||||
result = kind & "(" & $n & ")"
|
||||
|
||||
#!STEP expect: ZERO(0) small(2) big(99); noop
|
||||
|
||||
# --- interface edit: the expression-`case` now yields `int`, changing
|
||||
# `classify`'s return type. `main`'s source is byte-identical (`echo` prints
|
||||
# either) yet must re-sem & recodegen -> the cookie changes and 2 modules
|
||||
# rebuild. The final step also runs the clean==incremental check.
|
||||
#!FILE classify.nim
|
||||
proc classify*(n: int): int =
|
||||
result = case n
|
||||
of 0: 0
|
||||
of 1, 2, 3: 1
|
||||
of 4, 5, 6: 5
|
||||
else: 9
|
||||
|
||||
#!STEP expect: 0 1 9; iface-edit; modules: 2
|
||||
33
tests/ic/tmeta_generic.nim
Normal file
33
tests/ic/tmeta_generic.nim
Normal file
@@ -0,0 +1,33 @@
|
||||
discard """
|
||||
description: '''metamorphic IC: generic instantiation cache stability across edits'''
|
||||
"""
|
||||
|
||||
# A generic lives in `gen` but is *instantiated* in `main` (the per-module
|
||||
# backend emits instance bodies at the instantiation site). Editing the generic
|
||||
# body must therefore rebuild both `gen` and `main`, and an incremental edit must
|
||||
# still converge to exactly the same artifacts as a clean build. This exercises
|
||||
# the static/generic-instance cache path that has historically been bug-prone.
|
||||
|
||||
#? metamorphic
|
||||
|
||||
#!FILE gen.nim
|
||||
proc box*[T](x: T): seq[T] = @[x, x]
|
||||
|
||||
#!FILE main.nim
|
||||
import gen
|
||||
echo box(3).len, " ", box("hi")[0]
|
||||
|
||||
#!STEP expect: 2 hi
|
||||
|
||||
# --- edit the generic body: the importer holds the instantiations, so both the
|
||||
# definer and the instantiation site rebuild (2 modules).
|
||||
#!FILE gen.nim
|
||||
proc box*[T](x: T): seq[T] = @[x, x, x]
|
||||
|
||||
#!STEP expect: 3 hi; modules: 2
|
||||
|
||||
# --- re-emit identical content: nothing may change.
|
||||
#!FILE gen.nim
|
||||
proc box*[T](x: T): seq[T] = @[x, x, x]
|
||||
|
||||
#!STEP expect: 3 hi; noop
|
||||
61
tests/ic/tmeta_ortype.nim
Normal file
61
tests/ic/tmeta_ortype.nim
Normal file
@@ -0,0 +1,61 @@
|
||||
discard """
|
||||
description: '''metamorphic IC: or-type (type-class union) idiom from nimbus forks.nim'''
|
||||
"""
|
||||
|
||||
# nimbus-eth2 writes a lot of generic code over `A | B | C` type-class unions
|
||||
# (consensus forks). This models that idiom: a `Fruit = Apple | Banana` union
|
||||
# with a generic `describe[T: Fruit]` dispatched by `when T is ...`. The generic
|
||||
# is instantiated in `main`, so editing its body rebuilds both modules, and the
|
||||
# incremental result must match a clean build. See doc/ic_ideas.md.
|
||||
|
||||
#? metamorphic
|
||||
|
||||
#!FILE forks.nim
|
||||
type
|
||||
Apple* = object
|
||||
weight*: int
|
||||
Banana* = object
|
||||
length*: int
|
||||
Fruit* = Apple | Banana
|
||||
|
||||
proc describe*[T: Fruit](x: T): string =
|
||||
when T is Apple: "apple " & $x.weight
|
||||
else: "banana " & $x.length
|
||||
|
||||
#!FILE main.nim
|
||||
import forks
|
||||
echo describe(Apple(weight: 5)), " | ", describe(Banana(length: 9))
|
||||
|
||||
#!STEP expect: apple 5 | banana 9
|
||||
|
||||
# --- edit the generic body (no signature change). The union/constraint is
|
||||
# untouched, so no interface cookie changes; but `main` holds the two
|
||||
# instantiations, so both modules' codegen rebuilds.
|
||||
#!FILE forks.nim
|
||||
type
|
||||
Apple* = object
|
||||
weight*: int
|
||||
Banana* = object
|
||||
length*: int
|
||||
Fruit* = Apple | Banana
|
||||
|
||||
proc describe*[T: Fruit](x: T): string =
|
||||
when T is Apple: "APPLE " & $x.weight
|
||||
else: "BANANA " & $x.length
|
||||
|
||||
#!STEP expect: APPLE 5 | BANANA 9; body-edit; modules: 2
|
||||
|
||||
# --- re-emit identical content: nothing may change.
|
||||
#!FILE forks.nim
|
||||
type
|
||||
Apple* = object
|
||||
weight*: int
|
||||
Banana* = object
|
||||
length*: int
|
||||
Fruit* = Apple | Banana
|
||||
|
||||
proc describe*[T: Fruit](x: T): string =
|
||||
when T is Apple: "APPLE " & $x.weight
|
||||
else: "BANANA " & $x.length
|
||||
|
||||
#!STEP expect: APPLE 5 | BANANA 9; noop
|
||||
93
tests/ic/tmeta_result.nim
Normal file
93
tests/ic/tmeta_result.nim
Normal file
@@ -0,0 +1,93 @@
|
||||
discard """
|
||||
description: '''metamorphic IC: Result[T, E] (nim-results style) variant-object idiom'''
|
||||
"""
|
||||
|
||||
# nimbus-eth2 threads errors through nim-results' `Result[T, E]`, a generic
|
||||
# *variant* (case) object. This models a minimal hermetic version and exercises
|
||||
# the generic-variant cache path across edits: a body edit of a generic accessor,
|
||||
# adding a public overload (an interface edit), and a no-op — with a final
|
||||
# clean==incremental check. See doc/ic_ideas.md.
|
||||
|
||||
#? metamorphic
|
||||
|
||||
#!FILE results.nim
|
||||
type
|
||||
ResultKind = enum rOk, rErr
|
||||
Result*[T, E] = object
|
||||
case kind: ResultKind
|
||||
of rOk: v: T
|
||||
of rErr: e: E
|
||||
|
||||
proc ok*[T, E](x: T): Result[T, E] = Result[T, E](kind: rOk, v: x)
|
||||
proc err*[T, E](x: E): Result[T, E] = Result[T, E](kind: rErr, e: x)
|
||||
proc isOk*[T, E](r: Result[T, E]): bool = r.kind == rOk
|
||||
proc get*[T, E](r: Result[T, E]): T = r.v
|
||||
proc error*[T, E](r: Result[T, E]): E = r.e
|
||||
|
||||
#!FILE main.nim
|
||||
import results
|
||||
proc parse(s: string): Result[int, string] =
|
||||
if s == "42": ok[int, string](42)
|
||||
else: err[int, string]("bad: " & s)
|
||||
let a = parse("42")
|
||||
let b = parse("x")
|
||||
echo (if a.isOk: $a.get else: a.error), " ", (if b.isOk: $b.get else: b.error)
|
||||
|
||||
#!STEP expect: 42 bad: x
|
||||
|
||||
# --- body-only edit of a generic accessor (`error`): signature unchanged, so no
|
||||
# interface cookie changes; the importer holds the instantiation, so both
|
||||
# modules' codegen rebuilds.
|
||||
#!FILE results.nim
|
||||
type
|
||||
ResultKind = enum rOk, rErr
|
||||
Result*[T, E] = object
|
||||
case kind: ResultKind
|
||||
of rOk: v: T
|
||||
of rErr: e: E
|
||||
|
||||
proc ok*[T, E](x: T): Result[T, E] = Result[T, E](kind: rOk, v: x)
|
||||
proc err*[T, E](x: E): Result[T, E] = Result[T, E](kind: rErr, e: x)
|
||||
proc isOk*[T, E](r: Result[T, E]): bool = r.kind == rOk
|
||||
proc get*[T, E](r: Result[T, E]): T = r.v
|
||||
proc error*[T, E](r: Result[T, E]): E = "ERR:" & r.e
|
||||
|
||||
#!STEP expect: 42 ERR:bad: x; body-edit; modules: 2
|
||||
|
||||
# --- re-emit identical content: nothing may change.
|
||||
#!FILE results.nim
|
||||
type
|
||||
ResultKind = enum rOk, rErr
|
||||
Result*[T, E] = object
|
||||
case kind: ResultKind
|
||||
of rOk: v: T
|
||||
of rErr: e: E
|
||||
|
||||
proc ok*[T, E](x: T): Result[T, E] = Result[T, E](kind: rOk, v: x)
|
||||
proc err*[T, E](x: E): Result[T, E] = Result[T, E](kind: rErr, e: x)
|
||||
proc isOk*[T, E](r: Result[T, E]): bool = r.kind == rOk
|
||||
proc get*[T, E](r: Result[T, E]): T = r.v
|
||||
proc error*[T, E](r: Result[T, E]): E = "ERR:" & r.e
|
||||
|
||||
#!STEP expect: 42 ERR:bad: x; noop
|
||||
|
||||
# --- interface edit: add a public `get` overload (a new exported signature).
|
||||
# `main` doesn't call it, yet importing `results` whose interface changed
|
||||
# forces a re-sem; the cookie changes and >= 2 modules rebuild. The final
|
||||
# step also runs the clean==incremental check.
|
||||
#!FILE results.nim
|
||||
type
|
||||
ResultKind = enum rOk, rErr
|
||||
Result*[T, E] = object
|
||||
case kind: ResultKind
|
||||
of rOk: v: T
|
||||
of rErr: e: E
|
||||
|
||||
proc ok*[T, E](x: T): Result[T, E] = Result[T, E](kind: rOk, v: x)
|
||||
proc err*[T, E](x: E): Result[T, E] = Result[T, E](kind: rErr, e: x)
|
||||
proc isOk*[T, E](r: Result[T, E]): bool = r.kind == rOk
|
||||
proc get*[T, E](r: Result[T, E], fallback: T): T = (if r.kind == rOk: r.v else: fallback)
|
||||
proc get*[T, E](r: Result[T, E]): T = r.v
|
||||
proc error*[T, E](r: Result[T, E]): E = "ERR:" & r.e
|
||||
|
||||
#!STEP expect: 42 ERR:bad: x; iface-edit; modules: 2
|
||||
50
tests/ic/tmeta_smoke.nim
Normal file
50
tests/ic/tmeta_smoke.nim
Normal file
@@ -0,0 +1,50 @@
|
||||
discard """
|
||||
description: '''metamorphic IC: clean==incremental, no-op stability, body vs interface boundary'''
|
||||
"""
|
||||
|
||||
# This is a *metamorphic* IC test (see the `#? metamorphic` marker and the
|
||||
# runner in testament/categories.nim). It drives a sequence of cross-module
|
||||
# edits through `nim ic` in one fixed build directory and checks the invariants
|
||||
# the incremental backend must uphold (doc/ic_ideas.md).
|
||||
|
||||
#? metamorphic
|
||||
|
||||
#!FILE a.nim
|
||||
proc greet*(): string = "hi"
|
||||
proc secret(): int = 41 # private, body-only churn target
|
||||
proc value*(): int = secret() + 1
|
||||
|
||||
#!FILE main.nim
|
||||
import a
|
||||
echo greet(), " ", value()
|
||||
|
||||
#!STEP expect: hi 42
|
||||
|
||||
# --- body-only edit: a private body changes, no signature does.
|
||||
# => no `*.iface.bif` cookie changes, exactly 1 module's codegen rebuilds,
|
||||
# the importer is left untouched.
|
||||
#!FILE a.nim
|
||||
proc greet*(): string = "hi"
|
||||
proc secret(): int = 999
|
||||
proc value*(): int = secret() + 1
|
||||
|
||||
#!STEP expect: hi 1000; body-edit; modules: 1
|
||||
|
||||
# --- no-op edit: re-emit byte-identical content. Nothing downstream may change.
|
||||
#!FILE a.nim
|
||||
proc greet*(): string = "hi"
|
||||
proc secret(): int = 999
|
||||
proc value*(): int = secret() + 1
|
||||
|
||||
#!STEP expect: hi 1000; noop
|
||||
|
||||
# --- interface edit: `value`'s return type changes (a signature change) while
|
||||
# main.nim's source stays byte-identical. The interface cookie must change
|
||||
# and the importer must be re-sem'd & recodegen'd (>= 2 modules rebuilt).
|
||||
# The final step also runs the clean==incremental check.
|
||||
#!FILE a.nim
|
||||
proc greet*(): string = "hi"
|
||||
proc secret(): int = 999
|
||||
proc value*(): int64 = secret().int64 + 1
|
||||
|
||||
#!STEP expect: hi 1000; iface-edit
|
||||
39
tests/ic/tmeta_transitive.nim
Normal file
39
tests/ic/tmeta_transitive.nim
Normal file
@@ -0,0 +1,39 @@
|
||||
discard """
|
||||
description: '''metamorphic IC: edit propagation across a 3-module import chain'''
|
||||
"""
|
||||
|
||||
# Chain: main -> b -> a. Demonstrates that a body edit stays local to the edited
|
||||
# module, while an interface edit propagates to its direct importer but stops
|
||||
# where the next signature is unchanged. See doc/ic_ideas.md and the runner in
|
||||
# testament/categories.nim.
|
||||
|
||||
#? metamorphic
|
||||
|
||||
#!FILE a.nim
|
||||
proc base*(): int = 1
|
||||
|
||||
#!FILE b.nim
|
||||
import a
|
||||
proc mid*(): int = base() + 10
|
||||
|
||||
#!FILE main.nim
|
||||
import b
|
||||
echo mid()
|
||||
|
||||
#!STEP expect: 11
|
||||
|
||||
# --- body-only edit of `a.base`: no signature changes, so nothing re-sems;
|
||||
# only module `a`'s own codegen rebuilds.
|
||||
#!FILE a.nim
|
||||
proc base*(): int = 7
|
||||
|
||||
#!STEP expect: 17; body-edit; modules: 1
|
||||
|
||||
# --- interface edit of `a.base` (return type int -> int64). `b` uses `base`, so
|
||||
# `a`'s cookie change forces `b` to re-sem & recodegen; but `b.mid`'s own
|
||||
# signature is unchanged, so `main` is NOT rebuilt -> exactly 2 modules.
|
||||
# The final step also runs the clean==incremental check.
|
||||
#!FILE a.nim
|
||||
proc base*(): int64 = 7
|
||||
|
||||
#!STEP expect: 17; iface-edit; modules: 2
|
||||
33
tests/ic/tmethitanium.nim
Normal file
33
tests/ic/tmethitanium.nim
Normal file
@@ -0,0 +1,33 @@
|
||||
discard """
|
||||
output: '''woof
|
||||
generic-animal-sound
|
||||
generic-animal-sound/woof'''
|
||||
"""
|
||||
|
||||
# NOTE: the `--debugger:native` that triggers the Itanium mangling lives in the
|
||||
# sibling `tmethitanium_temp.nim.cfg` (the IC test harness compiles the generated
|
||||
# `_temp.nim` and does not thread a `matrix`/`$options` switch into the cg
|
||||
# children; a project cfg is read by the driver, which forwards it).
|
||||
|
||||
# Regression test: under `nim ic --debugger:native` the per-module backend uses
|
||||
# the Itanium C name mangling, which encodes the signature and drops the
|
||||
# `disamb`. A `{.base.}` method (owner module mmethanimal) and its whole-program
|
||||
# dispatcher (synthesized into this main module) then share a signature, so the
|
||||
# clean-name uniqueness probe (`m.g.mangledPrcs`) — which only sees the current
|
||||
# module — gave the base a clean name at its owner but an unstable
|
||||
# `itemId.item`-based `speak_u<n>` at each demander. Result: the base was defined
|
||||
# once (clean) but referenced under names defined nowhere ("undefined reference
|
||||
# to speak_u1") while the dispatcher collided with the clean base ("multiple
|
||||
# definition of speak"). This was the bulk of nimbus-eth2's libp2p method link
|
||||
# failures under `nim ic`. Fixed by making the Itanium scheme use the stable
|
||||
# `disamb` (ccgutils.makeUnique) and always uniquify routine names under the
|
||||
# per-module backend (ccgtypes.fillBackendName), plus forwarding
|
||||
# `--debugger:native` to the cg children (deps.computeForwardedArgs).
|
||||
|
||||
import mmethanimal, mmethdog
|
||||
|
||||
let a: Animal = Dog()
|
||||
echo speak(a) # dispatches -> override
|
||||
let b: Animal = Animal()
|
||||
echo speak(b) # dispatches -> base body
|
||||
echo speakBoth(Dog()) # procCall to base from non-owner module
|
||||
38
tests/ic/tstaticgenfield.nim
Normal file
38
tests/ic/tstaticgenfield.nim
Normal file
@@ -0,0 +1,38 @@
|
||||
discard """
|
||||
output: '''9'''
|
||||
"""
|
||||
|
||||
# Regression test for object-field serialization of static-generic instances
|
||||
# under `nim ic`.
|
||||
#
|
||||
# A generic object's instances SHARE one field PSym (same itemId) while each
|
||||
# instance carries a DISTINCT field type, e.g. `Digest[32].data: array[32,byte]`
|
||||
# vs `Digest[48].data: array[48,byte]` (this is exactly nimcrypto's `MDigest`,
|
||||
# which crashed compiling nimbus's `altair.nim`). The `.s.bif` writer DEFs each
|
||||
# field once inside its owning type's reclist and references it as a bare SymUse
|
||||
# elsewhere, deduping by a per-Writer `emittedFieldSyms` set. That set wrongly
|
||||
# spanned DIFFERENT type reclists: after the first instance's `data` def, every
|
||||
# other instance's reclist got a typeless SymUse stub instead of its own typed
|
||||
# def. On load that field had a nil `typ`/`owner`, and `=destroy` lifting
|
||||
# (`liftdestructors.fillBodyObj`) dereferenced it -> SIGSEGV. The fix scopes the
|
||||
# dedup per-reclist so each instance reclist is a self-contained typed def.
|
||||
#
|
||||
# The `seq` field forces `=destroy` to be lifted for `Outer`, which walks the
|
||||
# reclists of both `Digest` instances (the crash path).
|
||||
|
||||
type
|
||||
Digest[n: static int] = object
|
||||
data: array[n, byte]
|
||||
Outer = object
|
||||
a: Digest[32]
|
||||
b: Digest[48]
|
||||
s: seq[int]
|
||||
|
||||
proc use(o: Outer): int =
|
||||
result = o.a.data[0].int + o.b.data[0].int + o.s.len
|
||||
|
||||
var o: Outer
|
||||
o.a.data[0] = 4
|
||||
o.b.data[0] = 2
|
||||
o.s = @[1, 2, 3]
|
||||
echo use(o)
|
||||
@@ -28,3 +28,17 @@ block:
|
||||
fun[(int, string)]()
|
||||
fun[ref Foo]()
|
||||
fun[seq[int]]()
|
||||
|
||||
block: # shrinking an `@[...]` seq literal in the VM
|
||||
# A `@[a, b, c]` seq value keeps the array-literal type in the VM; shrinking it
|
||||
# to length 1 must not be misread as a broadcast default array (was: the whole
|
||||
# thing collapsed to `len` copies of the first element).
|
||||
proc shrink =
|
||||
var s = @[10, 20, 30]
|
||||
s.setLen(1)
|
||||
doAssert s == @[10]
|
||||
var t = @["foo", "bar"]
|
||||
t.delete(1)
|
||||
doAssert t == @["foo"]
|
||||
static: shrink()
|
||||
shrink()
|
||||
|
||||
Reference in New Issue
Block a user