`bnode`'s header still said the generator "has to move together, and it needs
write-side capability this seam does not have". It has moved, and `origin` was
the write-side answer, so the section described a state two commits out of date —
the kind of stale map that sends the next person looking for a problem that is
already solved. Rewritten to say what runs on a cursor now, that `origin` is what
keeps `TLoc.lode` a `PNode`, and that the generator's own in-place rewrites run
on the origin (with the hazard spelled out: where a mutation is read back,
generation has to continue on the origin, because the buffer is a snapshot).
Two blockers were also described wrongly:
* THE ALIAS FAMILY is no longer blocked by field identity — that is exact on a
bridged buffer. I checked whether `isPartOf` could migrate now and it cannot,
for a different reason: every call site passes `d.lode` as one operand and that
is a `PNode`, so a generic `isPartOf` would still be handed a `PNode` on one
side and buy nothing. It moves when `TLoc.lode` does.
* `sym`'s note said `isPartOf` "CANNOT be migrated as written" without
qualifying that this is a FILE-path property; a bridged buffer hands back the
object it was given, and the grinder asserts exactly that.
And the cost, which I flagged twice as unmeasured and is now measured on a
50-module target:
baseline (PNode) 6.75s
bridge built, not read 6.79s -- encoding is inside the noise
generator driven off it 8.85s -- +31%
So the encoder is as cheap as claimed and the whole cost is in READING: `son` is
O(i), `kind` indexes a memo per call, `sym`/`typ` go through the nav, `origin` is
a hash lookup per location built. None of it is inherent and none of it has been
optimised. Since a compile is mostly frontend, codegen itself is slowed by well
over 31%.
Verified: both configurations build; cursor-driven and `PNode`-driven output
still identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
`initLoc`, `fillLoc`, `putIntoDest` and `putDataIntoDest` take `AnyNode` and
store `origin(lode)`. That is the gate the previous commit was for: 99 of the
180 generator procs build a `TLoc` from their node, so until these four accept
a cursor none of those 99 could migrate, and with them accepting one they all
can — `TLoc.lode` is still a `PNode`, and on a bridged buffer it is the SAME
`PNode` a tree-driven build would have stored.
`bnode.origin` is the ambient accessor (through `currentNav`, like `sym` and
`typ`), so a generator proc does not have to be handed the buffer to build a
location. It answers nil for a file-backed body, which is correct — those tokens
came from no `PNode` — and asserts on a bridged one, where a node head always
has an origin and a miss means the cursor is not where the caller thinks.
The origin check in the grinder now goes through that ambient path rather than
calling `originOf` directly, because the direct call is not the path the
generator will take and testing it would have proved the wrong thing.
Verified: grind clean, origins exact by reference at every node of 1431 bodies;
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
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
The generator does not migrate to a `Cursor` and should not: transf, destructor
injection, closure lifting and the tree codegen builds as it goes all CONSTRUCT
nodes, and a cursor is a read pointer into a shared token buffer. `nodebridge`
is the seam instead — a rewriting pass keeps producing a `PNode`, and anything
that only reads is handed a `TokenBuf`, from which a `Cursor` (and so a `BNode`)
is a pointer.
The bridge is NOT the `.bif` format, and the difference is the point. A `.bif`
is read by a different process, so every symbol and type has to be written as a
NAME to look up again. A bridged buffer is read by the process that built it, so
a symbol reference is `(bsym <idx>)` into a side table holding the very `PSym`
the encoder was handed, and the type slot is `(btyp <idx>)` the same way. The
node shape is otherwise identical to the file format, so `bnode` reads a bridged
buffer with the accessors it already has; `bodynav` grows one branch each in
`symAt`/`typeAt`, and `bnode` learns that `bsym` is an `nkSym`.
That buys the property this branch has been blocked on: **`sym` IS IDEMPOTENT ON
A BRIDGED BUFFER, FIELDS INCLUDED.** On the file path it cannot be —
`loadFieldStub` mints a fresh stub per use because two distinct fields can share
a name and a position across types — which is what stops `aliases.isPartOf`
moving to the seam. A bridge hands back the object it was given. The grinder now
asserts exactly that: field syms are excluded from the idempotence check on the
file path and INCLUDED on a bridged one.
Verification is the existing oracle pointed at a harder target. `grindBNode`
compares two decodings of one file and has to excuse two differences; the bridge
is compared against its own live input and must excuse NEITHER, so both
tolerances are counted and the bridge asserts it took neither. `toPNode` is
covered without a hand-written comparator that could share the encoder's bugs:
decode, RE-ENCODE, and grade the second buffer against the ORIGINAL tree, so
anything the decoder drops shows up as a disagreement.
Reach, measured: 1431 bodies and 260_431 nodes, against 782 and 67_857 for the
file path — the bridge sees every body, including the one-line `nkAsgn` ones
`ast2nif` never defers and the grinder therefore never saw. 0 disagreements.
Not vacuous: dropping node flags, perturbing the sym index and dropping the type
slot each make it fail immediately (`flags`, `sym identity`, `typ nil-ness`).
A bridged buffer must never be written to a file — `(bsym …)` means nothing
without the tables beside it. `ast2nif` remains the only serializer.
Verified: 215/215 byte-identical `.c` against HEAD on the default path; all four
build configurations compile, plus `nodebridge` standalone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR