Compare commits

..

2 Commits

Author SHA1 Message Date
Araq
cd7feabc97 refactorings: progress 2026-07-08 20:28:49 +02:00
Araq
bd0de5f9aa massive PType refactor; the compiler moves to a NIF based type representation internally 2026-07-08 11:32:50 +02:00
76 changed files with 1567 additions and 4253 deletions

View File

@@ -38,7 +38,7 @@ jobs:
fetch-depth: 2
- name: 'Install node.js'
uses: actions/setup-node@v7
uses: actions/setup-node@v6
with:
node-version: 24

View File

@@ -22,7 +22,7 @@ jobs:
fetch-depth: 2
- name: 'Install node.js'
uses: actions/setup-node@v7
uses: actions/setup-node@v6
with:
node-version: 24

View File

@@ -626,7 +626,14 @@ proc `[]`*(n: PType, i: int): PType {.inline.} =
else:
n.sonsImpl[i]
proc `[]=`*(n: PType, i: int; x: PType) {.inline.} =
proc replaceSon*(n: PType, i: int; x: PType) {.inline.} =
## The single low-level "replace son `i` in place" primitive. All in-place son
## mutation funnels through here -- call sites go via `TypeBuilder.setSon`
## (`typebuilders.nim`, the reopen/mutable-staging seam); this is its backing.
## The `PType.[]=` operators used to do this inline; they were removed so that
## son replacement is named and greppable, and the PType->NifCursor swap (where
## this becomes a token rewrite over a thawed cursor) touches one proc, not
## every caller.
if n.state == Partial: loadType(n)
if n.kind == tyProc and i > 0:
assert n.nImpl[i] != nil and n.nImpl[i].sym != nil
@@ -638,9 +645,9 @@ proc `[]`*(n: PType, i: BackwardsIndex): PType {.inline.} =
if n.state == Partial: loadType(n)
n[n.sonsImpl.len - i.int]
proc `[]=`*(n: PType, i: BackwardsIndex; x: PType) {.inline.} =
proc replaceSon*(n: PType, i: BackwardsIndex; x: PType) {.inline.} =
if n.state == Partial: loadType(n)
n[n.sonsImpl.len - i.int] = x
replaceSon(n, n.sonsImpl.len - i.int, x)
proc getDeclPragma*(n: PNode): PNode =
## return the `nkPragma` node for declaration `n`, or `nil` if no pragma was found.
@@ -1165,7 +1172,7 @@ proc assignType*(dest, src: PType) =
dest.sonsImpl[0] = src.sonsImpl[0]
else:
newSons(dest, src.len)
for i in 0..<src.len: dest[i] = src[i]
for i in 0..<src.len: replaceSon(dest, i, src[i])
proc copyType*(t: PType, idgen: IdGenerator, owner: PSym): PType =
result = newType(t.kind, idgen, owner)

View File

