diff --git a/compiler/ast.nim b/compiler/ast.nim index 80de2317f5..b6cef5c6ed 100644 --- a/compiler/ast.nim +++ b/compiler/ast.nim @@ -1723,36 +1723,56 @@ proc addParam*(procType: PType; param: PSym) = procType.n.add newSymNode(param) rawAddSon(procType, param.typ) -const magicsThatCanRaise = { +const magicsThatCanRaise* = { mNone, mSlurp, mStaticExec, mParseExprToAst, mParseStmtToAst, mEcho} -proc canRaiseConservative*(fn: PNode): bool = - if fn.kind == nkSym and fn.sym.magic notin magicsThatCanRaise: - result = false - else: - result = true +# `canRaise` and `canRaiseConservative` are asked by the C backend, which is +# migrating to reading routine bodies straight off a `.bif` `Cursor` rather than +# off a materialised `PNode` tree (see `compiler/bnode.nim`). Both predicates +# only ever look at a node's `kind`, `sym` and `typ`, so ONE body serves either +# spelling -- but `BNode` is defined in `bnode.nim`, which imports this module, +# so the `BNode` overloads cannot live here. The bodies therefore live in +# templates and `bnode.nim` instantiates them for its own node type: one source +# of truth, no import cycle, and no second copy to keep in sync. +# +# `fn.typ.n` below is a *type's* formal-params node, not a routine body: it is +# always fully materialised, so indexing it is not the hazard that indexing a +# body node is. -proc canRaise*(fn: PNode): bool = - if fn.kind == nkSym and (fn.sym.magic notin magicsThatCanRaise or - {sfImportc, sfInfixCall} * fn.sym.flags == {sfImportc} or - sfGeneratedOp in fn.sym.flags): - result = false - elif fn.kind == nkSym and fn.sym.magic == mEcho: - result = true - elif fn.typ != nil and fn.typ.kind == tyProc and fn.typ.n != nil: - # TODO check for n having sons? or just return false for now if not - if fn.typ.n[0].kind == nkSym: - result = false +template canRaiseConservativeImpl*(fnArg: typed): bool = + block: + let fn = fnArg + not (fn.kind == nkSym and fn.sym.magic notin magicsThatCanRaise) + +template canRaiseImpl*(fnArg: typed): bool = + block: + let fn = fnArg + var res: bool + if fn.kind == nkSym and (fn.sym.magic notin magicsThatCanRaise or + {sfImportc, sfInfixCall} * fn.sym.flags == {sfImportc} or + sfGeneratedOp in fn.sym.flags): + res = false + elif fn.kind == nkSym and fn.sym.magic == mEcho: + res = true + elif fn.typ != nil and fn.typ.kind == tyProc and fn.typ.n != nil: + # TODO check for n having sons? or just return false for now if not + if fn.typ.n[0].kind == nkSym: + res = false + else: + # A proc-typed value with no explicit raises slot still has + # unspecified effects, which sempass2 treats conservatively. + # Codegen needs to do the same in order to keep goto-exception + # checks after indirect/closure calls. + res = ((fn.typ.n[0].len < effectListLen) or + fn.typ.n[0][exceptionEffects] == nil or + fn.typ.n[0][exceptionEffects].safeLen > 0) else: - # A proc-typed value with no explicit raises slot still has - # unspecified effects, which sempass2 treats conservatively. - # Codegen needs to do the same in order to keep goto-exception - # checks after indirect/closure calls. - result = ((fn.typ.n[0].len < effectListLen) or - fn.typ.n[0][exceptionEffects] == nil or - fn.typ.n[0][exceptionEffects].safeLen > 0) - else: - result = false + res = false + res + +proc canRaiseConservative*(fn: PNode): bool = canRaiseConservativeImpl(fn) + +proc canRaise*(fn: PNode): bool = canRaiseImpl(fn) proc toHumanStrImpl[T](kind: T, num: static int): string = result = $kind diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index a6842ff530..3183b37bc2 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -205,9 +205,9 @@ will tell us the precise offsets anyway. ]# const - hiddenTypeTagName = "ht" - symDefTagName = "sd" - typeDefTagName = "td" + hiddenTypeTagName* = "ht" + symDefTagName* = "sd" + typeDefTagName* = "td" bindingIdTagName = "bid" var @@ -241,6 +241,19 @@ type emittedCanonTypes: Table[string, int32] # canonical type name -> itemId.item of the def +when defined(icLocalSymStats): + # TEMPORARY instrumentation: how is `localSyms` actually populated? The + # snapshot-vs-shared-table question only matters if body-local NIF names exist + # at all, and `isLocalSym` below returns a hardwired `false`. + import std / exitprocs + + var lsLocalHit, lsFieldStub, lsMiss, lsSdReg, lsExtractReg: int + addExitProc proc () = + if lsLocalHit + lsFieldStub + lsMiss + lsSdReg + lsExtractReg > 0: + stderr.writeLine "LOCALSYM localHit=" & $lsLocalHit & + " fieldStub=" & $lsFieldStub & " miss=" & $lsMiss & + " sdReg=" & $lsSdReg & " extractReg=" & $lsExtractReg + proc isLocalSym(sym: PSym): bool {.inline.} = ## Every symbol is emitted as a *global* (module-suffixed) name so that its ## `sdef` gets an index entry and is resolvable by index lookup even when @@ -394,7 +407,7 @@ proc stripFieldMarker(rawName: string): string {.inline.} = else: rawName[0 ..< rawName.len - FieldMarker.len] -proc isFieldNifName(name: string): bool {.inline.} = +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) @@ -1363,7 +1376,7 @@ var modFlagsTag = registerTag("modflags") # instead of a plain construction, and no read was ever recognised as a move. # Only wrap when there is something to say, so the common sym use stays a bare # token. -const symNodeFlagsTagName = "nflags" +const symNodeFlagsTagName* = "nflags" var symNodeFlagsTag = registerTag(symNodeFlagsTagName) const PersistedSymNodeFlags = PersistentNodeFlags - {nfLazyType, nfHasComment} @@ -2833,6 +2846,7 @@ proc extractLocalSymsFromTree(c: var DecodeContext; n: var Cursor; thisModule: s let sym = PSym(itemId: id, kindImpl: skStub, name: stubName, disamb: sn.count.int32, state: Complete) localSyms[symName] = sym + when defined(icLocalSymStats): inc lsExtractReg # `loadSymFromCursor` enters the `(sd` and consumes the whole block, # leaving n positioned after the closing `)`. loadSymFromCursor(c, sym, n, thisModule, localSyms) @@ -2898,12 +2912,15 @@ proc loadSymStub(c: var DecodeContext; symAsStr: string; thisModule: string; if sn.module.len == 0: result = localSyms.getOrDefault(symAsStr) if result != nil: + when defined(icLocalSymStats): inc lsLocalHit return result elif isFieldMarked(sn.name): + when defined(icLocalSymStats): inc lsFieldStub # 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: + when defined(icLocalSymStats): inc lsMiss raiseAssert "local symbol '" & symAsStr & "' not found in localSyms." # Global symbol - look up in index for lazy loading result = c.syms.getOrDefault(symAsStr)[0] @@ -3263,6 +3280,18 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; s = c.loadSymStub(n, thisModule, localSyms) result = newSymNode(s, info) result.typField = typ + if typ == nil: + # `(ht . )` — an EXPLICITLY nil node type. Without this the + # node's type is LOAD-ORDER DEPENDENT: `newSymNode` above marks the + # node lazy only when the symbol was still an unloaded stub at this + # moment, so the very same `.bif` node answers `sym.typ` or `nil` + # for `n.typ` depending on whether something else happened to touch + # that symbol first. Pin it to the lazy reading — the one `newSymNode` + # exists to provide (see `nifcBackendActive` in astdef: snapshotting + # a nil leaves the node permanently typeless and the backend then + # reads `t.flags` off it) — so the answer is a property of the file, + # not of the traversal order. + result.flags.incl nfLazyType elif tagIs(n, symDefTagName): let info = c.infos.oldLineInfo(n.info, cursorPool(n)) let name = n.firstSon @@ -3288,6 +3317,7 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; sym = PSym(itemId: id, kindImpl: skStub, name: stubName, disamb: sn.count.int32, state: Complete) localSyms[symName] = sym # register for later references + when defined(icLocalSymStats): inc lsSdReg # Now fully load the symbol from the sdef loadSymFromCursor(c, sym, n, thisModule, localSyms) sym.state = c.loadedState # mark as fully loaded @@ -3466,6 +3496,80 @@ proc materializeLazyBody*(c: var DecodeContext; node: PNode) = node.typField = real.typField node.flags = real.flags +# --------------------------------------------------------------------------- +# Cursor-native backend seam (see `bnode.nim`) +# +# The three things a `.bif` `Cursor` cannot answer on its own — what symbol a +# `Symbol` token names, what type a node's type slot denotes, and what +# `TLineInfo` its packed line info maps to — all need the decoder's state. They +# are exposed here rather than reimplemented in `bnode` so that the Cursor +# backend and the `PNode` loader resolve names through exactly the same code. +# --------------------------------------------------------------------------- + +type + BodyScope* = object + ## Resolution scope for reading ONE routine body straight off a cursor. + ## `thisModule` is the owning module's NIF suffix (a `Symbol` token with no + ## module suffix is body-local and appears in no index) and `localSyms` is + ## the enclosing sym def's local symbols, so a param/local reference + ## resolves to the SAME `PSym` the signature already created. + thisModule*: string + localSyms*: Table[string, PSym] + +proc lazyBodyCursor*(c: var DecodeContext; node: PNode; scope: var BodyScope; + body: var Cursor): bool = + ## Non-destructive lookup of a deferred routine body: the cursor at its + ## `(stmtlist ...)` plus the scope its symbol references resolve in. Unlike + ## `materializeLazyBody` this does NOT consume the pending entry, so the + ## `PNode` path still works afterwards and the two representations of the same + ## body can be walked side by side and compared — which is how a proc migrated + ## to `BNode` is checked against the one it replaces. + let key = cast[int](node) + if not c.pendingBodies.hasKey(key): return false + let pb = c.pendingBodies[key] + body = pb.cursor + scope = BodyScope(thisModule: pb.thisModule, localSyms: pb.localSyms) + result = true + +proc symFromCursor*(c: var DecodeContext; n: Cursor; scope: var BodyScope): PSym = + ## The `PSym` a `Symbol` / `SymbolDef` / `(sd ...)` token names. Non-consuming + ## (`loadSymStub` advances a `var Cursor`; this one works on a copy). + ## + ## The bare `SymbolDef` case goes through the by-name overload: the cursor + ## overload of `loadSymStub` deliberately rejects it, because inside the + ## loader a def token is always reached through its `(sd ...)` wrapper and a + ## bare one means a malformed stream. A reader that starts at an arbitrary + ## token has no such guarantee, and the def NAMES the same symbol the use + ## does. + var cur = n + if cur.kind == SymbolDef: + result = loadSymStub(c, symName(cur), scope.thisModule, scope.localSyms) + else: + result = loadSymStub(c, cur, scope.thisModule, scope.localSyms) + +proc typeFromCursor*(c: var DecodeContext; n: Cursor; scope: var BodyScope): PType = + ## The `PType` a node's type slot denotes — a `Symbol`, an inline `(td ...)`, + ## or a `DotToken` for "no type of its own". Non-consuming. + var cur = n + result = loadTypeStub(c, cur, scope.localSyms) + +proc nodeFlagsFromCursor*(n: Cursor): TNodeFlags = + ## The node-flags slot: an `Ident` naming the set, or a `DotToken` for empty. + ## Non-consuming. + var cur = n + result = loadAtom(TNodeFlags, cur) + +proc identFromCursor*(c: var DecodeContext; n: Cursor): PIdent = + ## The `PIdent` an `Ident` token names, interned in the SAME cache the loader + ## uses — `nkIdent` nodes compare by identity in places. + result = c.cache.getIdent(strVal(n)) + +proc lineInfoFromCursor*(c: var DecodeContext; n: Cursor): TLineInfo = + ## The `TLineInfo` for a token's packed line info. The `FileId` inside belongs + ## to the `.bif`'s OWN filename pool, so the mapping needs both the pool and + ## the `ConfigRef` the `LineInfoWriter` holds. + result = c.infos.oldLineInfo(n.info, cursorPool(n)) + forceLazyBodyHook = proc (n: PNode) {.nimcall, raises: [], tags: [], gcsafe.} = # `len` (the sole caller path) MUST stay effect-free, so this hook is typed # `raises: []`. The underlying `loadNode` chain infers `raises: [KeyError]` diff --git a/compiler/astdef.nim b/compiler/astdef.nim index c775d7efd9..965e6e7b98 100644 --- a/compiler/astdef.nim +++ b/compiler/astdef.nim @@ -957,7 +957,10 @@ iterator items*(n: PNode): PNode = iterator sons*(n: PNode): PNode = ## Iterates over the children of `n`. Preferred over `for i in 0.. ...) +## +## so a node's own flags and its `typ` occupy the FIRST TWO raw children and the +## AST's child 0 is the THIRD. `nifcore.childCursor` — and `ast2nif`'s own +## `firstSon(n: Cursor)`, which is a RAW structural accessor used to reach the +## name inside an `(sd ...)`/`(td ...)` — return the flags slot, not child 0. +## Every accessor below therefore steps over that two-token prefix. The +## exceptions are exactly: +## +## * `(none)` and a type-less `(empty)`, written bare with NO prefix and no +## children — so "zero raw children" means "no prefix", never "prefix but no +## AST children", which is what makes the two cases distinguishable at all; +## * an `nkSym`, which is not a `TagLit` node: it is a bare `Symbol` token, or +## `(ht )` when the node's type differs from the symbol's, or +## `(nflags )` when persisted node flags need saying, or an +## `(sd ...)` definition; +## * a nil child, written as a `DotToken` — `isNilNode` is the `n == nil` of the +## `PNode` world, which has no `Cursor` counterpart. +## +## `len` follows `safeLen`'s rule and answers 0 for `nkNone..nkNilLit`: the +## payload token of an `nkIntLit`/`nkStrLit`/`nkIdent` sits where a child would, +## and is not one. +## +## `BNode` is DISTINCT from `Cursor` for that same reason. The two accessor sets +## are both spelled `firstSon` and differ by two positions, so a raw structural +## cursor reaching a call site that wants AST children must not silently +## typecheck — and the distinction also keeps `ast2nif.firstSon(n: Cursor)` from +## colliding with this module's. +## +## THE COST MODEL DIFFERS from a `PNode`'s, and that is what the vocabulary is +## shaped around. A `Cursor` is a position in a token stream, and a child is +## reached by stepping over each preceding sibling. Stepping is cheap — a +## `TagLit` token stores the width of its whole subtree, so `nifcore.skip` is a +## single pointer add regardless of how big that subtree is — but there is no +## random access and no way to walk backwards: ## ## * `firstSon` / `secondSon` / `son(n, k)` — O(k) in the NUMBER of preceding ## siblings, not in their size. Cheap for the small constant `k` that nearly @@ -51,18 +83,75 @@ ## OWN tag pool, so a tag id means nothing outside the file it came from and ## there can be no process-global id -> `TNodeKind` table; the answer is ## memoized per pool instead, and the memo is dropped when the pool changes. -## `when isMainModule` at the bottom of this file checks that — run it over -## several `.bif` files at once, since one file alone cannot catch a memo that -## fails to notice the switch. +## +## RESOLUTION CONTEXT. `sym`, `typ` and `info` cannot be answered by the cursor +## alone: a `Symbol` token holds only a NAME, a type slot only a type's name, +## and a packed line info a `FileId` in the `.bif`'s own filename pool. All +## three need the decoder's state, and the decision recorded here is that they +## take it from AMBIENT STATE rather than from a parameter: +## +## * the `DecodeContext` is the process-wide `ast.program`, which already exists +## and is already what `ast.loadSym`/`loadType` resolve through; +## * the per-body half is a `bodynav.BodyNav` on a STACK, pushed by +## `withBodyScope` for the span of one routine body. It has to be a stack and +## not a single slot because generating one routine can pull in another +## (`genProcNoForward`) before the first is finished. +## +## Threading a `DecodeContext` parameter through the ~230 `PNode`-taking procs +## in the cgen files instead would be exactly the churn the `BNode` seam exists +## to avoid, and codegen is already single-threaded within a stage process +## (`--icBackendStage:cg --icBackendModule:X` compiles one module per process). +## +## The nav is not merely a renamed snapshot: it is a SCOPE CHAIN THE TRAVERSAL +## MAINTAINS (`openScope` / `closeScope` / `registerDefHere`), ported from +## Nimony's `typenav`, so a reader descending a body always has exactly the +## definitions it has already walked past and nothing is copied ahead of time. +## `bodynav`'s module doc has the measurement that says how much of a live +## problem the old snapshot was — the honest answer is "none yet" — and why the +## mechanism is still the right shape. `sym` goes through it; `typ` does not, +## because `ast2nif` already materialises types lazily from the module's type +## index and a frame has nothing to add. +## +## HOW THIS IS VERIFIED. Two oracles, neither of them a hand-written +## expectation: +## +## * the `isMainModule` self-test at the bottom of this file checks the +## accessors against the raw token stream of real `.bif` files. Necessary but +## NOT sufficient, and the reason is worth remembering: a vocabulary that is +## uniformly wrong — every accessor off by the same two positions — satisfies +## every accessor-against-accessor check there is. This test passed 5.2M +## assertions on exactly that wrong model. +## * `cgen.grindBNode` (opt-in, `NIM_IC_BNODE_GRIND=1` on an `--ic:on` build) +## walks the cursor and the materialised `PNode` for the SAME body in +## LOCKSTEP and requires `kind`, `len`, `info`, `flags`, the literal payloads, +## `sym` and `typ` to agree at every node of every routine body in the +## dependency closure — and, separately, requires each migrated `AnyNode` proc +## to return the same answer for the whole body. The `PNode` is the oracle, so +## it compares everything rather than what someone thought to check. That walk +## also DRIVES the nav — it opens a scope per node and offers each child to +## `registerDefHere` before descending — so what is graded is the vocabulary as +## a cursor-native pass would actually use it, not a random-access shortcut. +## +## The second oracle is what found the `typ` bug (a bare `Symbol` answered `nil` +## where `ast.typ` falls back to the symbol's type — which silently turns +## `canRaise` into "cannot raise" and drops the goto-exception check after a +## call) and the `.sons` bug named below. A clean run means nothing until you +## have broken an accessor on purpose and watched the grinder fire. ## ## The cgen files hold to one invariant, which is what makes the eventual flip -## mechanical: NO `[]` ON A `PNode` OUTSIDE OF TREE CONSTRUCTION. Every read is +## mechanical: NO `[]` AND NO `.sons` ON A `PNode` OUTSIDE OF TREE +## CONSTRUCTION. `.sons` is not merely the un-portable spelling of the walk: it +## is the raw FIELD, so it bypasses the `len` hook that materializes a deferred +## `nfLazyBody` body, and `for x in n.sons` over a routine body that is still a +## placeholder silently visits NOTHING. `for x in sons(n)` goes through +## `safeLen` and materializes. (This is not hypothetical — it is what the +## differential grinder in `cgen.grindBNode` reported the first time it ran, and +## it had been sitting in `containsResult` itself.) Every read is ## `firstSon`/`secondSon`/`lastSon`/`son(n, k)` or one of the iterators; the ## remaining subscripts are writes that build a fresh `nkProcDef` ## (`theProc[namePos] = ...`), which a `Cursor` backend will not do at all, and ## accesses to a `PType`, a `string`, a `seq` or a `Table`, none of which are -## `BNode`s. The `firstSon`/`secondSon`/`lastSon`/`son` family is defined for -## `PNode` only, so a mistaken base does not compile. +## `BNode`s. ## ## A `PType` has its own vocabulary and its own reason for preferring it: `t[0]` ## is the return type, the base class, the index type or the generic head @@ -79,11 +168,17 @@ ## materializes them lazily from the module's type index, which is the seam ## that matters on that side. -import ast, lineinfos +import ast, lineinfos, idents + +when defined(nimPreviewSlimSystem): + import std / assertions when defined(newIcBackend): import "../dist/nimony/src/lib/nifcore" except pool import ic / enum2nif + import ast2nif, icnifcore, bodynav + export ast2nif.BodyScope + export bodynav # Imported only under the define: `cgen` is compiled during the koch # bootstrap, where the nimony libs are unavailable (`ast2nif` is guarded the # same way). `nifcore` — NOT `nifcursors` — is the reader half of NIF: `bif` @@ -91,7 +186,17 @@ when defined(newIcBackend): # `nifcore.Cursor`. `nifcursors` is the writer/builder cursor over # `PackedToken`s and is a different type entirely. - type BNode* = Cursor + type BNode* = distinct Cursor + ## A cursor at an AST NODE, as distinct from a raw structural cursor: see + ## "THE NODE ENCODING" above. The conversion is deliberately not implicit. + + template raw*(n: BNode): Cursor = Cursor(n) + ## Escape hatch to the underlying structural cursor. Only this module and + ## the decoder should need it. + + proc toBNode*(c: Cursor): BNode {.inline.} = BNode(c) + ## A raw cursor known to sit on an AST node — e.g. the body cursor + ## `ast2nif.lazyBodyCursor` hands back. # `nifcore` also has a `kind(c: Cursor): NifKind` — the TOKEN kind (TagLit, # SymUse, IntLit, ...). It and `kind(n: BNode): TNodeKind` below differ only @@ -99,6 +204,12 @@ when defined(newIcBackend): # nifcore one is always spelled `nifcore.kind`. Modules that import `bnode` # do not import `nifcore`, so they see only the `TNodeKind` one. + const + LeafKinds* = {nkNone..nkNilLit} + ## Exactly `astdef.safeLen`'s set: these have no children, and any token + ## sitting after their flags/type prefix is a PAYLOAD (an int, a string, + ## an ident), not a child. + var kindCachePool: TagPool = nil var kindCache: seq[int16] = @[] ## `TagId -> TNodeKind` for ONE tag pool, -1 where not yet resolved. @@ -107,151 +218,401 @@ when defined(newIcBackend): ## works through one module at a time, so a single-entry memo is enough; ## a pool switch just drops the cache. - proc kind*(n: BNode): TNodeKind = - ## The `TNodeKind` a `.bif` tag encodes — the inverse of `toNifTag`, which + proc tagKind(c: Cursor): TNodeKind = + ## The `TNodeKind` a `.bif` TAG encodes — the inverse of `toNifTag`, which ## is what wrote it (`ast2nif`: `pool.tags.getOrIncl(toNifTag(n.kind))`). ## `parse` is a compare against ~180 strings, far too much per node, so the - ## answer is memoized per tag id. - if nifcore.kind(n) != TagLit: return nkEmpty - let pool = n.tags + ## answer is memoized per tag id. The three wrapper tags that encode an + ## `nkSym` are folded into the memo; `parse` answers `nkNone` for them (and + ## for every non-AST tag, such as the module-level `(unusedid ...)`). + let pool = c.tags if pool != kindCachePool: kindCachePool = pool kindCache = @[] - let id = int(uint32(cursorTagId(n))) + let id = int(uint32(cursorTagId(c))) if id >= kindCache.len: let oldLen = kindCache.len kindCache.setLen(id + 1) for i in oldLen ..< kindCache.len: kindCache[i] = -1'i16 if kindCache[id] < 0: - kindCache[id] = int16(ord(parse(TNodeKind, pool.tagName(cursorTagId(n))))) + let name = pool.tagName(cursorTagId(c)) + let k = if name == hiddenTypeTagName or name == symDefTagName or + name == symNodeFlagsTagName: nkSym + else: parse(TNodeKind, name) + kindCache[id] = int16(ord(k)) result = TNodeKind(kindCache[id]) - proc hasSons*(n: BNode): bool {.inline.} = - nifcore.kind(n) == TagLit and n.cursorJump > 0 + proc kind*(n: BNode): TNodeKind = + ## The node kind. A bare `Symbol`/`SymbolDef` token IS an `nkSym` node — it + ## is how the common symbol use is written — and a `DotToken` is the nil + ## child, which has no kind at all and answers `nkNone`; test it with + ## `isNilNode` rather than comparing kinds. + case nifcore.kind(n.raw) + of TagLit: tagKind(n.raw) + of Symbol, SymbolDef: nkSym + else: nkNone - proc firstSon*(n: BNode): BNode {.inline.} = childCursor(n) + proc isNilNode*(n: BNode): bool {.inline.} = + ## The `n == nil` of the `PNode` world: `ast2nif` writes a nil child as a + ## `DotToken`, and a cursor is never itself nil. + nifcore.kind(n.raw) == DotToken + + proc hasPrefix(c: Cursor): bool {.inline.} = + ## Whether this `TagLit` carries the `withNode` flags/type prefix. `(none)` + ## and a type-less `(empty)` are written bare, and they are the only nodes + ## with zero raw children, so the test is exact. + cursorJump(c) > 0 + + proc astChildren(n: BNode): Cursor = + ## A cursor at AST child 0, or an exhausted cursor when there is none. + ## Steps over the two-token flags/type prefix; see "THE NODE ENCODING". + result = childCursor(n.raw) + if result.hasMore: skip result # flags + if result.hasMore: skip result # type + + template walkChildren(n: BNode; c, body: untyped) = + ## Shared guard for every child accessor: only a `TagLit` node that is not a + ## leaf kind has children at all. + if nifcore.kind(n.raw) == TagLit and kind(n) notin LeafKinds: + var c = astChildren(n) + body + + proc hasSons*(n: BNode): bool = + walkChildren(n, c): + return c.hasMore + result = false proc son*(n: BNode; i: int): BNode = ## Child `i`. O(i) — `skip` is a single pointer add, because a `TagLit` ## token carries the width of its whole subtree. - result = childCursor(n) - for _ in 0 ..< i: skip result + walkChildren(n, c): + for _ in 0 ..< i: + doAssert c.hasMore, "son: index out of range" + skip c + doAssert c.hasMore, "son: index out of range" + return BNode(c) + raiseAssert "son: node has no children" + proc firstSon*(n: BNode): BNode {.inline.} = son(n, 0) proc secondSon*(n: BNode): BNode {.inline.} = son(n, 1) proc len*(n: BNode): int = ## Counts the children — O(len). Never put this in a loop condition; the - ## iterators below and `hasSons` exist so it is not needed there. + ## iterators below and `hasSons` exist so it is not needed there. Follows + ## `safeLen`: a leaf kind answers 0 even though its payload token is there. result = 0 - if nifcore.kind(n) != TagLit: return 0 - var c = childCursor(n) - while c.hasMore: - inc result - skip c + walkChildren(n, c): + while c.hasMore: + inc result + skip c proc safeLen*(n: BNode): int {.inline.} = len(n) - ## Same as `len`: a non-`TagLit` token has no children and answers 0, so the - ## `PNode` distinction (`len` faults on a literal, `safeLen` does not) has - ## nothing to guard here. + ## Same as `len`: `len` already answers 0 for a leaf, so the `PNode` + ## distinction (`len` faults on a literal, `safeLen` does not) has nothing + ## to guard here. proc lastSon*(n: BNode): BNode = ## O(len) — the token stream has no back pointer. Fine once per node, a ## trap inside a loop; `sonsButLast` is the loop form. - var c = childCursor(n) - while true: - result = c - skip c - if not c.hasMore: break + walkChildren(n, c): + while c.hasMore: + result = BNode(c) + skip c + return result + raiseAssert "lastSon: node has no children" iterator sons*(n: BNode): BNode = - if nifcore.kind(n) == TagLit: - var c = childCursor(n) + walkChildren(n, c): while c.hasMore: - yield c + yield BNode(c) skip c iterator sonsFrom*(n: BNode; start: int): BNode = - if nifcore.kind(n) == TagLit: - var c = childCursor(n) + walkChildren(n, c): for _ in 0 ..< start: if not c.hasMore: break skip c while c.hasMore: - yield c + yield BNode(c) skip c iterator isons*(n: BNode; start = 0): tuple[i: int, n: BNode] = - if nifcore.kind(n) == TagLit: - var c = childCursor(n) + walkChildren(n, c): var i = 0 while i < start and c.hasMore: skip c inc i while c.hasMore: - yield (i, c) + yield (i, BNode(c)) skip c inc i iterator sonsButLast*(n: BNode; count = 1): BNode = ## One pass with `count` nodes of lookahead — the token stream cannot be ## walked backwards, so the tail is held back instead of subtracted. - if nifcore.kind(n) == TagLit: - var c = childCursor(n) - var pending: seq[BNode] = @[] + walkChildren(n, c): + var pending: seq[Cursor] = @[] while c.hasMore: pending.add c skip c if pending.len > count: - yield pending[0] + yield BNode(pending[0]) pending.delete(0) iterator isonsButLast*(n: BNode; count = 1): tuple[i: int, n: BNode] = - if nifcore.kind(n) == TagLit: - var c = childCursor(n) - var pending: seq[BNode] = @[] + walkChildren(n, c): + var pending: seq[Cursor] = @[] var i = 0 while c.hasMore: pending.add c skip c if pending.len > count: - yield (i, pending[0]) + yield (i, BNode(pending[0])) inc i pending.delete(0) - # The three that need MORE THAN THE CURSOR, which is the real boundary this - # migration now sits at: none of them can stay a unary accessor. + # ---- resolution: the three that need more than the cursor ----------------- + + var navStack {.threadvar.}: seq[BodyNav] + ## Stack, not a single slot: generating one routine can pull in another + ## before the first is finished. See "RESOLUTION CONTEXT" above. + + proc pushBodyNav*(nav: sink BodyNav) = + navStack.add nav + + proc popBodyNav*(): BodyNav {.discardable.} = + doAssert navStack.len > 0, "popBodyNav without a matching push" + result = navStack.pop() + + template withBodyScope*(scope: BodyScope; body: untyped) = + ## Runs `body` with a fresh `BodyNav` over `scope` current, so `sym`/`typ` + ## inside it resolve the body's names. Pops even if `body` raises, because a + ## codegen error is reported and compilation continues. + pushBodyNav(initBodyNav(scope)) + try: + body + finally: + popBodyNav() + + proc currentNav*(): ptr BodyNav = + ## The nav for the body being read. A `ptr` because the accessors below + ## MUTATE it (the frame cache), and because copying a nav per access would + ## defeat the point. Valid only until the next push — nothing between taking + ## it and using it pushes, and nothing may: `symAt` bottoms out in the + ## decoder, which never re-enters codegen. + doAssert navStack.len > 0, + "BNode.sym/typ outside withBodyScope: a Symbol token is a NAME and needs " & + "the owning module plus the body's scope to resolve" + result = addr navStack[^1] + + proc openScope*(kind = nsBlock) {.inline.} = openScope(currentNav()[], kind) + proc closeScope*() {.inline.} = closeScope(currentNav()[]) + proc registerDefs*(n: BNode) {.inline.} = registerDefs(currentNav()[], n.raw) + proc registerDefHere*(n: BNode) {.inline.} = + discard registerDefHere(currentNav()[], n.raw) + proc navStats*(): tuple[hits, fallbacks, registered: int] = + let nav = currentNav() + result = (nav.hits, nav.fallbacks, nav.registered) + + template withNodeScope*(kind: NavScopeKind; body: untyped) = + ## The traversal-side half of the nav: a walk brackets each scope-bearing + ## construct with this, and the definitions it passes are registered as it + ## goes. See `bodynav`. + openScope(kind) + try: + body + finally: + closeScope() + + proc sym*(n: BNode): PSym = + ## The `PSym` an `nkSym` node names, resolved through the current nav: its + ## scope chain first, the decoder second. The wrapper forms + ## (`(nflags )`, `(ht )`) are peeled by + ## `bodynav.symToken`, beside the code that derives the lookup key from them, + ## so the two cannot drift apart. + result = symAt(currentNav()[], n.raw) + + proc symTyp(n: BNode): PType = + ## The type of the symbol a sym-shaped node names, or nil. + let s = sym(n) + result = if s == nil: nil else: s.typ + + proc typ*(n: BNode): PType = + ## The node's type, INCLUDING the lazy fallback `ast.typ` performs. + ## + ## A sym node whose node type equals its symbol's is written as a bare + ## `Symbol` with no type slot at all (`writeSymNode`), and the `PNode` + ## loader marks such a node `nfLazyType` so `ast.typ` answers `n.sym.typ`. + ## Answering `nil` here instead is not a *smaller* answer, it is a DIFFERENT + ## one, and silently: `ast.canRaise` asks `fn.typ.kind == tyProc` about a + ## call's callee, so a nil type turns "this call can raise" into "it cannot" + ## and the goto-exception check after the call is dropped. The + ## `.bif`-vs-`PNode` grinder found exactly that. + let c = n.raw + case nifcore.kind(c) + of Symbol, SymbolDef: + result = symTyp(n) + of DotToken: + result = nil + of TagLit: + let name = c.tags.tagName(cursorTagId(c)) + if name == hiddenTypeTagName: + # `(ht )`: the node type is spelled out because it differed + # from the symbol's at write time. + result = typeAt(currentNav()[], childCursor(c)) + if result == nil: + # `(ht . )` — an EXPLICITLY nil node type, which the writer tries + # hard not to emit but does for a sym whose own type was nil. The + # loader does not read it as nil either: `newSymNode(sym, info)` runs + # first and sets `nfLazyType` whenever the symbol was still an + # unloaded stub (`typImpl == nil`), and only then is `typField` + # overwritten with this nil — so `ast.typ` falls back to `sym.typ`. + # Mirror that. (The AST's answer is strictly speaking load-order + # dependent — a sym already loaded at that moment would leave + # `nfLazyType` clear and yield nil — which is a fragility of the + # loader, not of this mirror. The grinder walks every body in the + # dependency closure and this is the branch it lands on.) + result = symTyp(n) + elif name == symNodeFlagsTagName: + var inner = childCursor(c) + skip inner + result = typ(BNode(inner)) + elif name == symDefTagName: + result = symTyp(n) + elif not hasPrefix(c): + result = nil + else: + var t = childCursor(c) + skip t # the flags slot + result = typeAt(currentNav()[], t) + else: + result = nil + + proc rawDesc*(n: BNode): string = + ## What the token stream literally says here — the NIF token kind and, for a + ## tag, its name. Diagnostics only: the vocabulary above is the interface, + ## this is the thing it is an interface TO, and a disagreement between the + ## two spellings is almost always explained by the raw shape. + let c = n.raw + result = $nifcore.kind(c) + case nifcore.kind(c) + of TagLit: result.add "/" & c.tags.tagName(cursorTagId(c)) + of Symbol, SymbolDef: result.add "/" & symName(c) + else: discard + + # ---- leaf payloads -------------------------------------------------------- # - # `sym` — a `Symbol` token holds only a NAME. `ast2nif.loadSymStub` turns one - # into a `PSym` from a `DecodeContext` plus the OWNING MODULE's name - # plus that routine body's `localSyms` table, because a name with no - # module suffix is body-local and is not in any index. - # `typ` — same shape: `ast2nif.createTypeStub(c, symName(n))`, so also a - # `DecodeContext`. - # `info` — `rawLineInfo(n)` gives a `NifLineInfo` whose `FileId` belongs to the - # `.bif`'s own pool; `ast2nif.oldLineInfo` maps it to a `TLineInfo` - # through a `LineInfoWriter`, which needs the `ConfigRef`. + # A literal is `( )`: the value is the single token + # after the prefix. These are the accessors `ccgexprs` reaches for on nearly + # every expression node, so nothing in the expression codegen can migrate + # until they exist. + + proc atom(n: BNode): Cursor {.inline.} = + ## The payload token of a leaf node. + result = astChildren(n) + + proc intVal*(n: BNode): BiggestInt = + let c = atom(n) + case nifcore.kind(c) + of IntLit: result = BiggestInt(nifcore.intVal(c)) + of UIntLit: result = cast[BiggestInt](nifcore.uintVal(c)) + of CharLit: result = BiggestInt(ord(charLit(c))) + else: raiseAssert "intVal on " & rawDesc(n) + + proc floatVal*(n: BNode): BiggestFloat = + let c = atom(n) + doAssert nifcore.kind(c) == FloatLit, "floatVal on " & rawDesc(n) + result = BiggestFloat(nifcore.floatVal(c)) + + proc strVal*(n: BNode): string = + let c = atom(n) + doAssert nifcore.kind(c) == StrLit, "strVal on " & rawDesc(n) + result = nifcore.strVal(c) + + proc ident*(n: BNode): PIdent = + let c = atom(n) + doAssert nifcore.kind(c) == Ident, "ident on " & rawDesc(n) + result = identFromCursor(program, c) + + proc flags*(n: BNode): TNodeFlags = + ## The node's own flags, MINUS the two the `PNode` side owns rather than the + ## file: `nfHasComment` is never written (comment text lives in a process- + ## local side channel) and `nfLazyType` is a marker the loader adds to say + ## "ask the symbol for my type" — which is what `typ` above does here + ## unconditionally, so on a cursor the flag has nothing to mark. + let c = n.raw + case nifcore.kind(c) + of TagLit: + let name = c.tags.tagName(cursorTagId(c)) + if name == symNodeFlagsTagName: + # `(nflags )`: the wrapper carries the flags the bare + # `Symbol` token had nowhere to put. + var inner = childCursor(c) + result = nodeFlagsFromCursor(inner) + skip inner + result = result + flags(BNode(inner)) + elif name == hiddenTypeTagName or name == symDefTagName: + result = {} + elif not hasPrefix(c): + result = {} + else: + result = nodeFlagsFromCursor(childCursor(c)) + else: + result = {} + + proc lazyBodyBNode*(node: PNode; scope: var BodyScope; body: var BNode): bool = + ## The cursor for a routine body that is still a deferred `nfLazyBody` + ## placeholder, plus the scope its names resolve in. This is where a `BNode` + ## comes FROM: until codegen is driven off `.bif` cursors end to end, it is + ## the only supply, and it is what lets a migrated proc be run against the + ## `PNode` proc it replaces on the same input. Non-destructive — the + ## placeholder is still materializable afterwards. + var c: Cursor = default(Cursor) + result = lazyBodyCursor(program, node, scope, c) + if result: body = BNode(c) + + proc info*(n: BNode): TLineInfo = + ## No body scope needed: the packed line info resolves through the + ## `.bif`'s own filename pool plus the `ConfigRef`. + result = lineInfoFromCursor(program, n.raw) + + # ---- predicates shared with the `PNode` spelling --------------------------- # - # So the next step is not "implement these three" but deciding where codegen - # gets that context from — a parameter, or module-global state for the span of - # one module's `cg` stage, the way `ast2nif` already keeps its writer state. - # Until then the `{.error.}` stubs report the exact missing piece AT ITS CALL - # SITE rather than collapsing into a cascade of unrelated type errors. - proc sym*(n: BNode): PSym {.error: - "BNode.sym: needs a resolution context, not just the cursor — a Symbol " & - "token is a NAME, and ast2nif.loadSymStub resolves it from a DecodeContext " & - "plus the owning module plus the body's localSyms.".} = discard - proc typ*(n: BNode): PType {.error: - "BNode.typ: needs a resolution context — ast2nif.createTypeStub takes a " & - "DecodeContext.".} = discard - proc info*(n: BNode): TLineInfo {.error: - "BNode.info: needs a resolution context — rawLineInfo(n) is a NifLineInfo " & - "in the .bif's own file pool; ast2nif.oldLineInfo maps it through a " & - "LineInfoWriter, which holds the ConfigRef.".} = discard + # `ast.canRaise` / `ast.canRaiseConservative` only ever look at a node's + # `kind`, `sym` and `typ`, all three of which this module now answers off a + # `Cursor`. They cannot be written as `AnyNode` procs in `ast.nim` because + # `BNode` is defined HERE and this module imports `ast`; so `ast.nim` keeps + # the body in a template and these two instantiate it. There is no second + # copy of the logic — change the template and both spellings change. + + proc canRaiseConservative*(fn: BNode): bool = canRaiseConservativeImpl(fn) + + proc canRaise*(fn: BNode): bool = canRaiseImpl(fn) + + # ---- mixed-mode plumbing -------------------------------------------------- + # + # `son` and `hasSons` are the two vocabulary members the AST does not already + # have, so during the migration they must exist for BOTH node types: the cgen + # files are full of call sites that read a `PSym.ast`, a `PType.n` or a + # freshly built tree, and those stay `PNode`s no matter how far codegen has + # moved. Without these the define does not compile at all and no proc can be + # migrated incrementally. + + template son*(n: PNode; i: int): PNode = n[i] + template hasSons*(n: PNode): bool = n.safeLen > 0 + template isNilNode*(n: PNode): bool = n == nil + + type AnyNode* = PNode | BNode + ## The migration vehicle. A proc written against the vocabulary and typed + ## `AnyNode` serves BOTH representations from one body, which means it can + ## be migrated without a flag day and — more usefully — that the two + ## instantiations can be run against each other on real input. else: type BNode* = PNode + type AnyNode* = PNode - # Only the two the AST does not already have. Everything else in the + # Only the three the AST does not already have. Everything else in the # vocabulary is `ast`/`astdef`'s own `PNode` API — see the module doc. template son*(n: BNode; i: int): BNode = ## Named indexed access. Exists so a call site states "child i" in a form @@ -263,75 +624,152 @@ else: ## `Cursor`. n.safeLen > 0 + template isNilNode*(n: BNode): bool = + ## `n == nil`, in the form that survives the flip: a `Cursor` is never nil, + ## and a nil child is a `DotToken` in the token stream. + n == nil + when isMainModule and defined(newIcBackend): - ## Self-test for the `Cursor` half, which nothing else can reach yet: the - ## backend still runs on `PNode`s, so these accessors have no call sites that - ## a normal build type-checks, let alone executes. Run it against real `.bif` - ## files: + ## Self-test for the `Cursor` half. The backend still runs on `PNode`s, so + ## these accessors have no call sites that a normal build type-checks, let + ## alone executes. Run it against real `.bif` files: ## ## nim c -d:newIcBackend compiler/bnode.nim - ## ./compiler/bnode /*.s.bif + ## ./compiler/bnode /*.bif ## ## Pass SEVERAL files — each `.bif` carries its own tag pool, so one file ## alone cannot catch a `kind` cache that fails to notice the pool changed. + ## + ## What this checks that the previous version could not: the accessors agree + ## with the ENCODING, not merely with each other. Checking `sons` against + ## `len` and `firstSon` is satisfied just as well by a vocabulary that is + ## uniformly off by two — which is what it was, and this is what caught it. + ## + ## `sym`/`typ` are NOT exercised here: they resolve through `ast.program` and + ## a `BodyScope`, i.e. a live compiler, so their checks belong to the backend + ## and not to a standalone binary. What IS checked is every shape assumption + ## they rest on — that `(ht ...)`/`(nflags ...)` have exactly two raw children + ## with the symbol second, and that `(sd ...)` opens with a `SymbolDef`. import std / [os, syncio, assertions] from "../dist/nimony/src/lib" / bif import load, BifModule var nodes = 0 var checks = 0 + var astNodes = 0 - proc walk(n: BNode; base: TokenBuf) = + proc walk(c: Cursor; base: TokenBuf) = inc nodes - if nifcore.kind(n) != TagLit: return - template pos(c: BNode): int = cursorToPosition(base, c) + let n = BNode(c) + template pos(x: Cursor): int = cursorToPosition(base, x) - var listed: seq[int] = @[] - for ch in sons(n): listed.add pos(ch) - doAssert listed.len == len(n), "sons/len disagree" - doAssert (listed.len > 0) == hasSons(n), "hasSons/len disagree" - inc checks, 2 + # Every RAW child, which is what the traversal follows: the AST model below + # deliberately does not see the module-level metadata tags, and a walk that + # only followed `sons` would never reach most of the file. + var rawKids: seq[Cursor] = @[] + if nifcore.kind(c) == TagLit: + var ch = childCursor(c) + while ch.hasMore: + rawKids.add ch + skip ch - if listed.len > 0: - doAssert pos(firstSon(n)) == listed[0], "firstSon" - doAssert pos(lastSon(n)) == listed[^1], "lastSon" - inc checks, 2 - if listed.len > 1: - doAssert pos(secondSon(n)) == listed[1], "secondSon" - inc checks - for i in 0 ..< listed.len: - doAssert pos(son(n, i)) == listed[i], "son " & $i - inc checks, listed.len - - for start in 0 .. min(3, listed.len): - var got: seq[int] = @[] - for ch in sonsFrom(n, start): got.add pos(ch) - doAssert got == listed[start .. ^1], "sonsFrom " & $start - var gotI: seq[int] = @[] - for i, ch in isons(n, start): - doAssert i == start + gotI.len, "isons index" - gotI.add pos(ch) - doAssert gotI == listed[start .. ^1], "isons " & $start - inc checks, 2 - - for count in 1 .. 2: - let want = if listed.len > count: listed[0 ..< listed.len - count] - else: newSeq[int]() - var got: seq[int] = @[] - for ch in sonsButLast(n, count): got.add pos(ch) - doAssert got == want, "sonsButLast " & $count - var gotI: seq[int] = @[] - for i, ch in isonsButLast(n, count): - doAssert i == gotI.len, "isonsButLast index" - gotI.add pos(ch) - doAssert gotI == want, "isonsButLast " & $count - inc checks, 2 - - # `kind` against an uncached lookup — this is what catches a stale cache - # when the tag pool changes from one file to the next. - doAssert kind(n) == parse(TNodeKind, n.tags.tagName(cursorTagId(n))), "kind" + doAssert isNilNode(n) == (nifcore.kind(c) == DotToken), "isNilNode" inc checks - for ch in sons(n): walk(ch, base) + if nifcore.kind(c) in {Symbol, SymbolDef}: + doAssert kind(n) == nkSym, "a Symbol token is an nkSym node" + doAssert not hasSons(n), "a Symbol node has no children" + doAssert len(n) == 0, "a Symbol node has length 0" + inc checks, 3 + + if nifcore.kind(c) == TagLit: + let k = kind(n) + let name = c.tags.tagName(cursorTagId(c)) + + # The encoding invariants the whole vocabulary rests on. + if name == symDefTagName: + doAssert k == nkSym, "(sd ...) is an nkSym node" + doAssert rawKids.len > 0 and nifcore.kind(rawKids[0]) == SymbolDef, + "(sd ...) opens with a SymbolDef" + inc checks, 2 + elif name == symNodeFlagsTagName or name == hiddenTypeTagName: + doAssert k == nkSym, name & " wraps an nkSym" + doAssert rawKids.len == 2, name & " is exactly (payload, symnode)" + doAssert nifcore.kind(rawKids[1]) in {Symbol, SymbolDef, TagLit}, + name & "'s second child is the symbol" + inc checks, 3 + elif k != nkNone: + # A real AST node written through `withNode`: either bare (no prefix and + # no children) or prefix + children. "Exactly one raw child" is + # impossible, and that is what pins the prefix down. + doAssert rawKids.len != 1, + "AST node " & name & " has a flags/type prefix or nothing at all" + inc checks + if rawKids.len == 0: + doAssert not hasSons(n), "bare " & name & " has no children" + doAssert len(n) == 0, "bare " & name & " has length 0" + inc checks, 2 + else: + inc astNodes + let want = + if k in LeafKinds: newSeq[int]() + else: (block: + var s: seq[int] = @[] + for i in 2 ..< rawKids.len: s.add pos(rawKids[i]) + s) + + var listed: seq[int] = @[] + for ch in sons(n): listed.add pos(ch.raw) + doAssert listed == want, + "sons of " & name & " must start AFTER the flags/type prefix" + doAssert listed.len == len(n), "sons/len disagree" + doAssert (listed.len > 0) == hasSons(n), "hasSons/len disagree" + inc checks, 3 + + if listed.len > 0: + doAssert pos(firstSon(n).raw) == listed[0], "firstSon" + doAssert pos(lastSon(n).raw) == listed[^1], "lastSon" + inc checks, 2 + if listed.len > 1: + doAssert pos(secondSon(n).raw) == listed[1], "secondSon" + inc checks + for i in 0 ..< listed.len: + doAssert pos(son(n, i).raw) == listed[i], "son " & $i + inc checks, listed.len + + for start in 0 .. min(3, listed.len): + var got: seq[int] = @[] + for ch in sonsFrom(n, start): got.add pos(ch.raw) + doAssert got == listed[start .. ^1], "sonsFrom " & $start + var gotI: seq[int] = @[] + for i, ch in isons(n, start): + doAssert i == start + gotI.len, "isons index" + gotI.add pos(ch.raw) + doAssert gotI == listed[start .. ^1], "isons " & $start + inc checks, 2 + + for count in 1 .. 2: + let wantB = if listed.len > count: listed[0 ..< listed.len - count] + else: newSeq[int]() + var got: seq[int] = @[] + for ch in sonsButLast(n, count): got.add pos(ch.raw) + doAssert got == wantB, "sonsButLast " & $count + var gotI: seq[int] = @[] + for i, ch in isonsButLast(n, count): + doAssert i == gotI.len, "isonsButLast index" + gotI.add pos(ch.raw) + doAssert gotI == wantB, "isonsButLast " & $count + inc checks, 2 + + # `kind` against an uncached lookup — this is what catches a stale cache + # when the tag pool changes from one file to the next. + let direct = + if name == hiddenTypeTagName or name == symDefTagName or + name == symNodeFlagsTagName: nkSym + else: parse(TNodeKind, name) + doAssert k == direct, "kind" + inc checks + + for ch in rawKids: walk(ch, base) let files = commandLineParams() if files.len == 0: @@ -341,4 +779,5 @@ when isMainModule and defined(newIcBackend): var c = beginRead(m.buf) walk(c, m.buf) endRead c - echo "bnode: files=", files.len, " nodes=", nodes, " checks=", checks, " OK" + echo "bnode: files=", files.len, " nodes=", nodes, " astNodes=", astNodes, + " checks=", checks, " OK" diff --git a/compiler/bodynav.nim b/compiler/bodynav.nim new file mode 100644 index 0000000000..d2aa430fa7 --- /dev/null +++ b/compiler/bodynav.nim @@ -0,0 +1,268 @@ +# +# +# The Nim Compiler +# (c) Copyright 2026 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## `BodyNav` — a scope-chained navigator over a `.bif` routine body. +## +## Ported from Nimony's `nimony/typenav.nim` (`TypeCache` / `TypeScope`). The +## idea being stolen is not the type algebra — we do not need it, `typ` returns +## a fully materialized `PType` — but the SHAPE of the resolution context: +## +## * a chain of scope frames, each a small table, linked to its parent; +## * `openScope` / `closeScope` / `registerLocal`, called BY THE TRAVERSAL as it +## descends and as it walks past each definition; +## * a lookup that consults the chain and, on a miss, falls through to the +## module index (`typenav`'s `tryLoadSym`; here the decoder's own +## `symFromCursor`). +## +## The consequence is the point: the scope is a PRODUCT OF THE WALK. Nothing is +## snapshotted, so nothing can be stale, and a reader that starts at the top of +## a body and descends always has exactly the definitions it has already passed. +## +## WHAT THIS REPLACES. `ast2nif.PendingBody` stashes `localSyms` — a COPY of the +## enclosing sym def's local symbols, taken when the body was deferred — and +## `bnode`'s `BodyScope` then copies it again. `materializeLazyBody` loads the +## body with its own `var pb`, so every definition the load creates lands in a +## table that is discarded on return. A cursor-side reader holding the earlier +## copy therefore cannot see them, and would mint its own `PSym` for the same +## name: two objects, one symbol. +## +## HOW BIG THAT PROBLEM ACTUALLY IS, measured rather than assumed. Build with +## `-d:icLocalSymStats` and every process reports its `localSyms` traffic on +## exit. Over a full `--ic:on` build of the standard-library closure (104 +## backend processes): +## +## localHit=0 fieldStub=2 miss=0 sdReg=5902 extractReg=45 +## +## Definitions register constantly and NOT ONE use ever resolves through the +## table. The reason is `ast2nif.isLocalSym`, which returns a hardwired `false`: +## every symbol is emitted with a module suffix and resolves through the +## decoder's global `syms` memo, so both spellings get the same `PSym` whatever +## either one has cached. The 5902 registrations are object FIELDS, whose uses +## deliberately go to `loadFieldStub` instead. +## +## So the stale snapshot is a LATENT hazard, not a live bug, and this module is +## not a bug fix — it is the mechanism that keeps it latent once `isLocalSym` +## stops being `false`, or once a body-local name appears for any other reason. +## Said plainly so nobody has to re-derive it: today the nav changes no answers, +## and the grinder in `cgen` proves that by requiring the navigated symbol to be +## the same object the `PNode` loader produced, at every node of every body. +## +## It is not decorative either, and that also has a number. Over the same build, +## the grinder's traversal reports `navHits=42236 navFallbacks=12658 +## navRegistered=311`: the chain answers 77% of lookups, and 311 definitions are +## registered by the walk rather than read from a table someone filled in +## earlier. Sabotaging the key (truncating it to three characters, so +## `c_fwrite` and `c_fflush` collide) makes the grinder fail on the first body +## it reaches — so a clean run means the resolution is right, not that the +## lookup never happened. +## +## FIELDS ARE NOT REGISTERED, and that is deliberate. `loadFieldStub` mints a +## fresh stub per use because two distinct fields can share a name (and a +## position) across types — `a.x` and `b.x` in one body are two different +## symbols. Caching a field by its bare name would hand the second use the first +## one's stub, and its type. The nav skips field names entirely and leaves that +## path exactly as it was. + +import std / tables +import ast, ast2nif + +when defined(nimPreviewSlimSystem): + import std / assertions + +import "../dist/nimony/src/lib/nifcore" except pool + +type + NavScopeKind* = enum + nsBlock, ## an ordinary nested scope + nsRoutine ## a routine boundary — see `crossedRoutines` + + NavScope {.acyclic.} = ref object + locals: Table[string, PSym] + parent: NavScope + kind: NavScopeKind + + BodyNav* = object + ## The resolution context for ONE routine body. `base` is what the decoder + ## itself needs (the owning module plus a table `loadSymStub` can write + ## into); the frame chain on top of it is this module's contribution. + base*: BodyScope + current: NavScope + hits*: int ## resolved from the chain + fallbacks*: int ## resolved through the decoder + registered*: int ## definitions the walk registered + +proc initBodyNav*(base: sink BodyScope): BodyNav = + ## A nav over a body, seeded with whatever resolution context the decoder + ## handed out. The root frame is a routine frame: a body IS one. + result = BodyNav(base: base, + current: NavScope(locals: initTable[string, PSym](), + parent: nil, kind: nsRoutine)) + +proc openScope*(nav: var BodyNav; kind = nsBlock) {.inline.} = + nav.current = NavScope(locals: initTable[string, PSym](), + parent: nav.current, kind: kind) + +proc closeScope*(nav: var BodyNav) {.inline.} = + doAssert nav.current.parent != nil, "closeScope past the root frame" + nav.current = nav.current.parent + +template withScope*(nav: var BodyNav; kind: NavScopeKind; body: untyped) = + openScope(nav, kind) + try: + body + finally: + closeScope(nav) + +proc registerLocal*(nav: var BodyNav; name: string; s: PSym) {.inline.} = + ## Record a definition the walk has just passed, in the innermost frame. + nav.current.locals[name] = s + inc nav.registered + +proc lookupLocal*(nav: BodyNav; name: string): PSym = + ## The chain only. `nil` when nothing in scope carries this name. + var it {.cursor.} = nav.current + while it != nil: + let s = it.locals.getOrDefault(name) + if s != nil: return s + it = it.parent + result = nil + +proc crossedRoutines*(nav: BodyNav; name: string): int = + ## How many routine frames separate the use from the definition — 0 when the + ## definition is in the current routine. `typenav` computes the same thing as + ## `LocalInfo.crossedProc`, and it is what tells a closure pass that a name is + ## captured rather than local. Nothing consumes it here yet; it is the reason + ## the frames carry a kind at all, and dropping the kind would make it + ## unrecoverable later. + var it {.cursor.} = nav.current + var crossed = 0 + while it != nil: + if it.locals.getOrDefault(name) != nil: return crossed + if it.kind == nsRoutine: inc crossed + it = it.parent + result = -1 + +# --------------------------------------------------------------------------- +# Names +# +# A symbol reaches the reader in four shapes and they all NAME the same thing; +# `navName` is the one place that knows which token holds the name, so the +# lookup key is derived identically no matter which wrapper the writer chose. + +proc navName*(n: Cursor): string = + ## The NIF name a token denotes, or `""` when the token names no symbol. + case nifcore.kind(n) + of Symbol, SymbolDef: + result = symName(n) + of TagLit: + let tag = n.tags.tagName(cursorTagId(n)) + if tag == symDefTagName: + let name = childCursor(n) + result = if nifcore.kind(name) in {Symbol, SymbolDef}: symName(name) else: "" + elif tag == hiddenTypeTagName: + # `(ht )` + var inner = childCursor(n) + skip inner + result = navName(inner) + elif tag == symNodeFlagsTagName: + # `(nflags )` + var inner = childCursor(n) + skip inner + result = navName(inner) + else: + result = "" + else: + result = "" + +proc symToken*(n: Cursor): Cursor = + ## The token that actually NAMES the symbol, with the wrappers stripped. + ## `loadSymStub` accepts a `Symbol`, a `SymbolDef` or an `(sd ...)` and + ## rejects everything else, so the `(ht ...)` / `(nflags ...)` forms have to be + ## peeled here rather than at each call site — the same peeling `navName` does + ## for the key, kept beside it so the two cannot drift apart. + result = n + while nifcore.kind(result) == TagLit: + let tag = result.tags.tagName(cursorTagId(result)) + if tag == hiddenTypeTagName or tag == symNodeFlagsTagName: + var inner = childCursor(result) + skip inner # the explicit type / the node flags + result = inner + else: + break + +proc cacheFrame(nav: var BodyNav): NavScope = + ## Where a decoder-resolved name is remembered: the nearest ROUTINE frame. + ## Not the innermost frame — a `.bif` name is unique within its module (see + ## `isLocalSym`), so its meaning cannot change between frames, and caching it + ## deeper would only throw it away sooner. Not the root either, so that a + ## nested routine's names die with the nested routine. + result = nav.current + while result.kind != nsRoutine and result.parent != nil: + result = result.parent + +proc symAt*(nav: var BodyNav; n: Cursor): PSym = + ## The symbol a token names: the chain first, the decoder second. + let name = navName(n) + if name.len > 0: + let cached = lookupLocal(nav, name) + if cached != nil: + inc nav.hits + return cached + inc nav.fallbacks + result = symFromCursor(program, symToken(n), nav.base) + if result != nil and name.len > 0 and not isFieldNifName(name): + cacheFrame(nav).locals[name] = result + +proc typeAt*(nav: var BodyNav; n: Cursor): PType {.inline.} = + ## Types are not navigated: `ast2nif` already materializes them lazily from + ## the module's type index, keyed by name, so there is no per-body state to + ## keep and nothing a frame could cache that the decoder does not already. + result = typeFromCursor(program, n, nav.base) + +# --------------------------------------------------------------------------- +# Registration during a walk + +proc registerDefHere*(nav: var BodyNav; n: Cursor): bool {.discardable.} = + ## Register `n` if `n` ITSELF is a definition; do not descend. This is the + ## incremental half: a walk calls it on each child before recursing into it, + ## so a use can only resolve from the chain to a definition the walk has + ## already passed. A use that precedes its definition simply misses and falls + ## through to the decoder, which is the behaviour there was before — the nav + ## degrades to the old path rather than answering wrongly. + result = false + if nifcore.kind(n) == TagLit and + n.tags.tagName(cursorTagId(n)) == symDefTagName: + let name = navName(n) + if name.len > 0 and not isFieldNifName(name): + let s = symFromCursor(program, n, nav.base) + if s != nil: + registerLocal(nav, name, s) + result = true + +proc registerDefs*(nav: var BodyNav; n: Cursor) = + ## Register every definition in the SUBTREE at `n` — `typenav.registerLocals` + ## with the recursion left in, because a Nim body puts `nkIdentDefs` under an + ## `nkVarSection` under the statement list rather than declaring at one level. + ## + ## Call it on entering a scope to get the eager behaviour (every definition + ## known before any use is resolved, which is what a RANDOM-ACCESS reader + ## needs), or per statement to get the incremental one (only definitions + ## already walked past are visible, which is what a real pass wants and what + ## makes use-before-def detectable rather than silently working). + if nifcore.kind(n) == TagLit and + n.tags.tagName(cursorTagId(n)) == symDefTagName: + let name = navName(n) + if name.len > 0 and not isFieldNifName(name): + let s = symFromCursor(program, n, nav.base) + if s != nil: registerLocal(nav, name, s) # `(sd ...)` needs no peeling + return + var c = childCursor(n) + while c.hasMore: + registerDefs(nav, c) + skip c diff --git a/compiler/ccgcalls.nim b/compiler/ccgcalls.nim index 6ae687e7c1..24e0cab2ee 100644 --- a/compiler/ccgcalls.nim +++ b/compiler/ccgcalls.nim @@ -9,7 +9,7 @@ # # included from cgen.nim -proc canRaiseDisp(p: BProc; n: PNode): bool = +proc canRaiseDisp(p: BProc; n: AnyNode): bool = # we assume things like sysFatal cannot raise themselves if n.kind == nkSym and {sfNeverRaises, sfImportc, sfCompilerProc} * n.sym.flags != {}: result = false diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 85dc776454..aa7b7d8c0c 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -1441,7 +1441,7 @@ proc genEcho(p: BProc, n: PNode) = var logCall: CallBuilder p.s(cpsStmts).addStmt(): p.s(cpsStmts).addCall(logCall, logName): - for it in n.sons: + for it in sons(n): if it.skipConv.kind == nkNilLit: p.s(cpsStmts).addArgument(logCall): p.s(cpsStmts).add("\"\"") @@ -3173,7 +3173,7 @@ proc genSetConstr(p: BProc, e: PNode, d: var TLoc) = p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "nimZeroMem"), rdLoc(d), cSizeof(getTypeDesc(p.module, e.typ))) - for it in e.sons: + for it in sons(e): if it.kind == nkRange: idx = getTemp(p, getSysType(p.module.g.graph, unknownLineInfo, tyInt)) # our counter a = initLocExpr(p, it.firstSon) @@ -3202,7 +3202,7 @@ proc genSetConstr(p: BProc, e: PNode, d: var TLoc) = # small set var ts = cUintType(size * 8) p.s(cpsStmts).addAssignment(rdLoc(d), cIntValue(0)) - for it in e.sons: + for it in sons(e): if it.kind == nkRange: idx = getTemp(p, getSysType(p.module.g.graph, unknownLineInfo, tyInt)) # our counter a = initLocExpr(p, it.firstSon) @@ -3853,7 +3853,7 @@ proc containsOpaqueImportcFieldAux(t: PType; n: PNode): bool = if n == nil: return false case n.kind of nkRecList: - for child in n.sons: + for child in sons(n): if containsOpaqueImportcFieldAux(t, child): return true of nkRecCase: @@ -3990,7 +3990,7 @@ proc getNullValueAux(p: BProc; t: PType; obj, constOrNil: PNode, case obj.kind of nkRecList: let isUnion = tfUnion in t.flags - for it in obj.sons: + for it in sons(obj): getNullValueAux(p, t, it, constOrNil, result, init, isConst, info) if isUnion: # generate only 1 field for default value of union @@ -4100,7 +4100,7 @@ proc genConstSimpleList(p: BProc, n: PNode; isConst: bool; result: var Builder) if p.vccAndC and not n.hasSons and n.typ.kind == tyArray: result.addField(arrInit, name = ""): getDefaultValue(p, n.typ.elementType, n.info, result) - for it in n.sons: + for it in sons(n): var ind, val: PNode if it.kind == nkExprColonExpr: ind = it.firstSon @@ -4152,7 +4152,7 @@ proc genConstSeq(p: BProc, n: PNode, t: PType; isConst: bool; result: var Builde def.addField(structInit, name = "data"): var arrInit: StructInitializer def.addStructInitializer(arrInit, kind = siArray): - for ni in n.sons: + for ni in sons(n): def.addField(arrInit, name = ""): genBracedInit(p, ni, isConst, base, def) p.module.s[cfsStrData].add extract(def) @@ -4180,7 +4180,7 @@ proc genConstSeqV2(p: BProc, n: PNode, t: PType; isConst: bool; result: var Buil def.addField(structInit, name = "data"): var arrInit: StructInitializer def.addStructInitializer(arrInit, kind = siArray): - for ni in n.sons: + for ni in sons(n): def.addField(arrInit, name = ""): genBracedInit(p, ni, isConst, base, def) p.module.s[cfsStrData].add extract(def) diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index 96715bf8bc..15d9c6e85b 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -522,7 +522,7 @@ proc genIf(p: BProc, n: PNode, d: var TLoc) = d = getTemp(p, n.typ) genLineDir(p, n) let lend = getLabel(p) - for it in n.sons: + for it in sons(n): # bug #4230: avoid false sharing between branches: if d.k == locTemp and isEmptyType(n.typ): d.k = locNone if it.len == 2: diff --git a/compiler/ccgtypes.nim b/compiler/ccgtypes.nim index 005170e19e..ac2692b525 100644 --- a/compiler/ccgtypes.nim +++ b/compiler/ccgtypes.nim @@ -767,7 +767,7 @@ proc genRecordFieldsAux(m: BModule; n: PNode, check: var IntSet; result: var Builder; unionPrefix = "") = case n.kind of nkRecList: - for ni in n.sons: + for ni in sons(n): genRecordFieldsAux(m, ni, rectype, check, result, unionPrefix) of nkRecCase: if n.firstSon.kind != nkSym: internalError(m.config, n.info, "genRecordFieldsAux") @@ -1075,7 +1075,7 @@ proc getTypeDescAux(m: BModule; origTyp: PType, check: var IntSet; kind: TypeDes let owner = hashOwner(t.sym) if not gDebugInfo.hasEnum(t.sym.name.s, t.sym.info.line, owner): var vals: seq[(string, int)] = @[] - for son in t.n.sons: + for son in sons(t.n): assert(son.kind == nkSym) let field = son.sym vals.add((field.name.s, field.position.int)) diff --git a/compiler/cgen.nim b/compiler/cgen.nim index 17d4dd1307..f8463de4d8 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -1344,20 +1344,20 @@ const harmless = {nkConstSection, nkTypeSection, nkEmpty, nkCommentStmt, nkTempl nkMacroDef, nkMixinStmt, nkBindStmt, nkFormalParams} + declarativeDefs -proc containsResult(n: BNode): bool = +proc containsResult(n: AnyNode): bool = result = false case n.kind of succ(nkEmpty)..pred(nkSym), succ(nkSym)..nkNilLit, harmless: discard of nkReturnStmt: - for ni in n.sons: + for ni in sons(n): if containsResult(ni): return true result = n.hasSons and n.firstSon.kind == nkEmpty of nkSym: if n.sym.kind == skResult: result = true else: - for ni in n.sons: + for ni in sons(n): if containsResult(ni): return true proc easyResultAsgn(n: PNode): PNode = @@ -1380,10 +1380,7 @@ proc easyResultAsgn(n: PNode): PNode = type InitResultEnum = enum Unknown, InitSkippable, InitRequired -proc allPathsAsgnResult(p: BProc; n: BNode): InitResultEnum = - ## Migrated to `BNode` (see bnode.nim). With `newIcBackend` off this is - ## `PNode` and nothing changes; with it on, this body is where the Cursor - ## vocabulary has to exist, and its `{.error.}` stubs name what is missing. +proc allPathsAsgnResult(p: BProc; n: AnyNode): InitResultEnum = # Exceptions coming from calls don't have not be considered here: # # proc bar(): string = raise newException(...) @@ -1409,7 +1406,7 @@ proc allPathsAsgnResult(p: BProc; n: BNode): InitResultEnum = result = Unknown case n.kind of nkStmtList, nkStmtListExpr: - for it in n: + for it in sons(n): result = allPathsAsgnResult(p, it) if result != Unknown: return result of nkAsgn, nkFastAsgn, nkSinkAsgn: @@ -1436,7 +1433,7 @@ proc allPathsAsgnResult(p: BProc; n: BNode): InitResultEnum = of nkIfStmt, nkIfExpr: var exhaustive = false result = InitSkippable - for it in n: + for it in sons(n): # Every condition must not use 'result': if it.len == 2 and containsResult(it.firstSon): return InitRequired @@ -1498,8 +1495,8 @@ proc allPathsAsgnResult(p: BProc; n: BNode): InitResultEnum = # arithmetic operations may raise exceptions result = InitRequired else: - for i in 0.." & path & " in " & + prc.name.s & ": cursor=" & cur & " ast=" & ast) + + if a == nil: + if not c.isNilNode: bail("nil-ness", "not-nil", "nil") + return + if c.isNilNode: bail("nil-ness", "nil", "not-nil") + if c.kind != a.kind: bail("kind", $c.kind, $a.kind) + let here = path & "." & $a.kind + if c.safeLen != a.safeLen: bail("len", $c.safeLen, $a.safeLen) + if c.info != a.info: + bail("info", $(m.config, c.info), $(m.config, a.info)) + + # Symbols first: a wrong symbol shows up as a wrong TYPE two lines below, + # and "cursor=nil ast=tyProc" is a much worse bug report than "these are + # different symbols". + if a.kind == nkSym: + let cs = c.sym + let asym = a.sym + template describe(x: PSym): string = + (if x == nil: "nil" + else: x.name.s & "/" & $x.kind & "/" & $x.itemId & "/" & $x.state) + if cs == nil or asym == nil: + if cs != asym: bail("sym nil-ness", describe(cs), describe(asym)) + elif cs != asym: + # A cross-context object-field reference is stubbed FRESH at every use + # (`loadFieldStub`: two distinct fields can share a local name and + # position across types, so ONE shared stub would mistype one of them). + # Pointer identity is therefore not part of the contract for fields — + # what codegen consumes is the name it re-navigates the reclist with + # (`lookupFieldAgain`) and, for tuples, the position. + if cs.kind == skField and asym.kind == skField: + if cs.name.s != asym.name.s or cs.position != asym.position: + bail("field sym", describe(cs) & "@" & $cs.position, + describe(asym) & "@" & $asym.position) + else: + bail("sym identity", describe(cs), describe(asym)) + + # `nfHasComment` is never serialised and `nfLazyType` is a `PNode`-side + # marker (see `bnode.flags`); everything else must round-trip exactly. + const ownedByTheAst = {nfHasComment, nfLazyType} + if c.flags - ownedByTheAst != a.flags - ownedByTheAst: + bail("flags", $(c.flags - ownedByTheAst), $(a.flags - ownedByTheAst)) + + case a.kind + of nkCharLit..nkUInt64Lit: + if c.intVal != a.intVal: bail("intVal", $c.intVal, $a.intVal) + of nkFloatLit..nkFloat128Lit: + # Compare the BITS: two NaNs are never `==`, and a float that survives the + # round trip must be the same float, not merely an equal one. + if cast[uint64](c.floatVal) != cast[uint64](a.floatVal): + bail("floatVal bits", $cast[uint64](c.floatVal), + $cast[uint64](a.floatVal)) + of nkStrLit..nkTripleStrLit: + if c.strVal != a.strVal: bail("strVal", c.strVal, a.strVal) + of nkIdent: + if c.ident != a.ident: bail("ident", c.ident.s, a.ident.s) + else: discard + + let ct = c.typ + let at = a.typ + if (ct == nil) != (at == nil): + bail("typ nil-ness", + (if ct == nil: "nil" else: $ct.kind) & " raw=" & c.rawDesc, + (if at == nil: "nil" else: $at.kind) & " kind=" & $a.kind & + " typField=" & (if a.typField == nil: "nil" else: $a.typField.kind) & + " lazy=" & $(nfLazyType in a.flags) & + (if a.kind != nkSym: "" else: + " sym=" & a.sym.name.s & "/" & $a.sym.kind & "/" & $a.sym.state & + " symTypImpl=" & (if a.sym.typImpl == nil: "nil" else: $a.sym.typImpl.kind))) + elif ct != nil and ct != at: + # Fields carry their own stub type, so a tolerated field-sym difference + # brings a tolerated type difference with it; compare by kind there. + if a.kind == nkSym and a.sym.kind == skField: + if ct.kind != at.kind: + bail("field typ", $ct.kind, $at.kind) + else: + bail("typ identity", $ct.kind & "/" & $ct.itemId, $at.kind & "/" & $at.itemId) + + # `safeLen` already matched, so indexed access stays in range on both sides. + # `son` rescans from the first child each time, which is quadratic — fine for + # a debug-only oracle over routine bodies, and it keeps the walk honest by + # exercising the same accessor migrated code will use. + # + # The descent is bracketed by a nav scope and each child is offered to + # `registerDefHere` BEFORE it is entered, so this walk maintains the scope + # chain exactly the way a cursor-native pass would have to (see `bodynav`). + # That is the part being graded here: not just that the accessors agree, but + # that they still agree when the resolution context is built by the + # traversal instead of handed to it. + if a.safeLen > 0: + withNodeScope(nsBlock): + var i = 0 + for child in sons(a): + let cc = son(c, i) + registerDefHere(cc) + grindLockstep(m, prc, cc, child, here & "[" & $i & "]") + inc i + + proc grindBNode(m: BModule; p: BProc; prc: PSym) = + ## Differential grinding for the migrating vocabulary, opt-in via + ## `NIM_IC_BNODE_GRIND`: run every proc that has moved to `AnyNode` over + ## BOTH representations of the SAME body and require the same answer. This + ## is the only thing that executes the `Cursor` accessors — codegen itself + ## is still driven off `PNode`s — and it is deliberately the same technique + ## that found the IC bugs earlier on this branch: an oracle beats a + ## hand-written expectation, because it compares everything, not what + ## someone thought to check. + ## + ## Order matters. The `PNode` walk calls `len`, which fires the lazy-body + ## hook and MATERIALIZES the deferred body; the cursor answer is therefore + ## taken first. `lazyBodyBNode` itself does not consume the pending entry. + ## + ## `allPathsAsgnResult` is graded here too, and it is the more valuable of + ## the two: it reaches `typ` (via `skipTypes` on a case selector) and + ## `canRaiseDisp` (via `sym`), so a disagreement exercises the resolution + ## path — `symFromCursor` / `typeFromCursor` against the body's `localSyms` + ## — and not just the child walk. + ## + ## `grindLockstep` runs last and grades the vocabulary itself rather than + ## these two procs; it is the check that actually covers accessors no + ## migrated proc happens to call yet. + if bnodeGrind < 0: + bnodeGrind = ord(existsEnv("NIM_IC_BNODE_GRIND")) + if bnodeGrind == 1: + addExitProc proc () = + stderr.writeLine "BNODEGRIND navHits=" & $navHits & + " navFallbacks=" & $navFallbacks & " navRegistered=" & $navRegistered + if bnodeGrind == 0: return + let ast = prc.ast + if ast == nil or ast.safeLen <= bodyPos: return + let body = son(ast, bodyPos) + if body == nil: return + var scope = default(BodyScope) + var viaCursor = default(BNode) + if not lazyBodyBNode(body, scope, viaCursor): return + + var curResult = false + var curPaths = Unknown + withBodyScope(scope): + curResult = containsResult(viaCursor) + curPaths = allPathsAsgnResult(p, viaCursor) + + let astResult = containsResult(body) + if curResult != astResult: + internalError(m.config, prc.info, + "BNode/PNode disagree on containsResult for " & prc.name.s & + ": cursor=" & $curResult & " ast=" & $astResult) + let astPaths = allPathsAsgnResult(p, body) + if curPaths != astPaths: + internalError(m.config, prc.info, + "BNode/PNode disagree on allPathsAsgnResult for " & prc.name.s & + ": cursor=" & $curPaths & " ast=" & $astPaths) + + withBodyScope(scope): + grindLockstep(m, prc, viaCursor, body, "") + let (hits, fallbacks, registered) = navStats() + navHits += hits + navFallbacks += fallbacks + navRegistered += registered + proc getProcTypeCast(m: BModule, prc: PSym): Rope = result = getTypeDesc(m, prc.loc.t) @@ -1581,6 +1765,8 @@ proc genProcLvl3*(m: BModule, prc: PSym) = # 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. + when defined(newIcBackend): + grindBNode(m, p, prc) let wasLoaded = m.config.cmd == cmdNifC and prc.transformedBody != nil var procBody = transformBody(m.g.graph, m.idgen, prc, {}) if sfInjectDestructors in prc.flags and not wasLoaded: