diff --git a/compiler/ast.nim b/compiler/ast.nim index 0c284a19f5..a76a675240 100644 --- a/compiler/ast.nim +++ b/compiler/ast.nim @@ -546,12 +546,30 @@ proc idGeneratorForPackage*(nextIdWillBe: int32): IdGenerator = proc nextSymId(x: IdGenerator): ItemId {.inline.} = assert(not x.sealed) + when not defined(nimKochBootstrap): + if x.backendMinted: + # Share the loader's per-module backend counter so a freshly-minted + # backend sym never collides with an `@bk` sym loaded from the module's + # `.t.bif` (see ast2nif.nextBackendSymItem). + let it = nextBackendSymItem(program, x.module) + if it >= 0'i32: + return backendItemId(x.module, it) inc x.symId result = if x.backendMinted: backendItemId(x.module, x.symId) else: itemId(x.module, x.symId) proc nextTypeId*(x: IdGenerator): ItemId {.inline.} = assert(not x.sealed) + when not defined(nimKochBootstrap): + if x.backendMinted: + # Share the loader's per-module backend TYPE counter (seeded from the + # module's `(unusedid)`) so a freshly-minted backend type sits ABOVE every + # loaded type — never colliding with a frontend type's `toId` (the bug that + # crashed cgen's `getTypeDescAux` cycle check on `AsyncBufferRef`). Mirrors + # `nextSymId` (see ast2nif.nextBackendTypeItem). + let it = nextBackendTypeItem(program, x.module) + if it >= 0'i32: + return backendItemId(x.module, it) inc x.typeId result = if x.backendMinted: backendItemId(x.module, x.typeId) else: itemId(x.module, x.typeId) diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index 3503040f9b..0504d18aca 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -18,11 +18,19 @@ import "../dist/checksums/src/checksums" / sha1 import astdef, idents, msgs, options import lineinfos as astli import pathutils #, modulegraphs -import "../dist/nimony/src/lib" / [bitabs, nifstreams, nifcursors, lineinfos, +import "../dist/nimony/src/lib" / [bitabs, nifstreams, lineinfos, nifindexes, nifreader] +# Step 2b: the READER speaks nifcore; the WRITER keeps nifstreams (global `pool`, +# PackedToken/PackedLineInfo). nifstreams does NOT export Cursor/TokenBuf/NifKind, +# so those resolve unambiguously to nifcore. `except pool`: nifcore's +# `pool(c: Cursor)` accessor would shadow nifstreams' global `pool` var the writer +# uses; the reader reaches pools via `symName(c)`/`strVal(c)` etc. +import "../dist/nimony/src/lib/nifcore" except pool +from "../dist/nimony/src/lib" / bif import load, BifModule import "../dist/nimony/src/gear2" / modnames import "../dist/nimony/src/models" / nifindex_tags import typekeys +import icnifcore import ic / [enum2nif] const SysModuleSuffix* = "@sys" @@ -152,22 +160,17 @@ proc nifLineInfoWithComment(w: var LineInfoWriter; info: TLineInfo; doc: string) let fid = get(w, info.fileIndex) result = packWithComment(pool.man, fid, info.line.int32, info.col, cid) -proc oldLineInfo(w: var LineInfoWriter; info: PackedLineInfo): TLineInfo = - if info == NoLineInfo: +proc oldLineInfo(w: var LineInfoWriter; info: NifLineInfo; p: Pool): TLineInfo = + ## Step 2b: the reader's line info arrives as a nifcore `NifLineInfo`; resolve + ## it to a `TLineInfo`. `info.file` indexes the loaded buffer's OWN filename + ## pool `p` (= `cursorPool(n)`), which is the shared `icPool` for a text-parsed + ## module but a fresh per-file pool for a `bif`-loaded one. + if info.file == NoFile: result = unknownLineInfo else: - var x = unpack(pool.man, info) - var fileIdx: FileIndex - if w.fileV == x.file: - fileIdx = w.fileK - elif x.file in w.revTab: - fileIdx = w.revTab[x.file] - else: - # Need to look up FileId -> FileIndex via the file path - let filePath = pool.files[x.file] - fileIdx = msgs.fileInfoIdx(w.config, AbsoluteFile filePath) - w.revTab[x.file] = fileIdx - result = TLineInfo(line: x.line.uint16, col: x.col.int16, fileIndex: fileIdx) + let filePath = p.filenames[info.file] + let fileIdx = msgs.fileInfoIdx(w.config, AbsoluteFile filePath) + result = TLineInfo(line: info.line.uint16, col: info.col.int16, fileIndex: fileIdx) # ------------- Writer --------------------------------------------------------------- @@ -200,7 +203,7 @@ var type Writer = object - deps: TokenBuf # include&import deps + deps: IcBuilder # include&import deps infos: LineInfoWriter currentModule: int32 decodedFileIndices: HashSet[FileIndex] @@ -211,7 +214,11 @@ type # can keep mutating its still-live query targets writtenPackages: HashSet[string] depSuffixes: HashSet[string] # module suffixes already emitted as `(import ...)` deps - emittedBackendTypes: HashSet[int32] # backend-local type items already def'd this module + emittedBackendTypes: HashSet[(int32, int32)] # backend-local types already def'd this + # module, keyed by (kind, item): the NIF name is `t..@bk`, + # so two `@bk` types sharing an item but differing in kind (e.g. `int` + # and `typedesc[int]`, both item 12) are DISTINCT defs — keying by item + # alone deduped the second to a dangling `SymUse` (`symbol has no offset`). emittedBackendSyms: HashSet[int32] # backend-local sym items already def'd this module lowering: bool # serializing the `lower` stage's whole-module `.t.nif` emittedFieldSyms: HashSet[ItemId] # lowering: derived env-field syms already def'd @@ -232,6 +239,21 @@ proc isLocalSym(sym: PSym): bool {.inline.} = false const + FieldMarker = "`f" + ## Appended to the ident of `skField` symbols in NIF names. Object fields are + ## emitted as *local* symbols (NIF spec sense): ``f.` with NO + ## module suffix, so they get no index entry and are never registered in the + ## global `c.syms` name table. A field reference is a leaf — its C member name + ## is a deterministic function of `name.s` (`ccgtypes.mangleField`) and is + ## struct-scoped, and the field's type already rides on the `PNode` — so there + ## is nothing to resolve across modules: the use site just stubs a `skField` + ## from the local name. This removes the whole foreign-suffix pollution class + ## (a derived/captured env field minted under a foreign module suffix used to + ## corrupt the loader's name→buffer seek). The `` `f `` marker keeps the field's + ## local name in a namespace disjoint from proc-locals (backtick cannot appear + ## in a Nim identifier), so a field use can never be misrouted to a same-named + ## local var/param. Mirrors the `` `t `` (`typeToNifSym`) and `PkgMarker` + ## namespaces. PkgMarker = "`pkg" ## Appended to the ident of `skPackage` symbols in NIF names. A package sym ## has no module of its own: it is written once into every module NIF that @@ -246,6 +268,22 @@ const proc toNifSymName(w: var Writer; sym: PSym): string = ## Generate NIF name for a symbol: local names are `ident.disamb`, ## global names are `ident.disamb.moduleSuffix` + if sym.kindImpl == skField: + # Object fields are LOCAL symbols (no module suffix, no index entry, not in the + # global `c.syms`). See `FieldMarker`. The same `toNifSymName` call produces this + # name at both the reclist def site and every use site (same `PSym`), so they + # agree by construction; the loader recovers `name.s` and `mangleField` produces + # the matching struct member name regardless of which module references it. + result = sym.name.s + result.add FieldMarker + result.add '.' + # Use the field's POSITION as the local name's numeric component: it is unique + # within the owning type (so the local name is unambiguous there) AND it is what + # tuple element access reads off a use-site field stub (`genRecordField`'s + # tyTuple branch emits `Field$position`). Named-object field uses re-navigate by + # name, so position is only load-bearing for tuples — but carrying it is free. + result.addInt sym.positionImpl + return if sym.itemId.isBackendMinted: # Process-local backend sym (closure env field / hidden `:env` param minted # during a VM transform): re-home to the current module with the `@bk` @@ -315,6 +353,12 @@ proc parseSymName*(s: string): ParsedSymName = dec i return ParsedSymName(name: s, module: "") +proc isFieldNifName(name: string): bool {.inline.} = + ## True for an object field's local NIF name ``f.` (see + ## `FieldMarker`): no module suffix, marker on the ident. + let sn = parseSymName(name) + sn.module.len == 0 and sn.name.endsWith(FieldMarker) + proc stubKindAndName(cache: IdentCache; rawName: string): (TSymKind, PIdent) = ## The user-visible name of a symbol stub must NOT keep NIF-only name ## decorations: the `PkgMarker` of package symbols would otherwise leak into @@ -324,18 +368,70 @@ proc stubKindAndName(cache: IdentCache; rawName: string): (TSymKind, PIdent) = ## the marked NIF name for the index lookup. if rawName.endsWith(PkgMarker): (skPackage, cache.getIdent(rawName[0 ..< rawName.len - PkgMarker.len])) + elif rawName.endsWith(FieldMarker): + # Object field (local NIF symbol, see `FieldMarker`): strip the marker so the + # backend mangles the clean field name, and record the kind so a use-site stub + # is a real `skField` (cgen branches on it for `obj.field` access). + (skField, cache.getIdent(rawName[0 ..< rawName.len - FieldMarker.len])) else: (skStub, cache.getIdent(rawName)) -template buildTree(dest: var TokenBuf; tag: TagId; body: untyped) = +# --- nifcore writer adapter ------------------------------------------------- +# The `write*` procs build the module into an `IcBuilder` (nifcore). These give +# the IcBuilder the SAME call shapes the old `nifstreams` TokenBuf had (taking +# `nifstreams` TagId/SymId/PackedToken + a PackedLineInfo), so the writer bodies +# are unchanged apart from the `var TokenBuf` -> `var IcBuilder` parameter type. +# Line info is unpacked from the PackedLineInfo and re-emitted via +# `IcBuilder.lineInfo` (absolute file/line/col; nifcore makes it relative on +# serialization), exactly as the deleted `serializeViaNifcore` bridge did. + +proc emitInfo(b: var IcBuilder; info: PackedLineInfo) {.inline.} = + if info.isValid: + let u = unpack(pool.man, info) + let fname = if u.file.isValid: pool.files[u.file] else: "" + let cstr = if u.comment != 0'u32: pool.strings[nifstreams.StrId(u.comment)] else: "" + b.lineInfo(fname, u.line, u.col, cstr) + +proc addParLe(b: var IcBuilder; tag: nifstreams.TagId; info = NoLineInfo) = + b.openTag(pool.tags[tag]); b.emitInfo(info) +proc addParRi(b: var IcBuilder) {.inline.} = b.closeTag() +proc addSymDef(b: var IcBuilder; s: nifstreams.SymId; info = NoLineInfo) = + b.addSymDef(pool.syms[s]); b.emitInfo(info) +proc addSymUse(b: var IcBuilder; s: nifstreams.SymId; info = NoLineInfo) = + b.addSymUse(pool.syms[s]); b.emitInfo(info) + +proc add(b: var IcBuilder; t: PackedToken) = + ## Bridge a single old-API token constructor (symToken/strToken/floatToken/ + ## charToken) into the IcBuilder. + case t.kind + of DotToken: b.addDotToken() + of Ident: b.addIdent(pool.strings[t.litId]) + of Symbol: b.addSymUse(pool.syms[t.symId]) + of SymbolDef: b.addSymDef(pool.syms[t.symId]) + of IntLit: b.addIntLit(pool.integers[t.intId]) + of UIntLit: b.addUIntLit(pool.uintegers[t.uintId]) + of FloatLit: b.addFloatLit(pool.floats[t.floatId]) + of CharLit: b.addCharLit(char(t.uoperand)) + of StringLit: b.addStrLit(pool.strings[t.litId]) + else: discard + if t.kind != ParRi: b.emitInfo(t.info) + +template buildTree(dest: var IcBuilder; tag: nifstreams.TagId; body: untyped) = dest.addParLe tag body dest.addParRi -template buildTree(dest: var TokenBuf; tag: string; body: untyped) = - buildTree dest, pool.tags.getOrIncl(tag), body +template buildTree(dest: var IcBuilder; tag: string; body: untyped) = + dest.openTag tag + body + dest.closeTag() -proc writeFlags[E](dest: var TokenBuf; flags: set[E]) = +template buildTree(dest: var IcBuilder; tag: nifstreams.TagId; info: PackedLineInfo; body: untyped) = + dest.addParLe tag, info + body + dest.addParRi + +proc writeFlags[E](dest: var IcBuilder; flags: set[E]) = var flagsAsIdent = "" genFlags(flags, flagsAsIdent) if flagsAsIdent.len > 0: @@ -346,11 +442,11 @@ proc writeFlags[E](dest: var TokenBuf; flags: set[E]) = proc trLineInfo(w: var Writer; info: TLineInfo): PackedLineInfo {.inline.} = result = nifLineInfo(w.infos, info) -proc writeNode(w: var Writer; dest: var TokenBuf; n: PNode; forAst = false) -proc writeType(w: var Writer; dest: var TokenBuf; typ: PType) -proc writeSym(w: var Writer; dest: var TokenBuf; sym: PSym) +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) -proc writeLoc(w: var Writer; dest: var TokenBuf; loc: TLoc) = +proc writeLoc(w: var Writer; dest: var IcBuilder; loc: TLoc) = dest.addIdent toNifTag(loc.k) dest.addIdent toNifTag(loc.storage) writeFlags(dest, loc.flags) # TLocFlags @@ -371,7 +467,7 @@ proc nifTypeName(w: Writer; typ: PType): string = else: result = typeToNifSym(typ, w.infos.config) -proc writeTypeDef(w: var Writer; dest: var TokenBuf; typ: PType) = +proc writeTypeDef(w: var Writer; dest: var IcBuilder; typ: PType) = dest.buildTree tdefTag: dest.addSymDef pool.syms.getOrIncl(nifTypeName(w, typ)), NoLineInfo dest.addDotToken # always private for the index generator @@ -414,7 +510,7 @@ proc writeTypeDef(w: var Writer; dest: var TokenBuf; typ: PType) = writeType(w, dest, ch) -proc writeType(w: var Writer; dest: var TokenBuf; typ: PType) = +proc writeType(w: var Writer; dest: var IcBuilder; typ: PType) = if typ == nil: dest.addDotToken() elif typ.uniqueId.isBackendMinted: @@ -422,7 +518,7 @@ proc writeType(w: var Writer; dest: var TokenBuf; typ: PType) = # `@bk` def the first time it is reached in this module, reference it after. # Per-Writer dedup (NOT the shared `state`), since every referencing module # must emit its own copy. - if not w.emittedBackendTypes.containsOrIncl(typ.uniqueId.item): + if not w.emittedBackendTypes.containsOrIncl((ord(typ.kind).int32, typ.uniqueId.item)): writeTypeDef(w, dest, typ) else: dest.addSymUse pool.syms.getOrIncl(nifTypeName(w, typ)), NoLineInfo @@ -440,11 +536,11 @@ proc writeType(w: var Writer; dest: var TokenBuf; typ: PType) = else: dest.addSymUse pool.syms.getOrIncl(nifTypeName(w, typ)), NoLineInfo -proc writeBool(dest: var TokenBuf; b: bool) = +proc writeBool(dest: var IcBuilder; b: bool) = dest.buildTree (if b: "true" else: "false"): discard -proc writeLib(w: var Writer; dest: var TokenBuf; lib: PLib) = +proc writeLib(w: var Writer; dest: var IcBuilder; lib: PLib) = if lib == nil: dest.addDotToken() else: @@ -471,7 +567,7 @@ proc docOfSym(sym: PSym): string = return nodeCommentReader(body[0]) return "" -proc writeSymDef(w: var Writer; dest: var TokenBuf; sym: PSym) = +proc writeSymDef(w: var Writer; dest: var IcBuilder; sym: PSym) = dest.addParLe sdefTag, nifLineInfoWithComment(w.infos, sym.infoImpl, docOfSym(sym)) dest.addSymDef pool.syms.getOrIncl(w.toNifSymName(sym)), NoLineInfo # The `x` marker means "importable as a bare identifier into an importer's @@ -572,21 +668,22 @@ proc shouldWriteSymDef(w: var Writer; sym: PSym): bool {.inline.} = return true # Normal case for global symbols return false -proc isLoweredPerEntryField(w: Writer; sym: PSym): bool {.inline.} = - ## In the `lower` stage every entry (each `(lowered)` body AND each `@bk` type - ## def, which carries its fields inline) is loaded independently, so it must be - ## SELF-CONTAINED. A derived closure-env FIELD is module-homed (its id derives - ## from the captured local, NOT `@bk` — see itemids.derivedFieldId) so - ## `shouldWriteSymDef` would seal it after the first entry and later entries - ## would reference a def that their indexed copy does not contain. Re-emit it - ## as a full def per entry, deduped within the entry via `emittedFieldSyms`. - w.lowering and sym.kindImpl == skField and not sym.itemId.isBackendMinted +proc fieldDefHere(w: var Writer; sym: PSym): bool {.inline.} = + ## An object field is a LOCAL symbol (see `FieldMarker`): it is DEF'd exactly once + ## — inline in its owning type's reclist — and referenced as a bare `SymUse` + ## everywhere else (resolved by the consumer re-navigating the object type by + ## name; nothing else to recover). So write a def iff we are inside that reclist + ## (`inTypeReclist > 0`); `emittedFieldSyms` guards against a field appearing + ## twice in one reclist (e.g. a discriminant). Each type's reclist is thus + ## self-contained, which is what a seek-load of a single `.t.bif` type entry needs. + sym.kindImpl == skField and w.inTypeReclist > 0 and + not w.emittedFieldSyms.containsOrIncl(sym.itemId) -proc writeSym(w: var Writer; dest: var TokenBuf; sym: PSym) = +proc writeSym(w: var Writer; dest: var IcBuilder; sym: PSym) = if sym == nil: dest.addDotToken() - elif isLoweredPerEntryField(w, sym): - if not w.emittedFieldSyms.containsOrIncl(sym.itemId): + elif sym.kindImpl == skField: + if fieldDefHere(w, sym): writeSymDef(w, dest, sym) else: dest.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), NoLineInfo @@ -606,7 +703,7 @@ proc writeSym(w: var Writer; dest: var TokenBuf; sym: PSym) = # unlike what we do for types! dest.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), NoLineInfo -proc writeSymNode(w: var Writer; dest: var TokenBuf; n: PNode; sym: PSym) = +proc writeSymNode(w: var Writer; dest: var IcBuilder; n: PNode; sym: PSym) = if sym == nil: dest.addDotToken() return @@ -627,18 +724,13 @@ proc writeSymNode(w: var Writer; dest: var TokenBuf; n: PNode; sym: PSym) = # dedup shared with `writeSym`), regardless of module: their itemId.module is # the systemModule of `vmTransfIdgen`, so `shouldWriteSymDef` (which gates on # currentModule) would otherwise only ever emit a SymUse → dangling def. - let perEntryField = isLoweredPerEntryField(w, sym) - # A field reached while writing its own type's reclist MUST be a self-contained - # def: the type can be seek-loaded by name in isolation, so a deduped SymUse - # (whose def lives elsewhere in the entry) would resolve to nil. - let reclistField = w.lowering and w.inTypeReclist > 0 and sym.kindImpl == skField + let isField = sym.kindImpl == skField let wantDef = - if reclistField: true + if isField: fieldDefHere(w, sym) # def only inside the owning reclist (see fieldDefHere) elif sym.itemId.isBackendMinted: not w.emittedBackendSyms.containsOrIncl(sym.itemId.item) - elif perEntryField: not w.emittedFieldSyms.containsOrIncl(sym.itemId) else: shouldWriteSymDef(w, sym) if wantDef: - if not sym.itemId.isBackendMinted and not perEntryField and not reclistField: sym.state = Sealed + if not sym.itemId.isBackendMinted and not isField: sym.state = Sealed if w.infos.config.ideActive: w.writtenSyms.add sym if nodeTyp != n.sym.typImpl: dest.buildTree hiddenTypeTag, trLineInfo(w, n.info): @@ -650,17 +742,27 @@ proc writeSymNode(w: var Writer; dest: var TokenBuf; n: PNode; sym: PSym) = # NIF has direct support for symbol references so we don't need to use a tag here, # unlike what we do for types! let info = trLineInfo(w, n.info) - if nodeTyp != n.sym.typImpl: + # A field SymUse is a typeless leaf stub on load (its def lives in another seek), + # so it cannot supply a lazy type — carry its type EXPLICITLY via the hidden-type + # wrapper. `genFieldObjConstr`/object-init read `nField.typ` directly, so the node + # must keep it. A field-use node often has a nil node-type (the type lives on the + # sym), so fall back to the field sym's own type. + if isField: + let fieldTyp = if nodeTyp != nil: nodeTyp else: sym.typImpl + dest.buildTree hiddenTypeTag, info: + writeType(w, dest, fieldTyp) + dest.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), info + elif nodeTyp != n.sym.typImpl: dest.buildTree hiddenTypeTag, info: writeType(w, dest, nodeTyp) dest.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), info else: dest.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), info -proc writeNodeFlags(dest: var TokenBuf; flags: set[TNodeFlag]) {.inline.} = +proc writeNodeFlags(dest: var IcBuilder; flags: set[TNodeFlag]) {.inline.} = writeFlags(dest, flags) -template withNode(w: var Writer; dest: var TokenBuf; n: PNode; body: untyped) = +template withNode(w: var Writer; dest: var IcBuilder; n: PNode; body: untyped) = dest.addParLe pool.tags.getOrIncl(toNifTag(n.kind)), trLineInfo(w, n.info) writeNodeFlags(dest, n.flags) writeType(w, dest, n.typField) @@ -754,6 +856,13 @@ var reexpModTag = registerTag("reexpmod") var offerTag = registerTag("offer") var typeOfferTag = registerTag("toffer") var modulesrcTag = registerTag("modulesrc") +# `(unusedid )` — 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 +# hooks, temps) start ABOVE every loaded id — no `toId` collision is possible +# by construction (replaces relying on the `@bk` module-marker bit, which the +# loader dropped on type USES). Mirrors NIF's `.unusedname` directive. +var unusedIdTag = registerTag("unusedid") proc registerNifAstTags*() = ## (Re)registers ast2nif's NIF tags explicitly. The top-level `registerTag` @@ -788,7 +897,7 @@ proc registerNifAstTags*() = typeOfferTag = registerTag("toffer") modulesrcTag = registerTag("modulesrc") -proc writeNode(w: var Writer; dest: var TokenBuf; n: PNode; forAst = false) = +proc writeNode(w: var Writer; dest: var IcBuilder; n: PNode; forAst = false) = if n == nil: dest.addDotToken else: @@ -931,7 +1040,7 @@ proc writeNode(w: var Writer; dest: var TokenBuf; n: PNode; forAst = false) = for i in 0 ..< n.len: writeNode(w, dest, n[i], forAst) -proc writeGlobal(w: var Writer; dest: var TokenBuf; n: PNode) = +proc writeGlobal(w: var Writer; dest: var IcBuilder; n: PNode) = case n.kind of nkVarTuple: writeNode(w, dest, n) @@ -949,12 +1058,12 @@ proc writeGlobal(w: var Writer; dest: var TokenBuf; n: PNode) = else: discard -proc writeGlobals(w: var Writer; dest: var TokenBuf; n: PNode) = +proc writeGlobals(w: var Writer; dest: var IcBuilder; n: PNode) = w.withNode dest, n: for child in n: writeGlobal(w, dest, child) -proc writeToplevelNode(w: var Writer; dest, bottom: var TokenBuf; n: PNode) = +proc writeToplevelNode(w: var Writer; dest, bottom: var IcBuilder; n: PNode) = case n.kind of nkStmtList, nkStmtListExpr: for son in n: writeToplevelNode(w, dest, bottom, son) @@ -972,12 +1081,12 @@ proc writeToplevelNode(w: var Writer; dest, bottom: var TokenBuf; n: PNode) = else: writeNode w, dest, n -proc createStmtList(buf: var TokenBuf; info: PackedLineInfo) {.inline.} = +proc createStmtList(buf: var IcBuilder; info: PackedLineInfo) {.inline.} = buf.addParLe pool.tags.getOrIncl(toNifTag(nkStmtList)), info buf.addDotToken # flags buf.addDotToken # type -proc writeOp(w: var Writer; content: var TokenBuf; op: LogEntry) = +proc writeOp(w: var Writer; content: var IcBuilder; op: LogEntry) = case op.kind of HookEntry: case op.op @@ -1073,100 +1182,98 @@ proc writeOp(w: var Writer; content: var TokenBuf; op: LogEntry) = type CookieCtx = object selfSuffix: string - tdRanges: Table[SymId, int] # td sym -> start of its first (td ...) tree - memo: Table[SymId, string] # td sym -> structural digest - expanding: HashSet[SymId] # cycle guard for recursive td expansion + tdRanges: Table[uint32, int] # td sym -> start of its first (td ...) tree + memo: Table[uint32, string] # td sym -> structural digest + expanding: HashSet[uint32] # cycle guard for recursive td expansion depSuffixes: seq[string] # module suffixes of the direct imports -proc nextTree(buf: TokenBuf; i: int): int = +proc nextTree(flat: seq[CookieTok]; i: int): int = ## Index just past the atom or balanced subtree starting at `i`. result = i+1 - if buf[i].kind != ParLe: return + if flat[i].kind != ckParLe: return var nested = 0 var j = i - while j < buf.len: - case buf[j].kind - of ParLe: inc nested - of ParRi: + while j < flat.len: + case flat[j].kind + of ckParLe: inc nested + of ckParRi: dec nested if nested == 0: return j+1 else: discard inc j - result = buf.len + result = flat.len -proc updateAtom(s: var Sha1State; t: PackedToken) = +proc updateAtom(s: var Sha1State; t: CookieTok) = # mirrors nimony's nifchecksums.update: token content only, no line infos case t.kind - of ParLe: + of ckParLe: s.update "(" - s.update pool.tags[t.tagId] - of ParRi: s.update ")" - of Ident: + s.update t.tag + of ckParRi: s.update ")" + of ckIdent: s.update " " - s.update pool.strings[t.litId] - of StringLit: + s.update t.str + of ckStr: s.update " \"" - s.update pool.strings[t.litId] - of IntLit: + s.update t.str + of ckInt: s.update " " - s.update $pool.integers[t.intId] - of UIntLit: + s.update $t.ival + of ckUInt: s.update " " - s.update $pool.uintegers[t.uintId] - of FloatLit: + s.update $t.uval + of ckFloat: # hash the bit pattern, not a formatted float (no formatting variance) s.update " f" - s.update $cast[uint64](pool.floats[t.floatId]) - of CharLit: + s.update $cast[uint64](t.fval) + of ckChar: s.update " c" - s.update $t.uoperand - of DotToken: s.update "." - of UnknownToken: s.update "?" - of EofToken: s.update "!" - of Symbol, SymbolDef: discard "handled by hashRegion" + s.update $t.cval + of ckDot: s.update "." + of ckSym, ckSymDef: discard "handled by hashRegion" proc isModuleLocalName(c: CookieCtx; name: string): bool = let sn = parseSymName(name) result = sn.module.len == 0 or sn.module == c.selfSuffix -proc hashRegion(s: var Sha1State; c: var CookieCtx; buf: TokenBuf; +proc hashRegion(s: var Sha1State; c: var CookieCtx; flat: seq[CookieTok]; start, theEnd: int; skipFrom = -1; skipTo = -1; keepFirstDefLiteral = false) -proc expandTd(c: var CookieCtx; buf: TokenBuf; name: SymId): string = +proc expandTd(c: var CookieCtx; flat: seq[CookieTok]; name: uint32; nameStr: string): string = ## Structural digest of a module-local type def: hashes the `(td ...)` tree ## instead of the volatile `tK.item counter name. Memoized; cycles fall back ## to the literal name (sound — at worst a spurious cookie change). if c.memo.hasKey(name): return c.memo[name] if not c.tdRanges.hasKey(name) or c.expanding.contains(name): - return pool.syms[name] + return nameStr c.expanding.incl name let start = c.tdRanges[name] var sub = newSha1State() - hashRegion(sub, c, buf, start, nextTree(buf, start)) + hashRegion(sub, c, flat, start, nextTree(flat, start)) result = "&" & $SecureHash(sub.finalize()) c.expanding.excl name c.memo[name] = result -proc hashRegion(s: var Sha1State; c: var CookieCtx; buf: TokenBuf; +proc hashRegion(s: var Sha1State; c: var CookieCtx; flat: seq[CookieTok]; start, theEnd: int; skipFrom = -1; skipTo = -1; keepFirstDefLiteral = false) = # pass 1: assign ordinals to every symbol DEFINED in the hashed region # (params, locals, embedded type defs). The region's own top-level name # (first SymbolDef) stays literal when requested — it is what importers # reference. - var ords = initTable[SymId, int]() + var ords = initTable[uint32, int]() var first = keepFirstDefLiteral var i = start while i < theEnd: if i == skipFrom: i = skipTo continue - if buf[i].kind == SymbolDef: - let sym = buf[i].symId + if flat[i].kind == ckSymDef: + let sym = flat[i].sym if first: first = false - elif isModuleLocalName(c, pool.syms[sym]) and not ords.hasKey(sym): + elif isModuleLocalName(c, flat[i].name) and not ords.hasKey(sym): ords[sym] = ords.len inc i # pass 2: hash @@ -1176,31 +1283,31 @@ proc hashRegion(s: var Sha1State; c: var CookieCtx; buf: TokenBuf; if i == skipFrom: i = skipTo continue - let t = buf[i] - if t.kind in {Symbol, SymbolDef}: - let sym = t.symId - let name = pool.syms[sym] - s.update(if t.kind == SymbolDef: " :" else: " ") - if t.kind == SymbolDef and first: + let t = flat[i] + if t.kind in {ckSym, ckSymDef}: + let sym = t.sym + let name = t.name + s.update(if t.kind == ckSymDef: " :" else: " ") + if t.kind == ckSymDef and first: first = false s.update name elif ords.hasKey(sym): s.update "%" s.update $ords[sym] elif name.startsWith("`t") and isModuleLocalName(c, name): - s.update expandTd(c, buf, sym) + s.update expandTd(c, flat, sym, name) else: s.update name else: updateAtom s, t inc i -proc cookieSd(s: var Sha1State; c: var CookieCtx; buf: TokenBuf; start: int): int = +proc cookieSd(s: var Sha1State; c: var CookieCtx; flat: seq[CookieTok]; start: int): int = ## Contributes one `(sd ...)` subtree to the cookie; returns the index past it. - result = nextTree(buf, start) - if buf[start+1].kind != SymbolDef: return - let marker = buf[start+2] - if not (marker.kind == Ident and pool.strings[marker.litId] == "x"): + result = nextTree(flat, start) + if flat[start+1].kind != ckSymDef: return + let marker = flat[start+2] + if not (marker.kind == ckIdent and marker.str == "x"): return # not importable -> invisible to dependents' sem (nimony parity) # field layout, see writeSymDef: kind magic flags options offset position # annex type owner ast loc constraint instantiatedFrom @@ -1208,10 +1315,10 @@ proc cookieSd(s: var Sha1State; c: var CookieCtx; buf: TokenBuf; start: int): in var i = start + 3 for f in 0 ..< 13: fields[f] = i - i = nextTree(buf, i) + i = nextTree(flat, i) var kind = skUnknown {.cast(uncheckedAssign).}: - kind = parse(TSymKind, pool.tags[buf[fields[0]].tagId]) + kind = parse(TSymKind, flat[fields[0]].tag) var skipFrom = -1 var skipTo = -1 if kind in routineKinds: @@ -1225,53 +1332,53 @@ proc cookieSd(s: var Sha1State; c: var CookieCtx; buf: TokenBuf; start: int): in # nifc backend's transitive NIF-mtime invalidation re-codegens their users. # So no routine body needs to live in the iface cookie. let ast = fields[9] - if buf[ast].kind == ParLe: + if flat[ast].kind == ckParLe: # skip son `bodyPos` (6) of the routine ast tree; NOT the last element — # sem appends the result sym at `resultPos` (7) after the body. - let astEnd = nextTree(buf, ast) + let astEnd = nextTree(flat, ast) var p = ast + 1 # the flags atom var ok = true for _ in 0 ..< 2 + bodyPos: # flags, type, sons 0..5 - p = nextTree(buf, p) + p = nextTree(flat, p) if p >= astEnd - 1: ok = false break if ok: skipFrom = p - skipTo = nextTree(buf, p) + skipTo = nextTree(flat, p) # non-routine kinds (consts carry their value, types their structure incl. # default field values): hash everything. - hashRegion(s, c, buf, start, result, skipFrom, skipTo, keepFirstDefLiteral = true) + hashRegion(s, c, flat, start, result, skipFrom, skipTo, keepFirstDefLiteral = true) -proc scanStmtsForCookie(s: var Sha1State; c: var CookieCtx; buf: TokenBuf) = +proc scanStmtsForCookie(s: var Sha1State; c: var CookieCtx; flat: seq[CookieTok]) = ## Walks the whole written module, hashing only the importer-visible pieces; ## unknown structure is descended into (var/let/type section wrappers, ## top-level code) but contributes nothing itself — nimony-style. - let exportTag = pool.tags.getOrIncl(toNifTag(nkExportStmt)) - let exportExceptTag = pool.tags.getOrIncl(toNifTag(nkExportExceptStmt)) + let exportName = toNifTag(nkExportStmt) + let exportExceptName = toNifTag(nkExportExceptStmt) var i = 0 - while i < buf.len: - let t = buf[i] - if t.kind == ParLe: - let tid = t.tagId - if tid == sdefTag: - i = cookieSd(s, c, buf, i) - elif tid == implTag: - i = nextTree(buf, i) - elif tid == replayTag or tid == repConverterTag or tid == repDestroyTag or - tid == repWasMovedTag or tid == repCopyTag or tid == repSinkTag or - tid == repDupTag or tid == repTraceTag or tid == repDeepCopyTag or - tid == repEnumToStrTag or tid == repMethodTag or - tid == exportTag or tid == exportExceptTag or tid == includeTag: - let e = nextTree(buf, i) - hashRegion(s, c, buf, i, e) + while i < flat.len: + let t = flat[i] + if t.kind == ckParLe: + let tg = t.tag + if tg == symDefTagName: + i = cookieSd(s, c, flat, i) + elif tg == "implementation": + i = nextTree(flat, i) + elif tg == "replay" or tg == "repconverter" or tg == "repdestroy" or + tg == "repwasmoved" or tg == "repcopy" or tg == "repsink" or + tg == "repdup" or tg == "reptrace" or tg == "repdeepcopy" or + tg == "repenumtostr" or tg == "repmethod" or + tg == exportName or tg == exportExceptName or tg == "include": + let e = nextTree(flat, i) + hashRegion(s, c, flat, i, e) i = e - elif tid == importTag: - let e = nextTree(buf, i) - hashRegion(s, c, buf, i, e) + elif tg == "import": + let e = nextTree(flat, i) + hashRegion(s, c, flat, i, e) for j in i ..< e: - if buf[j].kind == StringLit: - let suffix = pool.strings[buf[j].litId] + if flat[j].kind == ckStr: + let suffix = flat[j].str if suffix notin c.depSuffixes: c.depSuffixes.add suffix i = e else: @@ -1288,41 +1395,43 @@ proc icGroupSuffixes(config: ConfigRef): HashSet[string] = result.incl cachedModuleSuffix(config, fileInfoIdx(config, AbsoluteFile p)) proc writeCookieFile(config: ConfigRef; selfSuffix, tag, hex, ext: string) = - var dest = createTokenBuf(4) - dest.addParLe pool.tags.getOrIncl(tag), NoLineInfo - dest.addStrLit hex - dest.addParRi + # Binary NIF cookie `(tag "hex")`, content-stable so an unchanged hash keeps the + # sidecar mtime (nifmake prunes the re-sem cascade behind it). + var b = newIcBuilder(4) + b.openTag tag + b.addStrLit hex + b.closeTag() let path = toGeneratedFile(config, AbsoluteFile(selfSuffix), ext).string - writeFile(dest, path, OnlyIfChanged) + storeBifStable(b, path, "." & extractModuleSuffix(path)) -proc writeIfaceCookie(config: ConfigRef; thisModule: int32; buf: TokenBuf): string = +proc writeIfaceCookie(config: ConfigRef; thisModule: int32; flat: seq[CookieTok]): string = let selfSuffix = modname(thisModule, config) var c = CookieCtx(selfSuffix: selfSuffix) # pre-pass: first (td ...) occurrence per type name, wherever it is embedded var i = 0 - while i < buf.len: - if buf[i].kind == ParLe and buf[i].tagId == tdefTag and i+1 < buf.len and - buf[i+1].kind == SymbolDef: - let nm = buf[i+1].symId + while i < flat.len: + if flat[i].kind == ckParLe and flat[i].tag == typeDefTagName and i+1 < flat.len and + flat[i+1].kind == ckSymDef: + let nm = flat[i+1].sym if not c.tdRanges.hasKey(nm): c.tdRanges[nm] = i inc i var s = newSha1State() - scanStmtsForCookie(s, c, buf) + scanStmtsForCookie(s, c, flat) # chain the direct deps' cookies; co-members of an --icGroup cycle are # excluded (their sidecars are being produced by this very rule — chaining # them would make the hash depend on within-group write order). let groupSuffixes = icGroupSuffixes(config) for dep in c.depSuffixes: if dep == selfSuffix or dep in groupSuffixes: continue - let depIface = toGeneratedFile(config, AbsoluteFile(dep), ".iface.nif").string + let depIface = toGeneratedFile(config, AbsoluteFile(dep), ".iface.bif").string s.update "|" s.update dep s.update ":" s.update(try: readFile(depIface) except IOError, OSError: "") result = $SecureHash(s.finalize()) - writeCookieFile(config, selfSuffix, "iface", result, ".iface.nif") + writeCookieFile(config, selfSuffix, "iface", result, ".iface.bif") -proc writeImplCookie(config: ConfigRef; thisModule: int32; buf: TokenBuf; +proc writeImplCookie(config: ConfigRef; thisModule: int32; flat: seq[CookieTok]; ifaceHex: string) = ## The implementation cookie: a line-info-free hash of the module's ENTIRE ## serialized content (private defs and routine bodies included), with the @@ -1336,16 +1445,15 @@ proc writeImplCookie(config: ConfigRef; thisModule: int32; buf: TokenBuf; ## body change is exactly what NeedsImpl dependents must see. let selfSuffix = modname(thisModule, config) var s = newSha1State() - for i in 0 ..< buf.len: - let t = buf[i] - if t.kind in {Symbol, SymbolDef}: - s.update(if t.kind == SymbolDef: " :" else: " ") - s.update pool.syms[t.symId] + for t in flat: + if t.kind in {ckSym, ckSymDef}: + s.update(if t.kind == ckSymDef: " :" else: " ") + s.update t.name else: updateAtom s, t s.update "|iface:" s.update ifaceHex - writeCookieFile(config, selfSuffix, "impl", $SecureHash(s.finalize()), ".impl.nif") + writeCookieFile(config, selfSuffix, "impl", $SecureHash(s.finalize()), ".impl.bif") proc writeEdgesFile(config: ConfigRef; thisModule: int32; implDeps: seq[int]) = ## Records which modules' bodies this compilation consumed at compile time @@ -1362,20 +1470,21 @@ proc writeEdgesFile(config: ConfigRef; thisModule: int32; implDeps: seq[int]) = continue if suffix notin suffixes: suffixes.add suffix sort suffixes - var dest = createTokenBuf(4 + 2*suffixes.len) - dest.addParLe pool.tags.getOrIncl("edges"), NoLineInfo + # Native nifcore writer (Stage 2): `(edges "suffix" ...)`, byte-identical. + var b = newIcBuilder(4 + 2*suffixes.len) + b.openTag "edges" for suffix in suffixes: - dest.addStrLit suffix - dest.addParRi - let path = toGeneratedFile(config, AbsoluteFile(selfSuffix), ".edges.nif").string + b.addStrLit suffix + b.closeTag() + let path = toGeneratedFile(config, AbsoluteFile(selfSuffix), ".edges.bif").string # Deliberately ALWAYS written (unlike every other output of the nim_m rule): # nothing gates on this file's mtime — deps.nim only reads its content — so # it doubles as the rule's freshness stamp. nifmake's `needsRebuild` takes # the freshest output as proof of "ran since the inputs changed"; without an # always-written output a rule whose re-run produces only content-identical # (mtime-preserved) files would re-fire on every warm build (e.g. after an - # edit was reverted). Nimony's analog is its always-written `.s.nif`. - writeFile(dest, path) + # edit was reverted). Nimony's analog is its always-written `.s.bif`. + storeBif(b, path, "." & extractModuleSuffix(path)) proc writeSemDeps*(config: ConfigRef; thisModule: int32; importPaths: seq[string]) = ## The module's REAL direct imports as `nim m` sem resolved them — static @@ -1384,16 +1493,11 @@ proc writeSemDeps*(config: ConfigRef; thisModule: int32; importPaths: seq[string ## missed become new nodes (replacing the old build-failure discovery loop), ## and `when false` imports the scanner over-included are pruned. Always ## written so it is current after every successful sem (like `.edges`). - let selfSuffix = modname(thisModule, config) - var paths = importPaths - sort paths - var dest = createTokenBuf(4 + 2*paths.len) - dest.addParLe pool.tags.getOrIncl("semdeps"), NoLineInfo - for p in paths: - dest.addStrLit p - dest.addParRi - let path = toGeneratedFile(config, AbsoluteFile(selfSuffix), ".s.deps.nif").string - writeFile(dest, path) + ## + ## Ported to nifcore: delegates to `icnifcore.writeSemDeps` (Stage 1 of the + ## NIF-stack migration; see doc/ic_nifcore_port.md). Output is byte-identical + ## to the previous `nifstreams` writer. + icnifcore.writeSemDeps(config, thisModule, importPaths) proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode; opsLog: seq[LogEntry]; @@ -1404,9 +1508,11 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode; concreteTypes: seq[PType]; genericParamsCount: int]] = @[]; typeOffers: seq[tuple[generic: PSym; inst: PType]] = @[]; - resolvedImportDeps: seq[FileIndex] = @[]) = + resolvedImportDeps: seq[FileIndex] = @[]; + firstUnusedId: int32 = 0) = var w = Writer(infos: LineInfoWriter(config: config), currentModule: thisModule) - var content = createTokenBuf(300) + w.deps = newIcBuilder(64) + var content = newIcBuilder(300) let rootInfo = trLineInfo(w, n.info) createStmtList(content, rootInfo) @@ -1422,7 +1528,7 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode; if op.module == thisModule.int: writeOp(w, content, op) - var bottom = createTokenBuf(300) + var bottom = newIcBuilder(300) w.writeToplevelNode content, bottom, n # Resolved import edges that left no syntactic `import` node in the top-level @@ -1526,19 +1632,21 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode; # bottom of the file is the implementation of the module: content.addParLe implTag, NoLineInfo content.addParRi() - content.add bottom + addAll(content, bottom) content.addParRi() let m = modname(w.currentModule, w.infos.config) - let nifFilename = AbsoluteFile(m).changeFileExt(".s.nif") - let d = completeGeneratedFilePath(config, nifFilename).string + let bifPath = completeGeneratedFilePath(config, AbsoluteFile(m).changeFileExt(".s.bif")).string - var dest = createTokenBuf(600) + var dest = newIcBuilder(600) createStmtList(dest, rootInfo) - dest.add w.deps + # First child: the backend id seed (see `(unusedid)` / readUnusedId). + dest.addParLe unusedIdTag, NoLineInfo + dest.addIntLit firstUnusedId.int64 + dest.addParRi() + addAll(dest, w.deps) # do not write the (stmts .. ) wrapper: - for i in 3 ..< content.len-1: - dest.add content[i] + addStmtsBody(dest, content) # ensure the hooks we announced end up in the NIF file regardless of # whether they have been used: @@ -1567,73 +1675,86 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode; # mtime-based `needsRebuild` then prunes the rebuild cascade level by # level, and the nifc backend can trust "semmed NIF older than the cnif # artifact" as an honest per-module unchanged stamp. - writeFile(dest, d, OnlyIfChanged) + # CONTENT-STABLE (`storeBifStable`, the bif analogue of the old text `.s.nif`'s + # `OnlyIfChanged`): when `nim m` re-runs (e.g. it was scheduled because a sibling + # input churned) but produces byte-identical sem output, the `.s.bif` mtime MUST + # be preserved, else every dependent backend stage sees its input as "newer" and + # rebuilds — with whatever compiler this run uses. In a self-rebuild (`bootic`) + # that re-translates only SOME modules with the new compiler while others reuse + # the prior compiler's artifacts → a MIXED binary that needs a 3rd fixed-point + # iteration to wash out. The `nim m` rule still has its always-written run-marker: + # the `.edges.bif` (writeEdgesFile), so a no-op re-run does not re-fire. + # Step 3: emit the compact binary NIF as the SOLE on-disk module artifact (no + # text `.s.nif` twin — writing two files per module only slows the build; debug + # a `.bif` via `tools/bif2nif`). Re-homed into a private fresh pool + # so the file holds only THIS module's literals. + storeBifStable(dest, bifPath, "." & extractModuleSuffix(bifPath)) if not isDefined(config, "icNoIfaceGate"): - let ifaceHex = writeIfaceCookie(config, thisModule, dest) - writeImplCookie(config, thisModule, dest, ifaceHex) + var flat = flattenForCookie(dest) + let ifaceHex = writeIfaceCookie(config, thisModule, flat) + writeImplCookie(config, thisModule, flat, ifaceHex) writeEdgesFile(config, thisModule, implDeps) # --------------------------- Loader (lazy!) ----------------------------------------------- -proc nodeKind(n: Cursor): TNodeKind {.inline.} = - assert n.kind == ParLe - parse(TNodeKind, pool.tags[n.tagId]) +# Step 2b reader shims over nifcore cursors: +template info(n: Cursor): NifLineInfo = rawLineInfo(n) + ## line info of the token at `n` (was the inline `n.info` of nifcursors). +template cursorTag(n: Cursor): string = n.tags.tagName(cursorTagId(n)) + ## tag name of the TagLit at `n` — for VALUE uses only (parse into an enum, + ## error messages). For tag *checks* use `tagIs`. Resolved via the cursor's OWN + ## tag pool (`n.tags`), not the shared `icTags`: a `bif`-loaded module carries a + ## fresh per-file tag pool whose ids only line up with its own `tagName`. +template tagIs(n: Cursor; name: string): bool = n.tags.tagName(cursorTagId(n)) == name + ## True iff `n`'s TagLit is the IC tag `name`. A string compare against the + ## cursor's own tag pool — id comparison against a process-global cache is + ## impossible once `bif` mints fresh per-file tag pools (ids are per-pool). -proc expect(n: Cursor; k: set[NifKind]) = +proc nodeKind(n: Cursor): TNodeKind {.inline.} = + assert n.kind == TagLit + parse(TNodeKind, cursorTag(n)) + +proc expect(n: Cursor; k: set[nifcore.NifKind]) = if n.kind notin k: when defined(debug): writeStackTrace() - quit "[NIF decoder] expected: " & $k & " but got: " & $n.kind & toString n + quit "[NIF decoder] expected: " & $k & " but got: " & $n.kind -proc expect(n: Cursor; k: NifKind) {.inline.} = +proc expect(n: Cursor; k: nifcore.NifKind) {.inline.} = expect n, {k} -proc incExpect(n: var Cursor; k: set[NifKind]) = - inc n - expect n, k - -proc incExpect(n: var Cursor; k: NifKind) {.inline.} = - incExpect n, {k} - -proc skipParRi(n: var Cursor) = - expect n, {ParRi} - inc n - proc firstSon*(n: Cursor): Cursor {.inline.} = + ## Non-consuming peek at the first child of a TagLit. The `inc` is on a copy, + ## so it never advances the caller's cursor. result = n inc result -proc expectTag(n: Cursor; tagId: TagId) = - if n.kind == ParLe and n.tagId == tagId: - discard - else: - when defined(debug): - writeStackTrace() - if n.kind != ParLe: - quit "[NIF decoder] expected: ParLe but got: " & $n.kind & toString n - else: - quit "[NIF decoder] expected: " & pool.tags[tagId] & " but got: " & pool.tags[n.tagId] & toString n - -proc incExpectTag(n: var Cursor; tagId: TagId) = - inc n - expectTag(n, tagId) - proc loadBool(n: var Cursor): bool = - if n.kind == ParLe: - result = pool.tags[n.tagId] == "true" - inc n - skipParRi n + if n.kind == TagLit: + result = tagIs(n, "true") + n.into: + discard else: raiseAssert "(true)/(false) expected" type NifModule = ref object - stream: nifstreams.Stream - symCounter: int32 - index: Table[string, NifIndexEntry] # Simple embedded index for offsets + buf: TokenBuf # the WHOLE module, parsed eagerly (Step 2: replaces the + # lazy byte-offset stream entirely — symbol/type loading + # AND the body reader now cursor over this resident buffer) + symCounter: int32 # seeded from the file's `(unusedid)` so backend syms + # start above every frontend/lowered id (no collision) + typeCounter: int32 # ditto for backend TYPES (closure envs etc.) + index: Table[string, NifIndexEntry] # name -> entry; `offset` is a TOKEN + # position in `buf` (was a byte offset) suffix: string - contentStart: int # stream offset of the module body, so a full-AST load can - # rewind after lazy symbol loads moved the cursor + loweredPrimary: bool # `buf` is the lowered `.t.bif` (cg/emit stage). The lower + # stage never changes type DEFINITIONS, so they are NOT + # carried in `.t.bif`; a type def not in `index` is read + # from the `.s.bif` companion below (loaded on demand). + semBuf: TokenBuf # the `.s.bif` (semchecked) buffer — TYPE-def fallback + semIndex: Table[string, NifIndexEntry] + semTried: bool # `semBuf`/`semIndex` load attempted (idempotent) DecodeContext* = object infos: LineInfoWriter @@ -1651,6 +1772,40 @@ proc createDecodeContext*(config: ConfigRef; cache: IdentCache): DecodeContext = ## Supposed to be a global variable result = DecodeContext(infos: LineInfoWriter(config: config), cache: cache) +proc nextBackendSymItem*(c: var DecodeContext; module: int32): int32 = + ## Allocate the next backend-minted SYM item for `module` from the SAME + ## per-module counter the loader uses when it re-homes `@bk` syms loaded from + ## the module's `.t.bif` (loadSymStub/extractLocalSymsFromTree). The `lower` + ## stage serializes its lifted hooks/temps as `@bk` syms, and cg mints MORE + ## backend syms (RTTI destroy wrappers, ...) into the same module. Both are + ## keyed by `.id` (= `toId(itemId)`) in `declaredThings`/`declaredProtos`, so + ## if the two id producers (the loader's `symCounter` and cg's idgen) ran + ## independently they could mint the same item: e.g. a `rttiDestroy` wrapper + ## and the very `=destroy` hook it wraps both land on backend item 21 -> one + ## masks the other in `declaredThings` -> the hook's body is never emitted -> + ## "undefined reference" at link. Drawing every backend sym from this one + ## counter keeps them disjoint. Returns -1 if the module is not loaded yet + ## (then the caller falls back to the idgen's own counter — only reachable + ## for sem-time `@bk` minting, whose module is never loaded in that process). + let fi = module.FileIndex + if not c.mods.hasKey(fi): return -1'i32 + let p = addr c.mods[fi].symCounter + inc p[] + result = p[] + +proc nextBackendTypeItem*(c: var DecodeContext; module: int32): int32 = + ## TYPE analogue of `nextBackendSymItem`: the `lower`/`cg` stages mint fresh + ## backend TYPES (closure-env objects, ptr wrappers) whose itemId must not + ## collide with the module's loaded types. Drawn from the per-module + ## `typeCounter`, which `moduleId` seeds from the file's `(unusedid)` so the + ## first minted type sits ABOVE every frontend/lowered type item. Returns -1 + ## if the module is not loaded (caller falls back to the idgen's own counter). + let fi = module.FileIndex + if not c.mods.hasKey(fi): return -1'i32 + let p = addr c.mods[fi].typeCounter + inc p[] + result = p[] + proc setMainModule*(c: var DecodeContext; fileIdx: FileIndex) = ## Records the module that is being compiled fresh so that re-exports of its ## own symbols by dependencies are not turned into duplicate stubs. @@ -1672,63 +1827,80 @@ proc loadedState(c: DecodeContext): ItemState {.inline.} = ## must stay `Sealed` so accidental mutations are caught. if c.infos.config.cmd == cmdNifC or c.infos.config.ideActive: Complete else: Sealed -proc cursorFromIndexEntry(c: var DecodeContext; module: FileIndex; entry: NifIndexEntry; - buf: var TokenBuf): Cursor = - let s = addr c.mods[module].stream - s.r.jumpTo entry.offset - # A seek-load is self-contained: its tokens must decode their relative line - # info against `entry.info` ALONE. The stream's `parents` stack can be left at - # depth >1 by a prior non-seek read (e.g. loadNifModule reads `(stmts`/ - # `(implementation` without consuming their `)`), and `parse` only overwrites - # parents[0] while `rawNext` reads parents[^1] — so a stale top entry (the last - # symbol decoded, e.g. excpt.nim:746) would become the base. This is benign on - # a freshly-opened stream (cold/stdin) but corrupts EPC recompiles, which reuse - # the per-module stream (the global DecodeContext survives resetAllModules). - # Collapse the stack so parse's parentInfo is the sole base. - s[].parents.setLen 1 - nifcursors.parse(s[], buf, entry.info) - result = cursorAt(buf, 0) +proc cursorFromIndexEntry(c: var DecodeContext; module: FileIndex; entry: NifIndexEntry): Cursor = + ## Step 2a: O(1) cursor into the module's resident `buf` at the def's token + ## position. No I/O, no per-symbol materialization — and, because each call + ## returns an INDEPENDENT cursor, none of the old stream-cursor clobber hazards + ## (the `jumpTo(saved)` save/restore dance) apply anymore. + result = cursorAt(c.mods[module].buf, entry.offset) type LoadFlag* = enum LoadFullAst, AlwaysLoadInterface -proc readEmbeddedIndex(s: var Stream): Table[string, NifIndexEntry] = - ## Reads the simple embedded index (index (kv sym offset)...) from indexStartsAt position. +proc isGlobalIndexSym(s, dottedSuffix: string): bool = + ## Mirror of `nifbuilder.addSymbolDefRetIsGlobal` / `bif.isGlobalSymbol`: a sym + ## gets an index entry when its name — with a self-module `dottedSuffix` + ## compressed to one trailing dot — has >= 2 dots (counting from index 1). + var lim = s.len + if dottedSuffix.len > 0 and s.endsWith(dottedSuffix): + lim = s.len - dottedSuffix.len + 1 + if lim > s.len: lim = s.len + var dots = 0 + for i in 1 ..< lim: + if s[i] == '.': inc dots + dots >= 2 + +proc buildPosIndex(buf: var TokenBuf; suffix: string): Table[string, NifIndexEntry] = + ## Step 2a token-position index: scan the eagerly-parsed module `buf` for the + ## global `SymbolDef`s it OWNS and record each at the token position of its + ## enclosing tag (`(sd`/`(td`), with visibility from the marker that follows + ## the def. Replaces `readEmbeddedIndex` (whose byte offsets are meaningless + ## once the file is parsed); mirrors `bif.buildIndex` and the text writer's + ## `(.index …)`. Foreign symbols appear only as `Symbol` uses (never + ## `SymbolDef`s) so they are naturally excluded. result = initTable[string, NifIndexEntry]() - let indexPos = indexStartsAt(s.r) - if indexPos <= 0: - return - let contentPos = offset(s.r) # Save position - s.r.jumpTo(indexPos) + let dotted = "." & suffix + if buf.len == 0: return + var c = buf.beginRead() + var mostRecentTagPos = 0 + while c.hasMore: + case c.kind + of TagLit: + mostRecentTagPos = cursorToPosition(buf, c) + inc c # descend into the body (visit every token) + of SymbolDef: + let nm = symName(c) + let tagPos = mostRecentTagPos + inc c # advance to the marker / next sibling + if isGlobalIndexSym(nm, dotted): + let vis = if c.hasMore and c.kind == DotToken: Hidden else: Exported + result[nm] = NifIndexEntry(offset: tagPos, info: NoLineInfo, vis: vis) + else: + inc c - var previousOffset = 0 - var t = next(s) - let exportedTagId = pool.tags.getOrIncl("x") - if t.kind == ParLe and pool.tags[t.tagId] == ".index": - t = next(s) - while t.kind != EofToken and t.kind != ParRi: - if t.kind == ParLe: - let vis = if t.tagId == exportedTagId: Exported else: Hidden - let info = t.info - t = next(s) # skip (kv - var key = "" - if t.kind == Symbol: - key = pool.syms[t.symId] - elif t.kind == Ident: - key = pool.strings[t.litId] - t = next(s) # skip symbol - if t.kind == IntLit: - let offset = int(pool.integers[t.intId]) + previousOffset - result[key] = NifIndexEntry(offset: offset, info: info, vis: vis) - previousOffset = offset - t = next(s) # skip offset - if t.kind == ParRi: - t = next(s) # skip ) +proc readUnusedId(buf: var TokenBuf): int32 = + ## Find the module's `(unusedid )` directive — emitted as the FIRST child + ## of the top-level `(stmts ...)` by writeNifModule/writeLoweredModule — and + ## return its value (the first free itemId). 0 if absent (older artifact: the + ## backend then falls back to its own un-seeded counter, i.e. pre-`unusedid` + ## behaviour). + result = 0'i32 + if buf.len == 0: return + var c = buf.beginRead() + if c.kind != TagLit: return # outermost (stmts ...) + inc c # descend into stmts body + while c.hasMore: + if c.kind == TagLit: + if tagName(c.tags, c.cursorTagId) == "unusedid": + inc c # into the unusedid body + if c.hasMore and c.kind == IntLit: + result = int32 intVal(c) + return else: - t = next(s) - - s.r.jumpTo(contentPos) # Restore position + skip c # not it; skip this whole subtree + else: + inc c proc moduleId(c: var DecodeContext; suffix: string; flags: set[LoadFlag] = {}): FileIndex = var isKnownFile = false @@ -1738,17 +1910,34 @@ proc moduleId(c: var DecodeContext; suffix: string; flags: set[LoadFlag] = {}): # but haven't had their NIF index loaded yet let hasEntry = c.mods.hasKey(result) if not hasEntry or AlwaysLoadInterface in flags: - let modFile = (getNimcacheDir(c.infos.config) / RelativeFile(suffix & ".s.nif")).string + # Module artifacts are binary NIF (`.bif`). The `cg`/`emit` backend stages + # load the LOWERED whole-module `.t.bif` (transformed bodies + lambda-lifted + # signatures/entities baked in by the `lower` stage — see writeLoweredModule); + # the `lower` stage and the frontend (`cmdM`) load the semchecked `.s.bif`. + # This mirrors `toNifFilename` (kept in sync). `bif.load` mints FRESH per-file + # pools, so the buffer's literals/tags resolve through its own + # `cursorPool(n)`/`n.tags` (the reader is pool-agnostic); the token-position + # index is rebuilt name-based via `buildPosIndex`. + let conf = c.infos.config + let useLowered = conf.cmd == cmdNifC and + (conf.icBackendStage == "cg" or conf.icBackendStage == "emit") + var modFile = (getNimcacheDir(conf) / RelativeFile(suffix & ".t.bif")).string + let lowered = useLowered and fileExists(modFile) + if not lowered: + modFile = (getNimcacheDir(conf) / RelativeFile(suffix & ".s.bif")).string if not fileExists(modFile): raiseAssert "NIF file not found for module suffix '" & suffix & "': " & modFile & ". This can happen when loading a module from NIF that references another module " & "whose NIF file hasn't been written yet." - var stream = nifstreams.open(modFile) - let index = readEmbeddedIndex(stream) - # `readEmbeddedIndex` leaves the cursor at the start of the module body. - let contentStart = offset(stream.r) - c.mods[result] = NifModule(stream: stream, index: index, suffix: suffix, - contentStart: contentStart) + var m = bif.load(modFile) + let index = buildPosIndex(m.buf, suffix) + # Seed the backend id counters ABOVE every id the file already uses, so a + # freshly-minted backend sym/type (closure env, RTTI hook, temp) can never + # share a `toId` with a loaded one. See `readUnusedId` / `(unusedid)`. + let seed = readUnusedId(m.buf) + c.mods[result] = NifModule(buf: ensureMove m.buf, index: index, suffix: suffix, + symCounter: seed, typeCounter: seed, + loweredPrimary: lowered) proc getOffset(c: var DecodeContext; module: FileIndex; nifName: string): NifIndexEntry = let ii = addr c.mods[module].index @@ -1756,6 +1945,44 @@ proc getOffset(c: var DecodeContext; module: FileIndex; nifName: string): NifInd if result.offset == 0: raiseAssert "symbol has no offset: " & nifName +proc ensureSemBuf(c: var DecodeContext; module: FileIndex) = + ## Lazily load the module's `.s.bif` companion (`semBuf`/`semIndex`) for the TYPE + ## fallback. Only meaningful when the primary `buf` is the lowered `.t.bif`, which + ## omits frontend type defs (the lower stage never changes them). Idempotent. + let m = c.mods[module] + if m.semTried: return + m.semTried = true + let semFile = (getNimcacheDir(c.infos.config) / RelativeFile(m.suffix & ".s.bif")).string + if not fileExists(semFile): return + var sm = bif.load(semFile) + m.semIndex = buildPosIndex(sm.buf, m.suffix) + m.semBuf = ensureMove sm.buf + +proc hasTypeOffset(c: var DecodeContext; module: FileIndex; nifName: string): bool = + ## Does a TYPE def for `nifName` exist for `module` — in the primary buffer, or + ## (lowered primary) the `.s.bif` companion? + result = false + let m = c.mods[module] + if m.index.getOrDefault(nifName).offset != 0: return true + if m.loweredPrimary: + ensureSemBuf(c, module) + result = m.semIndex.getOrDefault(nifName).offset != 0 + +proc typeCursor(c: var DecodeContext; module: FileIndex; nifName: string): Cursor = + ## A cursor at a TYPE's `(td …)` def: the primary buffer if present (an `@bk` + ## closure-env type minted by the lower stage, or any `.s.bif`-primary module), + ## else the `.s.bif` companion (frontend type defs are NOT carried in `.t.bif`). + let m = c.mods[module] + let e = m.index.getOrDefault(nifName) + if e.offset != 0: + return cursorAt(m.buf, e.offset) + if m.loweredPrimary: + ensureSemBuf(c, module) + let se = m.semIndex.getOrDefault(nifName) + if se.offset != 0: + return cursorAt(m.semBuf, se.offset) + raiseAssert "symbol has no offset: " & nifName + proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; localSyms: var Table[string, PSym]): PNode @@ -1775,10 +2002,11 @@ proc reconstructSysType(c: var DecodeContext; name: string; k: int; itemVal: int result.alignImpl = int16 c.infos.config.target.ptrSize c.types[name] = (result, NifIndexEntry()) -proc tryCreateTypeStub(c: var DecodeContext; t: SymId): PType = +proc tryCreateTypeStub(c: var DecodeContext; name: string): PType = ## Like `createTypeStub` but returns nil instead of raising when the type has ## no offset in its module index (used by the best-effort `(offer …)` loader). - let name = pool.syms[t] + ## Step 2b: takes the sym NAME string (pool-agnostic) — the reader never juggles + ## a nifcore/nifstreams `SymId`. if not name.startsWith("`t"): return nil result = c.types.getOrDefault(name)[0] if result == nil: @@ -1800,15 +2028,15 @@ proc tryCreateTypeStub(c: var DecodeContext; t: SymId): PType = let realSuffix = if isBk: suffix[0 ..< suffix.len - BackendLocalMarker.len] else: suffix let modIdx = moduleId(c, realSuffix).int32 let id = if isBk: backendItemId(modIdx, itemVal) else: itemId(modIdx, itemVal) - let ii = addr c.mods[id.module.FileIndex].index - let offs = ii[].getOrDefault(name) - if offs.offset == 0: + let modFi = id.module.FileIndex + if not hasTypeOffset(c, modFi, name): return nil result = PType(itemId: id, uniqueId: id, kind: TTypeKind(k), state: Partial) - c.types[name] = (result, offs) + # `loadType` re-resolves the buffer via `typeCursor`, so the cached entry is a + # don't-care for types — store the primary one if any (else a 0-offset stub). + c.types[name] = (result, c.mods[modFi].index.getOrDefault(name)) -proc createTypeStub(c: var DecodeContext; t: SymId): PType = - let name = pool.syms[t] +proc createTypeStub(c: var DecodeContext; name: string): PType = assert name.startsWith("`t") result = c.types.getOrDefault(name)[0] if result == nil: @@ -1831,80 +2059,106 @@ proc createTypeStub(c: var DecodeContext; t: SymId): PType = let modIdx = moduleId(c, realSuffix).int32 let id = if isBk: backendItemId(modIdx, itemVal) else: itemId(modIdx, itemVal) let modFi = id.module.FileIndex - let ii = addr c.mods[modFi].index - var offs = ii[].getOrDefault(name) - if offs.offset == 0: + if not hasTypeOffset(c, modFi, name): raiseAssert "symbol has no offset: " & name result = PType(itemId: id, uniqueId: id, kind: TTypeKind(k), state: Partial) - c.types[name] = (result, offs) + # `loadType` re-resolves the buffer via `typeCursor`, so the cached entry is a + # don't-care for types — store the primary one if any (else a 0-offset stub). + c.types[name] = (result, c.mods[modFi].index.getOrDefault(name)) proc extractLocalSymsFromTree(c: var DecodeContext; n: var Cursor; thisModule: string; localSyms: var Table[string, PSym]) = ## Scan a tree for local symbol definitions (sdef tags) and add them to localSyms. ## For local symbols, fully load them immediately since they have no index offsets. ## After this proc returns, n is positioned AFTER the tree. - # Handle atoms (non-compound nodes) - just skip them - if n.kind != ParLe: - inc n + # Atoms (non-compound nodes): nothing to scan, just skip past them. + if n.kind != TagLit: + skip n return - var depth = 0 - while true: - if n.kind == ParLe: - if n.tagId == sdefTag: - # Found an sdef - check if it's local - let name = n.firstSon - expect name, SymbolDef - let symName = pool.syms[name.symId] - let sn = parseSymName(symName) - if sn.module.len == 0 and symName notin localSyms: - # Local symbol - create stub and immediately load it fully - # since local symbols have no index offsets for lazy loading - let module = moduleId(c, thisModule) - let val = addr c.mods[module].symCounter - inc val[] - let id = itemId(module.int32, val[]) - let sym = PSym(itemId: id, kindImpl: skStub, name: c.cache.getIdent(sn.name), - disamb: sn.count.int32, state: Complete) - localSyms[symName] = sym - # Load the full symbol definition immediately - # We're currently at the `(sd` position, need to skip to SymbolDef - inc n # skip past `sd` tag to get to SymbolDef - loadSymFromCursor(c, sym, n, thisModule, localSyms) - sym.state = c.loadedState # mark as fully loaded - # Continue processing - loadSymFromCursor already advanced n past the closing `)` - continue - inc depth - elif n.kind == ParRi: - dec depth - if depth == 0: - inc n # Move PAST the closing ) - break - inc n + if tagIs(n, typeDefTagName): + # A nested inline type owns its own field name-scope: its fields are object-LOCAL + # symbols (``f.`) that can collide name+position with a sibling/outer + # type's field (e.g. astdef's `TLoc.flags` vs `TNode.flags`, both inline). Do NOT + # pull them into this scope; `loadTypeFromCursor` loads each type's reclist in an + # isolated `localSyms`. + skip n + return + if tagIs(n, symDefTagName): + # Found an sdef - check if it's a new local symbol. + let name = n.firstSon + expect name, SymbolDef + let symName = symName(name) + let sn = parseSymName(symName) + if sn.module.len == 0 and symName notin localSyms: + # Local symbol - create stub and immediately load it fully + # since local symbols have no index offsets for lazy loading + let module = moduleId(c, thisModule) + let val = addr c.mods[module].symCounter + inc val[] + let id = itemId(module.int32, val[]) + # `stubKindAndName` strips NIF-only markers (e.g. a field's `` `f ``) so the + # backend mangles the clean name; `loadSymFromCursor` then fills the real kind. + let (_, stubName) = stubKindAndName(c.cache, sn.name) + let sym = PSym(itemId: id, kindImpl: skStub, name: stubName, + disamb: sn.count.int32, state: Complete) + localSyms[symName] = sym + # `loadSymFromCursor` enters the `(sd` and consumes the whole block, + # leaving n positioned after the closing `)`. + loadSymFromCursor(c, sym, n, thisModule, localSyms) + sym.state = c.loadedState # mark as fully loaded + return + # Otherwise descend into every child, scanning each for nested local sdefs. + n.loopInto: + extractLocalSymsFromTree(c, n, thisModule, localSyms) proc loadTypeFromCursor(c: var DecodeContext; n: var Cursor; t: PType; localSyms: var Table[string, PSym]) proc loadTypeStub(c: var DecodeContext; n: var Cursor; localSyms: var Table[string, PSym]): PType = if n.kind == DotToken: result = nil - inc n + skip n elif n.kind == Symbol: - let s = n.symId - result = createTypeStub(c, s) - inc n - elif n.kind == ParLe and n.tagId == tdefTag: - let s = n.firstSon.symId - result = createTypeStub(c, s) + result = createTypeStub(c, symName(n)) + skip n + elif n.kind == TagLit and tagIs(n, typeDefTagName): + result = createTypeStub(c, symName(n.firstSon)) if result.state == Partial: result.state = c.loadedState # Mark as loaded to prevent loadType from re-loading with empty localSyms - loadTypeFromCursor(c, n, result, localSyms) + # A type's reclist is its own field name-scope: object-local field names + # (``f.`) can collide name+position with the enclosing scope's or a + # sibling inline type's fields. Load it in an isolated `localSyms`. + var typeLocalSyms = initTable[string, PSym]() + loadTypeFromCursor(c, n, result, typeLocalSyms) else: skip n # Type already loaded, skip over the td block else: raiseAssert "type expected but got " & $n.kind -proc loadSymStub(c: var DecodeContext; t: SymId; thisModule: string; +proc loadFieldStub(c: var DecodeContext; symAsStr: string; thisModule: string; + localSyms: var Table[string, PSym]; typ: PType = nil): PSym = + ## A cross-context object-field reference (see `FieldMarker`): its def lives in + ## the owning type's reclist (a different seek, absent from this body's + ## `localSyms`), and it has no module suffix / index entry. There is nothing to + ## resolve — `cgen.genRecordField` re-navigates the object type's reclist by + ## `name` (`lookupFieldAgain`/`lookupInRecord`), so the use-site field need only + ## carry the clean field name (+ position for tuples, + type so a lower-stage + ## transform that builds a fresh node off this sym still re-serializes a type). + ## NOT shared across uses: each carries its own `typ`, and two distinct fields can + ## share a local name+position (cross-type), so a shared stub would mistype one. + let sn = parseSymName(symAsStr) + let (stubKind, stubName) = stubKindAndName(c.cache, sn.name) + let module = moduleId(c, thisModule) + let val = addr c.mods[module].symCounter + inc val[] + # `sn.count` is the field POSITION (see toNifSymName): tuple element access reads + # it directly off this stub, so preserve it. Named-object uses re-navigate by name. + result = PSym(itemId: itemId(module.int32, val[]), kindImpl: stubKind, + name: stubName, disamb: sn.count.int32, state: Complete) + result.positionImpl = sn.count.int32 + if typ != nil: result.typImpl = typ + +proc loadSymStub(c: var DecodeContext; symAsStr: string; thisModule: string; localSyms: var Table[string, PSym]): PSym = - let symAsStr = pool.syms[t] let sn = parseSymName(symAsStr) # For local symbols (no module suffix), they MUST be in localSyms. # Local symbols are not in the index - they're defined inline in the NIF file. @@ -1913,6 +2167,10 @@ proc loadSymStub(c: var DecodeContext; t: SymId; thisModule: string; result = localSyms.getOrDefault(symAsStr) if result != nil: return result + elif sn.name.endsWith(FieldMarker): + # A cross-context object-field reference reaching a non-dotExpr slot (e.g. a + # `{.guard.}` field, an owner): stub it like any other field use. + return c.loadFieldStub(symAsStr, thisModule, localSyms) else: raiseAssert "local symbol '" & symAsStr & "' not found in localSyms." # Global symbol - look up in index for lazy loading @@ -1949,18 +2207,17 @@ proc loadSymStub(c: var DecodeContext; n: var Cursor; thisModule: string; localSyms: var Table[string, PSym]): PSym = if n.kind == DotToken: result = nil - inc n + skip n elif n.kind == Symbol: - let s = n.symId - result = loadSymStub(c, s, thisModule, localSyms) - inc n - elif n.kind == ParLe and n.tagId == sdefTag: - let s = n.firstSon.symId + result = loadSymStub(c, symName(n), thisModule, localSyms) + skip n + elif n.kind == TagLit and tagIs(n, symDefTagName): + let s = symName(n.firstSon) skip n result = loadSymStub(c, s, thisModule, localSyms) else: raiseAssert "sym expected but got " & $n.kind & ( - if n.kind == Ident: " '" & pool.strings[n.litId] & "'" else: "") + if n.kind == Ident: " '" & strVal(n) & "'" else: "") proc isStub*(t: PType): bool {.inline.} = t.state == Partial proc isStub*(s: PSym): bool {.inline.} = s.state == Partial @@ -1968,30 +2225,30 @@ proc isStub*(s: PSym): bool {.inline.} = s.state == Partial proc loadAtom[T](t: typedesc[set[T]]; n: var Cursor): set[T] = if n.kind == DotToken: result = {} - inc n + skip n else: expect n, Ident - result = parse(T, pool.strings[n.litId]) - inc n + result = parse(T, strVal(n)) + skip n proc loadAtom[T: enum](t: typedesc[T]; n: var Cursor): T = if n.kind == DotToken: result = default(T) - inc n + skip n else: expect n, Ident - result = parse(T, pool.strings[n.litId]) - inc n + result = parse(T, strVal(n)) + skip n proc loadAtom(t: typedesc[string]; n: var Cursor): string = - expect n, StringLit - result = pool.strings[n.litId] - inc n + expect n, StrLit + result = strVal(n) + skip n proc loadAtom[T: int16|int32|int64](t: typedesc[T]; n: var Cursor): T = expect n, IntLit - result = pool.integers[n.intId].T - inc n + result = intVal(n).T + skip n template loadField(field) {.dirty.} = field = loadAtom(typeof(field), n) @@ -2003,12 +2260,12 @@ proc loadLoc(c: var DecodeContext; n: var Cursor; loc: var TLoc) = loadField loc.snippet proc loadTypeFromCursor(c: var DecodeContext; n: var Cursor; t: PType; localSyms: var Table[string, PSym]) = - expect n, ParLe - if n.tagId != tdefTag: + expect n, TagLit + if not tagIs(n, typeDefTagName): raiseAssert "(td) expected" var scanCursor = n # copy cursor at start of type - var typesModule = parseSymName(pool.syms[n.firstSon.symId]).module + var typesModule = parseSymName(symName(n.firstSon)).module if typesModule.endsWith(BackendLocalMarker): # A backend-minted (`@bk`) type's name carries the marker in its module part; # strip it so the nested-local pre-scan resolves the real module, not a @@ -2016,42 +2273,39 @@ proc loadTypeFromCursor(c: var DecodeContext; n: var Cursor; t: PType; localSyms typesModule = typesModule[0 ..< typesModule.len - BackendLocalMarker.len] extractLocalSymsFromTree(c, scanCursor, typesModule, localSyms) - inc n # move past (td - expect n, SymbolDef - # ignore the type's name, we have already used it to create this PType's itemId! - inc n - expect n, DotToken - inc n - #loadField t.kind - loadField t.flagsImpl - loadField t.callConvImpl - loadField t.sizeImpl - loadField t.alignImpl - loadField t.paddingAtEndImpl - t.itemId = itemId(t.itemId.module, loadAtom(int32, n)) # nonUniqueId - if n.kind == StringLit: - # itemId.module differs from uniqueId.module (an `exactReplica` of a - # foreign type): restore the canonical module half - t.itemId = itemId(int32(moduleId(c, pool.strings[n.litId])), t.itemId.item) - inc n - elif n.kind == DotToken: - inc n + n.into: # enter (td, body consumes all children, closing ) is consumed by `into` + expect n, SymbolDef + # ignore the type's name, we have already used it to create this PType's itemId! + skip n + expect n, DotToken + skip n + #loadField t.kind + loadField t.flagsImpl + loadField t.callConvImpl + loadField t.sizeImpl + loadField t.alignImpl + loadField t.paddingAtEndImpl + t.itemId = itemId(t.itemId.module, loadAtom(int32, n)) # nonUniqueId + if n.kind == StrLit: + # itemId.module differs from uniqueId.module (an `exactReplica` of a + # foreign type): restore the canonical module half + t.itemId = itemId(int32(moduleId(c, strVal(n))), t.itemId.item) + skip n + elif n.kind == DotToken: + skip n - t.typeInstImpl = loadTypeStub(c, n, localSyms) - t.nImpl = loadNode(c, n, typesModule, localSyms) - t.ownerFieldImpl = loadSymStub(c, n, typesModule, localSyms) - t.symImpl = loadSymStub(c, n, typesModule, localSyms) - loadLoc c, n, t.locImpl + t.typeInstImpl = loadTypeStub(c, n, localSyms) + t.nImpl = loadNode(c, n, typesModule, localSyms) + t.ownerFieldImpl = loadSymStub(c, n, typesModule, localSyms) + t.symImpl = loadSymStub(c, n, typesModule, localSyms) + loadLoc c, n, t.locImpl - while n.kind != ParRi: - t.sonsImpl.add loadTypeStub(c, n, localSyms) - - skipParRi n + while n.hasMore: + t.sonsImpl.add loadTypeStub(c, n, localSyms) proc loadType*(c: var DecodeContext; t: PType) = if t.state != Partial: return t.state = c.loadedState - var buf = createTokenBuf(30) # A backend-minted (`@bk`) closure-env type produced by the `lower` stage lives # ONLY in the `.t.nif` and is keyed by its `@bk` name (see nifTypeName), not the # canonical `typeToNifSym` (which asserts non-`@bk`). Reconstruct that name so a @@ -2063,112 +2317,121 @@ proc loadType*(c: var DecodeContext; t: PType) = else: typeToNifSym(t, c.infos.config) let modFi = t.itemId.module.FileIndex - var n = cursorFromIndexEntry(c, modFi, c.types[typeName][1], buf) + # `typeCursor` resolves to the primary `.t.bif` (`@bk` env types) or falls back to + # the `.s.bif` companion (frontend type defs, which `.t.bif` no longer carries). + var n = typeCursor(c, modFi, typeName) var localSyms = initTable[string, PSym]() loadTypeFromCursor(c, n, t, localSyms) proc loadAnnex(c: var DecodeContext; n: var Cursor; thisModule: string; localSyms: var Table[string, PSym]): PLib = if n.kind == DotToken: result = nil - inc n - elif n.kind == ParLe: - result = PLib(kind: parse(TLibKind, pool.tags[n.tagId])) - inc n - result.generated = loadBool(n) - result.isOverridden = loadBool(n) - expect n, StringLit - result.name = pool.strings[n.litId] - inc n - result.path = loadNode(c, n, thisModule, localSyms) - skipParRi n + skip n + elif n.kind == TagLit: + result = PLib(kind: parse(TLibKind, cursorTag(n))) + n.into: + result.generated = loadBool(n) + result.isOverridden = loadBool(n) + expect n, StrLit + result.name = strVal(n) + skip n + result.path = loadNode(c, n, thisModule, localSyms) else: raiseAssert "`lib/annex` information expected" proc loadSymFromCursor(c: var DecodeContext; s: PSym; n: var Cursor; thisModule: string; localSyms: var Table[string, PSym]) = - ## Loads a symbol definition from the current cursor position. - ## The cursor should be positioned after the opening (sd tag. - expect n, SymbolDef - # ignore the symbol's name, we have already used it to create this PSym instance! - inc n - if n.kind == Ident: - if pool.strings[n.litId] == "x": - s.flagsImpl.incl sfExported - inc n - else: - raiseAssert "expected `x` as the export marker" - elif n.kind == DotToken: - inc n - else: - raiseAssert "expected `x` or '.' but got " & $n.kind - - expect n, ParLe - {.cast(uncheckedAssign).}: - s.kindImpl = parse(TSymKind, pool.tags[n.tagId]) - inc n - - if s.kindImpl == skPackage and s.name.s.endsWith(PkgMarker): - # Fallback: stubs are normally created with the clean name already - # (see stubKindAndName); strip the NIF-only marker if one slipped through. - s.name = c.cache.getIdent(s.name.s[0 ..< s.name.s.len - PkgMarker.len]) - - case s.kindImpl - of skLet, skVar, skField, skForVar: - s.guardImpl = loadSymStub(c, n, thisModule, localSyms) - loadField s.bitsizeImpl - loadField s.alignmentImpl - else: - discard - skipParRi n - - loadField s.magicImpl - loadField s.flagsImpl - loadField s.optionsImpl - loadField s.offsetImpl - - if s.kindImpl == skModule: - expect n, DotToken - inc n - var isKnownFile = false - s.positionImpl = int c.infos.config.registerNifSuffix(thisModule, isKnownFile) - # do to the precompiled mechanism things end up as main modules which are not! - excl s.flagsImpl, sfMainModule - else: - loadField s.positionImpl - - s.annexImpl = loadAnnex(c, n, thisModule, localSyms) - - # Local symbols were already extracted upfront in loadSym, so we can use - # the simple loadTypeStub here. - s.typImpl = loadTypeStub(c, n, localSyms) - s.ownerFieldImpl = loadSymStub(c, n, thisModule, localSyms) - # Load the AST for routine symbols and constants - # Constants need their AST for astdef() to return the constant's value - s.astImpl = loadNode(c, n, thisModule, localSyms) - loadLoc c, n, s.locImpl - s.constraintImpl = loadNode(c, n, thisModule, localSyms) - s.instantiatedFromImpl = loadSymStub(c, n, thisModule, localSyms) - # The TRANSFORMED body slot (see writeSymDef). Reconstruct it ONLY in the - # backend (`cmdNifC`), where `transformBody` short-circuits on it; during - # frontend sem (`cmdM`) skip the tokens — a dependent never needs a foreign - # routine's lowered body, and reconstructing one must not perturb effect/ - # exception inference (the "never change frontend node-typing for IC" rule). - if c.infos.config.cmd == cmdNifC and s.kindImpl in routineKinds: - s.transformedBodyImpl = loadNode(c, n, thisModule, localSyms) - else: + ## Loads a symbol definition. The cursor must be positioned AT the opening + ## `(sd` TagLit; `into` consumes the whole sdef including its closing `)`. + n.into: + expect n, SymbolDef + # ignore the symbol's name, we have already used it to create this PSym instance! skip n - skipParRi n + if n.kind == Ident: + if strVal(n) == "x": + s.flagsImpl.incl sfExported + skip n + else: + raiseAssert "expected `x` as the export marker" + elif n.kind == DotToken: + skip n + else: + raiseAssert "expected `x` or '.' but got " & $n.kind + + expect n, TagLit + {.cast(uncheckedAssign).}: + s.kindImpl = parse(TSymKind, cursorTag(n)) + + if s.kindImpl == skPackage and s.name.s.endsWith(PkgMarker): + # Fallback: stubs are normally created with the clean name already + # (see stubKindAndName); strip the NIF-only marker if one slipped through. + s.name = c.cache.getIdent(s.name.s[0 ..< s.name.s.len - PkgMarker.len]) + + n.into: # the (kind ...) sub-block + case s.kindImpl + of skLet, skVar, skField, skForVar: + s.guardImpl = loadSymStub(c, n, thisModule, localSyms) + loadField s.bitsizeImpl + loadField s.alignmentImpl + else: + discard + + loadField s.magicImpl + loadField s.flagsImpl + loadField s.optionsImpl + loadField s.offsetImpl + + if s.kindImpl == skModule: + expect n, DotToken + skip n + var isKnownFile = false + s.positionImpl = int c.infos.config.registerNifSuffix(thisModule, isKnownFile) + # do to the precompiled mechanism things end up as main modules which are not! + excl s.flagsImpl, sfMainModule + else: + loadField s.positionImpl + + s.annexImpl = loadAnnex(c, n, thisModule, localSyms) + + # Local symbols were already extracted upfront in loadSym, so we can use + # the simple loadTypeStub here. + s.typImpl = loadTypeStub(c, n, localSyms) + s.ownerFieldImpl = loadSymStub(c, n, thisModule, localSyms) + # Load the AST for routine symbols and constants + # Constants need their AST for astdef() to return the constant's value + s.astImpl = loadNode(c, n, thisModule, localSyms) + loadLoc c, n, s.locImpl + s.constraintImpl = loadNode(c, n, thisModule, localSyms) + s.instantiatedFromImpl = loadSymStub(c, n, thisModule, localSyms) + # The TRANSFORMED body slot (see writeSymDef). It means DIFFERENT things by + # which file `moduleId` loaded (see toNifFilename): + # * `cg`/`emit` read `.t.bif` — the slot is the `lower` stage's AUTHORITATIVE + # lowered body; ALWAYS load it so `transformBody` short-circuits and the + # backend NEVER re-derives (the whole point of the artifact). + # * the `lower` stage reads `.s.bif` — the slot is the VM/CT lowering sem + # cached; load it only when REUSE is on (`icReuseSemLowering`), else leave + # `transformedBody` nil so the `lower` stage re-derives from the pristine + # body (the 2026-06-27 simplicity spec, doc/ic_backend_simplify.md §6). + # * frontend `cmdM` never loads it (a dependent needs no foreign lowered body, + # and reconstructing one must not perturb effect/exception inference). + let conf = c.infos.config + let loadSlot = s.kindImpl in routineKinds and conf.cmd == cmdNifC and + (conf.icBackendStage == "cg" or conf.icBackendStage == "emit" or + (conf.icBackendStage == "lower" and icReuseSemLowering(conf))) + if loadSlot: + s.transformedBodyImpl = loadNode(c, n, thisModule, localSyms) + else: + skip n proc loadSym*(c: var DecodeContext; s: PSym) = if s.state != Partial: return s.state = c.loadedState - var buf = createTokenBuf(30) let symsModule = s.itemId.module.FileIndex let nifname = globalName(s, c.infos.config) - var n = cursorFromIndexEntry(c, symsModule, c.syms[nifname][1], buf) + var n = cursorFromIndexEntry(c, symsModule, c.syms[nifname][1]) - expect n, ParLe - if n.tagId != sdefTag: + expect n, TagLit + if not tagIs(n, symDefTagName): raiseAssert "(sd) expected" # Pre-scan the ENTIRE symbol definition to extract ALL local symbols upfront. @@ -2179,15 +2442,15 @@ proc loadSym*(c: var DecodeContext; s: PSym) = extractLocalSymsFromTree(c, scanCursor, c.mods[symsModule].suffix, localSyms) # Now parse the symbol definition with all local symbols pre-registered - s.infoImpl = c.infos.oldLineInfo(n.info) + s.infoImpl = c.infos.oldLineInfo(n.info, cursorPool(n)) # The `##` doc comment (if any) rides as a NIF comment on the sym def token; # capture it before advancing, then restore it onto the loaded AST so that # suggest's `extractDocComment` (findDocComment on `s.ast`) finds it. - let docId = unpack(pool.man, n.info).comment - inc n + let docId = rawLineInfo(n).comment # nifcore StrId of the `#..#` doc comment + let docPool = cursorPool(n) # the buffer's own strings pool (shared or bif-fresh) loadSymFromCursor(c, s, n, c.mods[symsModule].suffix, localSyms) - if docId != 0'u32 and s.astImpl != nil and nodeCommentWriter != nil: - nodeCommentWriter(s.astImpl, pool.strings[StrId(docId)]) + if uint32(docId) != 0'u32 and s.astImpl != nil and nodeCommentWriter != nil: + nodeCommentWriter(s.astImpl, docPool.strings[docId]) proc sealLoadedRoutines*(c: var DecodeContext) = ## Before `writeLoweredModule` re-serializes the lowered module, seal ONLY the @@ -2203,62 +2466,77 @@ proc sealLoadedRoutines*(c: var DecodeContext) = if v[0] != nil and v[0].state == Complete and v[0].kindImpl in routineKinds: v[0].state = Sealed -proc resolveHookSym*(c: var DecodeContext; symId: nifstreams.SymId): PSym +proc resolveHookSym*(c: var DecodeContext; name: string): PSym template withNode(c: var DecodeContext; n: var Cursor; result: PNode; kind: TNodeKind; body: untyped) = - let info = c.infos.oldLineInfo(n.info) - inc n - let flags = loadAtom(TNodeFlags, n) + let info = c.infos.oldLineInfo(n.info, cursorPool(n)) result = newNodeI(kind, info) - result.flags = flags - result.typField = c.loadTypeStub(n, localSyms) - body - skipParRi n + n.into: + result.flags = loadAtom(TNodeFlags, n) + result.typField = c.loadTypeStub(n, localSyms) + body proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; localSyms: var Table[string, PSym]): PNode = result = nil case n.kind of Symbol: - let info = c.infos.oldLineInfo(n.info) - let symName = pool.syms[n.symId] + let info = c.infos.oldLineInfo(n.info, cursorPool(n)) + let symName = symName(n) # Check local symbols first let localSym = localSyms.getOrDefault(symName) if localSym != nil: result = newSymNode(localSym, info) - inc n + skip n + elif isFieldNifName(symName): + # Cross-context object-field reference: stub a `skField` from the local name + # (see `loadFieldStub`). The field's type is recovered from the object type at + # codegen time, so this leaf carries no type of its own. + result = newSymNode(c.loadFieldStub(symName, thisModule, localSyms), info) + result.flags.incl nfLazyType + skip n else: result = newSymNode(c.loadSymStub(n, thisModule, localSyms), info) if result.typField == nil: result.flags.incl nfLazyType of DotToken: result = nil - inc n - of StringLit: - result = newStrNode(pool.strings[n.litId], c.infos.oldLineInfo(n.info)) - inc n - of ParLe: + skip n + of StrLit: + result = newStrNode(strVal(n), c.infos.oldLineInfo(n.info, cursorPool(n))) + skip n + of TagLit: let kind = n.nodeKind case kind of nkNone: # special NIF introduced tag? - case pool.tags[n.tagId] - of hiddenTypeTagName: - inc n - let typ = c.loadTypeStub(n, localSyms) - let info = c.infos.oldLineInfo(n.info) - result = newSymNode(c.loadSymStub(n, thisModule, localSyms), info) - result.typField = typ - skipParRi n - of symDefTagName: - let info = c.infos.oldLineInfo(n.info) + if tagIs(n, hiddenTypeTagName): + n.into: + let typ = c.loadTypeStub(n, localSyms) + let info = c.infos.oldLineInfo(n.info, cursorPool(n)) + var s: PSym + if n.kind == Symbol and isFieldNifName(symName(n)): + # Field SymUse wrapped with its explicit type (see writeSymNode): stub + # the field, carrying the wrapper's type on BOTH the node and the sym so + # a lower-stage transform that builds a fresh node off the sym still has a + # type to re-serialize. + s = c.loadFieldStub(symName(n), thisModule, localSyms, typ) + skip n + else: + s = c.loadSymStub(n, thisModule, localSyms) + result = newSymNode(s, info) + result.typField = typ + elif tagIs(n, symDefTagName): + let info = c.infos.oldLineInfo(n.info, cursorPool(n)) let name = n.firstSon assert name.kind == SymbolDef - let symName = pool.syms[name.symId] + let symName = symName(name) # Check if this is a local symbol (no module suffix in name) let sn = parseSymName(symName) let isLocal = sn.module.len == 0 var sym: PSym + # In every branch below `n` stays at the `(sd` TagLit; `loadSymFromCursor` + # enters and consumes the whole block, and `skip n` consumes it wholesale. if isLocal: # Local symbol - not in the index, defined inline in NIF. # Check if we already have a stub from extractLocalSymsFromType @@ -2269,11 +2547,13 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; let val = addr c.mods[module].symCounter inc val[] let id = itemId(module.int32, val[]) - sym = PSym(itemId: id, kindImpl: skStub, name: c.cache.getIdent(sn.name), + # strip NIF-only markers (a field's `` `f ``) so the backend sees the + # clean name; `loadSymFromCursor` below fills the real kind. + let (_, stubName) = stubKindAndName(c.cache, sn.name) + sym = PSym(itemId: id, kindImpl: skStub, name: stubName, disamb: sn.count.int32, state: Complete) localSyms[symName] = sym # register for later references # Now fully load the symbol from the sdef - inc n # skip `sd` tag loadSymFromCursor(c, sym, n, thisModule, localSyms) sym.state = c.loadedState # mark as fully loaded result = newSymNode(sym, info) @@ -2282,10 +2562,9 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; # (not in any module index): create/find its cached stub and FILL it # from the sdef instead of skipping (which would leave the skModule/ # Partial stub `loadSymStub` made unresolved). - sym = c.loadSymStub(name.symId, thisModule, localSyms) + sym = c.loadSymStub(symName, thisModule, localSyms) if sym.state == Partial: sym.state = c.loadedState - inc n # skip `sd` tag loadSymFromCursor(c, sym, n, thisModule, localSyms) else: skip n @@ -2303,7 +2582,7 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; let m = moduleId(c, sn.module) let indexed = c.mods[m].index.hasKey(symName) if indexed: - sym = c.loadSymStub(name.symId, thisModule, localSyms) + sym = c.loadSymStub(symName, thisModule, localSyms) skip n # skip the entire sdef for indexed symbols else: sym = c.syms.getOrDefault(symName)[0] @@ -2315,86 +2594,81 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; state: Partial) c.syms[symName] = (sym, NifIndexEntry()) sym.state = c.loadedState - inc n # skip `sd` tag loadSymFromCursor(c, sym, n, thisModule, localSyms) result = newSymNode(sym, info) result.flags.incl nfLazyType - of typeDefTagName: + elif tagIs(n, typeDefTagName): raiseAssert "`td` tag in invalid context" - of "none": - result = newNodeI(nkNone, c.infos.oldLineInfo(n.info)) - inc n - result.flags = loadAtom(TNodeFlags, n) - skipParRi n + elif tagIs(n, "none"): + result = newNodeI(nkNone, c.infos.oldLineInfo(n.info, cursorPool(n))) + n.into: + result.flags = loadAtom(TNodeFlags, n) else: - raiseAssert "Unknown NIF tag " & pool.tags[n.tagId] + raiseAssert "Unknown NIF tag " & cursorTag(n) of nkEmpty: - result = newNodeI(nkEmpty, c.infos.oldLineInfo(n.info)) - inc n - if n.kind != ParRi: - result.flags = loadAtom(TNodeFlags, n) - result.typField = c.loadTypeStub(n, localSyms) - skipParRi n + result = newNodeI(nkEmpty, c.infos.oldLineInfo(n.info, cursorPool(n))) + n.into: + if n.hasMore: + result.flags = loadAtom(TNodeFlags, n) + result.typField = c.loadTypeStub(n, localSyms) of nkIdent: - let info = c.infos.oldLineInfo(n.info) - inc n - let flags = loadAtom(TNodeFlags, n) - let typ = c.loadTypeStub(n, localSyms) - expect n, Ident - result = newIdentNode(c.cache.getIdent(pool.strings[n.litId]), info) - inc n - result.flags = flags - result.typField = typ - skipParRi n + let info = c.infos.oldLineInfo(n.info, cursorPool(n)) + n.into: + let flags = loadAtom(TNodeFlags, n) + let typ = c.loadTypeStub(n, localSyms) + expect n, Ident + result = newIdentNode(c.cache.getIdent(strVal(n)), info) + skip n + result.flags = flags + result.typField = typ of nkSym: - #let info = c.infos.oldLineInfo(n.info) + #let info = c.infos.oldLineInfo(n.info, cursorPool(n)) #result = newSymNode(c.loadSymStub n, info) raiseAssert "nkSym should be mapped to a NIF symbol, not a tag" of nkCharLit: c.withNode n, result, kind: expect n, CharLit result.intVal = n.charLit.int - inc n + skip n of nkIntLit .. nkInt64Lit: c.withNode n, result, kind: expect n, IntLit - result.intVal = pool.integers[n.intId] - inc n + result.intVal = intVal(n) + skip n of nkUIntLit .. nkUInt64Lit: c.withNode n, result, kind: expect n, UIntLit - result.intVal = cast[BiggestInt](pool.uintegers[n.uintId]) - inc n + result.intVal = cast[BiggestInt](uintVal(n)) + skip n of nkFloatLit .. nkFloat128Lit: c.withNode n, result, kind: if n.kind == FloatLit: - result.floatVal = pool.floats[n.floatId] - inc n - elif n.kind == ParLe: - case pool.tags[n.tagId] - of "inf": + result.floatVal = floatVal(n) + skip n + elif n.kind == TagLit: + if tagIs(n, "inf"): result.floatVal = Inf - of "nan": + elif tagIs(n, "nan"): result.floatVal = NaN - of "neginf": + elif tagIs(n, "neginf"): result.floatVal = NegInf else: - raiseAssert "expected float literal but got " & pool.tags[n.tagId] - inc n - skipParRi n + raiseAssert "expected float literal but got " & cursorTag(n) + n.into: + discard else: raiseAssert "expected float literal but got " & $n.kind of nkStrLit .. nkTripleStrLit: c.withNode n, result, kind: - expect n, StringLit - result.strVal = pool.strings[n.litId] - inc n + expect n, StrLit + result.strVal = strVal(n) + skip n of nkNilLit: c.withNode n, result, kind: discard else: c.withNode n, result, kind: - while n.kind != ParRi: + while n.hasMore: result.sons.add c.loadNode(n, thisModule, localSyms) else: raiseAssert "expected string literal but got " & $n.kind @@ -2475,17 +2749,38 @@ proc moduleSymbolStubs*(c: var DecodeContext; module: FileIndex): seq[PSym] = if sym != nil: result.add sym c.mods[module].index = move indexTab +proc loadedModuleTypes*(c: var DecodeContext; module: FileIndex): seq[PType] = + ## Stubs for every TYPE this module owns — but, unlike before, WITHOUT force- + ## loading them. `writeLoweredModule` emits a real def into the `.t.bif` only for + ## the ones already `Complete` (= the lower stage actually loaded, hence possibly + ## MUTATED — lambda-lifting flips a proc type to `ccClosure` and grows env types + ## with captured fields). Every untouched type stays `Partial`, so a reference to + ## it serializes as a `SymUse` that a cg/emit consumer resolves from the `.s.bif` + ## (loader fallback `typeCursor`/`ensureSemBuf`) — the lower stage leaves those + ## defs unchanged, so re-emitting them into `.t.bif` was pure cost. Collect names + ## first: `createTypeStub` may register modules / mutate `c.types`, which must not + ## invalidate the index iterator. + result = @[] + if not c.mods.hasKey(module): return + var names: seq[string] = @[] + for nifName in c.mods[module].index.keys: + if nifName.startsWith("`t"): names.add nifName + for nm in names: + let t = createTypeStub(c, nm) + if t != nil: result.add t + proc toNifFilename*(conf: ConfigRef; f: FileIndex): string = let suffix = moduleSuffix(conf, f) # The `cg`/`emit` backend stages load the lowered whole-module NIF (transformed # bodies + lifted sigs baked in); the `lower` stage and the frontend (`cmdM`) - # read the semchecked `.s.nif`. + # read the semchecked `.s.bif`. All module artifacts are binary NIF (`.bif`) + # now — one file per stage, no text twin (debug via `tools/bif2nif`). if conf.cmd == cmdNifC and (conf.icBackendStage == "cg" or conf.icBackendStage == "emit"): - let t = toGeneratedFile(conf, AbsoluteFile(suffix), ".t.nif").string + let t = toGeneratedFile(conf, AbsoluteFile(suffix), ".t.bif").string if fileExists(t): return t - result = toGeneratedFile(conf, AbsoluteFile(suffix), ".s.nif").string + result = toGeneratedFile(conf, AbsoluteFile(suffix), ".s.bif").string proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: bool): PSym = result = c.syms.getOrDefault(symAsStr)[0] @@ -2519,11 +2814,10 @@ proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: boo disamb: sn.count.int32, state: Partial) c.syms[symAsStr] = (result, offs) -proc resolveHookSym*(c: var DecodeContext; symId: nifstreams.SymId): PSym = - ## Resolves a hook SymId to PSym. +proc resolveHookSym*(c: var DecodeContext; name: string): PSym = + ## Resolves a hook symbol NAME to a PSym. ## Hook symbols are often private (generated =destroy, =wasMoved, etc.) - let symAsStr = pool.syms[symId] - result = resolveSym(c, symAsStr, true) + result = resolveSym(c, name, true) proc tryResolveCompilerProc*(c: var DecodeContext; name: string; moduleFileIdx: FileIndex): PSym = ## Tries to resolve a compiler proc from a module by checking the NIF index. @@ -2551,51 +2845,19 @@ proc tryResolveCompilerProc*(c: var DecodeContext; name: string; moduleFileIdx: if sfCompilerProc in sym.flagsImpl: return sym -proc loadLogOp(c: var DecodeContext; logOps: var seq[LogEntry]; s: var Stream; kind: LogEntryKind; op: TTypeAttachedOp; module: int): PackedToken = - result = next(s) - var key = "" - if result.kind == StringLit: - key = pool.strings[result.litId] - result = next(s) - else: - raiseAssert "expected StringLit but got " & $result.kind - if result.kind == Symbol: - let sym = resolveHookSym(c, result.symId) - if sym != nil: - logOps.add LogEntry(kind: kind, op: op, module: module, key: key, sym: sym) - # else: symbol not indexed, skip this hook entry - result = next(s) - if result.kind == ParRi: - result = next(s) - else: - raiseAssert "expected ParRi but got " & $result.kind - -proc skipTree(s: var Stream): PackedToken = - result = next(s) - var nested = 1 - while nested > 0: - if result.kind == ParLe: - inc nested - elif result.kind == ParRi: - dec nested - elif result.kind == EofToken: - break - result = next(s) - -proc nextSubtree(r: var Stream; dest: var TokenBuf; tok: var PackedToken) = - r.parents[0] = tok.info - var nested = 1 - dest.add tok # tag - while true: - tok = r.next() - dest.add tok - if tok.kind == EofToken: - break - elif tok.kind == ParLe: - inc nested - elif tok.kind == ParRi: - dec nested - if nested == 0: break +proc loadLogOp(c: var DecodeContext; logOps: var seq[LogEntry]; cur: var Cursor; + kind: LogEntryKind; op: TTypeAttachedOp; module: int) = + ## Step 2 phase 2: read one `(rep* "key" sym)` from the resident-buffer cursor. + cur.into: + expect cur, StrLit + let key = strVal(cur) + skip cur + if cur.hasMore and cur.kind == Symbol: + let sym = resolveHookSym(c, symName(cur)) + if sym != nil: + logOps.add LogEntry(kind: kind, op: op, module: module, key: key, sym: sym) + # else: symbol not indexed, skip this hook entry + skip cur type ModuleSuffix* = distinct string @@ -2621,35 +2883,23 @@ type # nimsuggest can map a query in an include file back to # this module (`parentModule`) and recompile it. -proc loadImport(c: var DecodeContext; s: var Stream; deps: var seq[ModuleSuffix]; tok: var PackedToken) = - tok = next(s) # skip `(import` - if tok.kind == DotToken: - tok = next(s) # skip dot - if tok.kind == DotToken: - tok = next(s) # skip dot - if tok.kind == StringLit: - deps.add ModuleSuffix(pool.strings[tok.litId]) - tok = next(s) - else: - raiseAssert "expected StringLit but got " & $tok.kind - if tok.kind == ParRi: - tok = next(s) # skip ) - else: - raiseAssert "expected ParRi but got " & $tok.kind +proc loadImport(c: var DecodeContext; cur: var Cursor; deps: var seq[ModuleSuffix]) = + cur.into: + while cur.hasMore and cur.kind == DotToken: skip cur # flags / type + if cur.hasMore and cur.kind == StrLit: + deps.add ModuleSuffix(strVal(cur)) + skip cur + else: + raiseAssert "expected StrLit but got " & $cur.kind -proc loadInclude(c: var DecodeContext; s: var Stream; includes: var seq[string]; tok: var PackedToken) = +proc loadInclude(c: var DecodeContext; cur: var Cursor; includes: var seq[string]) = ## Reads an `(include . . "path"...)` entry written by `trInclude`. The paths ## are resolved full paths (see semstmts.evalInclude under cmdM/optCompress). - tok = next(s) # skip `(include` - if tok.kind == DotToken: tok = next(s) # flags - if tok.kind == DotToken: tok = next(s) # type - while tok.kind == StringLit: - includes.add pool.strings[tok.litId] - tok = next(s) - if tok.kind == ParRi: - tok = next(s) - else: - raiseAssert "expected ParRi in (include ...) but got " & $tok.kind + cur.into: + while cur.hasMore and cur.kind == DotToken: skip cur # flags / type + while cur.hasMore and cur.kind == StrLit: + includes.add strVal(cur) + skip cur proc scanIncludeGraph*(config: ConfigRef): seq[tuple[includer: string; includes: seq[string]]] = ## Standalone "full table" scan of every `.nif` in the nimcache: reads @@ -2665,37 +2915,35 @@ proc scanIncludeGraph*(config: ConfigRef): seq[tuple[includer: string; includes: result = @[] let dir = getNimcacheDir(config) if not dirExists(dir.string): return - for f in walkFiles((dir / RelativeFile"*.nif").string): - # only the primary module NIFs; skip the sidecars - # (.iface.nif/.impl.nif/.edges.nif/.s.deps.nif). - if f.endsWith(".iface.nif") or f.endsWith(".impl.nif") or - f.endsWith(".edges.nif") or f.endsWith(".deps.nif"): - continue - var s = nifstreams.open(f) + # The primary module artifacts are `.s.bif` (the sidecars are + # `.iface.nif`/`.impl.nif`/`.edges.nif`/`.s.deps.nif`, which this glob excludes). + for f in walkFiles((dir / RelativeFile"*.s.bif").string): + var m = bif.load(f) var includer = "" var includes: seq[string] = @[] - var t = next(s) # (stmts - if t.kind == ParLe: - t = next(s) # flags dot - t = next(s) # type dot - t = next(s) # first child (matches loadNifModule's priming) - # the dep records (import/include/reexpmod/modulesrc) are written first and - # contiguously; stop at the first body node or the (implementation) marker. - while t.kind == ParLe: - if t.tagId == includeTag or t.tagId == modulesrcTag: - let isInc = t.tagId == includeTag - t = next(s) # into the node (past its ParLe) - while t.kind != ParRi and t.kind != EofToken: - if t.kind == StringLit: - if isInc: includes.add pool.strings[t.litId] - else: includer = pool.strings[t.litId] - t = next(s) - if t.kind == ParRi: t = next(s) # past the ParRi - elif t.tagId == importTag or t.tagId == reexpModTag: - t = skip(s, t) + var c = m.buf.beginRead() + if c.kind == TagLit and tagIs(c, toNifTag(nkStmtList)): + # The dep records (import/include/reexpmod/modulesrc) are written first and + # contiguously; `done` short-circuits once the first body node is seen + # (`into` forbids an early `break`, so we skip the remainder instead). + var done = false + c.loopInto: + if done or c.kind != TagLit: + skip c + elif tagIs(c, "include") or tagIs(c, "modulesrc"): + let isInc = tagIs(c, "include") + var ic = c + ic.loopInto: + if ic.kind == StrLit: + if isInc: includes.add strVal(ic) + else: includer = strVal(ic) + skip ic + skip c + elif tagIs(c, "import") or tagIs(c, "reexpmod"): + skip c else: - break - close s + done = true + skip c if includer.len > 0 and includes.len > 0: result.add (includer, includes) @@ -2707,23 +2955,23 @@ proc nifModuleHasIncludes*(config: ConfigRef; fileIdx: FileIndex): bool = ## owner/line-info on reload). let f = toNifFilename(config, fileIdx) if not fileExists(f): return false - var s = nifstreams.open(f) + var m = bif.load(f) result = false - var t = next(s) # (stmts - if t.kind == ParLe: - t = next(s) # flags dot - t = next(s) # type dot - t = next(s) # first child - while t.kind == ParLe: - if t.tagId == includeTag: + var c = m.buf.beginRead() + if c.kind == TagLit and tagIs(c, toNifTag(nkStmtList)): + var done = false + c.loopInto: + if done or c.kind != TagLit: + skip c + elif tagIs(c, "include"): result = true - break - elif t.tagId == modulesrcTag or t.tagId == importTag or - t.tagId == reexpModTag: - t = skip(s, t) + done = true + skip c + elif tagIs(c, "modulesrc") or tagIs(c, "import") or tagIs(c, "reexpmod"): + skip c else: - break - close s + done = true + skip c proc addReexportedEnumFields(c: var DecodeContext; sym: PSym; interf: var TStrTable) = ## When a non-pure enum type is (re-)exported, its fields must also become @@ -2744,213 +2992,152 @@ proc addReexportedEnumFields(c: var DecodeContext; sym: PSym; interf: var TStrTa if f != nil and f.kind == nkSym and f.sym != nil: strTableAdd(interf, f.sym) -proc processTopLevel(c: var DecodeContext; s: var Stream; flags: set[LoadFlag]; +proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag]; interf: var TStrTable; suffix: string; module: int): PrecompiledModule = + ## Step 2 phase 2: walk the module body directly over the resident `buf` cursor + ## (was a `next(s)` stream walk). `cur` enters at the `(stmts` type dot. Lazy + ## loads done here (resolveSym/loadType/…) read INDEPENDENT cursors into the + ## resident buffers, never `cur` — so the old export/toffer `jumpTo(saved)` + ## save/restore dance is gone. result = PrecompiledModule(topLevel: newNode(nkStmtList)) var localSyms = initTable[string, PSym]() - var t = next(s) # skip dot - var cont = true - let exportTag = pool.tags.getOrIncl"export" + skip cur # the (stmts type dot # Top-level `let`/`var` sections are loaded even without LoadFullAst: they may # declare `{.compileTime.}` globals whose VM slots the importer initializes # eagerly (pipelines.initLoadedCompileTimeGlobals), which needs them visible in # `topLevel`. They sit in the module header before `(implementation)`. - let letTag = pool.tags.getOrIncl(toNifTag(nkLetSection)) - let varTag = pool.tags.getOrIncl(toNifTag(nkVarSection)) - while cont and t.kind != EofToken: - if t.kind == ParLe: - if t.tagId == replayTag: + var cont = true + while cont and cur.hasMore: + if cur.kind != TagLit: + cont = false + else: + if tagIs(cur, "replay"): # Always load replay actions (macro cache operations) - t = next(s) # move past (replay - while t.kind != ParRi and t.kind != EofToken: - if t.kind == ParLe: - var buf = createTokenBuf(50) - nextSubtree(s, buf, t) - var cursor = cursorAt(buf, 0) - let replayNode = loadNode(c, cursor, suffix, localSyms) + cur.into: + while cur.hasMore: + let replayNode = loadNode(c, cur, suffix, localSyms) if replayNode != nil: result.topLevel.sons.add replayNode - t = next(s) - if t.kind == ParRi: - t = next(s) - else: - raiseAssert "expected ParRi but got " & $t.kind - elif t.tagId == repConverterTag: - t = loadLogOp(c, result.logOps, s, ConverterEntry, attachedTrace, module) - elif t.tagId == repDestroyTag: - t = loadLogOp(c, result.logOps, s, HookEntry, attachedDestructor, module) - elif t.tagId == repWasMovedTag: - t = loadLogOp(c, result.logOps, s, HookEntry, attachedWasMoved, module) - elif t.tagId == repCopyTag: - t = loadLogOp(c, result.logOps, s, HookEntry, attachedAsgn, module) - elif t.tagId == repSinkTag: - t = loadLogOp(c, result.logOps, s, HookEntry, attachedSink, module) - elif t.tagId == repDupTag: - t = loadLogOp(c, result.logOps, s, HookEntry, attachedDup, module) - elif t.tagId == repTraceTag: - t = loadLogOp(c, result.logOps, s, HookEntry, attachedTrace, module) - elif t.tagId == repDeepCopyTag: - t = loadLogOp(c, result.logOps, s, HookEntry, attachedDeepCopy, module) - elif t.tagId == repEnumToStrTag: - t = loadLogOp(c, result.logOps, s, EnumToStrEntry, attachedTrace, module) - elif t.tagId == repMethodTag: - t = loadLogOp(c, result.logOps, s, MethodEntry, attachedTrace, module) - elif t.tagId == repPureEnumTag: - t = loadLogOp(c, result.logOps, s, PureEnumEntry, attachedTrace, module) - #elif t.tagId == repClassTag: - # t = loadLogOp(c, logOps, s, ClassEntry, attachedTrace, module) - elif t.tagId == exportTag: - var lastGood = "" - t = next(s) # skip (export - if t.kind == DotToken: - t = next(s) # skip dot - if t.kind == DotToken: - t = next(s) # skip dot - while true: - if t.kind == Symbol: - let symAsStr = pool.syms[t.symId] - lastGood = symAsStr - # Skip symbols that are re-exported by this dependency but actually - # belong to the module we are compiling fresh: loading them as stubs - # would shadow/collide with the freshly compiled originals. - if c.mainModuleSuffix.len == 0 or - parseSymName(symAsStr).module != c.mainModuleSuffix: - # Resolving an exported symbol of this very module (`export` of a - # symbol that lives in a `when` branch of the same file) lazily - # loads it from the stream we are currently iterating, moving the - # cursor into the symbol's `(sd ...)` definition. Save/restore the - # position so the export-list parse continues where it left off. - let saved = offset(s.r) - let sym = resolveSym(c, symAsStr, false) - if sym != nil: - strTableAdd(interf, sym) - addReexportedEnumFields(c, sym, interf) - s.r.jumpTo(saved) - t = next(s) - elif t.kind == ParRi: - break - else: - raiseAssert "expected Symbol or ParRi but got " & $t.kind & - " (" & (if t.kind == ParLe: pool.tags[t.tagId] else: "") & - ") in export list of module " & suffix & ", last symbol: " & lastGood - t = next(s) - elif t.tagId == includeTag: - loadInclude(c, s, result.includes, t) - elif t.tagId == importTag: - loadImport(c, s, result.deps, t) - elif t.tagId == reexpModTag: - # a re-exported MODULE: (reexpmod "name" "suffix"); the module sym - # is a qualifier in this module's interface — materialized by the - # caller (modulegraphs), which can register interface tables - t = next(s) - var mname = "" - var msuffix = "" - if t.kind == StringLit: - mname = pool.strings[t.litId] - t = next(s) - if t.kind == StringLit: - msuffix = pool.strings[t.litId] - t = next(s) - if t.kind != ParRi: - raiseAssert "expected ParRi in reexpmod entry of module " & suffix - t = next(s) + elif tagIs(cur, "unusedid"): + # backend id seed — consumed eagerly by `moduleId`/`readUnusedId`; just + # skip past it here so the rest of the header still loads. + skip cur + elif tagIs(cur, "repconverter"): loadLogOp(c, result.logOps, cur, ConverterEntry, attachedTrace, module) + elif tagIs(cur, "repdestroy"): loadLogOp(c, result.logOps, cur, HookEntry, attachedDestructor, module) + elif tagIs(cur, "repwasmoved"): loadLogOp(c, result.logOps, cur, HookEntry, attachedWasMoved, module) + elif tagIs(cur, "repcopy"): loadLogOp(c, result.logOps, cur, HookEntry, attachedAsgn, module) + elif tagIs(cur, "repsink"): loadLogOp(c, result.logOps, cur, HookEntry, attachedSink, module) + elif tagIs(cur, "repdup"): loadLogOp(c, result.logOps, cur, HookEntry, attachedDup, module) + elif tagIs(cur, "reptrace"): loadLogOp(c, result.logOps, cur, HookEntry, attachedTrace, module) + elif tagIs(cur, "repdeepcopy"): loadLogOp(c, result.logOps, cur, HookEntry, attachedDeepCopy, module) + elif tagIs(cur, "repenumtostr"): loadLogOp(c, result.logOps, cur, EnumToStrEntry, attachedTrace, module) + elif tagIs(cur, "repmethod"): loadLogOp(c, result.logOps, cur, MethodEntry, attachedTrace, module) + elif tagIs(cur, "reppureenum"): loadLogOp(c, result.logOps, cur, PureEnumEntry, attachedTrace, module) + elif tagIs(cur, "export"): + cur.into: + while cur.hasMore and cur.kind == DotToken: skip cur # flags / type + while cur.hasMore: + if cur.kind == Symbol: + let symAsStr = symName(cur) + # Skip symbols re-exported by this dependency but owned by the module + # being compiled fresh (they would collide with the fresh originals). + if c.mainModuleSuffix.len == 0 or + parseSymName(symAsStr).module != c.mainModuleSuffix: + let sym = resolveSym(c, symAsStr, false) + if sym != nil: + strTableAdd(interf, sym) + addReexportedEnumFields(c, sym, interf) + skip cur + else: + raiseAssert "expected Symbol or ParRi but got " & $cur.kind & + " in export list of module " & suffix + elif tagIs(cur, "include"): loadInclude(c, cur, result.includes) + elif tagIs(cur, "import"): loadImport(c, cur, result.deps) + elif tagIs(cur, "reexpmod"): + # a re-exported MODULE: (reexpmod "name" "suffix"); the module sym is a + # qualifier in this module's interface — materialized by modulegraphs. + var mname, msuffix = "" + cur.into: + if cur.hasMore and cur.kind == StrLit: (mname = strVal(cur); skip cur) + if cur.hasMore and cur.kind == StrLit: (msuffix = strVal(cur); skip cur) if mname.len > 0 and msuffix.len > 0: result.reexportedModules.add (mname, msuffix) - elif t.tagId == offerTag: - # (offer ...) — see the - # writer. Resolve to PSyms/PTypes here; modulegraphs registers them into - # `procInstCache`. Best-effort: a type that fails to resolve drops the - # whole offer (the consumer then re-instantiates, the prior behaviour). - t = next(s) # skip (offer + elif tagIs(cur, "offer"): + # (offer ...) — resolve + # to PSyms/PTypes; modulegraphs registers them into `procInstCache`. + # Best-effort: a type that fails to resolve drops the whole offer. var genSym, instSym: PSym = nil var paramsCount = 0 var cts: seq[PType] = @[] var idx = 0 var ok = true - while t.kind != ParRi and t.kind != EofToken: - if t.kind == Symbol: - if idx == 0: genSym = resolveHookSym(c, t.symId) - elif idx == 1: instSym = resolveHookSym(c, t.symId) - else: - let ct = tryCreateTypeStub(c, t.symId) - if ct == nil: ok = false - else: cts.add ct - inc idx - elif t.kind == IntLit: - paramsCount = int(pool.integers[t.intId]) - t = next(s) - if t.kind != ParRi: - raiseAssert "expected ParRi in offer entry of module " & suffix - t = next(s) + cur.into: + while cur.hasMore: + if cur.kind == Symbol: + if idx == 0: genSym = resolveHookSym(c, symName(cur)) + elif idx == 1: instSym = resolveHookSym(c, symName(cur)) + else: + let ct = tryCreateTypeStub(c, symName(cur)) + if ct == nil: ok = false + else: cts.add ct + inc idx + skip cur + elif cur.kind == IntLit: + paramsCount = int(intVal(cur)) + skip cur + else: skip cur if ok and genSym != nil and instSym != nil: result.genericOffers.add (genSym, instSym, cts, paramsCount) - elif t.tagId == typeOfferTag: - # (toffer "" "") — see the writer. The two - # full names arrive as string literals; intern them and resolve to a - # PSym/PType, then FULLY load the instance (its array bounds/fields) so - # `searchInstTypes` can match its params (a Partial stub has empty kids). - # Best-effort: a type that fails to resolve drops the offer. - t = next(s) # skip (toffer + elif tagIs(cur, "toffer"): + # (toffer "" "") — intern the two full names, + # resolve, FULLY load the instance (so `searchInstTypes` can match its + # params). Best-effort: a failure to resolve drops the offer. var genName, instName = "" var idx = 0 - while t.kind != ParRi and t.kind != EofToken: - if t.kind == StringLit: - if idx == 0: genName = pool.strings[t.litId] - elif idx == 1: instName = pool.strings[t.litId] - inc idx - t = next(s) - if t.kind != ParRi: - raiseAssert "expected ParRi in toffer entry of module " & suffix - t = next(s) + cur.into: + while cur.hasMore: + if cur.kind == StrLit: + if idx == 0: genName = strVal(cur) + elif idx == 1: instName = strVal(cur) + inc idx + skip cur + else: skip cur if genName.len > 0 and instName.len > 0: - # Resolving/loading these entities lazily reads from the SAME stream we - # are iterating, moving its cursor — save/restore around it (cf. the - # export-list handling above). - let saved = offset(s.r) - let genSym = resolveHookSym(c, pool.syms.getOrIncl(genName)) - let inst = tryCreateTypeStub(c, pool.syms.getOrIncl(instName)) + let genSym = resolveHookSym(c, genName) + let inst = tryCreateTypeStub(c, instName) if genSym != nil and inst != nil: loadType(c, inst) result.typeOffers.add (genSym, inst) - s.r.jumpTo(saved) - elif t.tagId == modulesrcTag: + elif tagIs(cur, "modulesrc"): # self-identification record for the standalone include-graph scanner; - # not needed by the lazy loader, just skip past it. - t = skip(s, t) - elif t.tagId == implTag: + # not needed by the loader, just skip past it. + skip cur + elif tagIs(cur, "implementation"): cont = false - elif LoadFullAst in flags or t.tagId == letTag or t.tagId == varTag: + elif LoadFullAst in flags or tagIs(cur, toNifTag(nkLetSection)) or tagIs(cur, toNifTag(nkVarSection)): # Parse the full statement. let/var sections are loaded unconditionally # (see above) so `{.compileTime.}` globals reach the eager initializer. - var buf = createTokenBuf(50) - nextSubtree(s, buf, t) - t = next(s) # skip ParRi - var cursor = cursorAt(buf, 0) - let stmtNode = loadNode(c, cursor, suffix, localSyms) + let stmtNode = loadNode(c, cur, suffix, localSyms) if stmtNode != nil: result.topLevel.sons.add stmtNode else: cont = false - else: - cont = false proc loadNifModule*(c: var DecodeContext; suffix: ModuleSuffix; interf, interfHidden: var TStrTable; flags: set[LoadFlag] = {}): PrecompiledModule = # Ensure module index is loaded - moduleId returns the FileIndex for this suffix let module = moduleId(c, string(suffix), flags) - # Load the module AST (or just replay actions if loadFullAst is false) - # processTopLevel also collects export instructions. - # Lazy symbol loading may have moved the stream cursor since the module was - # opened, so rewind to the start of the module body before reading it. - let s = addr c.mods[module].stream - s[].r.jumpTo(c.mods[module].contentStart) - var t = next(s[]) - if t.kind == ParLe and pool.tags[t.tagId] == toNifTag(nkStmtList): - t = next(s[]) # skip (stmts - t = next(s[]) # skip flags - result = processTopLevel(c, s[], flags, interf, string(suffix), module.int) + # Load the module AST (or just replay actions if loadFullAst is false). + # processTopLevel also collects export instructions. Step 2 phase 2: read the + # body straight from the resident `buf` cursor (no stream, no rewind — lazy + # loads use independent cursors so they never disturb this one). + var cur = beginRead(c.mods[module].buf) + if cur.kind == TagLit and tagIs(cur, toNifTag(nkStmtList)): + inc cur # enter (stmts (past the tag head, onto the flags dot) + skip cur # flags dot (processTopLevel skips the type dot itself) + result = processTopLevel(c, cur, flags, interf, string(suffix), module.int) else: result = PrecompiledModule(topLevel: newNode(nkStmtList)) @@ -2983,9 +3170,10 @@ proc writeLoweredModule*(c: var DecodeContext; config: ConfigRef; # the sole source the cg stage reads — no `.s.nif` fallback for them). sealLoadedRoutines(c) var w = Writer(infos: LineInfoWriter(config: config), currentModule: thisModule) + w.deps = newIcBuilder(64) w.inProc = 1 w.lowering = true - var content = createTokenBuf(300) + var content = newIcBuilder(300) let rootInfo = trLineInfo(w, precomp.topLevel.info) createStmtList(content, rootInfo) @@ -2998,15 +3186,34 @@ proc writeLoweredModule*(c: var DecodeContext; config: ConfigRef; for op in hooks: writeOp(w, content, op) - var bottom = createTokenBuf(300) + var bottom = newIcBuilder(300) # Imperative init code + global let/var/const sections + replay actions — all # that a backend-loaded `topLevel` carries (routines are lazy index sdefs, not # here). Emits + seals the module's globals. w.writeToplevelNode content, bottom, precomp.topLevel - # Routine DEFS with transformed bodies, sourced from the index. + # TYPE DEFS: emit into the `.t.bif` ONLY the owned types the lower stage actually + # loaded (`Complete`) — those are the ones it can have MUTATED (proc type → + # `ccClosure`, env type grown with captured fields), so the `.t.bif` must carry + # the mutated version. Every untouched owned type stays `Partial` → a reference to + # it below writes a `SymUse` that a cg/emit consumer resolves from the `.s.bif` + # (loader fallback `typeCursor`/`ensureSemBuf`), so we no longer force-load + + # re-serialize the whole type table here. Guard on `Complete`: a type reached as + # an owned son of an earlier def is already `Sealed` (emitted inline) — skip it. + for t in loadedModuleTypes(c, FileIndex thisModule): + if t.state == Complete: + writeType(w, bottom, t) + + # Routine DEFS (with transformed bodies) + CONST DEFS this module owns, sourced + # from the index. Consts are lazy index sdefs too (like routines): the + # backend-loaded `topLevel` carries only runtime init (module var/let sections), + # NOT consts — especially `importc`/magic consts (`SIG_DFL`, `hasAllocStack`, …) + # which have no runtime init at all. `writeNifModule` emitted them via the full + # AST walk; here we must enumerate them from the index, else a cross-module + # `SymUse` resolves to a nil `skModule` stub (`expr(skModule); unknown symbol`). for s in moduleSymbolStubs(c, FileIndex thisModule): - if s.kindImpl in routineKinds and s.itemId.module == thisModule: + if (s.kindImpl in routineKinds or s.kindImpl == skConst) and + s.itemId.module == thisModule: writeSymDef(w, bottom, s) # Lifted hook ROUTINES (`@bk`, NEW in the lower stage — no `.s.nif` sdef, so @@ -3059,16 +3266,28 @@ proc writeLoweredModule*(c: var DecodeContext; config: ConfigRef; # (implementation) ). content.addParLe implTag, NoLineInfo content.addParRi() - content.add bottom + addAll(content, bottom) content.addParRi() - var dest = createTokenBuf(600) + var dest = newIcBuilder(600) createStmtList(dest, rootInfo) - dest.add w.deps - for i in 3 ..< content.len-1: - dest.add content[i] + # Carry the seed FORWARD: the lower stage minted backend syms/types from the + # per-module counters (seeded out of the `.s.bif`'s `(unusedid)`), so they now + # hold the post-lower high-water mark. Record it so the `cg` stage — which + # loads THIS `.t.bif` and mints still more (RTTI hooks) — seeds above it too. + let lfi = FileIndex thisModule + let loweredSeed = if c.mods.hasKey(lfi): + max(c.mods[lfi].symCounter, c.mods[lfi].typeCounter) + else: 0'i32 + dest.addParLe unusedIdTag, NoLineInfo + dest.addIntLit loweredSeed.int64 dest.addParRi() - writeFile(dest, outfile) + addAll(dest, w.deps) + addStmtsBody(dest, content) + dest.addParRi() + # Step 3: the lowered whole-module artifact is binary NIF (`.t.bif`) too — the + # cg/emit stages load it via `toNifFilename`. No text twin (debug via bif2nif). + storeBif(dest, outfile, "." & extractModuleSuffix(outfile)) when isMainModule: import std / syncio diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 2c1bf17590..171d656230 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -1071,7 +1071,7 @@ proc genRecordField(p: BProc, e: PNode, d: var TLoc) = proc genInExprAux(p: BProc, e: PNode, a, b, d: var TLoc) -proc genFieldCheck(p: BProc, e: PNode, obj: Rope, field: PSym) = +proc genFieldCheck(p: BProc, e: PNode, obj: Rope, field: PSym, ty: PType) = var test, u, v: TLoc for i in 1..` while module B (the =destroy owner) defines a + # different number → undefined at link. Give it a content-derived `disamb` + # (stable across processes) + `HookDisambBit`, exactly like `symPrototype` does + # for the hook itself: same `typ` ⇒ same C name everywhere, and the bit makes + # `emitsBodyInThisModule` emit the body in every demander (merge dedups). The + # `"rttiDestroy"` op-name keeps its key disjoint from the real `=destroy` hook's. + setHookDisamb(g, result, "rttiDestroy", typ) proc genHook(m: BModule; t: PType; info: TLineInfo; op: TTypeAttachedOp; result: var Builder) = let theProc = getAttachedOp(m.g.graph, t, op) diff --git a/compiler/ccgutils.nim b/compiler/ccgutils.nim index 9960ea807b..430c16f172 100644 --- a/compiler/ccgutils.nim +++ b/compiler/ccgutils.nim @@ -11,7 +11,7 @@ import ast, types, msgs, wordrecg, - platform, trees, options, cgendata, mangleutils, renderer + platform, trees, options, cgendata, mangleutils, renderer, modulegraphs import std/[hashes, strutils, formatfloat] @@ -116,7 +116,17 @@ proc makeUnique(m: BModule; s: PSym, name: string = ""): string = # restarts at 0 and would collide with loaded symbols' ids if s.itemId.isBackendMinted: result.add "_c" - result.add $s.itemId.item + if (s.disamb and HookDisambBit) != 0'i32: + # A backend-minted sym whose `disamb` is content-derived (setHookDisamb gave + # it HookDisambBit) — e.g. the `rttiDestroy` wrapper. Its `itemId.item` is a + # PER-PROCESS backend counter, so using it makes the C name diverge across + # the emit-everywhere processes: the type's RTTI table (emit-everywhere, + # merge-deduped) ends up referencing one process's `_c` while the + # wrapper is defined with another's -> undefined at link (`rttiDestroy_c23`). + # The content-derived disamb is stable across processes, so use it. + result.add $s.disamb + else: + result.add $s.itemId.item else: result.add "_u" # Mirror `mangleProcNameExt`: use the per-(module,name) `disamb`, NOT diff --git a/compiler/cgen.nim b/compiler/cgen.nim index 411be49778..b54160edf8 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -185,9 +185,13 @@ proc fillLoc(a: var TLoc, k: TLocKind, lode: PNode, s: TStorageLoc) {.inline.} = a.storage = s proc t(a: TLoc): PType {.inline.} = - if a.lode.kind == nkSym: + if a.lode.kind == nkSym and a.lode.sym.typ != nil: result = a.lode.sym.typ else: + # Under `nim ic` an object-field reference is a typeless leaf stub (its def + # lives in another seek; see ast2nif `FieldMarker`) that carries its type on + # the NODE instead. Fall back to the node type. Byte-neutral for non-IC, where + # a real sym always has a type. result = a.lode.typ proc lodeTyp(t: PType): PNode = @@ -1218,8 +1222,17 @@ proc closeNamespaceNim(result: var Builder) = proc closureSetup(p: BProc, prc: PSym) = if tfCapturesEnv notin prc.typ.flags: return - # prc.ast[paramsPos].last contains the type we're after: - var ls = lastSon(prc.ast[paramsPos]) + # prc.ast[paramsPos].last contains the type we're after — BUT a closure loaded + # from a `.t.bif` (a lambda-lifted nested proc / generic instance the `lower` + # stage transformed) can arrive with an EMPTY AST param node: the lifted hidden + # `:env` param lives in `typ.n`, the authoritative signature (`genProc` already + # reads `typ.n`, not the AST). The two param nodes diverge across the NIF + # boundary; fall back to `typ.n` so the env param resolves instead of indexing + # an empty container. + var params = prc.ast[paramsPos] + if params.safeLen == 0 and prc.typ.n != nil and prc.typ.n.kind == nkFormalParams: + params = prc.typ.n + var ls = lastSon(params) if ls.kind != nkSym: internalError(p.config, prc.info, "closure generation failed") var env = ls.sym @@ -1464,8 +1477,21 @@ proc genProcLvl3*(m: BModule, prc: PSym) = var returnStmt: Snippet = "" assert(prc.ast != nil) + # A body LOADED from `.t.bif` was already FULLY lowered by the `lower` stage — + # transformed AND destructor-injected (see nifbackend.generateLowerStage). The + # `.t.bif` is the authoritative backend artifact; re-injecting here would lower + # it twice (double `=destroy` calls) and, worse, re-lift the env hooks per cg + # process (owned by nobody → undefined at link). So inject ONLY when the body + # was re-derived in this process (`wasLoaded == false`). Capture before + # `transformBody`, which returns the cached body (non-nil) when it was loaded. + # ONLY under IC: in a normal `nim c` build `transformedBody` is the ordinary + # transform cache (set whenever `transformBody` already ran for `prc`, e.g. a + # CT-evaluated or earlier-referenced routine), NOT a `.t.bif` load — gating on + # it there would WRONGLY skip destructor injection and miscompile (orc + # decref-on-freed). The `.t.bif`-loaded-body concept exists only under cmdNifC. + let wasLoaded = m.config.cmd == cmdNifC and prc.transformedBody != nil var procBody = transformBody(m.g.graph, m.idgen, prc, {}) - if sfInjectDestructors in prc.flags: + if sfInjectDestructors in prc.flags and not wasLoaded: procBody = injectDestructorCalls(m.g.graph, m.idgen, prc, procBody) let tmpInfo = prc.info @@ -1525,6 +1551,17 @@ proc genProcLvl3*(m: BModule, prc: PSym) = for i in 1..