@@ -118,24 +118,13 @@ proc toClassSymId*(config: ConfigRef; typeId: ItemId): nifstreams.SymId =
type
LineInfoWriter = object
# `fileK`/`fileV` cache the most recently resolved (FileIndex -> FileId) pair,
# faster than the hash table. `fileK` MUST be constructed at an invalid
# sentinel (see `newLineInfoWriter`), never zero: `FileIndex(0)` is a real file
# index, and `fileV` zero-inits to `FileId(0)` == `NoFile`, so a zero `fileK`
# would make the first lookup of the module-at-index-0 falsely hit this cache
# and return `NoFile` — silently dropping ALL of that module's line info.
fileK: FileIndex
fileK: FileIndex # remember the current pair, even faster than the hash table
fileV: FileId
tab: Table[FileIndex, FileId]
revTab: Table[FileId, FileIndex] # reverse mapping for oldLineInfo
man: LineInfoManager
config: ConfigRef
proc newLineInfoWriter(config: ConfigRef): LineInfoWriter =
# `fileK` starts invalid so the one-entry cache never collides with a real
# `FileIndex(0)` (see the type's doc comment).
LineInfoWriter(config: config, fileK: astli.InvalidFileIdx)
proc get(w: var LineInfoWriter; key: FileIndex): FileId =
if w.fileK == key:
result = w.fileV
@@ -222,8 +211,9 @@ type
decodedFileIndices: HashSet[FileIndex]
locals: HashSet[ItemId] # track proc-local symbols
inProc: int
writtenTypes: seq[PType] # types sealed during a non-owning emit
writtenSyms: seq[PSym] # reset afterwards so their owner can keep using them
writtenTypes: seq[PType] # types sealed during this emit; under ideActive
writtenSyms: seq[PSym] # they are reset to Complete afterwards so nimsuggest
# can keep mutating its still-live query targets
writtenPackages: HashSet[string]
depSuffixes: HashSet[string] # module suffixes already emitted as `(import ...)` deps
emittedBackendTypes: HashSet[(int32, int32)] # backend-local types already def'd this
@@ -472,9 +462,6 @@ proc writeNode(w: var Writer; dest: var IcBuilder; n: PNode; forAst = false)
proc writeType(w: var Writer; dest: var IcBuilder; typ: PType)
proc writeSym(w: var Writer; dest: var IcBuilder; sym: PSym)
func restoresWrittenState(config: ConfigRef): bool {.inline.} =
config.ideActive or optGenBif in config.globalOptions
proc writeLoc(w: var Writer; dest: var IcBuilder; loc: TLoc) =
dest.addIdent toNifTag(loc.k)
dest.addIdent toNifTag(loc.storage)
@@ -571,7 +558,7 @@ proc writeType(w: var Writer; dest: var IcBuilder; typ: PType) =
# module (or nowhere), leaving dangling references (e.g. `symbol has no
# offset` for a `pointer` type whose itemId.module drifted away).
typ.state = Sealed
if restoresWrittenState(w.infos.config): w.writtenTypes.add typ
if w.infos.config.ideActive: w.writtenTypes.add typ
writeTypeDef(w, dest, typ)
else:
dest.addSymUse pool.syms.getOrIncl(nifTypeName(w, typ)), NoLineInfo
@@ -736,7 +723,7 @@ proc writeSym(w: var Writer; dest: var IcBuilder; sym: PSym) =
dest.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), NoLineInfo
elif shouldWriteSymDef(w, sym):
sym.state = Sealed
if restoresWrittenState(w.infos.config): w.writtenSyms.add sym
if w.infos.config.ideActive: w.writtenSyms.add sym
writeSymDef(w, dest, sym)
else:
# NIF has direct support for symbol references so we don't need to use a tag here,
@@ -771,7 +758,7 @@ proc writeSymNode(w: var Writer; dest: var IcBuilder; n: PNode; sym: PSym) =
else: shouldWriteSymDef(w, sym)
if wantDef:
if not sym.itemId.isBackendMinted and not isField: sym.state = Sealed
if restoresWrittenState(w.infos.config): w.writtenSyms.add sym
if w.infos.config.ideActive: w.writtenSyms.add sym
if nodeTyp != n.sym.typImpl:
dest.buildTree hiddenTypeTag, trLineInfo(w, n.info):
writeType(w, dest, nodeTyp)
@@ -896,16 +883,6 @@ var reexpModTag = registerTag("reexpmod")
var offerTag = registerTag("offer")
var typeOfferTag = registerTag("toffer")
var modulesrcTag = registerTag("modulesrc")
var expansionTag = registerTag("expansion")
# `(sig <symUse @src>)*` — signature occurrences (parameter names and the symbols
# in their type expressions). A semchecked routine's params are dropped from the
# serialized AST (`skipParams`) and reconstructed from `s.typ`, which holds the
# RESOLVED type — so the source parameter names and the written type names (e.g.
# an alias `Stream`, not `StreamObj`) carry no position in the module body. Like
# the `expansion` records, these are teed into the `deps` side-channel: the loader
# skips the tag, but `idetools` scans every Symbol token, so goto-def / find-usages
# work on signatures.
var sigTag = registerTag("sig")
# `(unusedid <int>)` — the module's first FREE itemId after the frontend
# (`.s.bif`) or the lower stage (`.t.bif`). The backend seeds its per-module
# sym/type counters here so freshly-minted backend ids (closure envs, RTTI
@@ -946,51 +923,6 @@ proc registerNifAstTags*() =
offerTag = registerTag("offer")
typeOfferTag = registerTag("toffer")
modulesrcTag = registerTag("modulesrc")
expansionTag = registerTag("expansion")
sigTag = registerTag("sig")
proc emitSigOccurrences(w: var Writer; n: PNode) =
## Record every `nkSym` in a routine-signature subtree (parameter names and the
## symbols inside their type expressions, incl. the return type) as a `(sig ...)`
## occurrence in the `deps` side-channel, carrying the SOURCE position. Called on
## the params AST that `skipParams` is about to drop, so tooling keeps a
## positioned token for each signature symbol without changing the module body
## the loader / backend actually consume.
if n == nil: return
if n.kind == nkSym:
w.deps.addParLe sigTag, NoLineInfo
w.deps.addSymUse pool.syms.getOrIncl(w.toNifSymName(n.sym)), trLineInfo(w, n.info)
w.deps.addParRi
else:
for i in 0 ..< n.safeLen: emitSigOccurrences(w, n[i])
proc emitFwdDecl(w: var Writer; n: PNode; sym: PSym) =
## A routine's forward declaration (`proc foo(...)` with no body, later followed
## by `proc foo(...) = ...`) is a distinct top-level node, but the routine has a
## SINGLE `sdef`, emitted at the IMPLEMENTATION site (`sym.infoImpl`) — so the
## prototype's own position would otherwise vanish from the `.bif`. Tee it into
## the `deps` side-channel as a POSITIONED `(sig @proto <symDef>)`: the loader
## skips the `sig` tag (processTopLevel), but `idetools.scanDef` finds the
## `SymbolDef` and reports the enclosing tag's line info — so a `--def` on a
## forward-declared proc returns TWO results (prototype + implementation), which
## is desired. Safe against symbol resolution: the loader rebuilds its name->pos
## table from the CONTENT body (`buildPosIndex`, written after `deps`, last write
## wins) so the real `sdef` still resolves; the extra on-disk index entry has no
## resolution consumer. The prototype's signature symbols (param names and the
## symbols in their type expressions) are teed too, positioned at the prototype,
## exactly as `emitSigOccurrences` records them for the implementation.
# The `SymbolDef` carries the prototype line info too (not just the enclosing
# tag): `scanDef` reads the position from the tag, but pass-1 `findPos` matches
# a token by its OWN line info, so this is what makes a query issued AT the
# prototype position resolve the symbol.
let protoInfo = trLineInfo(w, n[namePos].info)
let sid = pool.syms.getOrIncl(w.toNifSymName(sym))
w.deps.addParLe sigTag, protoInfo
w.deps.addSymDef sid, protoInfo # scanDef reports this as a def
w.deps.addSymUse sid, protoInfo # findPos (pass 1) / scanUses match a Symbol use
w.deps.addParRi
if sfFromGeneric notin sym.flagsImpl and paramsPos < n.safeLen:
emitSigOccurrences(w, n[paramsPos])
proc writeNode(w: var Writer; dest: var IcBuilder; n: PNode; forAst = false) =
if n == nil:
@@ -1065,16 +997,7 @@ proc writeNode(w: var Writer; dest: var IcBuilder; n: PNode; forAst = false) =
# For top-level named routines (not forAst), just write the symbol.
# The full AST will be stored in the symbol's sdef.
if not forAst and n[namePos].kind == nkSym:
let s = n[namePos].sym
writeSym(w, dest, s)
# A forward declaration is a SECOND top-level node for `s` (body-less here;
# the real body — and the lone sdef — lands at the implementation). Tee the
# prototype's own position so goto-def / find-usages surface it as well.
let impl = s.astImpl
if n.safeLen > bodyPos and n[bodyPos].kind == nkEmpty and
impl != nil and impl != n and
impl.safeLen > bodyPos and impl[bodyPos].kind != nkEmpty:
emitFwdDecl(w, n, s)
writeSym(w, dest, n[namePos].sym)
else:
# Writing AST inside sdef or anonymous proc: write full structure
inc w.inProc
@@ -1095,13 +1018,6 @@ proc writeNode(w: var Writer; dest: var IcBuilder; n: PNode; forAst = false) =
w.withNode dest, ast:
for i in 0 ..< ast.len:
if i == paramsPos and skipParams:
# The dropped params still hold the source positions and the WRITTEN
# type names (before alias/type resolution); tee them into the `deps`
# side-channel for goto-def / find-usages (see `emitSigOccurrences`).
# Skip generic INSTANCES: their param syms are instance-specific, and
# the generic's own signature already records the source occurrences.
if sfFromGeneric notin n[namePos].sym.flagsImpl:
emitSigOccurrences(w, ast[i])
# Parameters are redundant with s.typ.n (and re-emitting their syms
# is dangerous for generic instances — we do not adapt the symbols
# properly). Emit an `nkEmpty` placeholder rather than a dot token:
@@ -1632,9 +1548,8 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
genericParamsCount: int]] = @[];
typeOffers: seq[tuple[generic: PSym; inst: PType]] = @[];
resolvedImportDeps: seq[FileIndex] = @[];
firstUnusedId: int32 = 0;
expansions: seq[(PSym, TLineInfo)] = @[]) =
var w = Writer(infos: newLineInfoWriter(config), currentModule: thisModule)
firstUnusedId: int32 = 0) =
var w = Writer(infos: LineInfoWriter(config: config), currentModule: thisModule)
w.deps = newIcBuilder(64)
var content = newIcBuilder(300)
@@ -1711,17 +1626,6 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
w.deps.addStrLit toFullPath(config, FileIndex(thisModule))
w.deps.addParRi
# Template/macro expansions leave no trace in the sem'checked AST, so record
# each as `(expansion <symUse @call-site>)`: a `Symbol` use of the expanded
# routine carrying the ORIGINAL call-site line info. The loader skips the tag
# (processTopLevel), but `idetools` scans every `Symbol` token in the buffer,
# so this restores "find usages / goto-def" for templates and macros.
for (sym, info) in expansions:
if sym == nil: continue
w.deps.addParLe expansionTag, NoLineInfo
w.deps.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), trLineInfo(w, info)
w.deps.addParRi
# Generic TYPE-instance OFFERS: the `tyGenericInst` types this module created
# (e.g. `HashArray[8192, Gwei]`). Non-IC keeps ONE such instance in the global
# `typeInstCache`, so a structural bound computed at the first instantiation
@@ -1790,15 +1694,17 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
let s = op.sym
if s.state != Sealed:
s.state = Sealed
if restoresWrittenState(config): w.writtenSyms.add s
if config.ideActive: w.writtenSyms.add s
writeSymDef w, dest, s
dest.addParRi()
# Nimsuggest and normal code generation reuse these symbols/types as live,
# mutable targets. Sealing is only needed for intra-emit dedup; once the NIF
# is built, un-seal them. The guard stays in force for a real `nim m` build.
if restoresWrittenState(config):
# nimsuggest reuses these symbols/types as live, mutable query targets (sem
# re-runs, usage tracking, flag updates). Sealing is only needed for intra-emit
# dedup; once the NIF is built, un-seal so suggest can keep mutating them
# (matches `loadedState` loading Complete under ideActive). The `Sealed` guard
# stays in force for a real `nim m`/`nim nifc` build.
if config.ideActive:
for s in w.writtenSyms:
if s.state == Sealed: s.state = Complete
for t in w.writtenTypes:
@@ -1918,7 +1824,7 @@ type
proc createDecodeContext*(config: ConfigRef; cache: IdentCache): DecodeContext =
## Supposed to be a global variable
result = DecodeContext(infos: newLineInfoWriter(config), cache: cache)
result = DecodeContext(infos: LineInfoWriter(config: config), cache: cache)
var loadStatsInit {.threadvar.}: int # 0=unknown 1=on 2=off
var statsCtxPtr {.threadvar.}: ptr DecodeContext
@@ -3345,14 +3251,6 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag]
# self-identification record for the standalone include-graph scanner;
# not needed by the loader, just skip past it.
skip cur
elif tagIs(cur, "expansion"):
# template/macro expansion usage record for tooling (`idetools` scans it
# as a `Symbol` use); the loader itself needs nothing from it.
skip cur
elif tagIs(cur, "sig"):
# signature-symbol occurrence record for tooling (`idetools` scans it as a
# `Symbol` use); the loader itself needs nothing from it.
skip cur
elif tagIs(cur, "implementation"):
cont = false
elif LoadFullAst in flags or tagIs(cur, toNifTag(nkLetSection)) or
@@ -3414,7 +3312,7 @@ proc writeLoweredModule*(c: var DecodeContext; config: ConfigRef;
# types/globals/params/locals stay Complete and emit real defs (the `.t.nif` is
# the sole source the cg stage reads — no `.s.nif` fallback for them).
sealLoadedRoutines(c)
var w = Writer(infos: newLineInfoWriter(config), currentModule: thisModule)
var w = Writer(infos: LineInfoWriter(config: config), currentModule: thisModule)
w.deps = newIcBuilder(64)
w.inProc = 1
w.lowering = true
@@ -3540,3 +3438,4 @@ when isMainModule:
echo obj.name, " ", obj.module, " ", obj.count
let objb = parseSymName("abcdef.0121")
echo objb.name, " ", objb.module, " ", objb.count

View File

@@ -3143,12 +3143,6 @@ proc genMagicExpr(p: BProc, e: PNode, d: var TLoc, op: TMagic) =
localError(p.config, e.info,
"for --mm:arc|atomicArc|orc 'deepcopy' support has to be enabled with --deepcopy:on")
let typ = e[1].typ.skipTypes({tyVar, tyRef, tyGenericInst, tyTypeDesc,
tyAlias, tyInferred, tySink, tyLent, tyOwned})
if hasDisabledAsgn(p.module.g.graph, typ):
localError(p.config, e.info,
"'deepCopy' is not available for type <" & typeToString(typ) & ">")
let x = if e[1].kind in {nkAddr, nkHiddenAddr}: e[1][0] else: e[1]
var a = initLocExpr(p, x)
var b = initLocExpr(p, e[2])

View File

@@ -509,7 +509,6 @@ proc parseCommand*(command: string): Command =
of "nifc": cmdNifC # generate C from NIF files
of "ic": cmdIc # generate .build.nif for nifmake
of "icconfig": cmdIcConfig # produce the precompiled config artifact
of "track": cmdTrack # IDE goto-def / find-usages over `nim ic`'s NIF output
else: cmdUnknown
proc setCmd*(conf: ConfigRef, cmd: Command) =
@@ -826,8 +825,6 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
localError(conf, info, "expected nim|cpp but found " & arg)
of "compress":
conf.globalOptions.incl optCompress
of "genbif":
processOnOffSwitchG(conf, {optGenBif}, arg, pass, info)
of "g": # alias for --debugger:native
conf.globalOptions.incl optCDebug
conf.options.incl optLineDir
@@ -1327,16 +1324,8 @@ proc processArgument*(pass: TCmdLinePass; p: OptParser;
# support UNIX style filenames everywhere for portable build scripts:
if config.projectName.len == 0:
config.projectName = unixToNativePath(p.key)
if config.cmd == cmdTrack:
# `nim track PROJ --def:...`: unlike a normal command (where everything
# after the project file is passed to the compiled program), `track`
# accepts its IDE-query switches AFTER the project — the natural,
# nimsuggest-like invocation form. So don't swallow the rest of the line
# into `arguments`; keep parsing the remaining tokens as switches.
result = false
else:
config.arguments = cmdLineRest(p)
result = true
config.arguments = cmdLineRest(p)
result = true
else:
result = false
inc argsCount

View File

@@ -590,98 +590,6 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
elif t.kind == ParRi: dec depth
t = next(s)
proc collectIncludeNames(depsPath: string; names: var seq[string]) =
## Lightweight scan of a `.deps.nif` prelude: collect the raw path text of
## every entry inside an `(include ...)` node (idents like `semexprs`, string
## literals like `"system/mmdisp"`, and the leaves of `a/b` path infixes).
## Liberal by design — it also picks up entries under a statically-false
## `(when ...)`; that is harmless for the only caller (`includerSbifs`), whose
## over-collection just costs an extra, result-free bif scan downstream.
if not fileExists(depsPath): return
var s = nifstreams.open(depsPath)
defer: nifstreams.close(s)
discard processDirectives(s.r)
var depth = 0
var includeDepth = 0 # the `depth` at which the current `(include` opened; 0 = not inside one
var t = next(s)
while t.kind != EofToken:
case t.kind
of ParLe:
inc depth
if includeDepth == 0 and pool.tags[t.tagId] == "include":
includeDepth = depth
of ParRi:
if includeDepth != 0 and depth == includeDepth:
includeDepth = 0
dec depth
of Ident, StringLit:
if includeDepth != 0:
names.add pool.strings[t.litId]
else: discard
t = next(s)
proc entryStemBase(roots: seq[string]; name: string): (string, string) =
## Resolve include entry `name` to (deps-stem, base-name); ("","") if unfound.
for r in roots:
let p = r / name.addFileExt("nim")
if fileExists(p):
return (moduleSuffix(p, []), splitFile(p).name)
result = ("", "")
proc includerSbifs*(conf: ConfigRef; targetFile: AbsoluteFile): seq[string] =
## For an include file `targetFile`, return the `.s.bif` paths of every module
## that includes it — directly OR transitively (following the include chain
## `module -> incA -> incB -> targetFile`). `nim track` uses this to avoid
## loading and scanning every module bif: an include file has no bif of its
## own, so its type-checked tokens live in the *including* module's bif. Only
## the small `.deps.nif` preludes are read here, never a `.s.bif`.
const depsExt = ".deps.nif"
let nc = getNimcacheDir(conf).string
# Candidate roots for resolving an `(include X)` entry to a real file, so its
# module suffix (== its own deps-file stem) can be computed. Include entries
# carry any sub-path (`system/mmdisp`), so the file's *directory* roots suffice:
# the target's own dir, the project dir, and the search paths cover the
# compiler, the stdlib and typical single-tree projects.
var roots: seq[string] = @[parentDir(targetFile.string)]
if conf.projectPath.string.len > 0: roots.add conf.projectPath.string
for sp in conf.searchPaths: roots.add sp.string
# One pass over every prelude builds the reverse include graph, keyed by base
# file name: `includedBy[b]` = deps stems whose owner directly `include`s a
# file named `b`. `stemBase` maps an include-only file's deps stem back to its
# own base name, so the walk can climb through nested includes.
var includedBy = initTable[string, seq[string]]()
var stemBase = initTable[string, string]()
for depsPath in walkFiles(nc / "*" & depsExt):
let base = extractFilename(depsPath)
if base.endsWith(".p" & depsExt): continue # `.p.deps.nif` twin
let ownerStem = base[0 ..< base.len - depsExt.len]
var names: seq[string] = @[]
collectIncludeNames(depsPath, names)
for n in names:
let (childStem, childBase) = entryStemBase(roots, n)
if childBase.len == 0: continue
includedBy.mgetOrPut(childBase, @[]).add ownerStem
stemBase[childStem] = childBase # this child's stem -> its base name
# Walk UP from the target: a deps stem that includes the current base name is
# either a module (has a `.s.bif` -> collect it) or itself an include file
# (recurse via its own base name).
result = @[]
var seenBase = initHashSet[string]()
var work = @[splitFile(targetFile.string).name]
while work.len > 0:
let b = work.pop()
if seenBase.containsOrIncl(b): continue
for stem in includedBy.getOrDefault(b):
let sbif = nc / stem & ".s.bif"
if fileExists(sbif):
if sbif notin result: result.add sbif # module owner
else:
let ob = stemBase.getOrDefault(stem) # include-only owner: climb higher
if ob.len > 0: work.add ob
proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) =
## Process a module: run nifler and read deps
if not runNifler(c, pair.nimFile):
@@ -1226,12 +1134,8 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
b.endTree() # stmts
proc commandIc*(conf: ConfigRef; frontendOnly = false) =
## Main entry point for `nim ic`. With `frontendOnly` (used by `nim track` for
## IDE queries) it runs only Phase 1 — the incremental nifler + `nim m`
## frontend that writes every module's `.s.bif` — and skips the whole-program
## backend (`nim nifc` -> C -> link), which a goto-def / find-usages scan does
## not need.
proc commandIc*(conf: ConfigRef) =
## Main entry point for `nim ic`
when not defined(nimKochBootstrap):
let nifler = findNifler()
if nifler.len == 0:
@@ -1371,12 +1275,10 @@ proc commandIc*(conf: ConfigRef; frontendOnly = false) =
if nifmake.len == 0:
rawMessage(conf, hintSuccess, "run:" & " nifmake run" & parallel & " " & buildFile)
# without nifmake we can only print the manual commands; emit the
# backend's too (best effort — discovery cannot run) and stop. An IDE
# query (`frontendOnly`) needs no backend, so skip it there.
if not frontendOnly:
let backendFile = generateBackendBuildFile(c, forwardedArgs)
rawMessage(conf, hintSuccess, "generated: " & backendFile)
rawMessage(conf, hintSuccess, "run:" & " nifmake run" & parallel & " " & backendFile)
# backend's too (best effort — discovery cannot run) and stop.
let backendFile = generateBackendBuildFile(c, forwardedArgs)
rawMessage(conf, hintSuccess, "generated: " & backendFile)
rawMessage(conf, hintSuccess, "run:" & " nifmake run" & parallel & " " & backendFile)
return
let cmd = quoteShell(nifmake) & " run" & parallel & " " & quoteShell(buildFile)
rawMessage(conf, hintExecuting, cmd)
@@ -1420,9 +1322,7 @@ proc commandIc*(conf: ConfigRef; frontendOnly = false) =
# Phase 2 — backend (whole-program `nim nifc`), run once over the now-final
# graph. Kept a separate nifmake run so backend rebuilds are decided purely
# by nifmake's input mtimes, independent of frontend discovery.
# An IDE query (`frontendOnly`) stops after Phase 1: the `.s.bif` it scans
# are all produced by the frontend; codegen + link would be wasted work.
if frontendOk and not frontendOnly:
if frontendOk:
let backendFile = generateBackendBuildFile(c, forwardedArgs)
rawMessage(conf, hintSuccess, "generated: " & backendFile)
let cmd = quoteShell(nifmake) & " run" & parallel & " " & quoteShell(backendFile)

View File

@@ -260,26 +260,17 @@ proc ensureIcConfig*(conf: ConfigRef) =
if not fileExists(outPath) or sourcesChanged(outPath):
createDir(cacheDir)
# Re-invoke ourselves as the config producer: reuse this process's command
# line, dropping the command argument (`ic`/`track`) in favour of `icconfig`
# and the explicit output path. Every switch must land BEFORE the project
# file, because anything after the project is swallowed into
# `config.arguments` by `cmdLineRest` (and a non-empty `arguments` without
# `--run` is a hard error). Callers may legitimately put switches after the
# project — `nim track PROJ --def:...` — so we re-order rather than replay
# verbatim: all `-`-prefixed switches first (in encounter order), then the
# non-switch project token(s). The producer re-reads `nim.cfg` itself.
# line, dropping the command argument (`ic`) in favour of `icconfig` and the
# explicit output path, both BEFORE the project file (anything after the
# project is swallowed into `config.arguments` by `cmdLineRest`). The
# producer re-reads `nim.cfg` itself.
var pargs = @["icconfig", "--icConfigOut:" & outPath]
var rest: seq[string] = @[]
var droppedCmd = false
for a in commandLineParams():
if a.len == 0: continue
if a[0] == '-':
pargs.add a
elif not droppedCmd:
droppedCmd = true # drop the original command token (`ic`/`track`)
if not droppedCmd and a.len > 0 and a[0] != '-':
droppedCmd = true # drop the original command token (`ic`)
else:
rest.add a # project file (and any further non-switch tokens) go last
for a in rest: pargs.add a
pargs.add a
let p = startProcess(getAppFilename(), args = pargs,
options = {poStdErrToStdOut})
let outp = p.outputStream.readAll()

View File

@@ -1,279 +0,0 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## NIF-based goto-definition / find-all-usages for `nim track`.
##
## This is the mainline-Nim port of nimony's `idetools.nim`. It answers a
## `--def:FILE,LINE,COL` / `--usages:FILE,LINE,COL` query by *scanning the
## `.s.bif` files* (binary NIF, see `dist/nimony/src/lib/bif.nim`) that the
## preceding `nim ic` frontend (`nim track`) emitted into the nimcache directory
## — NOT by re-running sem. NIF distinguishes a definition (`SymbolDef` token) from a use
## (`Symbol` token) syntactically, so goto-def / find-uses become plain token
## scans over type-checked NIF, which is more reliable than the classic PSym
## engine because generics and macros are type-checked in the NIF too.
##
## Two passes (mirroring nimony's `usages`):
## 1. Load the queried module's `.s.bif` and find the `Symbol`/`SymbolDef`
## token whose line info + identifier length contains `conf.m.trackPos`.
## That yields the mangled symbol NAME and whether it is global (>= 2 dots).
## 2. `--usages`: emit every `Symbol` (use) token; `--def`: every `SymbolDef`.
## A global symbol is scanned across every module `.s.bif`; a local one only
## within the queried module.
##
## IMPORTANT porting note: `bif.load` mints FRESH per-file pools, so a `SymId`
## from module A's buffer is meaningless in module B's. The cross-module match is
## therefore by the mangled NAME string, never by `SymId` (nimony can compare ids
## because it parses every text NIF into one shared global pool; we cannot).
import std / [os, strutils, sets]
import options, msgs, pathutils
import lineinfos as astli
import ast2nif # toNifFilename
from deps import includerSbifs # deps-guided include-file lookup
import "../dist/nimony/src/lib/nifcore"
from "../dist/nimony/src/lib" / bif import load, BifModule, containsSym
proc identLen(name: string): int =
## Length of the displayed identifier: the run before the first `.` of a
## mangled NIF name (`ident.disamb[.moduleSuffix]`). Bounds the column match.
let d = name.find('.')
result = if d < 0: name.len else: d
proc isGlobalName(name: string): bool =
## A global symbol carries `ident.disamb.moduleSuffix` (>= 2 dots); a local at
## most `ident.disamb` (<= 1 dot). `moduleSuffix` is a dot-free hash, so a raw
## dot count is equivalent to nifbuilder's suffix-compressed test for our use.
var dots = 0
for i in 1 ..< name.len:
if name[i] == '.': inc dots
result = dots >= 2
proc posMatch(c: Cursor; conf: ConfigRef; target: TLineInfo; tokenLen: int): bool =
## True when `target` (the queried position) falls within the identifier span
## of the Symbol/SymbolDef token at `c`. Mirrors nimony's `lineInfoMatch`; the
## filename is resolved through the loaded buffer's own pool (fresh per file),
## then mapped to a `FileIndex` exactly like `ast2nif.oldLineInfo`.
let li = rawLineInfo(c)
if not li.isValid: return false
if li.line.int != target.line.int: return false
let f = fileInfoIdx(conf, AbsoluteFile lineInfoFile(c))
if f != target.fileIndex: return false
if target.col.int < li.col.int: return false
if target.col.int > li.col.int + tokenLen: return false
result = true
const sep = '\t'
proc formatSuggest(s: Suggest): string =
## Reproduce `suggest.$Suggest` for the `ideDef`/`ideUse` sections without
## importing `suggest` (which would create an import cycle). Layout:
## `section⭾symkind⭾qualifiedPath⭾forth⭾filePath⭾line⭾column⭾⭾quality`.
## symkind is always `skUnknown` here — the raw NIF scan has no PSym to give a
## real kind (like nimony's `foundSymbol`, which leaves it empty).
result = $s.section
result.add sep
result.add "skUnknown"
result.add sep
if s.qualifiedPath.len != 0:
result.add s.qualifiedPath.join(".")
result.add sep
result.add s.forth
result.add sep
result.add s.filePath
result.add sep
result.add $s.line
result.add sep
result.add $s.column
result.add sep # empty doc field (docgen is off outside nimsuggest)
if s.version == 0 or s.version == 3:
result.add sep
result.add $s.quality
proc emit(conf: ConfigRef; c: Cursor; section: IdeCmd; name: string;
seen: var HashSet[string]) =
## Report one hit as a nimsuggest-compatible result (routed through the
## structured-output hook / `--stdout`). We only have the mangled name + line
## info from the raw NIF, so symkind/type are left empty — like nimony's
## `foundSymbol`. `seen` deduplicates: the same source location can back
## several NIF `Symbol` tokens (e.g. a call argument re-emitted in a lowered
## form), which must surface as one hit.
let li = rawLineInfo(c)
if not li.isValid: return
let key = $section.int & ":" & lineInfoFile(c) & ":" & $li.line.int & ":" & $li.col.int
if seen.containsOrIncl(key):
return # already reported this location for this section
let s = Suggest(section: section,
qualifiedPath: @[name[0 ..< identLen(name)]],
filePath: lineInfoFile(c),
line: li.line.int,
column: li.col.int,
tokenLen: identLen(name),
forth: "",
symkind: 0'u8,
quality: 100,
version: conf.suggestVersion)
if conf.suggestionResultHook != nil:
conf.suggestionResultHook(s)
else:
conf.suggestWriteln(formatSuggest(s))
proc tokenSymId(c: Cursor): SymId {.inline.} =
## SymId (in the cursor's own per-file pool) of a `Symbol`/`SymbolDef` token,
## or `SymId(0)` for an inline-encoded one — which is never our search target:
## a mangled name (`ident.disamb.suffix`) is always longer than
## `StrInlineMaxLen`, so every occurrence of the symbol we look for is stored by
## pool id, decoded here with a shift and no string materialization.
if isInlineLit(c): SymId(0) else: SymId(combinedPayload(c) shr 1)
template symMatches(c: Cursor): bool =
## True when the token at `c` is the searched symbol. The fast path is a pure
## integer compare against `targetSym` (the symbol's id in THIS module's pool,
## resolved once per file by the caller). `targetSym == 0` means the name is not
## representable as a pool id (a rare <=3-byte local): fall back to a string
## compare, correct for both inline and pooled encodings.
(if targetSym != SymId(0): tokenSymId(c) == targetSym else: symName(c) == targetName)
proc scanUses(conf: ConfigRef; m: var BifModule; targetSym: SymId; targetName: string;
seen: var HashSet[string]) =
## `--usages`: report every `Symbol` (use) occurrence with valid line info.
if m.buf.len == 0: return
var c = m.buf.beginRead()
while c.hasMore:
if c.kind == Symbol and symMatches(c) and rawLineInfo(c).isValid:
emit(conf, c, ideUse, targetName, seen)
inc c
c.endRead()
proc scanDef(conf: ConfigRef; m: var BifModule; targetSym: SymId; targetName: string;
seen: var HashSet[string]) =
## `--def`: report the declaration of the target symbol if this module owns it
## (has its `SymbolDef`). The `SymbolDef` token itself carries no line info; the
## declaration location lives on the *enclosing tag* (e.g. `(sd @file:line:col`,
## like `bif.buildIndex`'s `mostRecentTagPos`). When that tag has no line info
## either, fall back to the declaration-site `Symbol` occurrence — but only in
## the owning module, so a plain user of the symbol is never reported as a def.
if m.buf.len == 0: return
var c = m.buf.beginRead()
var mostRecentTagPos = 0
var sawDef = false
var emitted = false
var fallbackPos = -1
while c.hasMore:
case c.kind
of TagLit:
mostRecentTagPos = cursorToPosition(m.buf, c)
inc c
of SymbolDef:
if symMatches(c):
sawDef = true
var tc = cursorAt(m.buf, mostRecentTagPos)
if rawLineInfo(tc).isValid:
emit(conf, tc, ideDef, targetName, seen)
emitted = true
tc.endRead()
inc c
of Symbol:
if fallbackPos < 0 and symMatches(c) and rawLineInfo(c).isValid:
fallbackPos = cursorToPosition(m.buf, c)
inc c
else:
inc c
c.endRead()
if sawDef and not emitted and fallbackPos >= 0:
var fc = cursorAt(m.buf, fallbackPos)
emit(conf, fc, ideDef, targetName, seen)
fc.endRead()
proc scanBuf(conf: ConfigRef; m: var BifModule; section: IdeCmd;
targetSym: SymId; targetName: string; seen: var HashSet[string]) =
## Emit hits for the target symbol in `m` per the query kind. `ideDus`
## (`--defusages`) reports both the definition and every usage.
if section in {ideDef, ideDus}:
scanDef(conf, m, targetSym, targetName, seen)
if section in {ideUse, ideDus}:
scanUses(conf, m, targetSym, targetName, seen)
proc findPos(conf: ConfigRef; m: var BifModule; target: TLineInfo;
foundName: var string): bool =
## Scan `m` for the `Symbol`/`SymbolDef` token covering the queried position
## `target` and set `foundName` to its mangled name. Returns true on a hit.
if m.buf.len == 0: return false
var c = m.buf.beginRead()
result = false
while c.hasMore:
let k = c.kind
if k == Symbol or k == SymbolDef:
let nm = symName(c)
if posMatch(c, conf, target, identLen(nm)):
foundName = nm
result = true
break
inc c
c.endRead()
proc runIdeQuery*(conf: ConfigRef) =
## Entry point: called from `main.nim` after `commandCheck` when a
## `--def`/`--usages` query is active. Assumes the check just emitted the
## project's `.s.bif` files into `getNimcacheDir(conf)`.
let section = conf.ideCmd
if section notin {ideDef, ideUse, ideDus}: return
let target = conf.m.trackPos
if target.fileIndex.int32 < 0: return
# Pass 1: position -> symbol. Try the queried file's own module bif first (the
# fast path when the position is inside a real module). An include file has no
# module bif of its own — its tokens live in the *including* module's bif with
# include-file line info — so when the direct lookup misses, consult the
# `.deps.nif` preludes (`includerSbifs`) to load only the module(s) that
# include the queried file (directly or transitively), never every bif in the
# nimcache. `ownerFile` is the bif that owns the hit.
let modFile = toNifFilename(conf, target.fileIndex)
var foundName = ""
var ownerFile = ""
if fileExists(modFile):
var qm = load(modFile)
if findPos(conf, qm, target, foundName):
ownerFile = modFile
if foundName.len == 0:
for cand in includerSbifs(conf, toFullPath(conf, target.fileIndex).AbsoluteFile):
if cand == modFile: continue
var m = load(cand)
if findPos(conf, m, target, foundName):
ownerFile = cand
break
if foundName.len == 0: return
# Pass 2: emit definition / usages. `seen` spans every module so a location is
# reported once even when scanned across the whole nimcache.
#
# Cross-file matching is by SymId, not by decoding every token's name. Two
# filters keep it cheap:
# 1. `bif.containsSym` — a sym-table-only probe that reads just the small
# trailing pools, NOT the token block or any `BiTable`. A module that never
# references the symbol is rejected here without a full `load` (no pools
# built, no token block mapped) — so a query whose symbol lives in a few
# modules no longer pays to load the whole nimcache.
# 2. For a module that does contain it, `bif.load` mints a fresh per-file pool,
# so the name is resolved to THIS file's SymId once via `getKeyId`; the scan
# then compares integer ids per token instead of materializing a string for
# each (see `symMatches`).
var seen = initHashSet[string]()
if isGlobalName(foundName):
for f in walkFiles((getNimcacheDir(conf).string) / "*.s.bif"):
if not containsSym(f, foundName): continue
var m = load(f)
let tid = m.buf.pool.syms.getKeyId(foundName)
if tid != SymId(0):
scanBuf(conf, m, section, tid, foundName, seen)
else:
# Local symbol: its mangled name is not unique across modules, so restrict
# the scan to the module it lives in (the one that owns the queried position).
var qm = load(ownerFile)
let tid = qm.buf.pool.syms.getKeyId(foundName)
scanBuf(conf, qm, section, tid, foundName, seen)

View File

@@ -144,18 +144,19 @@ proc lowerSwap*(g: ModuleGraph; n: PNode; idgen: IdGenerator; owner: PSym): PNod
result.add newFastAsgnStmt(n[2], tempAsNode)
proc createObj*(g: ModuleGraph; idgen: IdGenerator; owner: PSym, info: TLineInfo; final=true): PType =
result = newType(tyObject, idgen, owner)
var b = openType(tyObject, idgen, owner)
if final:
rawAddSon(result, nil)
incl result, tfFinal
b.addRaw nil
b.incl tfFinal
else:
rawAddSon(result, getCompilerProc(g, "RootObj").typ)
result.n = newNodeI(nkRecList, info)
b.addRaw getCompilerProc(g, "RootObj").typ
b.setN newNodeI(nkRecList, info)
let s = newSym(skType, getIdent(g.cache, "Env_" & toFilename(g.config, info) & "_" & $owner.name.s),
idgen, owner, info, owner.options)
incl s.flagsImpl, sfAnon
b.setSym s
result = finish b
s.typ = result
result.sym = s
template fieldCheck {.dirty.} =
when false:
@@ -163,13 +164,6 @@ template fieldCheck {.dirty.} =
echo "missed field ", field.name.s
writeStackTrace()
proc rawAddField*(obj: PType; field: PSym) =
assert field.kind == skField
field.position = obj.n.len
obj.n.add newSymNode(field)
propagateToOwner(obj, field.typ)
fieldCheck()
proc rawIndirectAccess*(a: PNode; field: PSym; info: TLineInfo): PNode =
# returns a[].field as a node
assert field.kind == skField
@@ -238,40 +232,70 @@ proc lookupCapturedField(n: PNode, s: PSym): PSym =
result = n.sym
else: discard
proc addField*(obj: PType; s: PSym; cache: IdentCache; idgen: IdGenerator): PSym =
# Idempotent w.r.t. the captured symbol (mirrors `addUniqueField`): re-lifting
# a LOADED routine re-derives its transformed body (never serialized under IC)
# and re-captures the same locals, but the env object loaded from the NIF
# already carries their fields. Re-adding would duplicate the field and, worse,
# mutate a Sealed loaded type via `propagateToOwner` (the `t.state != Sealed`
# crash). Return the existing field instead.
let existing = lookupInRecord(obj.n, s.itemId)
if existing != nil:
return existing
# Re-lifting a LOADED routine during a VM transform (its transformed body is
# re-derived per process, never serialized) re-captures the same locals, but
# for a macro-generated gensym (e.g. libp2p `p2pProtocolBackendImpl`'s
# `msgVar`) its process-local id diverges from the one baked into the loaded
# env field, so the id match above misses. Reuse the existing same-named field
# rather than appending a divergent duplicate, which keeps the re-derived
# closure consistent (else a stale `:env` access reaches `cannotEval`).
# Confined to a loaded (Sealed) env: in a freshly built env ids are consistent,
# and two distinct same-named captures legitimately get distinct fields there.
if obj.state == Sealed:
let byName = lookupCapturedField(obj.n, s)
if byName != nil:
return byName
# Genuinely new field. Under IC the env may be a loaded Sealed type whose
# transform-time mutation is process-local (the body is discarded after the
# macro runs), so downgrade it to mutable instead of crashing on
# `t.state != Sealed` (mirrors `markAsClosure`).
type
ObjectBuilder* = object
## Extends an existing object type with record fields -- the deferred
## object-BODY counterpart to `typebuilders.TypeBuilder`. The object's
## identity is fixed (a shell from `createObj` or a type loaded from NIF);
## only its `nkRecList` body grows, possibly after thawing a loaded Sealed
## type. Lives here rather than in `typebuilders.nim` because it needs the
## record-walk reuse lookups above; hoist it once those move.
## See `doc/ic_type_body_builder.md` for the NIF-cursor migration story.
obj {.cursor.}: PType
cache {.cursor.}: IdentCache
idgen {.cursor.}: IdGenerator
proc reopenObject*(obj: PType; cache: IdentCache; idgen: IdGenerator): ObjectBuilder {.inline.} =
## Positions a builder to append fields to `obj`, keeping its identity. Does
## not thaw yet: the idempotency lookups must observe the pre-thaw `Sealed`
## state first (see `findField`).
ObjectBuilder(obj: obj, cache: cache, idgen: idgen)
proc findField*(b: ObjectBuilder; s: PSym; byName: bool): PSym =
## The idempotency lookup, load-bearing for correctness (not a fast path):
## re-lifting a LOADED routine re-derives its transformed body per process and
## re-captures the same locals, but the loaded env already carries their
## fields -- re-adding would duplicate and mutate Sealed memory.
##
## By derived item id first. Then, for a loaded (`Sealed`) body and when
## `byName`, by the stable name+position key: a macro-generated gensym (e.g.
## libp2p `p2pProtocolBackendImpl`'s `msgVar`) has a process-local id that
## diverges from the one baked into the loaded env field, so the id match
## misses; the same-named field is reused instead of appending a divergent
## duplicate (else a stale `:env` access reaches `cannotEval`). A freshly
## built env keeps consistent ids, so two same-named captures there
## legitimately get distinct fields -- hence the `Sealed`-only gate.
result = lookupInRecord(b.obj.n, s.itemId)
if result != nil: return
if byName and b.obj.state == Sealed:
result = lookupCapturedField(b.obj.n, s)
proc appendField*(b: var ObjectBuilder; field: PSym) =
## Low-level append of a prebuilt `skField` (replaces `rawAddField`): set its
## position, append it, fold its type into the object.
assert field.kind == skField
let obj = b.obj
field.position = obj.n.len
obj.n.add newSymNode(field)
propagateToOwner(obj, field.typ)
fieldCheck()
proc captureField*(b: var ObjectBuilder; s: PSym): PSym {.discardable.} =
## Idempotent capture of local `s` (= `addField`). On a `findField` miss,
## thaws the env if needed then mints the field. Under IC the env may be a
## loaded Sealed type whose transform-time mutation is process-local (the body
## is discarded after the macro runs), so `unsealForTransform` downgrades it to
## mutable instead of crashing on `t.state != Sealed` (mirrors `markAsClosure`).
result = b.findField(s, byName = true)
if result != nil: return
let obj = b.obj
unsealForTransform(obj)
# because of 'gensym' support, we have to mangle the name with its ID.
# This is hacky but the clean solution is much more complex than it looks.
var field = newSym(skField, getIdent(cache, s.name.s & $obj.n.len),
idgen, s.owner, s.info, s.options)
var field = newSym(skField, getIdent(b.cache, s.name.s & $obj.n.len),
b.idgen, s.owner, s.info, s.options)
field.itemId = derivedFieldId(s.itemId)
let t = skipIntLit(s.typ, idgen)
let t = skipIntLit(s.typ, b.idgen)
field.typ = t
if s.kind in {skLet, skVar, skField, skForVar}:
#field.bitsize = s.bitsize
@@ -285,19 +309,44 @@ proc addField*(obj: PType; s: PSym; cache: IdentCache; idgen: IdGenerator): PSym
fieldCheck()
result = field
proc captureUniqueField*(b: var ObjectBuilder; s: PSym): PSym {.discardable.} =
## `addUniqueField`: idempotent by item id ONLY (no name fallback, no thaw,
## no alignment/flag copy).
result = b.findField(s, byName = false)
if result != nil: return
let obj = b.obj
var field = newSym(skField, getIdent(b.cache, s.name.s & $obj.n.len),
b.idgen, s.owner, s.info, s.options)
field.itemId = derivedFieldId(s.itemId)
let t = skipIntLit(s.typ, b.idgen)
field.typ = t
assert t.kind != tyTyped
propagateToOwner(obj, t)
field.position = obj.n.len
obj.n.add newSymNode(field)
result = field
proc finishObject*(b: sink ObjectBuilder) {.inline.} =
## Publish the completed body. A no-op today (the thawed env stays `Complete`,
## process-local, never re-serialized); the seam where the NIF backend will
## `beginRead` the record buffer into a read-only cursor and republish it
## under the object's SymId.
discard
proc rawAddField*(obj: PType; field: PSym) =
var b = reopenObject(obj, nil, nil) # prebuilt field: cache/idgen unused
b.appendField(field)
finishObject b
proc addField*(obj: PType; s: PSym; cache: IdentCache; idgen: IdGenerator): PSym =
var b = reopenObject(obj, cache, idgen)
result = b.captureField(s)
finishObject b
proc addUniqueField*(obj: PType; s: PSym; cache: IdentCache; idgen: IdGenerator): PSym {.discardable.} =
result = lookupInRecord(obj.n, s.itemId)
if result == nil:
var field = newSym(skField, getIdent(cache, s.name.s & $obj.n.len), idgen,
s.owner, s.info, s.options)
field.itemId = derivedFieldId(s.itemId)
let t = skipIntLit(s.typ, idgen)
field.typ = t
assert t.kind != tyTyped
propagateToOwner(obj, t)
field.position = obj.n.len
obj.n.add newSymNode(field)
result = field
var b = reopenObject(obj, cache, idgen)
result = b.captureUniqueField(s)
finishObject b
proc newDotExpr*(obj, b: PSym): PNode =
result = newNodeI(nkDotExpr, obj.info)
@@ -385,8 +434,9 @@ proc indirectAccess*(a, b: PSym, info: TLineInfo): PNode =
proc genAddrOf*(n: PNode; idgen: IdGenerator; typeKind = tyPtr): PNode =
result = newNodeI(nkAddr, n.info, 1)
result[0] = n
result.typ = newType(typeKind, idgen, n.typ.owner)
result.typ.rawAddSon(n.typ)
var b = openType(typeKind, idgen, n.typ.owner)
b.addRaw n.typ
result.typ = finish b
proc genDeref*(n: PNode; k = nkHiddenDeref): PNode =
result = newNodeIT(k, n.info,

View File

@@ -11,9 +11,10 @@
import
ast, msgs, platform, idents,
modulegraphs, lineinfos, types
modulegraphs, lineinfos, types, typebuilders
export createMagic
export typebuilders
proc nilOrSysInt*(g: ModuleGraph): PType = g.sysTypes[tyInt]
@@ -91,24 +92,13 @@ proc getFloatLitType*(g: ModuleGraph; literal: PNode): PType =
result = newSysType(g, tyFloat, size=8)
result.n = literal
proc skipIntLit*(t: PType; id: IdGenerator): PType {.inline.} =
if t.n != nil and t.kind in {tyInt, tyFloat}:
result = copyType(t, id, t.owner)
result.n = nil
else:
result = t
proc addSonSkipIntLit*(father, son: PType; id: IdGenerator) =
let s = son.skipIntLit(id)
father.add(s)
propagateToOwner(father, s)
proc makeVarType*(owner: PSym; baseType: PType; idgen: IdGenerator; kind = tyVar): PType =
if baseType.kind == kind:
result = baseType
else:
result = newType(kind, idgen, owner)
addSonSkipIntLit(result, baseType, idgen)
var b = openType(kind, idgen, owner)
b.add baseType
result = finish b
proc getCompilerProc*(g: ModuleGraph; name: string): PSym =
let ident = getIdent(g.cache, name)
@@ -158,8 +148,9 @@ proc getMagicEqSymForType*(g: ModuleGraph; t: PType; info: TLineInfo): PSym =
"can't find magic equals operator for type kind " & $t.kind)
proc makePtrType*(baseType: PType; idgen: IdGenerator): PType =
result = newType(tyPtr, idgen, baseType.owner)
addSonSkipIntLit(result, baseType, idgen)
var b = openType(tyPtr, idgen, baseType.owner)
b.add baseType
result = finish b
proc makeAddr*(n: PNode; idgen: IdGenerator): PNode =
if n.kind == nkHiddenAddr:

View File

@@ -34,7 +34,6 @@ from icconfig import produceIcConfig
when not defined(nimKochBootstrap):
import nifbackend
import deps
import idetools
when not defined(leanCompiler):
import docgen
@@ -417,20 +416,6 @@ proc mainCommand*(graph: ModuleGraph) =
for it in conf.searchPaths: msgWriteln(conf, it.string)
of cmdCheck:
commandCheck(graph)
of cmdTrack:
# `nim track --def:/--usages:/--track:` — IDE goto-definition / find-usages.
# Runs `nim ic`'s incremental frontend (nifler + per-module `nim m`, so only
# changed modules recompile and each writes a faithful, VM-executed `.s.bif`
# — covering stdlib too), then scans those NIF files (idetools.runIdeQuery).
# Shares the `nim ic` nimcache dir, so a prior `nim ic` build is reused.
setUseIc(true)
wantMainModule(conf)
setOutFile(conf)
when not defined(nimKochBootstrap):
commandIc(conf, frontendOnly = true)
runIdeQuery(conf)
else:
rawMessage(conf, errGenerated, "nim track not available in bootstrap build")
of cmdM:
# cmdM uses NIF files, not ROD files
graph.config.symbolFiles = disabledSf

View File

@@ -177,11 +177,6 @@ type
procGlobals*: seq[PNode]
nifReplayActions*: Table[int32, seq[PNode]] # module position -> replay actions for NIF
nifExpansions*: Table[int32, seq[(PSym, TLineInfo)]]
# module position -> (template/macro sym, call-site info) for every expansion
# in that module. Templates/macros leave no trace in the sem'checked AST, so
# this side-channel (written into the `.bif`, see ast2nif) is what lets
# `nim track --usages`/`--def` find them. Populated by `rememberExpansion`.
cachedMods: IntSet
hookClosure: IntSet # modules whose serialized hooks were already registered

154
compiler/nifcmain.nim Normal file
View File

@@ -0,0 +1,154 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Backend-only driver for `nim ic`'s C-generation stages (the `nifc` command:
## `--icBackendStage:lower|cg|merge|emit|link`). This produces the separate
## `bin/nifc` binary that `deps.nim` invokes per rule instead of re-entering the
## monolithic `nim` compiler.
##
## Crucially, it imports NEITHER `main`/`pipelines` NOR `cmdlinehelper`/`nimconf`.
## Those are the only two edges that pull the frontend semantic analyzer (`sem`)
## and the NimScript VM (`scriptconfig`) into the ordinary compiler binary. The
## backend graph (`nifbackend` -> `cgen`/`ast2nif`/`transf`/`injectdestructors`/
## `modulegraphs`) is entirely sem-free, and config is replayed sem-free from the
## precompiled `.cfg.nif` via `icconfig.applyIcConfig` (no file read, no VM run).
##
## Keeping `sem` out of the closure is the prerequisite for building this binary
## with `-d:nimBackend`, under which `astdef` swaps `PNode` to a cursor-backed
## value representation: `sem`'s pervasive `PNode(kind: ...)` literal construction
## could not compile against such a type, but it is no longer linked here.
import std/[os, parseopt, strutils]
when defined(nimPreviewSlimSystem):
import std/assertions
import
commands, options, msgs, extccomp, idents, lineinfos,
pathutils, modulegraphs, condsyms, platform, modules
import "../dist/checksums/src/checksums/sha1"
from ast import setUseIc
from ast2nif import registerNifAstTags
import icconfig
import nifbackend
proc hashMainCompilationParams(conf: ConfigRef): string =
## Mirrors `main.hashMainCompilationParams` (inlined to avoid importing `main`,
## which pulls in `pipelines`/`sem`).
var state = newSha1State()
state.update os.getAppFilename()
state.update conf.commandLine
state.update $conf.projectFull
result = $SecureHash(state.finalize())
proc setOutFile(conf: ConfigRef) =
## Mirrors `main.setOutFile` (inlined, same reason).
if conf.outFile.isEmpty:
var base = conf.projectName
if optUseNimcache in conf.globalOptions:
base.add "_" & hashMainCompilationParams(conf)
let targetName =
if optGenDynLib in conf.globalOptions:
platform.OS[conf.target.targetOS].dllFrmt % base
elif optGenStaticLib in conf.globalOptions:
(if conf.target.targetOS == osWindows: "$1.lib" else: "lib$1.a") % base
else: base & platform.OS[conf.target.targetOS].exeExt
conf.outFile = RelativeFile targetName
proc addCmdPrefix(result: var string, kind: CmdLineKind) =
case kind
of cmdLongOption: result.add "--"
of cmdShortOption: result.add "-"
of cmdArgument, cmdEnd: discard
proc processCmdLine(pass: TCmdLinePass, cmd: string; config: ConfigRef) =
## Slim copy of `nim.processCmdLine` (no nimble-lock probing, no stdin project):
## the `nifc` child is always launched by `deps.nim` with an explicit project
## NIF and forwarded switches.
var p = parseopt.initOptParser(cmd)
var argsCount = 0
config.commandLine.setLen 0
while true:
parseopt.next(p)
case p.kind
of cmdEnd: break
of cmdLongOption, cmdShortOption:
config.commandLine.add " "
config.commandLine.addCmdPrefix p.kind
config.commandLine.add p.key.quoteShell
if p.val.len > 0:
config.commandLine.add ':'
config.commandLine.add p.val.quoteShell
processSwitch(pass, p, config)
of cmdArgument:
config.commandLine.add " "
config.commandLine.add p.key.quoteShell
if processArgument(pass, p, argsCount, config): break
proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
# NIF tag registration must run before any NIF read/write, independent of
# module init order (see ast2nif.registerNifAstTags).
registerNifAstTags()
condsyms.initDefines(conf.symbols)
defineSymbol(conf.symbols, "nim_compiler")
if paramCount() == 0:
rawMessage(conf, errGenerated, "nifc: no arguments (expected a NIF project)")
return
# Pass 1: learn the command (`nifc`), the project NIF, and switches including
# `--icPreparsedConfig` (needed before config replay below).
processCmdLine(passCmd1, "", conf)
if conf.projectName != "":
setFromProjectName(conf, conf.projectName)
else:
conf.projectPath = AbsoluteDir canonicalizePath(conf, AbsoluteFile getCurrentDir())
var graph = newModuleGraph(cache, conf)
# Sem-free config: replay the precompiled `.cfg.nif` produced once by the
# `nim icconfig` process. No `nimconf`, no `scriptconfig`, no VM. A missing or
# format-incompatible artifact is fatal here (unlike the frontend, this binary
# has no fallback config parser on purpose).
setDefaultLibpath(conf)
if conf.icPreparsedConfig.len == 0 or not applyIcConfig(conf, conf.icPreparsedConfig):
rawMessage(conf, errGenerated,
"nifc backend requires a valid precompiled config (--icPreparsedConfig)")
return
if conf.backend != backendJs: extccomp.initVars(conf)
# Pass 2: command-line switches override the replayed config.
processCmdLine(passCmd2, "", conf)
if conf.selectedGC == gcUnselected:
initOrcDefines(conf)
if conf.cmd != cmdNifC:
rawMessage(conf, errGenerated, "nifc: only the 'nifc' command is supported")
return
# cmdNifC arm, mirroring `main.mainCommand`:
setUseIc(true)
excl conf.features, Feature.vtables
wantMainModule(conf)
setOutFile(conf)
# `main.commandNifC` body, inlined:
extccomp.initVars(conf)
if not extccomp.ccHasSaneOverflow(conf):
conf.symbols.defineSymbol("nimEmulateOverflowChecks")
nifbackend.generateCode(graph, conf.projectMainIdx)
when compileOption("gc", "refc"):
GC_disableMarkAndSweep()
let conf = newConfigRef()
handleCmdLine(newIdentCache(), conf)
msgQuit(int8(conf.errorCounter > 0))

View File

@@ -120,7 +120,7 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
# so `loadConfigs` replays it instead of re-parsing the `nim.cfg` chain — the
# driver runs on the exact same config its children will. See icconfig.nim.
when not defined(nimKochBootstrap):
if conf.cmd in {cmdIc, cmdTrack}:
if conf.cmd == cmdIc:
ensureIcConfig(conf)
var graph = newModuleGraph(cache, conf)
@@ -134,7 +134,7 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
if conf.selectedGC == gcUnselected:
if conf.backend in {backendC, backendCpp, backendObjc} or
(conf.cmd in cmdDocLike and conf.backend != backendJs) or
conf.cmd in {cmdGendepend, cmdNifC, cmdIc, cmdM, cmdTrack}:
conf.cmd in {cmdGendepend, cmdNifC, cmdIc, cmdM}:
initOrcDefines(conf)
if conf.selectedStrings == stringSso and

View File

@@ -29,7 +29,7 @@ const
nimEnableCovariance* = defined(nimEnableCovariance)
icFormatVersion* = "30"
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`
@@ -140,7 +140,6 @@ type # please make sure we have under 32 options
optDocRaw # for documentation: Don't render markdown for JSON output
optItaniumMangle # mangling follows the Itanium spec
optCompress # turn on AST compression by converting it to NIF
optGenBif # generate semantic BIF alongside ordinary code generation
optWithinConfigSystem # we still compile within the configuration system
TGlobalOptions* = set[TGlobalOption]
@@ -206,7 +205,6 @@ type
cmdNifC # generate C code from NIF files
cmdIc # generate .build.nif for nifmake
cmdIcConfig # `nim ic`'s precompiled-config producer (writes ic_config.cfg.nif)
cmdTrack # `nim track --def/--usages`: IC frontend build + NIF scan for IDE queries
const
cmdBackends* = {cmdCompileToC, cmdCompileToCpp, cmdCompileToOC,

View File

@@ -167,8 +167,7 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
s = stream
graph.interactive = stream.kind == llsStdIn
var topLevelStmts =
if {optCompress, optGenBif} * graph.config.globalOptions != {} or
graph.config.cmd == cmdM:
if optCompress in graph.config.globalOptions or graph.config.cmd == cmdM:
newNodeI(nkStmtList, module.info)
else:
nil
@@ -256,7 +255,7 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
graph.config.cmd == cmdM and graph.config.errorCounter == 0 and
graph.config.m.fileInfos[module.position].dirtyFile.isEmpty
else:
({optCompress, optGenBif} * graph.config.globalOptions != {}) or
(optCompress in graph.config.globalOptions) or
(graph.config.cmd == cmdM and
(sfMainModule in module.flags or
(graph.config.icGroup.len > 0 and
@@ -318,12 +317,9 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
# the backend seeds its id minting ABOVE this so closure envs / RTTI hooks
# never share a `toId` with a frontend sym/type. See ast2nif `(unusedid)`.
let firstUnusedId = max(idgen.symId, idgen.typeId)
var expansions: seq[(PSym, TLineInfo)] = @[]
discard graph.nifExpansions.take(module.position.int32, expansions)
writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog,
replayActions, implDeps, reexportedModuleSyms(graph, module),
genericOffers, typeOffers, resolvedImportDeps, firstUnusedId,
expansions)
genericOffers, typeOffers, resolvedImportDeps, firstUnusedId)
# The module's REAL direct imports (incl. macro-generated) for `nim ic`'s
# graph re-derivation; see ast2nif.writeSemDeps / semdata.addImportFileDep.
var semDepPaths: seq[string] = @[]

View File

@@ -108,16 +108,6 @@ proc fitNodePostMatch(c: PContext, formal: PType, arg: PNode): PNode =
markUsed(c, a.info, a[0].sym)
template isAutoReturnType(t: PType): bool =
# `auto` return types are copied and marked so they are not generic params.
t.kind == tyAnything and tfRetType in t.flags
template isUnresolvedAutoReturnType(c: PContext; t: PType): bool =
# During return-type inference a recursive call has the routine's exact
# `auto` placeholder type. It contributes no type information of its own.
c.p != nil and c.p.owner != nil and c.p.owner.typ != nil and
c.p.owner.typ.returnType == t and isAutoReturnType(t)
proc fitNode(c: PContext, formal: PType, arg: PNode; info: TLineInfo): PNode =
if arg.typ.isNil:
localError(c.config, arg.info, "expression has no type: " &
@@ -135,10 +125,6 @@ proc fitNode(c: PContext, formal: PType, arg: PNode; info: TLineInfo): PNode =
if sameType(ch.typ.skipTypes({tyVar, tyLent}), formal):
return ch
typeMismatch(c.config, info, formal, arg.typ, arg)
elif isUnresolvedAutoReturnType(c, arg.typ):
# A concrete sibling branch supplies the missing type for this branch.
result = arg
changeType(c, result, formal, check=true)
else:
result = indexTypesMatch(c, formal, arg.typ, arg)
if result == nil:
@@ -172,10 +158,8 @@ proc commonType*(c: PContext; x, y: PType): PType =
var a = skipTypes(x, {tyGenericInst, tyAlias, tySink})
var b = skipTypes(y, {tyGenericInst, tyAlias, tySink})
result = x
# Recursive calls cannot contribute to their own `auto` return type, so let
# the other branch determine the common type when it has concrete evidence.
if a.kind in {tyUntyped, tyNil} or isUnresolvedAutoReturnType(c, a): result = y
elif b.kind in {tyUntyped, tyNil} or isUnresolvedAutoReturnType(c, b): result = x
if a.kind in {tyUntyped, tyNil}: result = y
elif b.kind in {tyUntyped, tyNil}: result = x
elif a.kind == tyTyped: result = a
elif b.kind == tyTyped: result = b
elif a.kind == tyTypeDesc:
@@ -199,7 +183,8 @@ proc commonType*(c: PContext; x, y: PType): PType =
nt = copyType(a, c.idgen, a.owner)
copyTypeProps(c.graph, c.idgen.module, nt, a)
nt[i] = if aEmpty: bb else: aa
var ntb = reopen(nt)
ntb.setSon(i, if aEmpty: bb else: aa)
if not nt.isNil: result = nt
#elif b[idx].kind == tyEmpty: return x
elif a.kind == tyRange and b.kind == tyRange:
@@ -304,14 +289,6 @@ proc newSymG*(kind: TSymKind, n: PNode, c: PContext): PSym =
result = copySym(result)
result.ast = n.sym.ast
put(c.p, n.sym, result)
if result.state == Sealed:
# the symbol was loaded from another module's NIF cache (e.g. a param
# symbol spliced out of an imported proc type by a `typed` macro) and is
# therefore immutable; the caller re-owns it and assigns its type/flags,
# so hand back a fresh, mutable copy owned by the current module instead.
let fresh = copySym(result, c.idgen)
fresh.ast = result.ast
result = fresh
# when there is a nested proc inside a template, semtmpl
# will assign a wrong owner during the first pass over the
# template; we must fix it here: see #909
@@ -600,12 +577,10 @@ const
proc semMacroExpr(c: PContext, n, nOrig: PNode, sym: PSym,
flags: TExprFlags = {}; expectedType: PType = nil): PNode =
let info = getCallLineInfo(n)
# the callee identifier's position is the usage site tooling expects (matches
# `markUsed` below), not the whole-call `nOrig.info`.
rememberExpansion(c, info, sym)
rememberExpansion(c, nOrig.info, sym)
pushInfoContext(c.config, nOrig.info, sym.detailedInfo)
let info = getCallLineInfo(n)
markUsed(c, info, sym)
onUse(info, sym)
if sym == c.p.owner:

View File

@@ -924,11 +924,6 @@ proc semResolvedCall(c: PContext, x: var TCandidate,
result[0] = newSymNode(finalCallee, getCallLineInfo(result[0]))
if finalCallee.magic notin {mArrGet, mArrPut}:
result.typ = finalCallee.typ.returnType
# Remember that this body contains a self-call still sharing its unresolved
# `auto` placeholder; a later concrete return must resolve that placeholder.
if c.p != nil and result.typ != nil and finalCallee == c.p.owner and
isAutoReturnType(result.typ):
c.p.hasUnresolvedAutoCall = true
updateDefaultParams(c, result)
proc canDeref(n: PNode): bool {.inline.} =

View File

@@ -17,7 +17,9 @@ when defined(nimPreviewSlimSystem):
import
options, ast, msgs, idents, renderer,
magicsys, vmdef, modulegraphs, lineinfos, pathutils, layeredtable,
types, lowerings, trees, parampatterns, astalgo
types, lowerings, trees, parampatterns, astalgo, typebuilders
export typebuilders
type
TOptionEntry* = object # entries to put on a stack for pragma parsing
@@ -43,7 +45,6 @@ type
mapping*: SymMapping
caseContext*: seq[tuple[n: PNode, idx: int]]
localBindStmts*: seq[PNode]
hasUnresolvedAutoCall*: bool # a self-call still uses the `auto` return placeholder
TMatchedConcept* = object
candidateType*: PType
@@ -383,9 +384,8 @@ proc addImportFileDep*(c: PContext; f: FileIndex) =
if f notin deps[]: deps[].add f
proc addPragmaComputation*(c: PContext; n: PNode) =
# Also store whenever the semchecked module is serialized to NIF/BIF.
if {optCompress, optGenBif} * c.config.globalOptions != {} or
c.config.cmd == cmdM:
# Also store for NIF-based IC (cmdM mode or optCompress)
if optCompress in c.config.globalOptions or c.config.cmd == cmdM:
addNifReplayAction(c.graph, c.module.position.int32, n)
proc inclSym(sq: var seq[PSym], s: PSym): bool =
@@ -447,8 +447,14 @@ proc addToLib*(lib: PLib, sym: PSym) =
proc newTypeS*(kind: TTypeKind; c: PContext; son: sink PType = nil): PType =
result = newType(kind, c.idgen, getCurrOwner(c), son = son)
proc openType*(c: PContext; kind: TTypeKind): TypeBuilder {.inline.} =
## `PContext`-flavored `openType`: the type is owned by the current owner.
openType(kind, c.idgen, getCurrOwner(c))
proc makePtrType*(owner: PSym, baseType: PType; idgen: IdGenerator): PType =
result = newType(tyPtr, idgen, owner, skipIntLit(baseType, idgen))
var b = openType(tyPtr, idgen, owner)
b.addKeep skipIntLit(baseType, idgen) # son= fast path: skip int-lit, no propagate
result = finish b
proc makePtrType*(c: PContext, baseType: PType): PType =
makePtrType(getCurrOwner(c), baseType, c.idgen)
@@ -461,27 +467,33 @@ proc makeTypeWithModifier*(c: PContext,
if modifier in {tyVar, tyLent, tyTypeDesc} and baseType.kind == modifier:
result = baseType
else:
result = newTypeS(modifier, c, skipIntLit(baseType, c.idgen))
var b = openType(c, modifier)
b.addKeep skipIntLit(baseType, c.idgen)
result = finish b
proc makeVarType*(c: PContext, baseType: PType; kind = tyVar): PType =
if baseType.kind == kind:
result = baseType
else:
result = newTypeS(kind, c, skipIntLit(baseType, c.idgen))
var b = openType(c, kind)
b.addKeep skipIntLit(baseType, c.idgen)
result = finish b
proc makeTypeSymNode*(c: PContext, typ: PType, info: TLineInfo): PNode =
let typedesc = newTypeS(tyTypeDesc, c)
incl typedesc.flagsImpl, tfCheckedForDestructor
internalAssert(c.config, typ != nil)
typedesc.addSonSkipIntLit(typ, c.idgen)
var b = openType(c, tyTypeDesc)
b.incl tfCheckedForDestructor
b.add typ
let typedesc = finish b
let sym = newSym(skType, c.cache.idAnon, c.idgen, getCurrOwner(c), info,
c.config.options).linkTo(typedesc)
result = newSymNode(sym, info)
proc makeTypeFromExpr*(c: PContext, n: PNode): PType =
result = newTypeS(tyFromExpr, c)
assert n != nil
result.n = n
var b = openType(c, tyFromExpr)
b.setN n
result = finish b
when false:
proc newTypeWithSons*(owner: PSym, kind: TTypeKind, sons: seq[PType];
@@ -495,42 +507,49 @@ when false:
proc makeStaticExpr*(c: PContext, n: PNode): PNode =
result = newNodeI(nkStaticExpr, n.info)
result.sons = @[n]
result.typ = if n.typ != nil and n.typ.kind == tyStatic: n.typ
else: newTypeS(tyStatic, c, n.typ)
result.typ =
if n.typ != nil and n.typ.kind == tyStatic: n.typ
else:
var b = openType(c, tyStatic)
b.addKeep n.typ
finish b
proc makeAndType*(c: PContext, t1, t2: PType): PType =
result = newTypeS(tyAnd, c)
result.rawAddSon t1
result.rawAddSon t2
propagateToOwner(result, t1)
propagateToOwner(result, t2)
result.flagsImpl.incl((t1.flags + t2.flags) * {tfHasStatic})
result.flagsImpl.incl tfHasMeta
var b = openType(c, tyAnd)
b.addRaw t1
b.addRaw t2
b.propagateFrom t1
b.propagateFrom t2
b.incl((t1.flags + t2.flags) * {tfHasStatic})
b.incl tfHasMeta
result = finish b
proc makeOrType*(c: PContext, t1, t2: PType): PType =
var b = openType(c, tyOr)
if t1.kind != tyOr and t2.kind != tyOr:
result = newTypeS(tyOr, c)
result.rawAddSon t1
result.rawAddSon t2
b.addRaw t1
b.addRaw t2
else:
result = newTypeS(tyOr, c)
template addOr(t1) =
if t1.kind == tyOr:
for x in t1.kids: result.rawAddSon x
for x in t1.kids: b.addRaw x
else:
result.rawAddSon t1
b.addRaw t1
addOr(t1)
addOr(t2)
propagateToOwner(result, t1)
propagateToOwner(result, t2)
result.incl((t1.flags + t2.flags) * {tfHasStatic})
result.incl tfHasMeta
b.propagateFrom t1
b.propagateFrom t2
b.incl((t1.flags + t2.flags) * {tfHasStatic})
b.incl tfHasMeta
result = finish b
proc makeNotType*(c: PContext, t1: PType): PType =
result = newTypeS(tyNot, c, son = t1)
propagateToOwner(result, t1)
result.flagsImpl.incl(t1.flags * {tfHasStatic})
result.flagsImpl.incl tfHasMeta
var b = openType(c, tyNot)
b.addKeep t1
b.propagateFrom t1
b.incl(t1.flags * {tfHasStatic})
b.incl tfHasMeta
result = finish b
proc nMinusOne(c: PContext; n: PNode): PNode =
result = newTreeI(nkCall, n.info, newSymNode(getSysMagic(c.graph, n.info, "pred", mPred)), n)
@@ -538,19 +557,22 @@ proc nMinusOne(c: PContext; n: PNode): PNode =
# Remember to fix the procs below this one when you make changes!
proc makeRangeWithStaticExpr*(c: PContext, n: PNode): PType =
let intType = getSysType(c.graph, n.info, tyInt)
result = newTypeS(tyRange, c, son = intType)
var b = openType(c, tyRange)
b.addKeep intType
if n.typ != nil and n.typ.n == nil:
result.incl tfUnresolved
result.n = newTreeI(nkRange, n.info, newIntTypeNode(0, intType),
b.incl tfUnresolved
b.setN newTreeI(nkRange, n.info, newIntTypeNode(0, intType),
makeStaticExpr(c, nMinusOne(c, n)))
result = finish b
template rangeHasUnresolvedStatic*(t: PType): bool =
tfUnresolved in t.flags
proc errorType*(c: PContext): PType =
## creates a type representing an error state
result = newTypeS(tyError, c)
result.flagsImpl.incl tfCheckedForDestructor
var b = openType(c, tyError)
b.incl tfCheckedForDestructor
result = finish b
proc errorNode*(c: PContext, n: PNode): PNode =
result = newNodeI(nkEmpty, n.info)
@@ -587,9 +609,10 @@ proc makeRangeType*(c: PContext; first, last: BiggestInt;
var n = newNodeI(nkRange, info)
n.add newIntTypeNode(first, intType)
n.add newIntTypeNode(last, intType)
result = newTypeS(tyRange, c)
result.n = n
addSonSkipIntLit(result, intType, c.idgen) # basetype of range
var b = openType(c, tyRange)
b.setN n
b.add intType # basetype of range
result = finish b
proc isSelf*(t: PType): bool {.inline.} =
## Is this the magical 'Self' type from concepts?
@@ -599,8 +622,10 @@ proc makeTypeDesc*(c: PContext, typ: PType): PType =
if typ.kind == tyTypeDesc and not isSelf(typ):
result = typ
else:
result = newTypeS(tyTypeDesc, c, skipIntLit(typ, c.idgen))
incl result, tfCheckedForDestructor
var b = openType(c, tyTypeDesc)
b.addKeep skipIntLit(typ, c.idgen)
b.incl tfCheckedForDestructor
result = finish b
proc symFromType*(c: PContext; t: PType, info: TLineInfo): PSym =
if t.sym != nil: return t.sym
@@ -670,15 +695,7 @@ proc rememberExpansion*(c: PContext; info: TLineInfo; expandedSym: PSym) =
## ("find all usages of this template" would not work). We need special
## logic to remember macro/template expansions. This is done here and
## delegated to the "NIF" file mechanism.
##
## We only bother when a NIF file is actually going to be written (IC / `nim m`,
## `--compress`, semantic BIF output, or a running suggestion engine); a plain
## `nim c` throws the record away, so recording it would be pure overhead.
if info.fileIndex == InvalidFileIdx: return
if c.config.cmd == cmdM or
{optCompress, optGenBif} * c.config.globalOptions != {} or
c.config.ideActive:
c.graph.nifExpansions.mgetOrPut(c.module.position.int32, @[]).add (expandedSym, info)
discard "XXX To implement"
const
errVarForOutParamNeededX = "for a 'var' type a variable needs to be passed; but '$1' is immutable"

View File

@@ -26,15 +26,13 @@ const
proc semTemplateExpr(c: PContext, n: PNode, s: PSym,
flags: TExprFlags = {}; expectedType: PType = nil): PNode =
let info = getCallLineInfo(n)
# `info` (the callee identifier's position, not the whole call node) is what
# tooling wants to see as the usage site — matches `markUsed` below.
rememberExpansion(c, info, s)
rememberExpansion(c, n.info, s)
# IC: this expands `s`'s body into the current module's sem, so the module
# depends on that body — record a NeedsImpl (strong) edge to `s`'s module.
# The iface cookie hashes only signatures now, so a template body edit moves
# only the impl cookie, and just the modules that expanded it re-sem.
recordIcImplDep(c.graph, s)
let info = getCallLineInfo(n)
markUsed(c, info, s)
onUse(info, s)
# Note: This is n.info on purpose. It prevents template from creating an info
@@ -2125,15 +2123,6 @@ proc semAsgn(c: PContext, n: PNode; mode=asgnNormal): PNode =
internalAssert c.config, c.p.resultSym != nil
# Make sure the type is valid for the result variable
typeAllowedCheck(c, n.info, rhsTyp, skResult)
# Earlier self-calls retain the old placeholder pointer. Resolve it
# in place as an alias before the routine switches to the concrete
# type, so those already-typed calls see the inferred type too.
if c.p.hasUnresolvedAutoCall and not rhsTyp.isMetaType and
isAutoReturnType(lhs.sym.typ):
let resolved = newTypeS(tyAlias, c)
rawAddSon(resolved, rhsTyp)
assignType(lhs.sym.typ, resolved)
c.p.hasUnresolvedAutoCall = false
lhs.typ = rhsTyp
c.p.resultSym.typ = rhsTyp
c.p.owner.typ.setReturnType rhsTyp
@@ -2205,11 +2194,7 @@ proc semProcBody(c: PContext, n: PNode; expectedType: PType = nil): PNode =
" flags=", c.p.resultSym.typ.flags,
" uid=", c.p.resultSym.typ.uniqueId.module, ".", c.p.resultSym.typ.uniqueId.item,
" state=", c.p.resultSym.typ.state
# With no concrete return, the recursive placeholder is still circular.
if c.p.hasUnresolvedAutoCall:
localError(c.config, c.p.resultSym.info, errCannotInferReturnType %
c.p.owner.name.s)
elif isEmptyType(result.typ):
if isEmptyType(result.typ):
# we inferred a 'void' return type:
c.p.resultSym.typ = errorType(c)
c.p.owner.typ.setReturnType nil
@@ -2265,7 +2250,8 @@ proc semYield(c: PContext, n: PNode): PNode =
if resultTypeIsInferrable(restype):
let inferred = n[0].typ
iterType[0] = inferred
var b = reopen(iterType)
b.setSon(0, inferred)
if c.p.resultSym != nil:
c.p.resultSym.typ = inferred
else:

View File

@@ -24,7 +24,7 @@ when defined(nimPreviewSlimSystem):
proc errorType*(g: ModuleGraph): PType =
## creates a type representing an error state
result = newType(tyError, g.idgen, g.owners[^1])
result.flagsImpl.incl tfCheckedForDestructor
result.incl tfCheckedForDestructor
proc getIntLitTypeG(g: ModuleGraph; literal: PNode; idgen: IdGenerator): PType =
# we cache some common integer literal types for performance:

View File

@@ -436,7 +436,8 @@ proc semUnown(c: PContext; n: PNode): PNode =
result = copyType(t, c.idgen, t.owner)
copyTypeProps(c.graph, c.idgen.module, result, t)
result[^1] = b
var rb = reopen(result)
rb.setSon(^1, b)
result.excl tfHasOwned
else:
result = t

View File

@@ -1784,18 +1784,13 @@ proc setEffectsForProcType*(g: ModuleGraph; t: PType, n: PNode; s: PSym = nil) =
elif s != nil and (s.magic != mNone or {sfImportc, sfExportc} * s.flags == {sfImportc}):
effects[exceptionEffects] = newNodeI(nkArgList, effects.info)
let forbidsSpec = effectSpec(n, wForbids)
let tagsSpec = effectSpec(n, wTags)
if not isNil(tagsSpec):
effects[tagEffects] = tagsSpec
elif not isNil(forbidsSpec):
# `.forbids` without `.tags` still declares a known empty tag set.
# Leaving this as nil would mean "unknown tags", which later widens
# indirect calls to `RootEffect`.
effects[tagEffects] = newNodeI(nkArgList, effects.info)
elif s != nil and (s.magic != mNone or {sfImportc, sfExportc} * s.flags == {sfImportc}):
effects[tagEffects] = newNodeI(nkArgList, effects.info)
let forbidsSpec = effectSpec(n, wForbids)
if not isNil(forbidsSpec):
effects[forbiddenEffects] = forbidsSpec
elif s != nil and (s.magic != mNone or {sfImportc, sfExportc} * s.flags == {sfImportc}):

View File

@@ -1152,10 +1152,12 @@ proc semForVars(c: PContext, n: PNode; flags: TExprFlags): PNode =
case iter[i].kind
of tyVar:
mutable = true
iter[i] = iter[i].skipTypes({tyVar})
var b = reopen(iter)
b.setSon(i, iter[i].skipTypes({tyVar}))
of tyLent:
isLent = true
iter[i] = iter[i].skipTypes({tyLent})
var b = reopen(iter)
b.setSon(i, iter[i].skipTypes({tyLent}))
else: discard
if n[i].len-1 != iter[i].len:
@@ -1680,7 +1682,8 @@ proc typeSectionRightSidePass(c: PContext, n: PNode) =
# object might have been assumed to be final
if tfInheritable in oldFlags and tfFinal in body.flags:
excl(body, tfFinal)
s.typ[^1] = body
var b = reopen(s.typ)
b.setSon(^1, body)
if tfCovariant in s.typ.flags:
checkCovariantParamsUsages(c, s.typ)
# XXX: This is a temporary limitation:
@@ -2892,8 +2895,7 @@ proc incMod(c: PContext, n: PNode, it: PNode, includeStmtResult, resolvedIncStmt
proc evalInclude(c: PContext, n: PNode): PNode =
result = newNodeI(nkStmtList, n.info)
var resolvedIncStmt: PNode = nil
if {optCompress, optGenBif} * c.config.globalOptions != {} or
c.config.cmd == cmdM:
if optCompress in c.config.globalOptions or c.config.cmd == cmdM:
# New resolve the include filenames to string literals that contain absolute paths,
# nicer for IC:
resolvedIncStmt = newNodeI(nkIncludeStmt, n.info)

View File

@@ -44,6 +44,29 @@ proc reusePrev(prev: PType): bool {.inline.} =
# partial object marks sym as `sfForward`
(sfForward in prev.sym.flags or prev.sym.magic != mNone)))
proc openType(c: PContext; kind: TTypeKind; prev: PType): TypeBuilder =
## Prev-aware `openType`. This folds the identity decision into one place: for
## a forward-declared / partial `prev` the reserved *name* is kept and its
## *tree structure* is rebuilt in place; otherwise a fresh type (and identity)
## is minted -- at the same sequence point as `newTypeS`, so type ids stay
## byte-identical. Callers become the uniform "build structure, then `finish`".
if reusePrev(prev):
if prev.kind == tyForward: prev.kind = kind
result = reopen(prev, c.idgen)
else:
result = openType(c, kind)
proc openPair(c: PContext; kind: TTypeKind; prev: PType): TypePairBuilder =
## Prev-aware deferred (`TypePair`) open -- the deferred analogue of the
## prev-aware `openType` above, for types whose identity is published before
## their body is finished. Keeps a forward/partial `prev`'s reserved name,
## else mints a fresh identity at the same sequence point as `newTypeS`.
if reusePrev(prev):
if prev.kind == tyForward: prev.kind = kind
result = reopenPair(prev, c.idgen)
else:
result = openPair(kind, c.idgen, getCurrOwner(c))
proc newOrPrevType(kind: TTypeKind, prev: PType, c: PContext, son: sink PType): PType =
if reusePrev(prev):
result = prev
@@ -54,16 +77,15 @@ proc newOrPrevType(kind: TTypeKind, prev: PType, c: PContext, son: sink PType):
#if kind == tyError: result.flags.incl tfCheckedForDestructor
proc newOrPrevType(kind: TTypeKind, prev: PType, c: PContext): PType =
if reusePrev(prev):
result = prev
if result.kind == tyForward: result.kind = kind
else:
result = newTypeS(kind, c)
# centralized on the `openType` chokepoint: keep the reserved name of a
# forward/partial `prev`, else mint a fresh identity.
finish openType(c, kind, prev)
proc newConstraint(c: PContext, k: TTypeKind): PType =
result = newTypeS(tyBuiltInTypeClass, c)
result.incl tfCheckedForDestructor
result.addSonSkipIntLit(newTypeS(k, c), c.idgen)
var b = openType(c, tyBuiltInTypeClass)
b.incl tfCheckedForDestructor
b.add newTypeS(k, c)
result = finish b
proc skipGenericPrev(prev: PType): PType =
result = prev
@@ -98,15 +120,19 @@ proc semEnum(c: PContext, n: PNode, prev: PType): PType =
counterSet = initPackedSet[BiggestInt]()
counter = 0
base = nil
result = newOrPrevType(tyEnum, prev, c)
result.n = newNodeI(nkEnumTy, n.info)
var b = openType(c, tyEnum, prev)
b.setN newNodeI(nkEnumTy, n.info)
checkMinSonsLen(n, 1, c.config)
if n[0].kind != nkEmpty:
base = semTypeNode(c, n[0][0], nil)
if base.kind != tyEnum:
localError(c.config, n[0].info, "inheritance only works with an enum")
counter = toInt64(lastOrd(c.config, base)) + 1
rawAddSon(result, base)
b.addRaw base
result = finish b
# the type each enum field belongs to; a field referring to its own enum is
# the canonical self-reference that will migrate to an id-based `TypePair.id`.
let self = typePair(result)
let isPure = result.sym != nil and sfPure in result.sym.flags
var symbols: TStrTable = initStrTable()
var hasNull = false
@@ -176,7 +202,7 @@ proc semEnum(c: PContext, n: PNode, prev: PType): PType =
elif counterSet.containsOrIncl(counter):
localError(c.config, n[i].info, errDuplicateAliasInEnumX % e.name.s)
e.typ = result
e.typ = self.decl
e.position = int(counter)
let symNode = newSymNode(e)
if identToReplace != nil and c.config.cmd notin cmdDocLike:
@@ -216,10 +242,11 @@ proc semEnum(c: PContext, n: PNode, prev: PType): PType =
setToStringProc(c.graph, result, genEnumToStrProc(result, n.info, c.graph, c.idgen))
proc semSet(c: PContext, n: PNode, prev: PType): PType =
result = newOrPrevType(tySet, prev, c)
var b = openType(c, tySet, prev)
if n.len == 2 and n[1].kind != nkEmpty:
var base = semTypeNode(c, n[1], nil)
addSonSkipIntLit(result, base, c.idgen)
b.add base
result = finish b
if base.kind in {tyGenericInst, tyAlias, tySink}: base = skipModifier(base)
if base.kind notin {tyGenericParam, tyGenericInvocation}:
if base.kind == tyForward:
@@ -230,45 +257,49 @@ proc semSet(c: PContext, n: PNode, prev: PType): PType =
localError(c.config, n.info, errSetTooBig)
else:
localError(c.config, n.info, errXExpectsOneTypeParam % "set")
addSonSkipIntLit(result, errorType(c), c.idgen)
b.add errorType(c)
result = finish b
proc semContainerArg(c: PContext; n: PNode, kindStr: string; result: PType) =
proc semContainerArg(c: PContext; n: PNode, kindStr: string; b: var TypeBuilder) =
if n.len == 2:
var base = semTypeNode(c, n[1], nil)
if base.kind == tyVoid:
localError(c.config, n.info, errTIsNotAConcreteType % typeToString(base))
addSonSkipIntLit(result, base, c.idgen)
b.add base
else:
localError(c.config, n.info, errXExpectsOneTypeParam % kindStr)
addSonSkipIntLit(result, errorType(c), c.idgen)
b.add errorType(c)
proc semContainer(c: PContext, n: PNode, kind: TTypeKind, kindStr: string,
prev: PType): PType =
result = newOrPrevType(kind, prev, c)
semContainerArg(c, n, kindStr, result)
var b = openType(c, kind, prev)
semContainerArg(c, n, kindStr, b)
result = finish b
proc semVarargs(c: PContext, n: PNode, prev: PType): PType =
result = newOrPrevType(tyVarargs, prev, c)
var b = openType(c, tyVarargs, prev)
if n.len == 2 or n.len == 3:
var base = semTypeNode(c, n[1], nil)
addSonSkipIntLit(result, base, c.idgen)
b.add base
if n.len == 3:
result.n = newIdentNode(considerQuotedIdent(c, n[2]), n[2].info)
b.setN newIdentNode(considerQuotedIdent(c, n[2]), n[2].info)
else:
localError(c.config, n.info, errXExpectsOneTypeParam % "varargs")
addSonSkipIntLit(result, errorType(c), c.idgen)
b.add errorType(c)
result = finish b
proc semVarOutType(c: PContext, n: PNode, prev: PType; flags: TTypeFlags): PType =
if n.len == 1:
result = newOrPrevType(tyVar, prev, c)
result.flags = flags
var b = openType(c, tyVar, prev)
b.setFlags flags
var base = semTypeNode(c, n[0], nil)
if base.kind == tyTypeDesc and not isSelf(base):
base = base[0]
if base.kind == tyVar:
localError(c.config, n.info, "type 'var var' is not allowed")
base = base[0]
addSonSkipIntLit(result, base, c.idgen)
b.add base
result = finish b
else:
result = newConstraint(c, tyVar)
@@ -379,31 +410,38 @@ proc isRecursiveType*(t: PType): bool =
var cycleDetector = initIntSet()
isRecursiveType(t, cycleDetector)
proc addSonSkipIntLitChecked(c: PContext; father, son: PType; it: PNode, id: IdGenerator) =
let s = son.skipIntLit(id)
father.add(s)
proc addSonSkipIntLitChecked(c: PContext; b: var TypeBuilder; son: PType; it: PNode) =
let s = son.skipIntLit(c.idgen)
b.addKeep s
if isRecursiveType(s):
localError(c.config, it.info, "illegal recursion in type '" & typeToString(s) & "'")
else:
propagateToOwner(father, s)
b.propagateFrom s
proc semDistinct(c: PContext, n: PNode, prev: PType): PType =
if n.len == 0: return newConstraint(c, tyDistinct)
if prevIsKind(prev, tyDistinct):
# the symbol already has a distinct type (likely resem), don't create a new type
return skipGenericPrev(prev)
result = newOrPrevType(tyDistinct, prev, c)
addSonSkipIntLitChecked(c, result, semTypeNode(c, n[0], nil), n[0], c.idgen)
if n.len > 1: result.n = n[1]
var b = openType(c, tyDistinct, prev)
addSonSkipIntLitChecked(c, b, semTypeNode(c, n[0], nil), n[0])
if n.len > 1: b.setN n[1]
result = finish b
proc semRangeAux(c: PContext, n: PNode, prev: PType): PType =
assert isRange(n)
checkSonsLen(n, 3, c.config)
result = newOrPrevType(tyRange, prev, c)
result.n = newNodeI(nkRange, n.info)
# Deferred build: a *valid* tyRange must exist before the throwing
# `semExprWithType` below (bug #6895), so its base type is minted as an
# `errorType` placeholder son up front and back-patched via `setSon(0, …)`
# once the real bounds are known. The `.n` (nkRange bound exprs) and flags
# stay direct pokes on the live shell, as in `semProcTypeNode`.
var rb = openPair(c, tyRange, prev)
rb.setN newNodeI(nkRange, n.info)
# always create a 'valid' range type, but overwrite it later
# because 'semExprWithType' can raise an exception. See bug #6895.
addSonSkipIntLit(result, errorType(c), c.idgen)
rb.add errorType(c)
result = rb.pair.decl
if (n[1].kind == nkEmpty) or (n[2].kind == nkEmpty):
localError(c.config, n.info, "range is empty")
@@ -444,7 +482,9 @@ proc semRangeAux(c: PContext, n: PNode, prev: PType): PType =
if weakLeValue(result.n[0], result.n[1]) == impNo:
localError(c.config, n.info, "range is empty")
result[0] = rangeT[0]
# overwrite the placeholder son minted above with the real base type, then seal
rb.setSon(0, rangeT[0])
result = finishPair(rb).decl
proc semRange(c: PContext, n: PNode, prev: PType): PType =
result = nil
@@ -557,29 +597,33 @@ proc semArray(c: PContext, n: PNode, prev: PType): PType =
# ensure we only construct a tyArray when there was no error (bug #3048):
# bug #6682: Do not propagate initialization requirements etc for the
# index type:
result = newOrPrevType(tyArray, prev, c, indx)
addSonSkipIntLit(result, base, c.idgen)
var b = openType(c, tyArray, prev)
b.addKeep indx
b.add base
result = finish b
else:
localError(c.config, n.info, errArrayExpectsTwoTypeParams)
result = newOrPrevType(tyError, prev, c)
proc semIterableType(c: PContext, n: PNode, prev: PType): PType =
result = newOrPrevType(tyIterable, prev, c)
var b = openType(c, tyIterable, prev)
if n.len == 2:
let base = semTypeNode(c, n[1], nil)
addSonSkipIntLit(result, base, c.idgen)
b.add base
result = finish b
else:
localError(c.config, n.info, errXExpectsOneTypeParam % "iterable")
result = newOrPrevType(tyError, prev, c)
proc semOrdinal(c: PContext, n: PNode, prev: PType): PType =
result = newOrPrevType(tyOrdinal, prev, c)
var b = openType(c, tyOrdinal, prev)
if n.len == 2:
var base = semTypeNode(c, n[1], nil)
if base.kind != tyGenericParam:
if not isOrdinalType(base):
localError(c.config, n[1].info, errOrdinalTypeExpected % typeToString(base, preferDesc))
addSonSkipIntLit(result, base, c.idgen)
b.add base
result = finish b
else:
localError(c.config, n.info, errXExpectsOneTypeParam % "ordinal")
result = newOrPrevType(tyError, prev, c)
@@ -587,10 +631,11 @@ proc semOrdinal(c: PContext, n: PNode, prev: PType): PType =
proc semAnonTuple(c: PContext, n: PNode, prev: PType): PType =
if n.len == 0:
localError(c.config, n.info, errTypeExpected)
result = newOrPrevType(tyTuple, prev, c)
var b = openType(c, tyTuple, prev)
for it in n:
let t = semTypeNode(c, it, nil)
addSonSkipIntLitChecked(c, result, t, it, c.idgen)
addSonSkipIntLitChecked(c, b, t, it)
result = finish b
proc firstRange(config: ConfigRef, t: PType): PNode =
if t.skipModifier().kind in tyFloat..tyFloat64:
@@ -601,8 +646,12 @@ proc firstRange(config: ConfigRef, t: PType): PNode =
proc semTuple(c: PContext, n: PNode, prev: PType): PType =
var typ: PType
result = newOrPrevType(tyTuple, prev, c)
result.n = newNodeI(nkRecList, n.info)
# Deferred build: the tuple's identity is handed to `semFieldDefault` (which
# propagates each default field's type into the owner) while its fields/sons
# are still being appended -- so it goes through `TypePairBuilder`, publishing
# `rb.pair` mid-build rather than `openType ... finish`.
var rb = openPair(c, tyTuple, prev)
rb.setN newNodeI(nkRecList, n.info)
var check = initIntSet()
var counter = 0
for i in ord(n.kind == nkBracketExpr)..<n.len:
@@ -612,7 +661,7 @@ proc semTuple(c: PContext, n: PNode, prev: PType): PType =
var hasDefaultField = a[^1].kind != nkEmpty
if hasDefaultField:
typ = if a[^2].kind != nkEmpty: semTypeNode(c, a[^2], nil) else: nil
typ = semFieldDefault(c, result, typ, a)
typ = semFieldDefault(c, rb.pair.decl, typ, a)
elif a[^2].kind != nkEmpty:
typ = semTypeNode(c, a[^2], nil)
if c.graph.config.isDefined("nimPreviewRangeDefault") and typ.skipTypes(abstractInst).kind == tyRange:
@@ -633,11 +682,12 @@ proc semTuple(c: PContext, n: PNode, prev: PType): PType =
if hasDefaultField:
fSym.sym.ast = a[^1]
fSym.sym.ast.flags.incl nfSkipFieldChecking
result.n.add fSym
addSonSkipIntLit(result, typ, c.idgen)
rb.addRecField fSym
rb.add typ
styleCheckDef(c, a[j].info, field)
onDef(field.info, field)
if result.n.len == 0: result.n = nil
if rb.pair.decl.n.len == 0: rb.setN nil
result = finishPair(rb).decl
if isRecursiveStructuralType(result):
localError(c.config, n.info, errIllegalRecursionInTypeX % typeToString(result))
@@ -1117,17 +1167,24 @@ proc semObjectNode(c: PContext, n: PNode, prev: PType; flags: TTypeFlags): PType
base = nil
realBase = nil
if n.kind != nkObjectTy: internalError(c.config, n.info, "semObjectNode")
result = newOrPrevType(tyObject, prev, c)
# Deferred build: the object's identity is published to `forwardTypeUpdates`
# (a retry pass), to `semRecordNodeAux` (field sem may reference the object
# itself), and to the pragma dummy sym -- all before its body is complete. The
# son-tree (base son) + initial `.n` (nkRecList) allocation + seal go through
# the builder; field growth (via `semRecordNodeAux` into `result.n`) and flags
# stay direct pokes on the live shell, as in `semProcTypeNode`/`semRangeAux`.
var rb = openPair(c, tyObject, prev)
result = rb.pair.decl
if needsForwardUpdate:
# if the inherited object is a forward type,
# the entire object needs to be checked again
c.forwardTypeUpdates.add (getCurrOwner(c), result, n) # we retry in the final pass
rawAddSon(result, realBase)
rb.addRaw realBase
if realBase == nil and tfInheritable in flags:
result.incl tfInheritable
if tfAcyclic in flags: result.incl tfAcyclic
if result.n.isNil:
result.n = newNodeI(nkRecList, n.info)
rb.setN newNodeI(nkRecList, n.info)
else:
# partial object so add things to the check
if not tryAddInheritedFields(c, check, pos, result, n, isPartial = true):
@@ -1143,6 +1200,7 @@ proc semObjectNode(c: PContext, n: PNode, prev: PType; flags: TTypeFlags): PType
incl(result, tfFinal)
if c.inGenericContext == 0 and computeRequiresInit(c, result):
result.incl tfRequiresInit
result = finishPair(rb).decl # seal the deferred object build
proc semAnyRef(c: PContext; n: PNode; kind: TTypeKind; prev: PType): PType =
if n.len < 1:
@@ -1163,7 +1221,7 @@ proc semAnyRef(c: PContext; n: PNode; kind: TTypeKind; prev: PType): PType =
t = t.base
if t.kind == tyVoid:
localError(c.config, n.info, "type '$1 void' is not allowed" % kind.toHumanStr)
result = newOrPrevType(kind, prev, c)
var b = openType(c, kind, prev)
var isNilable = false
var wrapperKind = tyNone
# check every except the last is an object:
@@ -1179,23 +1237,26 @@ proc semAnyRef(c: PContext; n: PNode; kind: TTypeKind; prev: PType): PType =
elif region.skipTypes({tyGenericInst, tyAlias, tySink}).kind notin {
tyError, tyObject}:
message c.config, n[i].info, errGenerated, "region needs to be an object type"
addSonSkipIntLit(result, region, c.idgen)
b.add region
else:
message(c.config, n.info, warnDeprecated, "region for pointer types is deprecated")
addSonSkipIntLit(result, region, c.idgen)
addSonSkipIntLit(result, t, c.idgen)
b.add region
b.add t
result = finish b
if tfPartial in result.flags:
if result.elementType.kind == tyObject: incl(result.elementType, tfPartial)
# if not isNilable: result.flags.incl tfNotNil
case wrapperKind
of tyOwned:
if optOwnedRefs in c.config.globalOptions:
let t = newTypeS(tyOwned, c, result)
t.incl tfHasOwned
result = t
var wrap = openType(c, tyOwned)
wrap.addKeep result
wrap.incl tfHasOwned
result = finish wrap
of tySink:
let t = newTypeS(tySink, c, result)
result = t
var wrap = openType(c, tySink)
wrap.addKeep result
result = finish wrap
else: discard
if result.kind == tyRef and
c.config.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcYrc} and
@@ -1299,7 +1360,9 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
let base = (if lifted != nil: lifted else: paramType.base)
if base.isMetaType and procKind == skMacro:
localError(c.config, info, errMacroBodyDependsOnGenericTypes % paramName)
result = addImplicitGeneric(c, newTypeS(tyStatic, c, base),
var b = openType(c, tyStatic)
b.addKeep base
result = addImplicitGeneric(c, finish b,
paramTypId, info, genericParams, paramName)
if result != nil: result.incl({tfHasStatic, tfUnresolved})
@@ -1311,9 +1374,10 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
paramTypId.id == getIdent(c.cache, "type").id):
# XXX Why doesn't this check for tyTypeDesc instead?
paramTypId = nil
let t = newTypeS(tyTypeDesc, c, paramType.base)
incl t, tfCheckedForDestructor
result = addImplicitGeneric(c, t, paramTypId, info, genericParams, paramName)
var b = openType(c, tyTypeDesc)
b.addKeep paramType.base
b.incl tfCheckedForDestructor
result = addImplicitGeneric(c, finish b, paramTypId, info, genericParams, paramName)
else:
result = nil
of tyDistinct:
@@ -1327,7 +1391,8 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
for i in 0..<paramType.len:
let t = recurse(paramType[i])
if t != nil:
paramType[i] = t
var b = reopen(paramType)
b.setSon(i, t)
result = paramType
of tyAlias, tyOwned:
@@ -1342,9 +1407,12 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
# Maybe there is another better place to associate
# the seq type class with the seq identifier.
if paramType.kind == tySequence and paramType.elementType.kind == tyNone:
let typ = newTypeS(tyBuiltInTypeClass, c,
newTypeS(paramType.kind, c))
result = addImplicitGeneric(c, typ, paramTypId, info, genericParams, paramName)
# allocate the inner son first so the type-id order matches the old
# argument-evaluation order (inner before outer).
let inner = newTypeS(paramType.kind, c)
var b = openType(c, tyBuiltInTypeClass)
b.addKeep inner
result = addImplicitGeneric(c, finish b, paramTypId, info, genericParams, paramName)
else:
result = nil
for i in 0..<paramType.len:
@@ -1352,32 +1420,38 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
globalError(c.config, info, errIllegalRecursionInTypeX % typeToString(paramType))
var lifted = recurse(paramType[i])
if lifted != nil:
paramType[i] = lifted
var b = reopen(paramType)
b.setSon(i, lifted)
result = paramType
of tyGenericBody:
result = newTypeS(tyGenericInvocation, c)
result.rawAddSon(paramType)
# A user-type-class body instantiates to a tyUserTypeClassInst, everything
# else to a tyGenericInvocation. The kind is decided up front (from the
# already-complete `paramType`), so the builder opens with the final tag
# rather than the old mint-as-invocation-then-mutate-kind dance.
let isUserTypeClass = paramType.typeBodyImpl.kind == tyUserTypeClass
var b = openType(c, if isUserTypeClass: tyUserTypeClassInst else: tyGenericInvocation)
b.addRaw paramType
for i in 0..<paramType.len - 1:
if paramType[i].kind == tyStatic:
var staticCopy = paramType[i].exactReplica(c.idgen)
staticCopy.incl tfInferrableStatic
result.rawAddSon staticCopy
b.addRaw staticCopy
else:
result.rawAddSon newTypeS(tyAnything, c)
b.addRaw newTypeS(tyAnything, c)
if paramType.typeBodyImpl.kind == tyUserTypeClass:
result.kind = tyUserTypeClassInst
result.rawAddSon paramType.typeBodyImpl
return addImplicitGeneric(c, result, paramTypId, info, genericParams, paramName)
if isUserTypeClass:
b.addRaw paramType.typeBodyImpl
return addImplicitGeneric(c, finish b, paramTypId, info, genericParams, paramName)
result = finish b
let x = instGenericContainer(c, paramType.sym.info, result,
allowMetaTypes = true)
result = newTypeS(tyCompositeTypeClass, c)
result.rawAddSon paramType
result.rawAddSon x
result = addImplicitGeneric(c, result, paramTypId, info, genericParams, paramName)
var cb = openType(c, tyCompositeTypeClass)
cb.addRaw paramType
cb.addRaw x
result = addImplicitGeneric(c, finish cb, paramTypId, info, genericParams, paramName)
of tyGenericInst:
result = nil
@@ -1391,7 +1465,8 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
for i in 1..<paramType.len-1:
var lifted = recurse(paramType[i])
if lifted != nil:
paramType[i] = lifted
var b = reopen(paramType)
b.setSon(i, lifted)
result = paramType
result.last.shouldHaveMeta
if paramType.isConcept:
@@ -1408,7 +1483,9 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
for i in 1..<paramType.len:
#if paramType[i].kind != tyTypeDesc:
let lifted = recurse(paramType[i])
if lifted != nil: paramType[i] = lifted
if lifted != nil:
var b = reopen(paramType)
b.setSon(i, lifted)
let body = paramType.base
if body.kind in {tyForward, tyError}:
@@ -1469,7 +1546,14 @@ proc semProcTypeNode(c: PContext, n, genericParams: PNode,
# for historical reasons (code grows) this is invoked for parameter
# lists too and then 'isType' is false.
checkMinSonsLen(n, 1, c.config)
# Deferred build: `newProcType` opens the shell with a nil return-type slot
# (son 0) and an effect-list `.n`; params are appended as interleaved son +
# `.n` entries below, and son 0 is back-patched once the return type is known.
# `openType ... finish` cannot model the placeholder-then-backpatch, so the
# son tree grows through a `TypePairBuilder` reopened on the shell. Flags and
# `.n.typ` remain direct pokes on the live shell (`result` == `rb.pair.decl`).
result = newProcType(c, n.info, prev)
var rb = reopenPair(result, c.idgen)
var check = initIntSet()
var counter = 0
template isCurrentlyGeneric: bool =
@@ -1553,8 +1637,11 @@ proc semProcTypeNode(c: PContext, n, genericParams: PNode,
# which will prevent other types from matching - clearly a very
# surprising behavior. We must instead fix the expected type of
# the proc to be the unbound typedesc type:
typ = newTypeS(tyTypeDesc, c, newTypeS(tyNone, c))
typ.incl tfCheckedForDestructor
let none = newTypeS(tyNone, c)
var b = openType(c, tyTypeDesc)
b.addKeep none
b.incl tfCheckedForDestructor
typ = finish b
elif def.typ != nil and def.typ.kind != tyFromExpr: # def.typ can be void
# if def.typ != nil and def.typ.kind != tyNone:
@@ -1604,8 +1691,8 @@ proc semProcTypeNode(c: PContext, n, genericParams: PNode,
inc(counter)
if def != nil and def.kind != nkEmpty:
arg.ast = copyTree(def)
result.n.add newSymNode(arg)
rawAddSon(result, finalType)
rb.addRecField newSymNode(arg)
rb.addRaw finalType
addParamOrResult(c, arg, kind)
styleCheckDef(c, a[j].info, arg)
onDef(a[j].info, arg)
@@ -1665,7 +1752,7 @@ proc semProcTypeNode(c: PContext, n, genericParams: PNode,
# we don't need to change the return type to iter[T]
result.incl tfIterator
# XXX Would be nice if we could get rid of this
result[0] = r
rb.setSon(0, r)
let oldFlags = result.flags
propagateToOwner(result, r)
if oldFlags != result.flags:
@@ -1683,6 +1770,8 @@ proc semProcTypeNode(c: PContext, n, genericParams: PNode,
n.sym.transitionGenericParamToType()
n.sym.typ.excl tfWildcard
result = finishPair(rb).decl # seal the deferred proc-type build
proc semStmtListType(c: PContext, n: PNode, prev: PType): PType =
checkMinSonsLen(n, 1, c.config)
for i in 0..<n.len - 1:
@@ -1751,24 +1840,32 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
var t = s.typ.skipTypes({tyAlias})
if t.kind == tyCompositeTypeClass and t.base.kind == tyGenericBody:
t = t.base
result = newOrPrevType(tyGenericInvocation, prev, c)
addSonSkipIntLit(result, t, c.idgen)
# Deferred build: the tyGenericInvocation's identity is published to
# `forwardTypeUpdates` (a retry pass) and consumed by `instGenericContainer`,
# both only after its arg sons are appended. The son-tree goes through one
# deferred `rb`; `result` stays the live shell (later branches may replace it
# with an error/forward type or the instantiated container). Sealed once the
# args are in, before any consumer reads it.
var rb = openPair(c, tyGenericInvocation, prev)
result = rb.pair.decl
rb.add t
template addToResult(typ, skip) =
if typ.isNil:
internalAssert c.config, false
rawAddSon(result, typ)
rb.addRaw typ
else:
if skip:
addSonSkipIntLit(result, typ, c.idgen)
rb.add typ
else:
rawAddSon(result, makeRangeWithStaticExpr(c, typ.n))
rb.addRaw makeRangeWithStaticExpr(c, typ.n)
if t.kind == tyForward:
for i in 1..<n.len:
var elem = semGenericParamInInvocation(c, n[i])
addToResult(elem, true)
result = finishPair(rb).decl # seal the deferred invocation build
c.forwardTypeUpdates.add (getCurrOwner(c), result, n)
return
elif t.kind != tyGenericBody:
@@ -1816,6 +1913,8 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
if typ.kind == tyForward:
hasForwardTypeParam = true
result = finishPair(rb).decl # seal the deferred invocation build (args complete)
if isConcrete:
if s.ast == nil and s.typ.kind != tyCompositeTypeClass:
# XXX: What kind of error is this? is it still relevant?
@@ -1870,9 +1969,10 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
proc maybeAliasType(c: PContext; typeExpr, prev: PType): PType =
if prev != nil and (prev.kind == tyGenericBody or
typeExpr.kind in {tyObject, tyEnum, tyDistinct, tyForward, tyGenericBody}):
result = newTypeS(tyAlias, c)
result.rawAddSon typeExpr
result.sym = prev.sym
var b = openType(c, tyAlias)
b.addRaw typeExpr
b.setSym prev.sym
result = finish b
if prev.kind != tyGenericBody:
assignType(prev, result)
else:
@@ -1880,9 +1980,10 @@ proc maybeAliasType(c: PContext; typeExpr, prev: PType): PType =
proc fixupTypeOf(c: PContext, prev: PType, typ: PType) =
if prev != nil:
let result = newTypeS(tyAlias, c)
result.rawAddSon typ
result.sym = prev.sym
var b = openType(c, tyAlias)
b.addRaw typ
b.setSym prev.sym
let result = finish b
if prev.kind != tyGenericBody:
assignType(prev, result)
@@ -1950,7 +2051,9 @@ proc semTypeClass(c: PContext, n: PNode, prev: PType): PType =
inherited = n[2]
var owner = getCurrOwner(c)
var candidateTypeSlot = newTypeS(tyAlias, c, c.errorType)
var slotB = openType(c, tyAlias)
slotB.addKeep c.errorType
var candidateTypeSlot = finish slotB
result = newOrPrevType(tyUserTypeClass, prev, c, son = candidateTypeSlot)
result.incl tfCheckedForDestructor
result.n = n
@@ -2070,10 +2173,11 @@ proc symFromExpectedTypeNode(c: PContext, n: PNode): PSym =
result = errorSym(c, n)
proc semStaticType(c: PContext, childNode: PNode, prev: PType): PType =
result = newOrPrevType(tyStatic, prev, c)
var b = openType(c, tyStatic, prev)
var base = semTypeNode(c, childNode, nil).skipTypes({tyTypeDesc, tyAlias})
result.rawAddSon(base)
result.incl tfHasStatic
b.addRaw base
b.incl tfHasStatic
result = finish b
proc semTypeOfImpl(c: PContext; n: PNode): PNode =
var m = BiggestInt 1 # typeOfIter
@@ -2397,20 +2501,23 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
let old = result
result = copyType(result, c.idgen, getCurrOwner(c))
copyTypeProps(c.graph, c.idgen.module, result, old)
var b = reopen(result, c.idgen)
for i in 1..<n.len:
result.rawAddSon(semTypeNode(c, n[i], nil))
b.addRaw(semTypeNode(c, n[i], nil))
of mDistinct:
checkSonsLen(n, 2, c.config)
result = newOrPrevType(tyDistinct, prev, c)
addSonSkipIntLit(result, semTypeNode(c, n[1], nil), c.idgen)
var b = openType(c, tyDistinct, prev)
b.add semTypeNode(c, n[1], nil)
result = finish b
of mVar:
checkSonsLen(n, 2, c.config)
result = newOrPrevType(tyVar, prev, c)
var b = openType(c, tyVar, prev)
var base = semTypeNode(c, n[1], nil)
if base.kind in {tyVar, tyLent}:
localError(c.config, n.info, "type 'var var' is not allowed")
base = base[0]
addSonSkipIntLit(result, base, c.idgen)
b.add base
result = finish b
of mRef: result = semAnyRef(c, n, tyRef, prev)
of mPtr: result = semAnyRef(c, n, tyPtr, prev)
of mTuple: result = semTuple(c, n, prev)
@@ -2506,7 +2613,7 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
of nkProcTy, nkIteratorTy:
if n.len == 0 or n[0].kind == nkEmpty:
# 0 length or empty param list with possible pragmas imply typeclass
result = newTypeS(tyBuiltInTypeClass, c)
var b = openType(c, tyBuiltInTypeClass)
let child = newTypeS(tyProc, c)
if n.kind == nkIteratorTy:
child.incl tfIterator
@@ -2518,7 +2625,8 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
s.typ = child
# for now only call convention pragmas supported in proc typeclass
pragma(c, s, n[1], {FirstCallConv..LastCallConv})
result.addSonSkipIntLit(child, c.idgen)
b.add child
result = finish b
else:
let symKind = if n.kind == nkIteratorTy: skIterator else: skProc
result = semProcTypeWithScope(c, n, prev, symKind)
@@ -2647,7 +2755,9 @@ proc processMagicType(c: PContext, m: PSym) =
else: localError(c.config, m.info, errTypeExpected)
proc semGenericConstraints(c: PContext, x: PType): PType =
result = newTypeS(tyGenericParam, c, x)
var b = openType(c, tyGenericParam)
b.addKeep x
result = finish b
proc semGenericParamList(c: PContext, n: PNode, father: PType = nil): PNode =
@@ -2674,8 +2784,11 @@ proc semGenericParamList(c: PContext, n: PNode, father: PType = nil): PNode =
if typ.kind != tyStatic or typ.len == 0:
if typ.kind == tyTypeDesc:
if typ.elementType.kind == tyNone:
typ = newTypeS(tyTypeDesc, c, newTypeS(tyNone, c))
incl typ, tfCheckedForDestructor
let none = newTypeS(tyNone, c)
var b = openType(c, tyTypeDesc)
b.addKeep none
b.incl tfCheckedForDestructor
typ = finish b
else:
typ = semGenericConstraints(c, typ)
@@ -2683,7 +2796,9 @@ proc semGenericParamList(c: PContext, n: PNode, father: PType = nil): PNode =
def = semConstExpr(c, def)
if typ == nil:
if def.typ.kind != tyTypeDesc:
typ = newTypeS(tyStatic, c, def.typ)
var b = openType(c, tyStatic)
b.addKeep def.typ
typ = finish b
else:
# the following line fixes ``TV2*[T:SomeNumber=TR] = array[0..1, T]``
# from manyloc/named_argument_bug/triengine:

View File

@@ -57,12 +57,17 @@ proc searchInstTypes*(g: ModuleGraph; key: PType): PType =
return inst
proc cacheTypeInst(c: PContext; inst: PType) =
let gt = inst[0]
proc cacheTypeInst(c: PContext; inst: TypePair) =
# Publishes an in-progress instance under its name, for recursive
# instantiations. Takes the (identity, tree) pair rather than a bare `PType`:
# the cache key is derived from the generic head's identity, and only the
# instance's identity is registered -- today via `inst.decl`, under NIF via
# `inst.id`.
let gt = inst.decl[0]
let t = if gt.kind == tyGenericBody: gt.typeBodyImpl else: gt
if t.kind in {tyStatic, tyError, tyGenericParam} + tyTypeClasses:
return
addToGenericCache(c, gt.sym, inst)
addToGenericCache(c, gt.sym, inst.decl)
type
TReplTypeVars* = object
@@ -466,7 +471,8 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
x = lookupTypeVar(cl, x)
if x != nil:
if header == t: header = instCopyType(cl, t)
header[i] = x
var hb = reopen(header)
hb.setSon(i, x)
propagateToOwner(header, x)
else:
# Under IC `t` may be a loaded dep type (Sealed/immutable); mutating it
@@ -493,16 +499,17 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
# the generic body's module (`t.genericHead.owner`) has no business owning a
# type that references instantiation-site types — that is the IC parent->child
# heap leak the write-barrier surfaces.
result = newType(tyGenericInst, cl.c.idgen, cl.c.module, son = header.genericHead)
result.flags = header.flags
var rb = openPair(tyGenericInst, cl.c.idgen, cl.c.module, son = header.genericHead)
rb.setFlags header.flags
# be careful not to propagate unnecessary flags here (don't use rawAddSon)
# ugh need another pass for deeply recursive generic types (e.g. PActor)
# we need to add the candidate here, before it's fully instantiated for
# recursive instantions:
# recursive instantions: publish the instance's *identity* (`rb.pair`) while
# its body is still open, so recursive instantiations find it under its name.
if not cl.allowMetaTypes:
cacheTypeInst(cl.c, result)
cacheTypeInst(cl.c, rb.pair)
else:
cl.localCache[t.itemId] = result
cl.localCache[t.itemId] = rb.pair.decl
let oldSkipTypedesc = cl.skipTypedesc
cl.skipTypedesc = true
@@ -516,17 +523,18 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
else:
header[i]
assert x.kind != tyGenericInvocation
header[i] = x
var hb = reopen(header)
hb.setSon(i, x)
propagateToOwner(header, x)
cl.typeMap.put(body[i-1], x)
for i in FirstGenericParamAt..<t.kidsLen:
# if one of the params is not concrete, we cannot do anything
# but we already raised an error!
rawAddSon(result, header[i], propagateHasAsgn = false)
rb.addRaw(header[i], propagateHasAsgn = false)
if body.kind == tyError:
return
return finishPair(rb).decl
let bbody = last body
var newbody = replaceTypeVarsT(cl, bbody, isInstValue = true)
@@ -538,7 +546,7 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
# builtin like `int` when the generic's body is computed by a macro) and is
# immutable under IC. Skip the in-place flag accumulation on the shared
# type; the instance `result` still receives the flags below.
result.flags = result.flags + newbodyFlags - tfInstClearedFlags
rb.setFlags(rb.flags + newbodyFlags - tfInstClearedFlags)
setToPreviousLayer(cl.typeMap)
@@ -549,7 +557,8 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
# handleGenericInvocation will handle the alias-to-alias-to-alias case
if newbody.isGenericAlias: newbody = newbody.skipGenericAlias
rawAddSon(result, newbody)
rb.addRaw newbody
result = finishPair(rb).decl
checkPartialConstructedType(cl.c.config, cl.info, newbody)
if not cl.allowMetaTypes:
let dc = cl.c.graph.getAttachedOp(newbody, attachedDeepCopy)
@@ -614,10 +623,11 @@ proc eraseTupleVoidFields*(t: PType) =
if t.n[i].kind == nkRecList or t[i].kind == tyVoid:
# found first void field, compact from here
var pos = i
var b = reopen(t)
for j in i+1..<t.kidsLen:
if t[j].kind != tyVoid and j < t.n.len and t.n[j].kind != nkRecList:
t.n[pos] = t.n[j]
t[pos] = t[j]
b.setSon(pos, t[j])
if t.n[pos].kind == nkSym:
t.n[pos].sym.position = pos
inc pos
@@ -627,11 +637,12 @@ proc eraseTupleVoidFields*(t: PType) =
break
proc skipIntLiteralParams*(t: PType; idgen: IdGenerator) =
var b = reopen(t)
for i, p in t.ikids:
if p == nil: continue
let skipped = p.skipIntLit(idgen)
if skipped != p:
t[i] = skipped
b.setSon(i, skipped)
if i > 0: t.n[i].sym.typ = skipped
# when the typeof operator is used on a static input
@@ -769,11 +780,12 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
bailout()
result = instCopyType(cl, t)
cl.localCache[t.itemId] = result
var b = reopen(result)
for i in FirstGenericParamAt..<result.kidsLen:
var r = result[i]
if r != nil:
r = replaceTypeVarsT(cl, r)
result[i] = r
b.setSon(i, r)
propagateToOwner(result, r)
result.n = replaceTypeVarsN(cl, result.n)
if not cl.allowMetaTypes and result.n != nil and
@@ -785,8 +797,9 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
bailout()
result = instCopyType(cl, t)
cl.localCache[t.itemId] = result
var b = reopen(result)
for i in FirstGenericParamAt..<result.kidsLen:
result[i] = replaceTypeVarsT(cl, result[i])
b.setSon(i, replaceTypeVarsT(cl, result[i]))
propagateToOwner(result, result.last)
else:
@@ -802,6 +815,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
cl.localCache[t.itemId] = result
let propagateInstValue = isInstValue and isRefPtrObject(t)
var b = reopen(result)
for i, resulti in result.ikids:
if resulti != nil:
if resulti.kind == tyGenericBody and not cl.allowMetaTypes:
@@ -817,7 +831,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
if r2.kind in {tyPtr, tyRef}:
r = skipTypes(r2, {tyPtr, tyRef})
if result.kind != tyProc or i == 0:
result[i] = r
b.setSon(i, r)
if result.kind != tyArray or i != 0:
propagateToOwner(result, r)
# bug #4677: Do not instantiate effect lists

View File

@@ -274,7 +274,6 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
c.hashTree(t.n, {}, conf)
of tyTuple:
c &= char(t.kind)
c &= t.len
if t.n != nil and CoType notin flags:
for i in 0..<t.n.len:
assert(t.n[i].kind == nkSym)

View File

@@ -884,11 +884,13 @@ proc matchUserTypeClass*(m: var TCandidate; ff, a: PType): PType =
openScope(c)
matchedConceptContext.candidateType = a
typeClass[0][0] = a
var tcb = reopen(typeClass[0])
tcb.setSon(0, a)
c.matchedConcept = addr(matchedConceptContext)
defer:
c.matchedConcept = prevMatchedConcept
typeClass[0][0] = prevCandidateType
var tcb2 = reopen(typeClass[0])
tcb2.setSon(0, prevCandidateType)
closeScope(c)
var typeParams: seq[(PSym, PType)] = @[]

231
compiler/typebuilders.nim Normal file
View File

@@ -0,0 +1,231 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## `TypeBuilder`: a small, TokenBuf-shaped surface for constructing a `PType`.
##
## The compiler builds types by creating a mutable `TType` and then poking its
## sons/flags/`n` field into place at various call sites. This module funnels
## that construction through a builder object instead:
##
## ```nim
## var b = openType(kind, idgen, owner) # or openType(c, kind) with a PContext
## b.incl someFlag
## b.add someSon # skip int-lit + propagate flags
## b.setN someNode
## result = finish b
## ```
##
## Today the backing store is a mutable `TType`, so `finish` simply returns it
## and there is no runtime cost. The point of routing construction through the
## builder is that the backing store can later become a nifcore `TokenBuf` --
## with `openType` becoming `openTag`, the `add*` family becoming token/subtree
## appends, and `finish` becoming `beginRead` yielding a read-only cursor --
## *without touching any call site*.
##
## Contract: fully configure the type between `openType` and `finish`, and treat
## the `finish` result as immutable. Types that need their identity before their
## body is complete (recursive / deferred object types, e.g. `semEnum` and
## object bodies) are out of scope and keep using the direct `newType` /
## `rawAddSon` API for now; they will be modeled via symbol indirection later.
import ast
from itemids import ItemId
type
SymId* = ItemId
## The stable identity of a type. Today an `ItemId`; this alias marks every
## place that will migrate to a content-based nifcore `SymId` later (once
## stable, content-derived names land -- so generic instances dedup across
## modules and processes). Keeping the alias means that migration is a
## one-line change here rather than a churn across call sites.
TypePair* = object
## A type as an (identity, tree) pair: `id` names it, `decl` is its tree.
## Today `decl.itemId == id`, so the pair is a thin, forward-looking handle
## -- the handle a *named* type's body uses to refer to itself (owner /
## recursive references). Its real payoff arrives when `decl` becomes a
## nameless `NifCursor` and those self/forward references go through `id`.
id*: SymId
decl*: PType
proc typePair*(t: PType): TypePair {.inline.} =
TypePair(id: t.itemId, decl: t)
proc skipIntLit*(t: PType; id: IdGenerator): PType {.inline.} =
if t.n != nil and t.kind in {tyInt, tyFloat}:
result = copyType(t, id, t.owner)
result.n = nil
else:
result = t
proc addSonSkipIntLit*(father, son: PType; id: IdGenerator) =
let s = son.skipIntLit(id)
father.add(s)
propagateToOwner(father, s)
type
TypeBuilder* = object
t: PType
idgen {.cursor.}: IdGenerator
## non-owning: the id generator outlives every builder (it lives for the
## whole compilation), so it must not be reference-counted here.
proc openType*(kind: TTypeKind; idgen: IdGenerator; owner: PSym): TypeBuilder {.inline.} =
## Begins a fresh type of the given `kind`. Mirrors `newType`.
TypeBuilder(t: newType(kind, idgen, owner), idgen: idgen)
proc add*(b: var TypeBuilder; son: PType) {.inline.} =
## Adds a son, skipping an int-literal wrapper and propagating type flags to
## the owner. Mirrors `addSonSkipIntLit` -- the common case.
addSonSkipIntLit(b.t, son, b.idgen)
proc addRaw*(b: var TypeBuilder; son: PType; propagateHasAsgn = true) {.inline.} =
## Adds a son verbatim (no int-lit skip) but still propagates type flags.
## Mirrors `rawAddSon`.
rawAddSon(b.t, son, propagateHasAsgn)
proc addKeep*(b: var TypeBuilder; son: PType) {.inline.} =
## Adds a son verbatim: no int-lit skip and no flag propagation. Mirrors the
## `newType(..., son = x)` fast path -- including that a nil son is skipped
## (produces a childless type) rather than added.
if son != nil: b.t.add son
proc reopen*(t: PType; idgen: IdGenerator): TypeBuilder {.inline.} =
## Continues building an *existing* type in place, preserving its identity
## (`itemId`). Used to bind a freshly-built structure onto the reserved name
## of a forward-declared / partial type -- the "distinguish name from tree"
## case: the name (`t`) stays, only its tree structure is (re)built.
TypeBuilder(t: t, idgen: idgen)
proc reopen*(t: PType): TypeBuilder {.inline.} =
## Reopens an existing type purely to *transform* its sons in place (see
## `setSon`), without adding fresh ones -- so no `idgen` is needed. This is the
## "mutable staging buffer" seam for son-replacement: today it is in-place
## mutation of `t`; under NIF `reopen` thaws `t`'s sealed cursor into a mutable
## buffer, `setSon` rewrites a token, and the buffer is re-sealed. Distinct from
## the id-minting `reopen(t, idgen)` used to (re)build a forward type's body.
TypeBuilder(t: t, idgen: nil)
proc setSon*(b: var TypeBuilder; i: int; son: PType) {.inline.} =
## Replaces son `i` of a reopened type -- the in-place transform seam. Mirrors
## the old `PType.[]=` (via `ast.replaceSon`), including the `tyProc` return/
## param slot handling. Distinct from `add` (append a new son) and from the
## whole-list `ast.setSon(dest, son)`. Under NIF this is a token rewrite in the
## buffer thawed by `reopen`.
replaceSon(b.t, i, son)
proc setSon*(b: var TypeBuilder; i: BackwardsIndex; son: PType) {.inline.} =
replaceSon(b.t, i, son)
proc setN*(b: var TypeBuilder; n: PNode) {.inline.} =
b.t.n = n
proc setFlags*(b: var TypeBuilder; flags: TTypeFlags) {.inline.} =
## Replaces the whole flag set (assignment, not union). Mirrors `t.flags = x`.
b.t.flags = flags
proc incl*(b: var TypeBuilder; flag: TTypeFlag) {.inline.} =
b.t.incl flag
proc incl*(b: var TypeBuilder; flags: TTypeFlags) {.inline.} =
b.t.incl flags
proc propagateFrom*(b: var TypeBuilder; son: PType; propagateHasAsgn = true) {.inline.} =
## Propagates a son type's properties (flags, owner) into the type under
## construction. Mirrors a bare `propagateToOwner(result, son)`.
propagateToOwner(b.t, son, propagateHasAsgn)
proc setCallConv*(b: var TypeBuilder; cc: TCallingConvention) {.inline.} =
b.t.callConv = cc
proc setSym*(b: var TypeBuilder; s: PSym) {.inline.} =
b.t.sym = s
template finish*(b: TypeBuilder): PType =
## Hands out the constructed type. A template so it collapses to a bare field
## read with no call/move/destroy overhead over the old direct construction.
## Later this becomes `beginRead`, yielding a read-only cursor.
b.t
type
TypePairBuilder* = object
## The *deferred* construction seam: like `TypeBuilder`, but its identity is
## published -- cached, stashed for a later pass, or handed to a recursive
## sem call -- *before* its body is finished. Recursive generic
## instantiation needs the in-progress instance to be findable under its
## name while its sons are still being appended; `TypeBuilder` cannot model
## that because `finish` is the seal point and nothing may be appended after
## it. `TypePairBuilder` can, because the thing it hands out early is a
## `TypePair` -- an (identity, tree) pair -- and early consumers take only
## its `id`.
##
## Contract: whatever observes `pair` before `finishPair` must rely on
## `pair.id` (the name) alone -- never the son count or son contents of the
## still-open `decl`. Today `decl` is the growing `PType` and `decl.itemId
## == id`, so this holds trivially; under NIF `id` is a `SymId` valid the
## instant the shell exists and `decl` is the open `TokenBuf`, sealed into a
## read-only cursor by `finishPair`.
t: PType
idgen {.cursor.}: IdGenerator
proc openPair*(kind: TTypeKind; idgen: IdGenerator; owner: PSym;
son: sink PType = nil): TypePairBuilder {.inline.} =
## Mints the shell (optionally with `son0` already set -- e.g. the generic
## head for `tyGenericInst`). Mirrors `newType(kind, idgen, owner, son)`. The
## `pair` is publishable the moment this returns.
TypePairBuilder(t: newType(kind, idgen, owner, son), idgen: idgen)
proc reopenPair*(t: PType; idgen: IdGenerator): TypePairBuilder {.inline.} =
## Continues building an *existing* (forward-declared / partial) type as a
## deferred pair, preserving its identity. The deferred analogue of
## `reopen(t, idgen)`; its `pair` is publishable immediately.
TypePairBuilder(t: t, idgen: idgen)
proc pair*(b: TypePairBuilder): TypePair {.inline.} =
## The publishable (identity, tree) handle -- cache it / stash it / thread it
## through recursive sem *before* the body is complete. Only `pair.id` may be
## relied upon by those early consumers.
typePair(b.t)
proc add*(b: var TypePairBuilder; son: PType) {.inline.} =
addSonSkipIntLit(b.t, son, b.idgen)
proc addRaw*(b: var TypePairBuilder; son: PType; propagateHasAsgn = true) {.inline.} =
## Appends a son verbatim while the body is open. Mirrors `rawAddSon` -- the
## incremental-append step of the deferred build.
rawAddSon(b.t, son, propagateHasAsgn)
proc setSon*(b: var TypePairBuilder; i: int; son: PType) {.inline.} =
replaceSon(b.t, i, son)
proc setN*(b: var TypePairBuilder; n: PNode) {.inline.} =
b.t.n = n
proc addRecField*(b: var TypePairBuilder; fieldNode: PNode) {.inline.} =
## Appends a field entry to the type's record list (`.n`), the way tuple /
## object / proc bodies grow their `nkRecList` / `nkFormalParams`. Mirrors
## `t.n.add fieldNode`, and pairs with `add`/`addRaw` for the parallel son.
b.t.n.add fieldNode
proc flags*(b: TypePairBuilder): TTypeFlags {.inline.} =
## Reads the shell's current flags (they may have accumulated via `addRaw`'s
## propagation since the last `setFlags`).
b.t.flags
proc setFlags*(b: var TypePairBuilder; flags: TTypeFlags) {.inline.} =
b.t.flags = flags
proc incl*(b: var TypePairBuilder; flag: TTypeFlag) {.inline.} =
b.t.incl flag
proc finishPair*(b: sink TypePairBuilder): TypePair {.inline.} =
## Seals the deferred build. Today returns the pair unchanged; under NIF this
## is `beginRead` -- the open `TokenBuf` becomes a read-only cursor, still
## reachable through `pair.id`, so recursive references bound to the name now
## resolve to the sealed tree.
typePair(b.t)

View File

@@ -11,7 +11,7 @@
import
ast, astalgo, trees, msgs, platform, renderer, options,
lineinfos, int128, modulegraphs, astmsgs, wordrecg
lineinfos, int128, modulegraphs, astmsgs, wordrecg, typebuilders
import std/[intsets, strutils]
@@ -1267,7 +1267,8 @@ proc baseOfDistinct*(t: PType; g: ModuleGraph; idgen: IdGenerator): PType =
parent = it
it = it.elementType
if it.kind == tyDistinct and parent != nil:
parent[0] = it[0]
var b = reopen(parent)
b.setSon(0, it[0])
proc safeInheritanceDiff*(a, b: PType): int =
# same as inheritanceDiff but checks for tyError:
@@ -1444,7 +1445,8 @@ proc takeType*(formal, arg: PType; g: ModuleGraph; idgen: IdGenerator): PType =
arg.isEmptyContainer:
let a = copyType(arg.skipTypes({tyGenericInst, tyAlias}), idgen, arg.owner)
copyTypeProps(g, idgen.module, a, arg)
a[ord(arg.kind == tyArray)] = formal[0]
var b = reopen(a)
b.setSon(ord(arg.kind == tyArray), formal[0])
result = a
elif formal.kind in {tyTuple, tySet} and arg.kind == formal.kind:
result = formal

View File

@@ -851,26 +851,14 @@ proc genBinaryStmt(c: PCtx; n: PNode; opc: TOpcode) =
c.freeTemp(tmp)
c.freeTemp(dest)
proc genMutatingValue(c: PCtx; n: PNode): TRegister =
## Loads the value of an in-place mutation target while keeping it attached to
## its original storage. Compound lvalues must be resolved through their
## address: a normal value load can return a detached copy (for example, when
## indexing a broadcast default array).
if needsAsgnPatch(n):
let address = c.genx(n, {gfNodeAddr})
result = c.getTemp(n.typ)
c.gABC(n, opcLdDeref, result, address)
c.freeTemp(address)
else:
result = c.genx(n)
proc genBinaryStmtVar(c: PCtx; n: PNode; opc: TOpcode) =
var x = n[1]
if x.kind in {nkAddr, nkHiddenAddr}: x = x[0]
let
dest = c.genMutatingValue(x)
dest = c.genx(x)
tmp = c.genx(n[2])
c.gABC(n, opc, dest, tmp, 0)
#c.genAsgnPatch(n[1], dest)
c.freeTemp(tmp)
c.freeTemp(dest)
@@ -1174,7 +1162,7 @@ proc genMagic(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags = {}, m: TMag
of mIncl, mExcl:
unused(c, n, dest)
var d = c.genMutatingValue(n[1])
var d = c.genx(n[1])
var tmp = c.genx(n[2])
c.genSetType(n[1], d)
c.gABC(n, if m == mIncl: opcIncl else: opcExcl, d, tmp)

View File

@@ -21,7 +21,6 @@ Advanced commands:
see also: --dump.format:json (useful with: `| jq`)
//check checks the project for syntax and semantics
(can be combined with --defusages)
//track goto-definition / find-usages via `nim ic`
Runtime checks (see -x):
--objChecks:on|off turn obj conversion checks on|off
@@ -34,8 +33,6 @@ Runtime checks (see -x):
--infChecks:on|off turn Inf checks on|off
Advanced options:
--def:FILE,LINE,COL find the definition of the symbol at the position
--usages:FILE,LINE,COL find all usages of the symbol at the position
--defusages:FILE,LINE,COL
find the definition and all usages of a symbol
-o:FILE, --out:FILE set the output filename
@@ -122,7 +119,6 @@ Advanced options:
--lineDir:on|off generation of #line directive on|off
--embedsrc:on|off embeds the original source code as comments
in the generated output
--genBif:on|off generate per-module semantic BIF metadata in nimcache
--tlsEmulation:on|off turn thread local storage emulation on|off
--implicitStatic:on|off turn implicit compile time evaluation on|off
--trmacros:on|off turn term rewriting macros on|off

View File

@@ -39,16 +39,6 @@ debugging a build).
Artifacts (the NIF zoo)
=======================
Semantic BIF from regular builds
--------------------------------
``--genBif:on`` makes a regular compiler invocation write each semantically
checked module as ``<suffix>.s.bif`` under the build's nimcache directory. This
reuses the semantic artifact format used by IC without enabling incremental
compilation or changing how the program is generated and linked. Tools such as
language servers, debuggers, and binding generators can request these artifacts
when they need resolved symbols and types from an ordinary build.
Per module ``<suffix>`` (a content hash of the path; see *NIF symbols* below),
under the nimcache directory:

View File

@@ -11,16 +11,16 @@
const
# examples of possible values for repos: Head, ea82b54
NimbleStableCommit = "a399f502dec7ffcd905c1cf54b13274ad990bada" # 0.24.1
NimbleStableCommit = "42ef70c2102a942c46f13eb76872326edd525cec" # 0.22.3
AtlasStableCommit = "aa6fb162006f3015aa84c4305e15cb4d230f5ad6" # 0.14.7
ChecksumsStableCommit = "5c132cd332cce5d64a0da9ac3e4c9664313dccb4" # 0.2.2
SatStableCommit = "9d52513b3c68bfb929dbd687d4fb2836cfee6936"
NimonyStableCommit = "f831b953d7c21d9a4b11d0042039e7f84d7c8dc9" # unversioned \
NimonyStableCommit = "6f9ac6655dc6724ae4e5ccb93b8123c18d54391a" # 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-07-10 -- stable .bif file format
# Commit from 2026-07-03 -- .bif files are memory mapped too
# 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", "temit", "ttraitparam"]
"tmodsymref", "tmethupref", "temit"]
proc icTest(args: string) =
temp("")

