IC: give the bridge origin tracking, so TLoc.lode stops blocking the generator

Measured before designing: of the 180 `PNode`-taking procs in `cgen`/`ccgexprs`/
`ccgstmts`/`ccgcalls`, **99 build a `TLoc` from a node**. So the generator cannot
move to the seam without an answer for `TLoc.lode`, and the obvious answers are
both bad — leaving it a `PNode` means a cursor-driven proc cannot fill it, and
changing its representation means editing `TLoc`, which lives in `astdef` at the
bottom of the module graph, pushing the seam far below the backend and forcing a
flag day.

There is a third answer, and nifcore already had the piece it needs.
`cursorToPosition` is documented as a stable per-token key, and `TokenBuf.len`
is where the next token lands — so the encoder records `position -> PNode` as it
walks, and `originOf` inverts it. A cursor-driven generator can then put the
ORIGINAL node into a location: `TLoc.lode` stays a `PNode`, and the identity
comparisons already in the backend (`preventNrvo`'s `dest != le`,
`isPartOf(d.lode, …)`) keep meaning what they meant, because it is the same
object and not an equal copy.

Asserted, not assumed: the bridge grinder now walks cursor and tree together and
requires `originOf(c) == a` by REFERENCE at every node — 1431 bodies, 0
failures. Recording the position one token off makes it fail on the first body,
so the check is not vacuous.

This unblocks the generator migration without a flag day: `expr` and its ~60
emitters can move to `AnyNode` with `TLoc` untouched.

Verified: grind clean (1431 bodies, 260_431 nodes, 0 disagreements); 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:15:37 +02:00
parent e988e0366c
commit 9f34f5e42b
2 changed files with 44 additions and 1 deletions

View File

@@ -1893,6 +1893,29 @@ when defined(newIcBackend):
grindLockstep(m, p, prc, BNode(rootCursor(enc)), body, "<bridge>",
gradeable = true)
# ORIGIN IDENTITY, at every node. The generator migration rests on this and
# on nothing else: if a cursor can name the very `PNode` it was encoded
# from, `TLoc.lode` stays a `PNode` and the identity comparisons already in
# the backend keep working, so the 99 of 180 generator procs that build a
# location from a node do not force `TLoc` to change representation.
# Asserted rather than assumed, with `==` on the reference: an equal copy
# would not do.
proc grindOrigins(enc: var BridgeBuf; c: BNode; a: PNode; path: string) =
if a == nil: return
let src = originOf(enc, c.raw)
if src != a:
internalError(m.config, prc.info,
"bridge origin is not the source node at <body>" & path & " in " &
prc.name.s & ": got " &
(if src == nil: "nil" else: $src.kind & "@" & $cast[int](src)) &
" want " & $a.kind & "@" & $cast[int](a))
if a.safeLen > 0:
var i = 0
for child in sons(a):
grindOrigins(enc, son(c, i), child, path & "[" & $i & "]")
inc i
grindOrigins(enc, BNode(rootCursor(enc)), body, "")
var rt = toPNode(enc)
var enc2 = toTokenBuf(rt, m.config)
withBridge(enc2.tables):

View File

@@ -84,10 +84,12 @@ 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]())
symIdx: initTable[int, int](), typeIdx: initTable[int, int](),
origins: initTable[int, PNode]())
# ---------------------------------------------------------------------------
# Encode
@@ -162,8 +164,20 @@ proc encodeSym(b: var BridgeBuf; n: PNode) =
proc encodeNode(b: var BridgeBuf; n: PNode) =
if n == nil:
# A nil child is a `DotToken` and has no origin: there is no node to
# remember, and `originOf` answering nil for it is the right answer.
b.bld.addDotToken()
return
# ORIGIN TRACKING. `len` is where this node's head token is about to land, and
# `cursorToPosition` is its inverse — nifcore documents that index as a stable
# key for exactly this. Recording it is what keeps `TLoc.lode` a `PNode`: a
# cursor-driven generator can still put the ORIGINAL node in a location, so
# the identity comparisons that already exist (`preventNrvo`'s `dest != le`,
# `isPartOf(d.lode, …)`) keep meaning what they meant. Without this the
# 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
if n.kind == nkSym and n.sym != nil:
encodeSym(b, n)
return
@@ -200,6 +214,12 @@ proc toTokenBuf*(n: PNode; conf: ConfigRef): BridgeBuf =
result = initBridgeBuf(conf)
encodeNode(result, n)
proc originOf*(b: var BridgeBuf; c: Cursor): PNode =
## 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)
proc rootCursor*(b: var BridgeBuf): Cursor {.inline.} =
## A read cursor at the encoded root. `beginRead` asserts every tag was
## closed, so a mis-nested encode is caught here rather than as nonsense