mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-01 19:33:42 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user