View File

@@ -804,24 +804,6 @@ when defined(gcDestructors):
sysAssert c.next == nil, "c.next pointer must be nil"
atomicPrepend a.sharedFreeListBigChunks, c
proc takeFromSharedFreeListBigChunks(a: var MemRegion): PBigChunk {.inline.} =
when hasThreadSupport:
while true:
result = atomicLoadN(addr a.sharedFreeListBigChunks, ATOMIC_ACQUIRE)
if result == nil:
break
let next = result.next.loada
var expected = result
if atomicCompareExchangeN(addr a.sharedFreeListBigChunks, addr expected, next,
weak = true, ATOMIC_ACQUIRE, ATOMIC_RELAXED):
result.next.storea nil
break
else:
result = a.sharedFreeListBigChunks
if result != nil:
a.sharedFreeListBigChunks = result.next
result.next = nil
proc addToSharedFreeList(c: PSmallChunk; f: ptr FreeCell; size: int) {.inline.} =
atomicPrepend c.owner.sharedFreeLists[size], f
@@ -845,14 +827,21 @@ when defined(gcDestructors):
inc(c.free, total)
dec(a.occ, total)
proc freeDeferredObjects(a: var MemRegion) =
# Pop only as many nodes as we can process. Detaching the entire list and
# re-enqueuing its unprocessed tail through atomicPrepend would overwrite
# that tail's next pointer and lose the rest of the list.
for _ in 0..MaxSteps:
let it = takeFromSharedFreeListBigChunks(a)
proc freeDeferredObjects(a: var MemRegion; root: PBigChunk) =
var it = root
var maxIters = MaxSteps # make it time-bounded
while true:
let rest = it.next.loada
it.next.storea nil
deallocBigChunk(a, cast[PBigChunk](it))
if maxIters == 0:
if rest != nil:
addToSharedFreeListBigChunks(a, rest)
sysAssert a.sharedFreeListBigChunks != nil, "re-enqueing failed"
break
it = rest
dec maxIters
if it == nil: break
deallocBigChunk(a, it)
when defined(heaptrack):
const heaptrackLib =
@@ -980,7 +969,13 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
trackSize(c.size)
else:
when defined(gcDestructors):
freeDeferredObjects(a)
when hasThreadSupport:
let deferredFrees = atomicExchangeN(addr a.sharedFreeListBigChunks, nil, ATOMIC_RELAXED)
else:
let deferredFrees = a.sharedFreeListBigChunks
a.sharedFreeListBigChunks = nil
if deferredFrees != nil:
freeDeferredObjects(a, deferredFrees)
# For big chunks with custom alignment, allocate extra space.
# Since chunks are page-aligned, the needed padding is a compile-time
@@ -1402,4 +1397,4 @@ template instantiateForRegion(allocator: untyped) {.dirty.} =
#sharedMemStatsShared(sharedHeap.currMem - sharedHeap.freeMem)
{.pop.}
{.pop.}
{.pop.}

