Files
Nim/tools/icgrind/readme.md
Araq 837082eb89 IC: grade every migrated predicate at every node, and migrate nine more
Nine `PNode`-typed codegen procs become `AnyNode`, all pure readers:
`bodyCanRaise` (the consumer of the `canRaise` work), `isAssignedImmediately`,
`branchHasTooBigRange`, `hasNoInit`, `reifiedOpenArray`,
`skipTrivialIndirections`, `isSimpleExpr`, `isConstClosure` and `fewCmps`.

The signatures are the small part. A migrated proc that nothing calls with a
`BNode` is not even type-checked, so the substance is `grindPredicates`: every
one of them runs on BOTH spellings of the SAME node, at EVERY node of every
graded body, inside the walk `grindLockstep` was already doing. 67_721 nodes
on the reference target, 0 disagreements.

Two things had to be gated, and neither by widening a guard until the run went
green.

The tolerated `(ht . <sym>)` type difference is benign for the vocabulary check
and NOT benign for a predicate that reads `typ` — and because the predicates
recurse, one excused node poisons every ancestor's answer too. `grindLockstep`
now reports whether a subtree is free of it, and only clean subtrees are
graded.

`isAssignedImmediately` and `fewCmps` hand `n.typ` to `getSize`/`mapType`,
which are total only over types the C backend can lay out. Asked at an
arbitrary node they meet a `tyGenericParam` or a `tyAnything` and abort — that
is a question with no answer in either spelling, not a disagreement between
them. Both are graded FROM THE PARENT, at the position production calls them
from. Declarative subtrees are skipped for the same reason, along the boundary
`bodyCanRaise` already draws.

Both exclusions are counted and printed beside the graded count, so a run that
grades nothing cannot pass for a run that grades everything.

`tools/icgrind` versions the grind target, because two ways of silently
getting no coverage turned up while writing it: the main module's routines are
never graded, and `ast2nif` defers only `nkStmtList` bodies, so a one-line
`proc f(x: int): int = case x ...` is invisible to the oracle. Shapes added
for `branchHasTooBigRange` and `fewCmps` produced exactly zero coverage until
both were found by counting rather than assumed.

Verified: 215/215 byte-identical `.c` against HEAD on the default path;
sabotaging `isAtom` and sabotaging the parent-driven node selection each make
the grinder fail on the first body it reaches.

Recorded rather than papered over: `isConstClosure` is graded only on its false
side — the whole closure contains one `nkClosure` node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 10:08:07 +02:00

2.4 KiB

NIM_IC_BNODE_GRIND target

Input for the differential oracle in compiler/cgen.nim (grindBNode), which runs every codegen proc that has moved to AnyNode over BOTH the .bif cursor and the materialised PNode for the same body and requires the same answer.

nim c -d:newIcBackend -o:bin/nim_grind compiler/nim.nim
NIM_IC_BNODE_GRIND=1 bin/nim_grind c --ic:on --nimcache:/tmp/ncgrind \
  tools/icgrind/grindme.nim

A disagreement is an internalError naming the proc, the path within the body and both answers. Each backend process reports its coverage on exit:

BNODEGRIND navHits=… navFallbacks=… navRegistered=… graded=… skipDecl=… skipTyp=…

graded is what the number "0 disagreements" is worth. The two skip counts are printed beside it on purpose, so a run that grades nothing cannot be mistaken for a run that grades everything.

What this target is for

The oracle grades whatever the dependency closure contains, so most of its coverage comes from the standard library for free. This target exists for the shapes the stdlib closure does NOT produce often enough to exercise both answers of a predicate — a case branch wider than RangeExpandLimit, a set literal narrow enough for fewCmps to prefer comparisons, an openArray parameter (the one shape reifiedOpenArray answers false for).

Two things that silently produce no coverage

Both were found by counting, after adding shapes here that turned out never to be graded at all:

  1. The main module's routines are never graded. They are built in-process and never arrive as a deferred body. Anything worth grading has to live in grindlib.nim, not in grindme.nim.

  2. Only nkStmtList bodies are deferred, so only those can be graded — see the placeholder site in ast2nif.loadRoutine. A one-line proc f(x: int): int = case x ... has an nkAsgn body, is loaded eagerly, and is invisible to the oracle. Every routine here opens with a statement for that reason. Measured on this target: 782 of 1434 bodies reach the grinder.

Known coverage gap

isConstClosure is graded but only ever on its false side: a const closure (nkClosure(<routine sym>, nil)) does not appear in any graded body of this closure — the whole run contains exactly one nkClosure node, the real closure in adder. Adding a shape here that produces one would be worth doing.