IC: make the location primitives representation-agnostic

`initLoc`, `fillLoc`, `putIntoDest` and `putDataIntoDest` take `AnyNode` and
store `origin(lode)`. That is the gate the previous commit was for: 99 of the
180 generator procs build a `TLoc` from their node, so until these four accept
a cursor none of those 99 could migrate, and with them accepting one they all
can — `TLoc.lode` is still a `PNode`, and on a bridged buffer it is the SAME
`PNode` a tree-driven build would have stored.

`bnode.origin` is the ambient accessor (through `currentNav`, like `sym` and
`typ`), so a generator proc does not have to be handed the buffer to build a
location. It answers nil for a file-backed body, which is correct — those tokens
came from no `PNode` — and asserts on a bridged one, where a node head always
has an origin and a miss means the cursor is not where the caller thinks.

The origin check in the grinder now goes through that ambient path rather than
calling `originOf` directly, because the direct call is not the path the
generator will take and testing it would have proved the wrong thing.

Verified: grind clean, origins exact by reference at every node of 1431 bodies;
the default path is byte-identical to HEAD; all four build configurations
compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
This commit is contained in:
Araq
2026-08-30 14:21:43 +02:00
parent 9f34f5e42b
commit e5bafa48c5
5 changed files with 70 additions and 18 deletions

View File

@@ -592,6 +592,26 @@ when defined(newIcBackend):
# every expression node, so nothing in the expression codegen can migrate
# until they exist.
proc origin*(n: BNode): PNode =
## The `PNode` this cursor was encoded from, when it is reading a bridged
## buffer. This is what lets a cursor-driven generator keep filling
## `TLoc.lode` with a `PNode`: the answer is the SAME OBJECT the encoder was
## handed, so the identity comparisons the backend already does still hold.
##
## A node head on a bridged buffer always has an origin, so a miss is a bug
## rather than a shrug — most likely a cursor that is not at a node head.
## Reading a FILE-backed body has no origins at all and answers nil, which is
## correct: there is no `PNode` those tokens came from.
let b = currentNav().bridge
if b == nil: return nil
result = originAt(b, n.raw)
doAssert result != nil or nifcore.kind(n.raw) == DotToken,
"bridged node has no origin: " & rawDesc(n)
template origin*(n: PNode): PNode = n
## The `PNode` spelling, so `AnyNode` code can ask for an origin without
## caring which representation it holds.
proc atom(n: BNode): Cursor {.inline.} =
## The payload token of a leaf node.
result = astChildren(n)
@@ -708,6 +728,9 @@ else:
# 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 origin*(n: BNode): BNode = n
## No bridge in this build: a node IS its own origin.
template son*(n: BNode; i: int): BNode =
## Named indexed access. Exists so a call site states "child i" in a form
## that survives `BNode` becoming a `Cursor`; keep `i` small and constant.

View File

@@ -98,6 +98,15 @@ type
## symbol kind, fields included.
syms*: seq[PSym]
types*: seq[PType]
origins*: Table[int, PNode]
## Token position -> the `PNode` encoded there, so a cursor can name the
## node it came from. Lives here rather than in `BridgeBuf` because the
## lookup has to be reachable from wherever a location is built, which is
## everywhere in the generator — the same reason `syms` is here.
buf*: ptr TokenBuf
## The buffer `origins` is keyed against; `cursorToPosition` needs it.
## Borrowed, not owned: it points into the `BridgeBuf` that a scoped
## `withBridge` is currently reading, and never outlives it.
BodyNav* = object
## The resolution context for ONE routine body. `base` is what the decoder
@@ -115,6 +124,12 @@ type
fallbacks*: int ## resolved through the decoder
registered*: int ## definitions the walk registered
proc originAt*(t: BridgeTables; c: Cursor): PNode =
## The source node a cursor was encoded from, or nil when there is none (a
## `DotToken`, or a cursor that is not at a node head).
if t == nil or t.buf == nil: return nil
result = t.origins.getOrDefault(cursorToPosition(t.buf[], c), nil)
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.

View File

@@ -539,7 +539,7 @@ proc putLocIntoDest(p: BProc, d: var TLoc, s: TLoc) =
else:
d = s # ``d`` is free, so fill it with ``s``
proc putDataIntoDest(p: BProc, d: var TLoc, n: PNode, r: Rope) =
proc putDataIntoDest(p: BProc, d: var TLoc, n: AnyNode, r: Rope) =
if d.k != locNone:
var a: TLoc = initLoc(locData, n, OnStatic)
# need to generate an assignment here
@@ -550,10 +550,10 @@ proc putDataIntoDest(p: BProc, d: var TLoc, n: PNode, r: Rope) =
# we cannot call initLoc() here as that would overwrite
# the flags field!
d.k = locData
d.lode = n
d.lode = origin(n)
d.snippet = r
proc putIntoDest(p: BProc, d: var TLoc, n: PNode, r: Rope; s=OnUnknown) =
proc putIntoDest(p: BProc, d: var TLoc, n: AnyNode, r: Rope; s=OnUnknown) =
if d.k != locNone:
# need to generate an assignment here
var a: TLoc = initLoc(locExpr, n, s)
@@ -564,7 +564,7 @@ proc putIntoDest(p: BProc, d: var TLoc, n: PNode, r: Rope; s=OnUnknown) =
# we cannot call initLoc() here as that would overwrite
# the flags field!
d.k = locExpr
d.lode = n
d.lode = origin(n)
d.snippet = r
proc binaryStmt(p: BProc, e: PNode, d: var TLoc, op: TypedBinaryOp) =