View File

@@ -36,12 +36,7 @@ type
rc: int # the object header is now a single RC field.
# we could remove it in non-debug builds for the 'owned ref'
# design but this seems unwise.
when defined(gcYrc):
rootIdx: int64 # the collector's claim word: collection tag or epoch
# stamp packed with the dense capture index. Explicitly
# 64 bit so that 32-bit targets run the same concurrent
# claim and epoch-stamp algorithms
elif defined(gcOrc):
when defined(gcOrc) or defined(gcYrc):
rootIdx: int # thanks to this we can delete potential cycle roots
# in O(1) without doubly linked lists
when defined(nimArcDebug) or defined(nimArcIds):

View File

@@ -892,6 +892,9 @@ when not defined(useNimRtl):
"API usage error: GC_enable called but GC is already enabled")
dec(gch.recGcLock)
proc GC_setStrategy(strategy: GC_Strategy) =
discard
proc GC_enableMarkAndSweep() =
gch.cycleThreshold = InitialCycleThreshold

View File

@@ -3,6 +3,14 @@
when not usesDestructors:
{.pragma: nodestroy.}
when hasAlloc:
type
GC_Strategy* = enum ## The strategy the GC should use for the application.
gcThroughput, ## optimize for throughput
gcResponsiveness, ## optimize for responsiveness (default)
gcOptimizeTime, ## optimize for speed
gcOptimizeSpace ## optimize for memory footprint
when hasAlloc and not defined(js) and not usesDestructors:
proc GC_disable*() {.rtl, inl, gcsafe, raises: [].}
## Disables the GC. If called `n` times, `n` calls to `GC_enable`
@@ -58,6 +66,9 @@ when hasAlloc and defined(js):
template GC_fullCollect* =
{.warning: "GC_fullCollect is a no-op in JavaScript".}
template GC_setStrategy* =
{.warning: "GC_setStrategy is a no-op in JavaScript".}
template GC_enableMarkAndSweep* =
{.warning: "GC_enableMarkAndSweep is a no-op in JavaScript".}

