From 9f34f5e42b38df71f4c94ad36d5a28f5223a5d30 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 30 Aug 2026 14:15:37 +0200 Subject: [PATCH] IC: give the bridge origin tracking, so `TLoc.lode` stops blocking the generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR --- compiler/cgen.nim | 23 +++++++++++++++++++++++ compiler/nodebridge.nim | 22 +++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/compiler/cgen.nim b/compiler/cgen.nim index 640dee50b8..2686f0f35a 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -1893,6 +1893,29 @@ when defined(newIcBackend): grindLockstep(m, p, prc, BNode(rootCursor(enc)), body, "", 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 " & 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): diff --git a/compiler/nodebridge.nim b/compiler/nodebridge.nim index 6b62232f7e..beff142c93 100644 --- a/compiler/nodebridge.nim +++ b/compiler/nodebridge.nim @@ -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