From e988e0366c86d94e5222b10e5759db17f099bd2d Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 30 Aug 2026 14:04:37 +0200 Subject: [PATCH] IC: hand the transformed body to cgen as a buffer, and read the analysis off it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `transf.handOffBody` is the seam between the two halves: rewriting ends, and from there the reading side works off a cursor. `cgen.genProcLvl3` takes the handoff once the body is final and runs `allPathsAsgnResult` — which has been `AnyNode` since the predicates landed — over the CURSOR rather than the `PNode`. Three things this had to get right, none of them obvious from the outside. WHERE the handoff goes. A bridged buffer is a SNAPSHOT, so it has to be taken after the last rewrite. Destructor injection runs after `transformBody` returns, and `easyResultAsgn` sets `nfPreventCg` on the tree later still — a buffer taken at the end of `transformBody` would describe a tree that no longer exists by the time anything read it. So the API lives in `transf`, which owns that invariant, and the call site is in `cgen`, which is where rewriting actually finishes. The one flag `easyResultAsgn` writes afterwards is read only by the `PNode` generator, and it sits in the branch that does not run the analysis at all. WHY it is gated on `-d:newIcBackend`. `expr` cannot migrate a piece at a time: it dispatches to ~60 emitters, so either all of them take `AnyNode` or the dispatch converts at every node. Until that happens the generator still needs a `PNode`, and building a buffer per routine in a default build would cost a tree walk and buy nothing. The analyses are the part that moves now. THAT IT IS ACTUALLY LOAD-BEARING. "Byte-identical output" is worth nothing if the new path never ran, so: cursor-driven and `PNode`-driven builds produce identical `.c` (50/50 on an 89k-line target, 12/12 on the grind target), and forcing the cursor-driven answer wrong changes 10 of 12 files. The first number alone would have been consistent with the code being dead. Verified: grind clean (1431 bodies, 260_431 nodes, 0 disagreements, neither tolerance taken); 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 | 31 +++++++++++++++++++++++++++---- compiler/transf.nim | 19 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/compiler/cgen.nim b/compiler/cgen.nim index 0a7229fd4b..640dee50b8 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -2061,12 +2061,33 @@ proc genProcLvl3*(m: BModule, prc: PSym) = var procBody = transformBody(m.g.graph, m.idgen, prc, {}) if sfInjectDestructors in prc.flags and not wasLoaded: procBody = injectDestructorCalls(m.g.graph, m.idgen, prc, procBody) + # THE HANDOFF (`transf.handOffBody`). Rewriting is done for this body — + # transformed, and destructor-injected when this process did the injecting — + # so from here the reading side works off a cursor. + # + # Under `-d:newIcBackend` only, because that is what the switch means: the + # generator still needs a `PNode` (`expr` dispatches to ~60 emitters that have + # to move together or not at all), so building a buffer in a default build + # would cost every routine a tree walk and buy nothing. The ANALYSES below are + # already `AnyNode`, and they are the part that moves now. when defined(newIcBackend): - # AFTER every rewrite this routine's body gets, which is the tree the bridge - # actually has to carry: transformed, and destructor-injected when this - # process did the injecting. + var bodyBuf = handOffBody(procBody, m.config) grindBridge(m, p, prc, procBody) + template readBody(res, call: untyped) = + ## Run a migrated `AnyNode` analysis over the body the READING side sees: + ## a cursor over the handed-off buffer when there is one, the `PNode` + ## otherwise. Both spellings type-check, and the generated C must not depend + ## on which one ran — which is what the byte-identical `.c` check verifies + ## end to end, a stronger statement than the node-level grinder can make. + when defined(newIcBackend): + withBridge(bodyBuf.tables): + let n {.inject.} = BNode(bodyBuf.rootCursor) + res = call + else: + let n {.inject.} = procBody + res = call + let tmpInfo = prc.info discard freshLineInfo(p, prc.info) @@ -2085,8 +2106,10 @@ proc genProcLvl3*(m: BModule, prc: PSym) = # declare the result symbol: assignLocalVar(p, resNode) assert(res.loc.snippet != "") + var paths = Unknown + readBody(paths, allPathsAsgnResult(p, n)) if p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc} and - allPathsAsgnResult(p, procBody) == InitSkippable: + paths == InitSkippable: # In an ideal world the codegen could rely on injectdestructors doing its job properly # and then the analysis step would not be required. discard "result init optimized out" diff --git a/compiler/transf.nim b/compiler/transf.nim index f0cf9dd33b..424464d75a 100644 --- a/compiler/transf.nim +++ b/compiler/transf.nim @@ -37,6 +37,7 @@ type proc transformBody*(g: ModuleGraph; idgen: IdGenerator; prc: PSym; flags: TransformFlags): PNode import closureiters, lambdalifting +import nodebridge type PTransCon = ref object # part of TContext; stackable @@ -1436,6 +1437,24 @@ proc transformBody*(g: ModuleGraph; idgen: IdGenerator; prc: PSym; flags: Transf #if prc.name.s == "main": # echo "transformed into ", renderTree(result, {renderIds}) +proc handOffBody*(body: PNode; conf: ConfigRef): BridgeBuf = + ## THE HANDOFF from the rewriting stage to the reading stage: the transformed + ## body, as a `TokenBuf` a reader can cursor over (`nodebridge`). + ## + ## It lives here because the invariant it carries is this module's: a bridged + ## buffer is a SNAPSHOT, so it must be taken after the LAST rewrite the body + ## will receive. Anything that mutates a node afterwards — `cgen.easyResultAsgn` + ## setting `nfPreventCg` is the one that does — leaves the buffer describing a + ## tree that no longer exists. + ## + ## The call site is in `cgen` rather than at the end of `transformBody` for + ## exactly that reason: destructor injection runs *after* `transformBody` + ## returns and is another rewrite, so transforming is not the last step and a + ## buffer taken here would be stale before it was read. `transformBody` returns + ## a `PNode` on purpose; this is the point where a caller that has finished + ## rewriting says so. + result = toTokenBuf(body, conf) + proc transformStmt*(g: ModuleGraph; idgen: IdGenerator; module: PSym, n: PNode; flags: TransformFlags = {}): PNode = if nfTransf in n.flags: result = n