View File

@@ -491,6 +491,8 @@ when not defined(useNimRtl):
"API usage error: GC_enable called but GC is already enabled")
dec(gch.recGcLock)
proc GC_setStrategy(strategy: GC_Strategy) = discard
proc GC_enableMarkAndSweep() =
gch.cycleThreshold = InitialThreshold

View File

@@ -415,6 +415,7 @@ when hasThreadSupport:
proc GC_disable() = discard
proc GC_enable() = discard
proc GC_fullCollect() = discard
proc GC_setStrategy(strategy: GC_Strategy) = discard
proc GC_enableMarkAndSweep() = discard
proc GC_disableMarkAndSweep() = discard
proc GC_getStatistics(): string = return ""

View File

@@ -76,6 +76,7 @@ when not defined(useNimRtl):
proc GC_disable() = boehmGC_disable()
proc GC_enable() = boehmGC_enable()
proc GC_fullCollect() = boehmGCfullCollect()
proc GC_setStrategy(strategy: GC_Strategy) = discard
proc GC_enableMarkAndSweep() = discard
proc GC_disableMarkAndSweep() = discard
proc GC_getStatistics(): string = return ""

View File

@@ -12,6 +12,7 @@ proc GC_disable() = discard
proc GC_enable() = discard
proc go_gc() {.importc: "go_gc", dynlib: goLib.}
proc GC_fullCollect() = go_gc()
proc GC_setStrategy(strategy: GC_Strategy) = discard
proc GC_enableMarkAndSweep() = discard
proc GC_disableMarkAndSweep() = discard

