mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-05 13:10:50 +00:00
The `BNode` accessors had no oracle. The self-test in `bnode.nim` checks them against each other and against the raw token stream, which a uniformly wrong vocabulary satisfies — it passed 5.2M assertions on an off-by-two model. This adds the missing oracle and lets it drive the next migration step. `cgen.grindLockstep` (opt-in, `NIM_IC_BNODE_GRIND=1` on an `--ic:on` build) walks the `.bif` cursor and the materialised `PNode` for the SAME body side by side and requires `kind`, `len`, `info`, `flags`, the literal payloads, `sym` and `typ` to agree at every node of every routine body in the dependency closure. The `PNode` is the oracle, so it compares everything rather than what someone thought to check. It found two bugs, both silent: * `BNode.typ` answered `nil` for every bare `Symbol`. `ast.typ` does not: it falls back to `n.sym.typ` when the loader set `nfLazyType`, which it does for exactly that shape. The consequence is not less information but a DIFFERENT answer — `canRaise` asks `fn.typ.kind == tyProc` about a call's callee, so nil turns "this call can raise" into "it cannot" and drops the goto-exception check after the call. * `(ht . <sym>)` — an explicitly nil node type — made `n.typ` LOAD-ORDER DEPENDENT in the loader itself. `newSymNode` marks the node lazy only if the symbol was still an unloaded stub at that moment, so the same `.bif` node answered `sym.typ` or `nil` depending on what happened to touch that symbol first. Pinned to the lazy reading, so the answer is a property of the file rather than of the traversal order. With `typ` correct, the blocker recorded in `allPathsAsgnResult` is gone. `ast.canRaise`/`canRaiseConservative` cannot become `AnyNode` procs where they live — `BNode` is defined in `bnode.nim`, which imports `ast` — so their bodies move into templates that `bnode` instantiates for its own node type. One source of truth, no cycle, no second copy. `ccgcalls.canRaiseDisp` and `cgen.allPathsAsgnResult` follow. Adds the leaf accessors (`intVal`, `floatVal`, `strVal`, `ident`, `flags`) because nothing in `ccgexprs` can migrate without them, and `rawDesc` for diagnosing a disagreement in terms of what the token stream literally says. `compiler/bodynav.nim` replaces the `BodyScope` snapshot with a scope chain the traversal maintains — `openScope`/`closeScope`/`registerDefHere`, lookup falling through to the decoder — ported from Nimony's `typenav`. The scope becomes a product of the walk, so nothing is copied ahead of time and nothing can be stale. How much of a live problem the snapshot was is measured rather than assumed: `-d:icLocalSymStats` reports `localHit=0 fieldStub=2 miss=0 sdReg=5902 extractReg=45` over the stdlib closure, i.e. definitions register constantly and not one use ever resolved through the table, because `isLocalSym` is a hardwired `false`. The hazard was latent; this keeps it latent once that stops being true. Verified: 0 disagreements over the whole `--ic:on` closure with the walk driving the nav; deliberately breaking `intVal`, `flags` and the nav key each make the grinder fire on the first bodies it reaches, so the clean run is not vacuous; the default path emits 215/215 byte-identical `.c` against the pre-change compiler and does not compile `bodynav` at all; `tests/ic` 39/39. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
269 lines
12 KiB
Nim
269 lines
12 KiB
Nim
#
|
|
#
|
|
# The Nim Compiler
|
|
# (c) Copyright 2026 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
## `BodyNav` — a scope-chained navigator over a `.bif` routine body.
|
|
##
|
|
## Ported from Nimony's `nimony/typenav.nim` (`TypeCache` / `TypeScope`). The
|
|
## idea being stolen is not the type algebra — we do not need it, `typ` returns
|
|
## a fully materialized `PType` — but the SHAPE of the resolution context:
|
|
##
|
|
## * a chain of scope frames, each a small table, linked to its parent;
|
|
## * `openScope` / `closeScope` / `registerLocal`, called BY THE TRAVERSAL as it
|
|
## descends and as it walks past each definition;
|
|
## * a lookup that consults the chain and, on a miss, falls through to the
|
|
## module index (`typenav`'s `tryLoadSym`; here the decoder's own
|
|
## `symFromCursor`).
|
|
##
|
|
## The consequence is the point: the scope is a PRODUCT OF THE WALK. Nothing is
|
|
## snapshotted, so nothing can be stale, and a reader that starts at the top of
|
|
## a body and descends always has exactly the definitions it has already passed.
|
|
##
|
|
## WHAT THIS REPLACES. `ast2nif.PendingBody` stashes `localSyms` — a COPY of the
|
|
## enclosing sym def's local symbols, taken when the body was deferred — and
|
|
## `bnode`'s `BodyScope` then copies it again. `materializeLazyBody` loads the
|
|
## body with its own `var pb`, so every definition the load creates lands in a
|
|
## table that is discarded on return. A cursor-side reader holding the earlier
|
|
## copy therefore cannot see them, and would mint its own `PSym` for the same
|
|
## name: two objects, one symbol.
|
|
##
|
|
## HOW BIG THAT PROBLEM ACTUALLY IS, measured rather than assumed. Build with
|
|
## `-d:icLocalSymStats` and every process reports its `localSyms` traffic on
|
|
## exit. Over a full `--ic:on` build of the standard-library closure (104
|
|
## backend processes):
|
|
##
|
|
## localHit=0 fieldStub=2 miss=0 sdReg=5902 extractReg=45
|
|
##
|
|
## Definitions register constantly and NOT ONE use ever resolves through the
|
|
## table. The reason is `ast2nif.isLocalSym`, which returns a hardwired `false`:
|
|
## every symbol is emitted with a module suffix and resolves through the
|
|
## decoder's global `syms` memo, so both spellings get the same `PSym` whatever
|
|
## either one has cached. The 5902 registrations are object FIELDS, whose uses
|
|
## deliberately go to `loadFieldStub` instead.
|
|
##
|
|
## So the stale snapshot is a LATENT hazard, not a live bug, and this module is
|
|
## not a bug fix — it is the mechanism that keeps it latent once `isLocalSym`
|
|
## stops being `false`, or once a body-local name appears for any other reason.
|
|
## Said plainly so nobody has to re-derive it: today the nav changes no answers,
|
|
## and the grinder in `cgen` proves that by requiring the navigated symbol to be
|
|
## the same object the `PNode` loader produced, at every node of every body.
|
|
##
|
|
## It is not decorative either, and that also has a number. Over the same build,
|
|
## the grinder's traversal reports `navHits=42236 navFallbacks=12658
|
|
## navRegistered=311`: the chain answers 77% of lookups, and 311 definitions are
|
|
## registered by the walk rather than read from a table someone filled in
|
|
## earlier. Sabotaging the key (truncating it to three characters, so
|
|
## `c_fwrite` and `c_fflush` collide) makes the grinder fail on the first body
|
|
## it reaches — so a clean run means the resolution is right, not that the
|
|
## lookup never happened.
|
|
##
|
|
## FIELDS ARE NOT REGISTERED, and that is deliberate. `loadFieldStub` mints a
|
|
## fresh stub per use because two distinct fields can share a name (and a
|
|
## position) across types — `a.x` and `b.x` in one body are two different
|
|
## symbols. Caching a field by its bare name would hand the second use the first
|
|
## one's stub, and its type. The nav skips field names entirely and leaves that
|
|
## path exactly as it was.
|
|
|
|
import std / tables
|
|
import ast, ast2nif
|
|
|
|
when defined(nimPreviewSlimSystem):
|
|
import std / assertions
|
|
|
|
import "../dist/nimony/src/lib/nifcore" except pool
|
|
|
|
type
|
|
NavScopeKind* = enum
|
|
nsBlock, ## an ordinary nested scope
|
|
nsRoutine ## a routine boundary — see `crossedRoutines`
|
|
|
|
NavScope {.acyclic.} = ref object
|
|
locals: Table[string, PSym]
|
|
parent: NavScope
|
|
kind: NavScopeKind
|
|
|
|
BodyNav* = object
|
|
## The resolution context for ONE routine body. `base` is what the decoder
|
|
## itself needs (the owning module plus a table `loadSymStub` can write
|
|
## into); the frame chain on top of it is this module's contribution.
|
|
base*: BodyScope
|
|
current: NavScope
|
|
hits*: int ## resolved from the chain
|
|
fallbacks*: int ## resolved through the decoder
|
|
registered*: int ## definitions the walk registered
|
|
|
|
proc initBodyNav*(base: sink BodyScope): BodyNav =
|
|
## A nav over a body, seeded with whatever resolution context the decoder
|
|
## handed out. The root frame is a routine frame: a body IS one.
|
|
result = BodyNav(base: base,
|
|
current: NavScope(locals: initTable[string, PSym](),
|
|
parent: nil, kind: nsRoutine))
|
|
|
|
proc openScope*(nav: var BodyNav; kind = nsBlock) {.inline.} =
|
|
nav.current = NavScope(locals: initTable[string, PSym](),
|
|
parent: nav.current, kind: kind)
|
|
|
|
proc closeScope*(nav: var BodyNav) {.inline.} =
|
|
doAssert nav.current.parent != nil, "closeScope past the root frame"
|
|
nav.current = nav.current.parent
|
|
|
|
template withScope*(nav: var BodyNav; kind: NavScopeKind; body: untyped) =
|
|
openScope(nav, kind)
|
|
try:
|
|
body
|
|
finally:
|
|
closeScope(nav)
|
|
|
|
proc registerLocal*(nav: var BodyNav; name: string; s: PSym) {.inline.} =
|
|
## Record a definition the walk has just passed, in the innermost frame.
|
|
nav.current.locals[name] = s
|
|
inc nav.registered
|
|
|
|
proc lookupLocal*(nav: BodyNav; name: string): PSym =
|
|
## The chain only. `nil` when nothing in scope carries this name.
|
|
var it {.cursor.} = nav.current
|
|
while it != nil:
|
|
let s = it.locals.getOrDefault(name)
|
|
if s != nil: return s
|
|
it = it.parent
|
|
result = nil
|
|
|
|
proc crossedRoutines*(nav: BodyNav; name: string): int =
|
|
## How many routine frames separate the use from the definition — 0 when the
|
|
## definition is in the current routine. `typenav` computes the same thing as
|
|
## `LocalInfo.crossedProc`, and it is what tells a closure pass that a name is
|
|
## captured rather than local. Nothing consumes it here yet; it is the reason
|
|
## the frames carry a kind at all, and dropping the kind would make it
|
|
## unrecoverable later.
|
|
var it {.cursor.} = nav.current
|
|
var crossed = 0
|
|
while it != nil:
|
|
if it.locals.getOrDefault(name) != nil: return crossed
|
|
if it.kind == nsRoutine: inc crossed
|
|
it = it.parent
|
|
result = -1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Names
|
|
#
|
|
# A symbol reaches the reader in four shapes and they all NAME the same thing;
|
|
# `navName` is the one place that knows which token holds the name, so the
|
|
# lookup key is derived identically no matter which wrapper the writer chose.
|
|
|
|
proc navName*(n: Cursor): string =
|
|
## The NIF name a token denotes, or `""` when the token names no symbol.
|
|
case nifcore.kind(n)
|
|
of Symbol, SymbolDef:
|
|
result = symName(n)
|
|
of TagLit:
|
|
let tag = n.tags.tagName(cursorTagId(n))
|
|
if tag == symDefTagName:
|
|
let name = childCursor(n)
|
|
result = if nifcore.kind(name) in {Symbol, SymbolDef}: symName(name) else: ""
|
|
elif tag == hiddenTypeTagName:
|
|
# `(ht <type> <sym>)`
|
|
var inner = childCursor(n)
|
|
skip inner
|
|
result = navName(inner)
|
|
elif tag == symNodeFlagsTagName:
|
|
# `(nflags <flags> <symnode>)`
|
|
var inner = childCursor(n)
|
|
skip inner
|
|
result = navName(inner)
|
|
else:
|
|
result = ""
|
|
else:
|
|
result = ""
|
|
|
|
proc symToken*(n: Cursor): Cursor =
|
|
## The token that actually NAMES the symbol, with the wrappers stripped.
|
|
## `loadSymStub` accepts a `Symbol`, a `SymbolDef` or an `(sd ...)` and
|
|
## rejects everything else, so the `(ht ...)` / `(nflags ...)` forms have to be
|
|
## peeled here rather than at each call site — the same peeling `navName` does
|
|
## for the key, kept beside it so the two cannot drift apart.
|
|
result = n
|
|
while nifcore.kind(result) == TagLit:
|
|
let tag = result.tags.tagName(cursorTagId(result))
|
|
if tag == hiddenTypeTagName or tag == symNodeFlagsTagName:
|
|
var inner = childCursor(result)
|
|
skip inner # the explicit type / the node flags
|
|
result = inner
|
|
else:
|
|
break
|
|
|
|
proc cacheFrame(nav: var BodyNav): NavScope =
|
|
## Where a decoder-resolved name is remembered: the nearest ROUTINE frame.
|
|
## Not the innermost frame — a `.bif` name is unique within its module (see
|
|
## `isLocalSym`), so its meaning cannot change between frames, and caching it
|
|
## deeper would only throw it away sooner. Not the root either, so that a
|
|
## nested routine's names die with the nested routine.
|
|
result = nav.current
|
|
while result.kind != nsRoutine and result.parent != nil:
|
|
result = result.parent
|
|
|
|
proc symAt*(nav: var BodyNav; n: Cursor): PSym =
|
|
## The symbol a token names: the chain first, the decoder second.
|
|
let name = navName(n)
|
|
if name.len > 0:
|
|
let cached = lookupLocal(nav, name)
|
|
if cached != nil:
|
|
inc nav.hits
|
|
return cached
|
|
inc nav.fallbacks
|
|
result = symFromCursor(program, symToken(n), nav.base)
|
|
if result != nil and name.len > 0 and not isFieldNifName(name):
|
|
cacheFrame(nav).locals[name] = result
|
|
|
|
proc typeAt*(nav: var BodyNav; n: Cursor): PType {.inline.} =
|
|
## Types are not navigated: `ast2nif` already materializes them lazily from
|
|
## the module's type index, keyed by name, so there is no per-body state to
|
|
## keep and nothing a frame could cache that the decoder does not already.
|
|
result = typeFromCursor(program, n, nav.base)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registration during a walk
|
|
|
|
proc registerDefHere*(nav: var BodyNav; n: Cursor): bool {.discardable.} =
|
|
## Register `n` if `n` ITSELF is a definition; do not descend. This is the
|
|
## incremental half: a walk calls it on each child before recursing into it,
|
|
## so a use can only resolve from the chain to a definition the walk has
|
|
## already passed. A use that precedes its definition simply misses and falls
|
|
## through to the decoder, which is the behaviour there was before — the nav
|
|
## degrades to the old path rather than answering wrongly.
|
|
result = false
|
|
if nifcore.kind(n) == TagLit and
|
|
n.tags.tagName(cursorTagId(n)) == symDefTagName:
|
|
let name = navName(n)
|
|
if name.len > 0 and not isFieldNifName(name):
|
|
let s = symFromCursor(program, n, nav.base)
|
|
if s != nil:
|
|
registerLocal(nav, name, s)
|
|
result = true
|
|
|
|
proc registerDefs*(nav: var BodyNav; n: Cursor) =
|
|
## Register every definition in the SUBTREE at `n` — `typenav.registerLocals`
|
|
## with the recursion left in, because a Nim body puts `nkIdentDefs` under an
|
|
## `nkVarSection` under the statement list rather than declaring at one level.
|
|
##
|
|
## Call it on entering a scope to get the eager behaviour (every definition
|
|
## known before any use is resolved, which is what a RANDOM-ACCESS reader
|
|
## needs), or per statement to get the incremental one (only definitions
|
|
## already walked past are visible, which is what a real pass wants and what
|
|
## makes use-before-def detectable rather than silently working).
|
|
if nifcore.kind(n) == TagLit and
|
|
n.tags.tagName(cursorTagId(n)) == symDefTagName:
|
|
let name = navName(n)
|
|
if name.len > 0 and not isFieldNifName(name):
|
|
let s = symFromCursor(program, n, nav.base)
|
|
if s != nil: registerLocal(nav, name, s) # `(sd ...)` needs no peeling
|
|
return
|
|
var c = childCursor(n)
|
|
while c.hasMore:
|
|
registerDefs(nav, c)
|
|
skip c
|