View File

@@ -252,23 +252,30 @@ proc emitsBodyInThisModule(m: BModule, prc: PSym): bool =
else:
result = prc.itemId.module == m.module.position
proc initLoc(k: TLocKind, lode: PNode, s: TStorageLoc, flags: TLocFlags = {}): TLoc =
result = TLoc(k: k, storage: s, lode: lode,
# `TLoc.lode` stays a `PNode` even when the generator is driven off a cursor,
# and `origin` is why: on a bridged buffer it answers the very node the encoder
# was handed, so a location built from a cursor holds the same object a location
# built from the tree would have held. That is what keeps the identity
# comparisons the backend already does (`preventNrvo`'s `dest != le`,
# `isPartOf(d.lode, …)`) meaning what they meant. Taking `AnyNode` here is what
# unblocks the 99 generator procs that build a location from their node.
proc initLoc(k: TLocKind, lode: AnyNode, s: TStorageLoc, flags: TLocFlags = {}): TLoc =
result = TLoc(k: k, storage: s, lode: origin(lode),
snippet: "", flags: flags)
proc fillLoc(a: var TLoc, k: TLocKind, lode: PNode, r: Rope, s: TStorageLoc) {.inline.} =
proc fillLoc(a: var TLoc, k: TLocKind, lode: AnyNode, r: Rope, s: TStorageLoc) {.inline.} =
# fills the loc if it is not already initialized
if a.k == locNone:
a.k = k
a.lode = lode
a.lode = origin(lode)
a.storage = s
if a.snippet == "": a.snippet = r
proc fillLoc(a: var TLoc, k: TLocKind, lode: PNode, s: TStorageLoc) {.inline.} =
proc fillLoc(a: var TLoc, k: TLocKind, lode: AnyNode, s: TStorageLoc) {.inline.} =
# fills the loc if it is not already initialized
if a.k == locNone:
a.k = k
a.lode = lode
a.lode = origin(lode)
a.storage = s
proc t(a: TLoc): PType {.inline.} =
@@ -1902,7 +1909,10 @@ when defined(newIcBackend):
# would not do.
proc grindOrigins(enc: var BridgeBuf; c: BNode; a: PNode; path: string) =
if a == nil: return
let src = originOf(enc, c.raw)
# Through the AMBIENT accessor (`bnode.origin`, via `currentNav`), which
# is the one a migrated generator proc will call from inside `initLoc` —
# not the direct `originOf`, which would test a path nothing uses.
let src = origin(c)
if src != a:
internalError(m.config, prc.info,
"bridge origin is not the source node at <body>" & path & " in " &
@@ -1914,7 +1924,8 @@ when defined(newIcBackend):
for child in sons(a):
grindOrigins(enc, son(c, i), child, path & "[" & $i & "]")
inc i
grindOrigins(enc, BNode(rootCursor(enc)), body, "")
withBridge(enc.tables):
grindOrigins(enc, BNode(rootCursor(enc)), body, "")
var rt = toPNode(enc)
var enc2 = toTokenBuf(rt, m.config)

View File

@@ -84,12 +84,10 @@ type
conf: ConfigRef
symIdx: Table[int, int] ## PSym identity -> index into `tables.syms`
typeIdx: Table[int, int] ## PType identity -> index into `tables.types`
origins: Table[int, PNode] ## token position -> the `PNode` encoded there
proc initBridgeBuf*(conf: ConfigRef; cap = 64): BridgeBuf =
BridgeBuf(bld: newIcBuilder(cap), tables: BridgeTables(), conf: conf,
symIdx: initTable[int, int](), typeIdx: initTable[int, int](),
origins: initTable[int, PNode]())
symIdx: initTable[int, int](), typeIdx: initTable[int, int]())
# ---------------------------------------------------------------------------
# Encode
@@ -177,7 +175,7 @@ proc encodeNode(b: var BridgeBuf; n: PNode) =
# generator could not migrate without `TLoc` itself changing representation —
# and `TLoc` lives in `astdef`, at the bottom of the module graph, so that
# would push the seam far below the backend.
b.origins[b.bld.buf.len] = n
b.tables.origins[b.bld.buf.len] = n
if n.kind == nkSym and n.sym != nil:
encodeSym(b, n)
return
@@ -213,12 +211,17 @@ proc toTokenBuf*(n: PNode; conf: ConfigRef): BridgeBuf =
## tokens, and the tables hold the `PSym`/`PType` objects the tree pointed at.
result = initBridgeBuf(conf)
encodeNode(result, n)
# The tables carry a BORROWED pointer to the buffer so `originAt` can key
# against it. Set once, here, after encoding is finished and the buffer will
# not be reallocated out from under it.
result.tables.buf = addr result.bld.buf
proc originOf*(b: var BridgeBuf; c: Cursor): PNode =
proc originOf*(b: var BridgeBuf; c: Cursor): PNode {.inline.} =
## The `PNode` that was encoded at `c`, or nil when `c` is a `DotToken` (a nil
## child) or does not point at a node head. Identity-preserving: this is the
## very object the encoder was handed, not a copy, which is the whole point.
b.origins.getOrDefault(cursorToPosition(b.bld.buf, c), nil)
b.tables.buf = addr b.bld.buf
originAt(b.tables, c)
proc rootCursor*(b: var BridgeBuf): Cursor {.inline.} =
## A read cursor at the encoded root. `beginRead` asserts every tag was