View File

@@ -55,6 +55,8 @@ when not defined(gcOrc) and not defined(gcYrc):
proc GC_enableMarkAndSweep() = discard
proc GC_disableMarkAndSweep() = discard
proc GC_setStrategy(strategy: GC_Strategy) = discard
proc getOccupiedMem(): int = discard
proc getFreeMem(): int = discard
proc getTotalMem(): int = discard

View File

@@ -8,6 +8,7 @@ proc initGC() = discard
proc GC_disable() = discard
proc GC_enable() = discard
proc GC_fullCollect() = discard
proc GC_setStrategy(strategy: GC_Strategy) = discard
proc GC_enableMarkAndSweep() = discard
proc GC_disableMarkAndSweep() = discard
proc GC_getStatistics(): string = return ""

View File

@@ -25,55 +25,31 @@ when defined(gcYrc):
HasCollectorLock
Collecting
AlignedCounter = object
## one counter per cache line to avoid false sharing between stripes
c {.align: 64.}: int
AlignedRwLock = object
## One RwLock per cache line. {.align: 64.} causes the compiler to round
## the struct size up to 64 bytes, so consecutive array elements never
## share a cache line (sizeof(RwLock) = 56 on Linux x86_64 → 8 byte pad).
lock {.align: 64.}: RwLock
# Asymmetric two-class exclusion: seq structure mutations and collections
# exclude each other, but seq ops run concurrently with seq ops and
# collections run concurrently with collections. This replaces the old
# RwLock scheme (which allowed only ONE collector, serializing parallel
# collection) and also sidesteps POSIX's requirement that a rwlock be
# unlocked by its acquiring thread.
var
gSeqActive: array[NumLockStripes, AlignedCounter] # in-flight seq ops
gGcActive: int # active collections
gYrcLocks: array[NumLockStripes, AlignedRwLock]
var
lockState {.threadvar.}: YrcLockState
proc getYrcStripe(): int {.inline.} =
## Map this thread to one of the NumLockStripes counter stripes.
## Map this thread to one of the NumLockStripes RwLock stripes.
## getThreadId() is already cached thread-locally in threadids.nim.
getThreadId() and (NumLockStripes - 1)
proc acquireMutatorLock() {.compilerRtl, inl.} =
if lockState == HasNoLock:
let s = getYrcStripe()
while true:
# SEQ_CST inc-then-check pairs with the collector's SEQ_CST
# inc-then-drain (Dekker-style store/load ordering)
discard atomicFetchAdd(addr gSeqActive[s].c, 1, ATOMIC_SEQ_CST)
if atomicLoadN(addr gGcActive, ATOMIC_SEQ_CST) == 0: break
discard atomicFetchSub(addr gSeqActive[s].c, 1, ATOMIC_SEQ_CST)
while atomicLoadN(addr gGcActive, ATOMIC_ACQUIRE) != 0:
discard
acquireRead gYrcLocks[getYrcStripe()].lock
lockState = HasMutatorLock
proc releaseMutatorLock() {.compilerRtl, inl.} =
if lockState == HasMutatorLock:
lockState = HasNoLock
discard atomicFetchSub(addr gSeqActive[getYrcStripe()].c, 1, ATOMIC_SEQ_CST)
proc yrcGcFenceEnter() =
## A collection announces itself and waits for in-flight seq structure
## mutations to drain. Multiple collections may hold the fence at once.
discard atomicFetchAdd(addr gGcActive, 1, ATOMIC_SEQ_CST)
for s in 0 ..< NumLockStripes:
while atomicLoadN(addr gSeqActive[s].c, ATOMIC_SEQ_CST) > 0:
discard
proc yrcGcFenceExit() =
discard atomicFetchSub(addr gGcActive, 1, ATOMIC_SEQ_CST)
releaseRead gYrcLocks[getYrcStripe()].lock
template yrcMutatorLock*(t: typedesc; body: untyped) =
{.noSideEffect.}:
@@ -95,6 +71,23 @@ when defined(gcYrc):
{.noSideEffect.}:
releaseMutatorLock()
template yrcCollectorLock(body: untyped) =
if lockState == HasMutatorLock: releaseMutatorLock()
let prevState = lockState
let hadToAcquire = prevState < HasCollectorLock
if hadToAcquire:
# Acquire all stripes in ascending order — the only thread ever holding
# multiple write locks is the collector, so there is no lock-order cycle.
for yrcI in 0..<NumLockStripes:
acquireWrite(gYrcLocks[yrcI].lock)
lockState = HasCollectorLock
try:
body
finally:
if hadToAcquire:
for yrcI in 0..<NumLockStripes:
releaseWrite(gYrcLocks[yrcI].lock)
lockState = prevState
else:
template yrcMutatorLock*(t: typedesc; body: untyped) =

