IC: hand the transformed body to cgen as a buffer, and read the analysis off it

`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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
This commit is contained in:
Araq
2026-08-30 14:04:37 +02:00
parent 47144c7962
commit e988e0366c
2 changed files with 46 additions and 4 deletions

View File

@@ -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"

View File

@@ -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