View File

@@ -223,9 +223,8 @@ proc addChar(s: NimString, c: char): NimString =
proc appendString(dest, src: NimString) {.compilerproc, inline.} =
## Raw, does not prepare `dest` space for copying
if src != nil:
copyMem(addr(dest.data[dest.len]), addr(src.data), src.len)
copyMem(addr(dest.data[dest.len]), addr(src.data), src.len + 1)
inc(dest.len, src.len)
dest.data[dest.len] = '\0'
proc setLengthStr(s: NimString, newLen: int): NimString {.compilerRtl.} =
## Sets the `s` length to `newLen` zeroing memory on growth.

View File

@@ -27,10 +27,6 @@ else:
template afterThreadRuns() =
for i in countdown(nimThreadDestructionHandlers.len-1, 0):
nimThreadDestructionHandlers[i]()
when declared(nimYrcThreadTeardown):
# YRC: spill this thread's candidate roots so its garbage remains
# collectible after the thread is gone
nimYrcThreadTeardown()
proc onThreadDestruction*(handler: proc () {.closure, gcsafe, raises: [].}) =
## Registers a *thread local* handler that is called at the thread's

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,594 +0,0 @@
/-
Tarjan-based deadness computation — correctness proof
=====================================================
Self-contained, no Mathlib. Checked with Lean 4 (v4.32.0).
Companion to yrc_proof.lean; models the NOVEL part of yrc.nim's
collector: cycle detection via a single Tarjan SCC traversal plus one
linear reverse scan over the condensation, replacing Bacon-style trial
deletion (three traversals: markGray / scan / collectWhite).
## The algorithm (capture / computeDeadness in yrc.nim)
`capture` runs an iterative Tarjan DFS from the candidate roots. Each
visited cell is claimed (dense index in the header), its rc word is
snapshotted, and every traversed slot contributes one edge record.
SCCs are numbered 0, 1, 2, … in POP (completion) order. Tarjan's
invariant: when an SCC is completed, every SCC it points to was
completed earlier — so every condensation cross edge goes from a
HIGHER SCC id to a LOWER one ("sinks first").
`computeDeadness` then makes ONE pass s = nScc1 … 0 (sources before
sinks, since in-edges come from higher ids):
ext(s) = sumRefs(s) internal(s) deadIn(s)
if not forcedLive(s) and ext(s) == 0:
s is DEAD; for each cross edge s → t: deadIn(t) += 1
else:
s is LIVE; for each cross edge s → t: forcedLive(t) := true
where sumRefs(s) = Σ rc over members, internal(s) = # captured edges
within s, and forcedLive is seeded from cells still registered in the
roots buffer (inRootsFlag).
## What we prove
Fix the SPEC of liveness on the condensation: an SCC is live iff it
has an external reference, a roots-buffer seed, or a captured cross
edge from a live SCC (`LiveScc`, an inductive definition).
1. `scan_dead_iff_not_live` — any deadness assignment satisfying the
scan's per-SCC equation (well-defined thanks to the sinks-first
edge order) marks an SCC dead IFF it is not live. Soundness AND
completeness in one theorem: the single reverse scan computes the
garbage set EXACTLY on the captured snapshot.
2. `impl_fixpoint_is_spec` — the implementation's ARITHMETIC form
(ext = sumRefs internal deadIn with forcedLive propagation) is
the same equation, given rc-exactness (sumRefs = external +
internal + cross-in; established by merge + commit validation, see
yrc_proof.lean §4).
3. Cell-level bridge: `tarjan_sound` — cells of dead SCCs are
unreachable in the snapshot; `tarjan_complete` — every captured
garbage cell IS marked dead (this needs strong connectivity of the
SCCs and exactness of the external counts; Bacon needs his second
and third traversals for the same guarantee).
4. `demotion_closure_sound` — validate-time demotion (an SCC dropped
from the dead set because a mutator dirtied it) must PROPAGATE
along captured cross edges: the freed set stays closed only if the
demoted set is successor-closed within the dead set. A demoted SCC
survives with its out-edges intact, so any still-dead target would
be freed while a surviving cell points at it.
## What is assumed (and where it is discharged)
• The sinks-first edge order (`horder`) — Tarjan's classical
invariant; the DFS itself is not modeled.
• rc-exactness (`hcount`) — discharged operationally by yrc_proof §4
(merge + dirty check + rc-word recheck).
• That `capture` records exactly the heap edges among captured cells
and that SCC members are mutually reachable (`h_edge_resp`,
`h_conn`, `h_cross_real`) — properties of the traversal + Tarjan.
-/
abbrev Obj := Nat
/-! ## §1 Descending induction
The scan processes higher SCC ids first; every recursive dependency
of `dead s` is on some `u > s`. This induction principle is the
well-definedness of the whole scheme. -/
theorem descending_induction {n : Nat} (P : Fin n Prop)
(step : s : Fin n, ( u : Fin n, s < u P u) P s) :
s, P s := by
have key : k, s : Fin n, n - s.val k P s := by
intro k
induction k with
| zero =>
intro s hs
have := s.isLt
omega
| succ k ih =>
intro s _
apply step
intro u hu
apply ih
have h1 := u.isLt
have h2 : s.val < u.val := hu
omega
intro s
exact key n s (by omega)
/-! ## §2 The condensation and the liveness spec
`edges` are the captured condensation cross edges (with multiplicity:
one entry per traversed slot, exactly like cap.edges bucketed into
crossTgt). `extRefs s` counts references into SCC `s` from OUTSIDE
the capture: stack refs, uncaptured heap cells, other collections'
partitions — everything in Σrc not explained by captured edges.
`seed s` is the inRootsFlag forcedLive seeding. -/
section Condensation
variable {n : Nat}
variable (edges : List (Fin n × Fin n))
variable (extRefs : Fin n Nat)
variable (seed : Fin n Bool)
/-- The SPEC: an SCC is live iff something external anchors it —
directly or through a chain of captured cross edges. -/
inductive LiveScc : Fin n Prop where
| ext (s : Fin n) : 0 < extRefs s LiveScc s
| root (s : Fin n) : seed s = true LiveScc s
| pred (u s : Fin n) : (u, s) edges LiveScc u LiveScc s
/-- The per-SCC equation the reverse scan establishes: dead iff no
external refs, no seed, and ALL cross predecessors dead. (The
sinks-first order makes this a valid definition: every predecessor
has a higher id and is decided first — see `descending_induction`;
without that order the "definition" would be circular.) -/
def ScanEq (dead : Fin n Bool) : Prop :=
s, dead s = true
(extRefs s = 0 seed s = false
e edges, e.2 = s dead e.1 = true)
/-- Live SCCs are never marked dead (soundness direction). -/
theorem live_not_dead (dead : Fin n Bool)
(hfix : ScanEq edges extRefs seed dead) :
s, LiveScc edges extRefs seed s dead s true := by
intro s hl
induction hl with
| ext s h =>
intro hd
have := ((hfix s).mp hd).1
omega
| root s h =>
intro hd
have := ((hfix s).mp hd).2.1
rw [h] at this
cases this
| pred u s hmem _ ih =>
intro hd
exact ih (((hfix s).mp hd).2.2 (u, s) hmem rfl)
/-- Non-live SCCs are always marked dead (completeness direction) —
by descending induction along the scan order. -/
theorem not_live_dead
(horder : e edges, e.2 < e.1)
(dead : Fin n Bool)
(hfix : ScanEq edges extRefs seed dead) :
s, ¬ LiveScc edges extRefs seed s dead s = true := by
refine descending_induction
(fun s => ¬ LiveScc edges extRefs seed s dead s = true) ?_
intro s ihs hnl
rw [hfix]
refine ?_, ?_, ?_
· cases Nat.eq_zero_or_pos (extRefs s) with
| inl h => exact h
| inr h => exact absurd (LiveScc.ext s h) hnl
· cases hsd : seed s with
| false => rfl
| true => exact absurd (LiveScc.root s hsd) hnl
· intro e he hes
have hlt : s < e.1 := by
have := horder e he
rw [hes] at this
exact this
apply ihs e.1 hlt
intro hlu
have hmem : (e.1, s) edges := by
rw [ hes]
simpa using he
exact hnl (LiveScc.pred e.1 s hmem hlu)
/-- **Main condensation theorem**: the single reverse scan computes
EXACTLY the non-live SCCs. One Tarjan DFS + one linear scan replace
Bacon's three graph traversals, with no loss of precision on the
snapshot. -/
theorem scan_dead_iff_not_live
(horder : e edges, e.2 < e.1)
(dead : Fin n Bool)
(hfix : ScanEq edges extRefs seed dead) :
s, dead s = true ¬ LiveScc edges extRefs seed s := by
intro s
constructor
· intro hd hl
exact live_not_dead edges extRefs seed dead hfix s hl hd
· exact not_live_dead edges extRefs seed horder dead hfix s
/-! ## §3 The implementation's arithmetic form
computeDeadness does not test "all predecessors dead" directly; it
maintains ext(s) = sumRefs(s) internal(s) deadIn(s) and a
forcedLive flag pushed along cross edges of live SCCs. We show this
is the same equation, given rc-exactness:
sumRefs s = extRefs s + internal s + (# cross edges into s).
ext(s) = 0 then says extRefs s = 0 AND every cross in-edge came from
a dead predecessor; ¬forcedLive says no seed and no LIVE predecessor
pushed the flag — together exactly `ScanEq`. -/
def inCount (s : Fin n) : Nat :=
edges.countP (fun e => e.2 == s)
def deadInCount (dead : Fin n Bool) (s : Fin n) : Nat :=
edges.countP (fun e => e.2 == s && dead e.1)
/-- countP is monotone under pointwise implication. -/
theorem countP_le_of_imp {α : Type} (l : List α) (p q : α Bool)
(himp : x l, p x = true q x = true) :
l.countP p l.countP q := by
induction l with
| nil => simp
| cons a l ih =>
have iht := ih (fun x hx => himp x (List.mem_cons_of_mem a hx))
by_cases hpa : p a = true
· have hqa := himp a (by simp) hpa
simp [hpa, hqa]
omega
· simp only [List.countP_cons]
have : p a = false := by
cases h : p a
· rfl
· exact absurd h hpa
simp [this]
omega
/-- If a stronger predicate matches as often as a weaker one, they
agree on every element. -/
theorem countP_eq_forces_all {α : Type} (l : List α) (p q : α Bool)
(himp : x l, q x = true p x = true)
(heq : l.countP p = l.countP q) :
x l, p x = true q x = true := by
induction l with
| nil => intro x hx; cases hx
| cons a l ih =>
have himpt : x l, q x = true p x = true :=
fun x hx => himp x (List.mem_cons_of_mem a hx)
have hmono := countP_le_of_imp l q p himpt
intro x hx hpx
simp only [List.countP_cons] at heq
cases List.mem_cons.mp hx with
| inl hxa =>
subst hxa
cases hqx : q x with
| true => rfl
| false =>
exfalso
simp [hpx, hqx] at heq
omega
| inr hxl =>
have hqa_pa : (if q a = true then 1 else 0) (if p a = true then 1 else 0) := by
by_cases hq : q a = true
· simp [hq, himp a (by simp) hq]
· simp [hq]
have heqt : l.countP p = l.countP q := by
by_cases hq : q a = true
· simp [hq, himp a (by simp) hq] at heq
omega
· have hqf : q a = false := by
cases h : q a
· rfl
· exact absurd h hq
by_cases hp : p a = true
· simp [hp, hqf] at heq
omega
· have hpf : p a = false := by
cases h : p a
· rfl
· exact absurd h hp
simp [hpf, hqf] at heq
omega
exact ih himpt heqt x hxl hpx
/-- If all cross predecessors of `s` are dead, deadIn equals the full
in-count (and vice versa). -/
theorem deadIn_eq_inCount_iff (dead : Fin n Bool) (s : Fin n) :
deadInCount edges dead s = inCount edges s
( e edges, e.2 = s dead e.1 = true) := by
constructor
· intro heq e he hes
have himp : x edges, (fun e => e.2 == s && dead e.1) x = true
(fun e => e.2 == s) x = true := by
intro x _ hx
simp only [Bool.and_eq_true] at hx
exact hx.1
have := countP_eq_forces_all edges
(fun e => e.2 == s) (fun e => e.2 == s && dead e.1)
himp heq.symm e he
have hbeq : (e.2 == s) = true := by
simp [hes]
have := this hbeq
simp only [Bool.and_eq_true] at this
exact this.2
· intro hall
unfold deadInCount inCount
apply List.countP_congr
intro e he
by_cases hes : e.2 = s
· simp [hes, hall e he hes]
· have : (e.2 == s) = false := by
simp [hes]
simp [this]
/-- The implementation's per-SCC decision, verbatim from
computeDeadness: NOT forced (no seed, no live predecessor pushed
the flag) and ext = sumRefs internal deadIn = 0 (stated
subtraction-free). -/
def ImplEq (sumRefs internal : Fin n Nat) (dead : Fin n Bool) : Prop :=
s, dead s = true
(¬ (seed s = true e edges, e.2 = s dead e.1 = false)
sumRefs s = internal s + deadInCount edges dead s)
/-- **The arithmetic is the spec**: under rc-exactness, the
implementation's equation is `ScanEq`, so `scan_dead_iff_not_live`
applies to computeDeadness as written. -/
theorem impl_fixpoint_is_spec
(sumRefs internal : Fin n Nat) (dead : Fin n Bool)
(hcount : s, sumRefs s = extRefs s + internal s + inCount edges s)
(himpl : ImplEq edges seed sumRefs internal dead) :
ScanEq edges extRefs seed dead := by
intro s
rw [himpl s]
constructor
· rintro hnf, harith
have hor := hnf
rw [not_or] at hor
obtain hseed, hnopred := hor
have hseedf : seed s = false := by
cases h : seed s
· rfl
· exact absurd h hseed
have hall : e edges, e.2 = s dead e.1 = true := by
intro e he hes
cases h : dead e.1 with
| true => rfl
| false => exact absurd e, he, hes, h hnopred
have hdc := (deadIn_eq_inCount_iff edges dead s).mpr hall
have hc := hcount s
refine by omega, hseedf, hall
· rintro hext, hseedf, hall
have hdc := (deadIn_eq_inCount_iff edges dead s).mpr hall
refine ?_, ?_
· rw [not_or]
refine by simp [hseedf], ?_
rintro e, he, hes, hdf
rw [hall e he hes] at hdf
cases hdf
· have hc := hcount s
omega
end Condensation
/-! ## §4 Cell-level correctness
Bridge from the condensation to the actual heap snapshot. `extRef`
covers every reference source outside the capture: mutator stacks,
the roots buffer, uncaptured heap cells' slots that the arithmetic
cannot explain, and other collections' partitions (cross-collection
edges — this is the SCC-side view of `cross_target_live` in
yrc_proof.lean §5). -/
structure CellGraph where
edge : Obj Obj Prop
extRef : Obj Prop
/-- A cell is live iff an external reference anchors it through heap
edges (the cell-level ground truth; `anchored` of yrc_proof.lean). -/
inductive CellLive (g : CellGraph) : Obj Prop where
| ext (x : Obj) : g.extRef x CellLive g x
| step (x y : Obj) : CellLive g x g.edge x y CellLive g y
/-- Paths through heap edges, used to move liveness around inside an
SCC (Tarjan guarantees SCC members are mutually reachable). -/
inductive EdgePath (g : CellGraph) : Obj Obj Prop where
| refl (x : Obj) : EdgePath g x x
| step (x y z : Obj) : EdgePath g x y g.edge y z EdgePath g x z
theorem cellLive_along_path (g : CellGraph) (u v : Obj)
(hl : CellLive g u) (hp : EdgePath g u v) : CellLive g v := by
induction hp with
| refl => exact hl
| step _ _ _ hedge ih => exact CellLive.step _ _ ih hedge
section CellBridge
variable {n : Nat}
variable (g : CellGraph)
variable (edges : List (Fin n × Fin n))
variable (extRefs : Fin n Nat)
variable (seed : Fin n Bool)
variable (captured : Obj Prop)
variable (scc : Obj Fin n)
/-- Any live captured cell sits in a live SCC.
Premises are properties of `capture`:
* `h_edge_resp` — every heap edge between captured cells was
recorded (same SCC → internal; different → cross edge);
* `h_closed` — an edge from an UNCAPTURED cell is unexplained by
the captured arithmetic, so it lands in extRefs;
* `h_ext` — direct external refs (stacks, roots buffer, foreign
partitions) are counted in extRefs. -/
theorem captured_live_scc
(h_edge_resp : u v, captured u captured v g.edge u v
scc u = scc v (scc u, scc v) edges)
(h_closed : u v, captured v g.edge u v ¬ captured u
0 < extRefs (scc v))
(h_ext : v, captured v g.extRef v 0 < extRefs (scc v)) :
x, CellLive g x captured x
LiveScc edges extRefs seed (scc x) := by
intro x hl
induction hl with
| ext x h =>
intro hc
exact LiveScc.ext _ (h_ext x hc h)
| step u v hu hedge ih =>
intro hcv
by_cases hcu : captured u
· cases h_edge_resp u v hcu hcv hedge with
| inl heq => rw [ heq]; exact ih hcu
| inr hmem => exact LiveScc.pred _ _ hmem (ih hcu)
· exact LiveScc.ext _ (h_closed u v hcv hedge hcu)
/-- **Soundness**: every cell of a dead SCC is unanchored in the
snapshot — freeing it is justified by yrc_proof.lean §1
(`yrc_safety`) + §3 (stability through the commit window). -/
theorem tarjan_sound
(dead : Fin n Bool)
(hfix : ScanEq edges extRefs seed dead)
(h_edge_resp : u v, captured u captured v g.edge u v
scc u = scc v (scc u, scc v) edges)
(h_closed : u v, captured v g.edge u v ¬ captured u
0 < extRefs (scc v))
(h_ext : v, captured v g.extRef v 0 < extRefs (scc v)) :
x, captured x dead (scc x) = true ¬ CellLive g x := by
intro x hc hd hl
exact live_not_dead edges extRefs seed dead hfix (scc x)
(captured_live_scc g edges extRefs seed captured scc
h_edge_resp h_closed h_ext x hl hc) hd
/-- Every cell of a live SCC is genuinely live. Needs the converse
premises: external counts are EXACT (no phantom refs — deferred
decs inflate rc, so in the running system this holds only after
the merge; overcounts delay collection by a round, they never
cause a wrong free), cross edges are real edges, and SCC members
are mutually reachable (Tarjan). -/
theorem live_scc_cells_live
(h_ext_exact : s : Fin n, 0 < extRefs s
v, captured v scc v = s g.extRef v)
(h_seed_exact : s : Fin n, seed s = true
v, captured v scc v = s g.extRef v)
(h_cross_real : (u s : Fin n), (u, s) edges
cu cv, captured cu captured cv scc cu = u scc cv = s
g.edge cu cv)
(h_conn : u v, captured u captured v scc u = scc v
EdgePath g u v) :
s, LiveScc edges extRefs seed s
x, captured x scc x = s CellLive g x := by
intro s hl
induction hl with
| ext s h =>
intro x hc hs
obtain v, hcv, hsv, hev := h_ext_exact s h
exact cellLive_along_path g v x (CellLive.ext v hev)
(h_conn v x hcv hc (by rw [hsv, hs]))
| root s h =>
intro x hc hs
obtain v, hcv, hsv, hev := h_seed_exact s h
exact cellLive_along_path g v x (CellLive.ext v hev)
(h_conn v x hcv hc (by rw [hsv, hs]))
| pred u s hmem _ ih =>
intro x hc hs
obtain cu, cv, hccu, hccv, hscu, hscv, he := h_cross_real u s hmem
have hculive : CellLive g cu := ih cu hccu hscu
exact cellLive_along_path g cv x (CellLive.step cu cv hculive he)
(h_conn cv x hccv hc (by rw [hscv, hs]))
/-- **Completeness**: every captured garbage cell is marked dead — the
scan collects ALL cycles reachable from the candidate set in one
round (on the snapshot; concurrent inflation only defers). -/
theorem tarjan_complete
(horder : e edges, e.2 < e.1)
(dead : Fin n Bool)
(hfix : ScanEq edges extRefs seed dead)
(h_ext_exact : s : Fin n, 0 < extRefs s
v, captured v scc v = s g.extRef v)
(h_seed_exact : s : Fin n, seed s = true
v, captured v scc v = s g.extRef v)
(h_cross_real : (u s : Fin n), (u, s) edges
cu cv, captured cu captured cv scc cu = u scc cv = s
g.edge cu cv)
(h_conn : u v, captured u captured v scc u = scc v
EdgePath g u v) :
x, captured x ¬ CellLive g x dead (scc x) = true := by
intro x hc hnl
apply not_live_dead edges extRefs seed horder dead hfix
intro hl
exact hnl (live_scc_cells_live g edges extRefs seed captured scc
h_ext_exact h_seed_exact h_cross_real h_conn (scc x) hl x hc rfl)
end CellBridge
/-! ## §5 Validate-time demotion must propagate
validateDead demotes a dead SCC when a mutator dirtied it (queue
entry or changed rc word). A demoted SCC becomes a survivor: its
slots are NOT nil'd at commit, so its captured out-edges remain in
the heap. If a cross target of a demoted SCC stayed in the dead set,
the commit would free a cell that a surviving cell still points to —
deadIn had explained that edge away under the assumption that the
predecessor dies too.
Minimal instance of the hazard: two SCCs, one edge 1 → 0, both
computed dead (ext = 0 for both; SCC 0's only reference comes from
SCC 1, subtracted as deadIn). Demote SCC 1 alone, and the freed set
{0} has a live in-edge from the surviving SCC 1.
The theorem below states the repair: if the demoted set `K` is
successor-closed within the dead set (demoting s also demotes every
dead t with a captured edge s → t, transitively — one countdown pass
suffices because edges go from higher to lower ids), then the freed
set F = dead K is predecessor-closed: every captured edge into F
comes from F. Combined with extRefs = 0 and no seed (ScanEq) this
makes F closed in the sense of yrc_proof.lean §3, so freeing F is
covered by `commit_free_safe` there. -/
theorem demotion_closure_sound {n : Nat}
(edges : List (Fin n × Fin n))
(extRefs : Fin n Nat) (seed : Fin n Bool)
(dead : Fin n Bool)
(hfix : ScanEq edges extRefs seed dead)
(K : Fin n Prop) -- the demoted SCCs
(hK_closed : e edges, K e.1 dead e.2 = true K e.2) :
-- every captured edge into the freed set comes from the freed set
e edges, (dead e.2 = true ¬ K e.2)
(dead e.1 = true ¬ K e.1) := by
intro e he hd2, hk2
have hd1 : dead e.1 = true :=
((hfix e.2).mp hd2).2.2 e he rfl
refine hd1, ?_
intro hk1
exact hk2 (hK_closed e he hk1 hd2)
/-- Without successor-closure the guarantee genuinely fails: in the
two-SCC instance above, demoting only SCC 1 leaves the freed set
{0} with an in-edge from a survivor. (Concrete witness, checked by
`decide`-style evaluation.) -/
example :
let edges : List (Fin 2 × Fin 2) := [(1, 0)]
let dead : Fin 2 Bool := fun _ => true
let K : Fin 2 Prop := fun s => s = 1 -- demote only SCC 1
-- ScanEq holds for `dead` (both SCCs legitimately computed dead) …
ScanEq edges (fun _ => 0) (fun _ => false) dead
-- … yet the freed set {0} has an in-edge from surviving SCC 1:
((1, 0) edges dead 0 = true ¬ K 0 K 1) := by
refine ?_, ?_
· intro s
simp
· refine by simp, rfl, by simp, rfl
/-! ## Summary (all QED, no sorry)
* `descending_induction` — the sinks-first SCC numbering makes the
reverse scan a well-founded definition.
* `scan_dead_iff_not_live` — the scan marks an SCC dead iff it is
not externally anchored: exact garbage identification in ONE
linear pass over the condensation.
* `impl_fixpoint_is_spec` — the implementation's arithmetic
(ext = sumRefs internal deadIn, forcedLive propagation) is
that same equation under rc-exactness.
* `tarjan_sound` / `tarjan_complete` — at the cell level: dead cells
are unanchored (frees are safe) and unanchored captured cells are
freed (nothing is missed on the snapshot).
* `demotion_closure_sound` + counterexample — demotion is sound iff
it propagates along captured cross edges to still-dead targets;
a lone demotion can leave the freed set with a surviving
predecessor.
Not modeled: the Tarjan DFS itself (its two classical invariants —
SCC partition and sinks-first emission — enter as premises), the
iterative traceStack encoding, crossPend (cross-collection edges are
folded into `extRefs`, justified by yrc_proof.lean §5), and the
temporal validity of rc-exactness (yrc_proof.lean §4).
-/

View File

@@ -1156,14 +1156,13 @@ proc executeNoHooksV3(cmd: IdeCmd, file: AbsoluteFile, dirtyfile: AbsoluteFile,
graph.suggestResult(s.sym, s.sym.info)
of ideType:
let s = graph.findSymData(file, line, col)
if not s.isNil and s.sym.typ != nil:
if not s.isNil:
let typeSym = s.sym.typ.sym
if typeSym != nil:
graph.suggestResult(typeSym, typeSym.info, ideType)
elif s.sym.typ.len != 0 and s.sym.typ[0] != nil:
elif s.sym.typ.len != 0:
let genericType = s.sym.typ[0].sym
if genericType != nil:
graph.suggestResult(genericType, genericType.info, ideType)
graph.suggestResult(genericType, genericType.info, ideType)
of ideUse, ideDus:
let symbol = graph.findSymData(file, line, col)
if not symbol.isNil:

View File

@@ -20,13 +20,6 @@ echo fo#[!]#oGeneric.bar
# bad type
echo unde#[!]#fined
# type of a void proc: typ[0] (return type) is nil, must not crash
var s = ""
s.a#[!]#dd('x')
# type of a module symbol: typ is nil, must not crash
import std/str#[!]#utils
discard """
$nimsuggest --v3 --tester $file
>type $1
@@ -36,6 +29,4 @@ type skType tv3_typeDefinition.Foo2 Foo2 $file 11 2 "" 100
>type $3
type skType tv3_typeDefinition.FooGeneric FooGeneric $file 14 2 "" 100
>type $4
>type $5
>type $6
"""

View File

@@ -36,4 +36,4 @@ proc main() =
main()
GC_fullCollect()
when not defined(useMalloc):
echo getOccupiedMem() < 10 * 1024 * 1024, " peak memory: ", getMaxMem() < 12 * 1024 * 1024
echo getOccupiedMem() < 10 * 1024 * 1024, " peak memory: ", getMaxMem() < 10 * 1024 * 1024

View File

@@ -1,45 +0,0 @@
discard """
targets: "c cpp"
output: "13"
"""
# bug #25883: C codegen assigns same type hash to tuples with different nesting
# but identical flattened content.
# ((Int[1], Int[2]), Int[13], Int[14]) and ((Int[1], Int[2], Int[13]), Int[14])
# must get distinct C type names.
type
Int[V: static int] = object
proc main() =
var b = ((1, 2), 13, 14)
var c = ((1, 2, 13), 14)
echo c[0][2]
main()
block:
type
Int[V: static int] = object
Layout[Sh, St] = object
shape: Sh
stride: St
func makeB(): auto =
Layout[((Int[2], Int[3]), Int[5], Int[7]), ((Int[1], Int[2]), Int[6], Int[30])](
shape: ((Int[2](), Int[3]()), Int[5](), Int[7]()),
stride: ((Int[1](), Int[2]()), Int[6](), Int[30]())
)
func makeC(): auto =
Layout[((Int[2], Int[3], Int[5]), Int[7]), ((Int[1], Int[2], Int[6]), Int[30])](
shape: ((Int[2](), Int[3](), Int[5]()), Int[7]()),
stride: ((Int[1](), Int[2](), Int[6]()), Int[30]())
)
proc main() =
let b = makeB()
let c = makeC()
main()

View File

@@ -1,14 +0,0 @@
discard """
output: "ok"
targets: "c"
matrix: "--genBif:on"
"""
import std/[compilesettings, os]
let cache = querySetting(nimcacheDir)
var hasSemanticBif = false
for path in walkFiles(cache / "*.s.bif"):
hasSemanticBif = true
doAssert hasSemanticBif
echo "ok"

View File

@@ -1,28 +0,0 @@
type
NestedPoll = object of RootEffect
CallbackFunc = proc(arg: pointer) {.gcsafe, raises: [], forbids: [NestedPoll].}
TaggedCallbackFunc = proc(arg: pointer) {.gcsafe, raises: [], tags: [], forbids: [NestedPoll].}
InternalAsyncCallback = object
fn: CallbackFunc
TaggedInternalAsyncCallback = object
fn: TaggedCallbackFunc
proc closeSocket(aftercb: CallbackFunc = nil) =
proc continuation(udata: pointer) =
aftercb(nil)
let acb = InternalAsyncCallback(fn: continuation)
discard acb
proc closeSocketTagged(aftercb: TaggedCallbackFunc = nil) =
proc continuation(udata: pointer) =
aftercb(nil)
let acb = TaggedInternalAsyncCallback(fn: continuation)
discard acb
closeSocket()
closeSocketTagged()

View File

@@ -1,9 +0,0 @@
discard """
errormsg: "cannot infer the return type of 'foo'"
line: 6
"""
proc foo(n: int): auto =
return foo(n + 1)
discard foo(0)

View File

@@ -1,12 +0,0 @@
discard """
errormsg: "cannot infer the return type of 'foo'"
line: 6
"""
proc foo(n: int): auto =
if n > 0:
foo(n - 1)
else:
foo(n + 1)
discard foo(1)

View File

@@ -1,9 +0,0 @@
discard """
errormsg: "cannot infer the return type of 'foo'"
line: 6
"""
proc foo[T](x: T): auto =
foo(x)
discard foo(1)

View File

@@ -1,34 +0,0 @@
import std/macros
type
Chunk* = ref object
x*: int32
ChunkTrait* = distinct tuple[
loaded: proc(self: pointer, chunk: Chunk)
]
macro makeVTable*(traitType: typedesc): untyped =
## Splice the param symbols out of an imported proc type into a fresh proc
## type nested in a fresh tuple type. Under `nim ic` the imported trait is
## loaded from a NIF cache, so its param symbols are `Sealed`; re-owning them
## in `semProcTypeNode` used to trip `ast.nim` `s.state != Sealed`.
var t = traitType.getTypeInst[1].getTypeImpl
if t.kind == nnkDistinctTy: t = t[0]
let formalParams = t[0][1][0]
var bridgeParams = nnkFormalParams.newTree(formalParams[0].copyNimTree)
bridgeParams.add nnkIdentDefs.newTree(ident"p", ident"pointer", newEmptyNode())
for j in 2 ..< formalParams.len:
bridgeParams.add formalParams[j].copyNimTree
let vtType = nnkTupleTy.newTree(
nnkIdentDefs.newTree(ident"m0",
nnkProcTy.newTree(bridgeParams, nnkPragma.newTree(ident"nimcall")),
newEmptyNode()))
let vtName = genSym(nskType, "VT")
let vtVar = genSym(nskVar, "vt")
result = nnkStmtList.newTree(
nnkTypeSection.newTree(
nnkTypeDef.newTree(vtName, newEmptyNode(), vtType)),
nnkVarSection.newTree(
nnkIdentDefs.newTree(
nnkPragmaExpr.newTree(vtVar, nnkPragma.newTree(ident"used")),
vtName, newEmptyNode())))

View File

@@ -1,14 +0,0 @@
discard """
output: '''ok'''
"""
# Regression test: a `typed` macro in an imported module splices param symbols
# out of an imported proc type into a freshly semchecked proc type. Under
# `nim ic` those param symbols are loaded `Sealed` from the NIF cache; reusing
# them in `newSymG`/`semProcTypeNode` used to fail `ast.nim` `s.state != Sealed`.
import mtraitparam
makeVTable(ChunkTrait)
echo "ok"

View File

@@ -1,46 +0,0 @@
proc byReturn(n: int): auto =
if n < 5:
return byReturn(n + 1)
else:
return 9
proc byResult(n: int): auto =
if n < 5:
result = byResult(n + 1)
else:
result = 9
proc byExpression(n: int): auto =
if n < 5:
byExpression(n + 1)
else:
9
proc generic[T](x: T; n: int): auto =
if n < 5:
return generic(x, n + 1)
else:
return x
proc concreteFirst(n: int): auto =
if n >= 5:
return 9
else:
return concreteFirst(n + 1)
proc multipleRecursiveBranches(n: int): auto =
if n < 0:
return multipleRecursiveBranches(n + 1)
elif n < 5:
return multipleRecursiveBranches(n + 1)
else:
return 9
doAssert byReturn(3) == 9
doAssert byResult(3) == 9
doAssert byExpression(3) == 9
doAssert generic("ok", 3) == "ok"
doAssert concreteFirst(3) == 9
doAssert multipleRecursiveBranches(-1) == 9

View File

@@ -1,12 +0,0 @@
discard """
matrix: "--mm:refc; --mm:orc --deepcopy:on"
errormsg: "'deepCopy' is not available for type <NoCopy>"
file: "system.nim"
"""
type NoCopy = object
proc `=copy`(a: var NoCopy; b: NoCopy) {.error.}
var a = new NoCopy
var b = deepCopy(a)

View File

@@ -1,15 +0,0 @@
discard """
matrix: "--mm:refc; --mm:orc --deepcopy:on"
errormsg: "'deepCopy' is not available for type <Container>"
file: "system.nim"
"""
type
NoCopy = object
Container = object
value: NoCopy
proc `=copy`(a: var NoCopy; b: NoCopy) {.error.}
var a = new Container
var b = deepCopy(a)

View File

@@ -1,40 +0,0 @@
discard """
matrix: "--mm:arc; --mm:orc"
"""
import std/[atomics, typedthreads]
const numChunks = 23 # More than the allocator's bounded drain can process.
var
pointers: array[numChunks, pointer]
allocated: Atomic[bool]
continueAllocating: Atomic[bool]
proc allocPointers() {.thread.} =
for i in 0..<pointers.len:
pointers[i] = allocShared(8192)
allocated.store(true, moRelease)
while not continueAllocating.load(moAcquire):
discard
# The first allocation drains MaxSteps + 1 chunks. The second allocation
# must still be able to find and drain the remainder.
for _ in 0..1:
let p = allocShared(8192)
deallocShared(p)
doAssert getOccupiedMem() == 0
var thread: Thread[void]
createThread(thread, allocPointers)
while not allocated.load(moAcquire):
discard
for p in pointers:
deallocShared(p)
continueAllocating.store(true, moRelease)
joinThread(thread)

View File

@@ -1,101 +0,0 @@
type
Container = object
numbers: seq[int]
text: string
chars: set[char]
Variant = object
case enabled: bool
of false:
numbers: seq[int]
else:
discard
Index = enum
index0, index1, index2, index3, index4, index5, index6, index7,
index8, index9, index10, index11, index12, index13, index14, index15,
index16, index17, index18, index19, index20, index21, index22, index23,
index24, index25, index26, index27, index28, index29, index30, index31,
index32
Outer = object
values: array[33, seq[int]]
proc directSeq(): array[33, seq[int]] =
result[32].add 1
proc directStringChar(): array[33, string] =
result[32].add 'a'
proc directStringString(): array[33, string] =
result[32].add "ab"
proc directSet(): array[33, set[char]] =
result[32].incl 'a'
proc fieldSeq(): array[33, Container] =
result[32].numbers.add 1
proc fieldStringChar(): array[33, Container] =
result[32].text.add 'a'
proc fieldStringString(): array[33, Container] =
result[32].text.add "ab"
proc fieldSet(): array[33, Container] =
result[32].chars.incl 'a'
proc nestedSeq(): array[33, array[33, seq[int]]] =
result[32][32].add 1
proc checkedFieldSeq(): array[33, Variant] =
result[32].numbers.add 1
proc enumIndexSeq(): array[Index, seq[int]] =
result[index32].add 1
proc rangeIndexSeq(): array[10..42, seq[int]] =
result[42].add 1
proc firstIndexSeq(): array[33, seq[int]] =
result[0].add 1
proc middleIndexSeq(): array[33, seq[int]] =
result[16].add 1
proc objectArraySeq(): Outer =
result.values[32].add 1
proc singleEvaluation(): tuple[values: array[33, seq[int]], evaluations: int] =
var evaluations = 0
proc index(): int =
inc evaluations
32
result.values[index()].add 1
result.evaluations = evaluations
proc test =
let direct = directSeq()
doAssert direct[0].len == 0
doAssert direct[31].len == 0
doAssert direct[32] == @[1]
doAssert directStringChar()[32] == "a"
doAssert directStringString()[32] == "ab"
doAssert 'a' in directSet()[32]
doAssert fieldSeq()[32].numbers == @[1]
doAssert fieldStringChar()[32].text == "a"
doAssert fieldStringString()[32].text == "ab"
doAssert 'a' in fieldSet()[32].chars
doAssert nestedSeq()[32][32] == @[1]
doAssert checkedFieldSeq()[32].numbers == @[1]
doAssert enumIndexSeq()[index32] == @[1]
doAssert rangeIndexSeq()[42] == @[1]
doAssert firstIndexSeq()[0] == @[1]
doAssert middleIndexSeq()[16] == @[1]
doAssert objectArraySeq().values[32] == @[1]
let evaluated = singleEvaluation()
doAssert evaluated.values[32] == @[1]
doAssert evaluated.evaluations == 1
static: test()
test()

View File

@@ -1,84 +0,0 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# Deterministic port of dumpster's `fuzz` test
# (https://claytonwramsey.com/blog/dumpster/): drive a mutable object graph
# through a long random sequence of node/edge inserts and removals, then drop
# every root and assert that *every allocation ever made is destroyed exactly
# once* -- no leak (count 0) and no double free (count > 1). The graph grows
# thick with overlapping and self cycles, so only the cycle collector can wind
# it down. A fixed LCG seed makes the shape reproducible across runs.
type
DropCount = object
id: int
live: bool # false in any moved-from temporary -> never miscounts
Node = ref object
refs: seq[Node]
dc: DropCount
var counts: seq[int] # counts[id] == times allocation `id` was destroyed
proc `=destroy`(x: DropCount) =
if x.live: inc counts[x.id]
var nextId = 0
proc newNode(): Node =
counts.add 0
result = Node(refs: @[], dc: DropCount(id: nextId, live: true))
inc nextId
# `child` is a by-value borrow (dumpster's `Gc::clone`): storing it copies the
# reference, leaving the caller's root slot still owning. Using `.refs.add`
# directly would move the root at its last read and change the graph shape.
proc link(parent, child: Node) = parent.refs.add child
# Small fixed-seed LCG (Numerical Recipes constants) for reproducible shape.
var rngState: uint32 = 12345
proc rnd(n: int): int =
rngState = rngState * 1664525'u32 + 1013904223'u32
int((rngState shr 16) mod uint32(n))
proc run =
const N = 20_000
var roots: seq[Node]
for i in 0 ..< 50: roots.add newNode()
for _ in 0 ..< N:
if roots.len == 0: roots.add newNode()
case rnd(4)
of 0: # allocate a fresh root
roots.add newNode()
of 1: # add edge from -> to (may self-loop)
let a = rnd(roots.len)
let b = rnd(roots.len)
link(roots[a], roots[b])
of 2: # drop a root handle (swap-remove)
let i = rnd(roots.len)
roots[i] = roots[roots.high]
roots.setLen roots.len - 1
else: # drop one outgoing edge of a root
let a = rnd(roots.len)
if roots[a].refs.len > 0:
let j = rnd(roots[a].refs.len)
roots[a].refs[j] = roots[a].refs[roots[a].refs.high]
roots[a].refs.setLen roots[a].refs.len - 1
roots.setLen 0 # release every remaining root
GC_fullCollect()
GC_fullCollect()
run()
var missing = 0
for id in 0 ..< nextId:
if counts[id] != 1:
inc missing
doAssert missing == 0, "graph not fully reclaimed: " & $missing & " of " &
$nextId & " allocations leaked or double-freed"
echo "ok"

View File

@@ -1,33 +0,0 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# Memory must stay bounded while creating cyclic garbage forever: the
# collector has to keep pace with allocation. A leak shows up as unbounded
# peak occupancy, which the assertion below catches.
type Node = ref object
next: Node
data: seq[int]
proc mk(n: int) =
var h = Node(data: newSeq[int](4))
var c = h
for i in 1 ..< n:
c.next = Node(data: newSeq[int](4))
c = c.next
c.next = h
var peak = 0
for round in 0 ..< 30:
for i in 0 ..< 10_000:
mk(8)
let occ = getOccupiedMem()
if occ > peak: peak = occ
doAssert peak < 64 * 1024 * 1024, "memory exploded: leak"
GC_fullCollect()
echo "ok"

View File

@@ -1,26 +0,0 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "done"
valgrind: "leaks"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# Smallest possible cycle: a three-node ring that is dead the instant `mk`
# returns. GC_fullCollect must reclaim it without touching freed memory.
type Node = ref object
next: Node
proc mk =
let a = Node()
let b = Node()
let c = Node()
a.next = b
b.next = c
c.next = a
mk()
GC_fullCollect()
echo "done"

View File

@@ -1,74 +0,0 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
valgrind: "leaks"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# The "parallel_loop" complex graph from Clayton Ramsey's `dumpster` collector
# (https://claytonwramsey.com/blog/dumpster/). Four allocations form a single
# SCC built from two *overlapping* cycles that share nodes 1 and 4:
#
# 1 -> 4 4 -> 2, 4 -> 3 2 -> 1, 3 -> 1
#
# so 1->4->2->1 and 1->4->3->1 traverse the same 1 and 4. Every node keeps a
# nonzero refcount from *inside* the SCC, so plain reference counting can never
# free any of them; only cycle collection can, and only once the last external
# handle is gone. We drop the four root handles one at a time and assert that
# nothing is reclaimed until the final drop, then all four die together -- the
# exact assertion sequence dumpster's test makes.
type
# A field whose destructor bumps a per-node counter when the cell is freed;
# `slot` is nil in any moved-from temporary, so those don't miscount.
DropCount = object
slot: ptr int
Node = ref object
refs: seq[Node]
dc: DropCount
proc `=destroy`(x: DropCount) =
if x.slot != nil: inc x.slot[]
# Add an edge parent -> child. `child` is a by-value borrow, so the caller's
# handle keeps owning its reference -- this is Nim's equivalent of dumpster's
# `Gc::clone`. Building edges with `g1.refs.add g2` instead would *move* g2 at
# its last read and silently collapse the graph's root set.
proc link(parent, child: Node) = parent.refs.add child
# drops[0] is unused; nodes are 1..4 to mirror the blog's gc1..gc4. The four
# handles live in an array so each stays an independent, still-owning root.
var drops: array[5, int]
proc scenario =
var g: array[1..4, Node]
for i in 1..4: g[i] = Node(dc: DropCount(slot: addr drops[i]))
link(g[2], g[1]) # 2 -> 1
link(g[3], g[1]) # 3 -> 1
link(g[4], g[2]) # 4 -> 2
link(g[4], g[3]) # 4 -> 3
link(g[1], g[4]) # 1 -> 4 (closes both cycles)
GC_fullCollect()
doAssert drops == [0, 0, 0, 0, 0], "nothing dead yet"
g[1] = nil # node1 still held by node2 and node3
GC_fullCollect()
doAssert drops == [0, 0, 0, 0, 0], "dropping root 1 frees nothing"
g[2] = nil # node2 still held by node4
GC_fullCollect()
doAssert drops == [0, 0, 0, 0, 0], "dropping root 2 frees nothing"
g[3] = nil # node3 still held by node4
GC_fullCollect()
doAssert drops == [0, 0, 0, 0, 0], "dropping root 3 frees nothing"
g[4] = nil # last external handle gone: the whole SCC is garbage
GC_fullCollect()
doAssert drops == [0, 1, 1, 1, 1], "the full cycle is reclaimed at once"
scenario()
echo "ok"

View File

@@ -1,33 +0,0 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
valgrind: "leaks"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# Exercise the manual collection API: disable automatic collections, build a
# batch of dead cycles, then reclaim them in halves via GC_partialCollect and
# confirm the pending count shrinks accordingly.
type Node = ref object
next: Node
proc mk(n: int) =
var h = Node()
var c = h
for i in 1 ..< n: (c.next = Node(); c = c.next)
c.next = h
GC_disableOrc() # no automatic collections; exercise the partial API
for i in 0 ..< 300: mk(4)
let pending = GC_prepareOrc()
doAssert pending > 0
GC_partialCollect(pending div 2) # collect only the upper half
let remaining = GC_prepareOrc()
doAssert remaining <= pending div 2, $remaining & " vs " & $pending
GC_partialCollect(0) # collect the rest
doAssert GC_prepareOrc() == 0
GC_fullCollect()
echo "ok"

View File

@@ -1,60 +0,0 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
valgrind: "leaks"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# Functional test for the Tarjan-based collector: doubly-linked dead rings,
# self-referential cells, and one surviving ring whose integrity is checked
# after a full collect.
type
Node = ref object
next: Node
prev: Node
data: string
proc makeRing(n: int): Node =
result = Node(data: "head")
var cur = result
for i in 1 ..< n:
let x = Node(data: $i)
cur.next = x
x.prev = cur
cur = x
cur.next = result
result.prev = cur
proc dropRings =
for i in 0 ..< 2000:
discard makeRing(10) # dead immediately
proc keepOne: Node =
for i in 0 ..< 100:
discard makeRing(5)
result = makeRing(7) # survives
proc selfRef =
type S = ref object
self: S
buf: seq[int]
for i in 0 ..< 500:
let s = S(buf: newSeq[int](8))
s.self = s
dropRings()
selfRef()
let keep = keepOne()
GC_fullCollect()
doAssert keep.data == "head"
var cnt = 0
var it = keep
while true:
inc cnt
it = it.next
if it == keep: break
doAssert cnt == 7, "live ring corrupted: " & $cnt
echo "ok"

View File

@@ -1,72 +0,0 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# Concurrent stress for the lock-free SATB collector: mutator threads rewire
# live cyclic structures (constant dirty traffic + capture aborts) and churn
# garbage cycles while a dedicated thread runs back-to-back collections. Live
# data corruption or a lost object trips a doAssert / a growing residual.
import std/typedthreads
type Node = ref object
next: Node # ring structure, stable
payload: Node # rewired constantly -> candidates + dirty SCCs
id: int
const NWorkers = 3
const Iters = 400_000
const RingLen = 64
var stopFlag: bool
var done: array[NWorkers, int]
proc mkRing(tag: int): seq[Node] =
result = newSeq[Node](RingLen)
for i in 0 ..< RingLen: result[i] = Node(id: tag + i)
for i in 0 ..< RingLen:
result[i].next = result[(i+1) mod RingLen]
result[i].payload = result[(i*13+7) mod RingLen]
proc verify(ring: seq[Node]; tag: int) =
for i in 0 ..< RingLen:
doAssert ring[i].id == tag + i, "node corrupted"
doAssert ring[i].next.id == tag + (i+1) mod RingLen, "ring broken"
doAssert ring[i].payload.id >= tag and ring[i].payload.id < tag + RingLen,
"payload points outside ring: live data corrupted"
proc worker(tid: int) {.thread.} =
var tag = tid * 1_000_000
var ring = mkRing(tag)
for i in 0 ..< Iters:
# lock-free barrier hot path: rewire a payload edge inside the live ring.
# decs the old target (rc > 0) -> candidate; collections capture the live
# ring concurrently and must rescue or abort, never free it.
ring[i mod RingLen].payload = ring[(i * 7 + 3) mod RingLen]
if (i and 8191) == 0:
verify(ring, tag)
if (i and 32767) == 0:
inc tag, RingLen
ring = mkRing(tag) # old ring becomes a garbage cycle tangle
verify(ring, tag)
done[tid] = 1
proc collector() {.thread.} =
while not stopFlag:
GC_runOrc()
var th: array[NWorkers, Thread[int]]
var col: Thread[void]
createThread(col, collector)
for i in 0 ..< NWorkers: createThread(th[i], worker, i)
joinThreads(th)
stopFlag = true
joinThread(col)
for i in 0 ..< NWorkers: doAssert done[i] == 1
GC_fullCollect()
GC_fullCollect()
echo "ok"

View File

@@ -1,60 +0,0 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# N threads each churn garbage cycles while maintaining one live ring that is
# verified continuously and replaced, plus explicit GC_runOrc collections from
# every thread. Corruption of live data trips a doAssert.
import std/typedthreads
type Node = ref object
next: Node
prev: Node
id: int
const NThreads = 4
const Iters = 30_000
proc mkRing(n, tag: int): Node =
result = Node(id: tag)
var c = result
for i in 1 ..< n:
let x = Node(id: tag + i)
c.next = x
x.prev = c
c = x
c.next = result
result.prev = c
proc checkRing(r: Node; n, tag: int) =
var c = r
for i in 0 ..< n:
doAssert c.id == tag + i, "ring corrupted!"
c = c.next
doAssert c == r, "ring not closed!"
var results: array[NThreads, int]
proc worker(tid: int) {.thread.} =
var keep = mkRing(5, tid * 1000)
for i in 0 ..< Iters:
discard mkRing(3 + (i and 7), 999999) # garbage
if (i and 255) == 0:
checkRing(keep, 5, tid * 1000)
keep = mkRing(5, tid * 1000) # old keep becomes garbage
if (i and 1023) == 0:
GC_runOrc() # explicit collections from all threads
checkRing(keep, 5, tid * 1000)
results[tid] = 1
var th: array[NThreads, Thread[int]]
for i in 0 ..< NThreads: createThread(th[i], worker, i)
joinThreads(th)
for i in 0 ..< NThreads: doAssert results[i] == 1
GC_fullCollect()
echo "ok"

View File

@@ -52,7 +52,7 @@ proc updateSubmodules*(dir: string, allowBundled = false) =
let oldDir = getCurrentDir()
setCurrentDir(dir)
try:
exec "git submodule update --init --recursive"
exec "git submodule update --init"
finally:
setCurrentDir(oldDir)
elif allowBundled: