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
This commit is contained in:
Araq
2026-08-30 10:08:07 +02:00
parent 512d2a8f26
commit 837082eb89
8 changed files with 378 additions and 20 deletions

View File

@@ -595,6 +595,12 @@ when defined(newIcBackend):
## `.bif`'s own filename pool plus the `ConfigRef`.
result = lineInfoFromCursor(program, n.raw)
proc isAtom*(n: BNode): bool {.inline.} =
## `ast.isAtom`, which is a pure `kind` test and so needs nothing from the
## body scope. It exists here only because `ast.isAtom` is typed `PNode`;
## the predicate itself is the same one.
result = n.kind >= nkNone and n.kind <= nkNilLit
# ---- predicates shared with the `PNode` spelling ---------------------------
#
# `ast.canRaise` / `ast.canRaiseConservative` only ever look at a node's

View File

@@ -76,7 +76,7 @@ proc preventNrvo(p: BProc; dest, le, ri: PNode): bool =
for r in sonsFrom(ri, 1):
if isPartOf(dest, r, {pfStructural}) != arNo: return true
proc hasNoInit(call: PNode): bool {.inline.} =
proc hasNoInit(call: AnyNode): bool {.inline.} =
result = call.firstSon.kind == nkSym and sfNoInit in call.firstSon.sym.flags
proc isHarmlessStore(p: BProc; canRaise: bool; d: TLoc): bool =
@@ -192,7 +192,7 @@ proc fixupCall(p: BProc, le, ri: PNode, d: var TLoc,
proc genBoundsCheck(p: BProc; arr, a, b: TLoc; arrTyp: PType)
proc reifiedOpenArray(n: PNode): bool {.inline.} =
proc reifiedOpenArray(n: AnyNode): bool {.inline.} =
var x = n
while true:
case x.kind
@@ -447,7 +447,10 @@ proc potentialAlias(n: PNode, potentialWrites: seq[PNode]): bool =
if p.aliases(n) != no or n.aliases(p) != no:
return true
proc skipTrivialIndirections(n: PNode): PNode =
proc skipTrivialIndirections[T: AnyNode](n: T): T =
## Explicitly generic rather than `(n: AnyNode): AnyNode`: two occurrences of
## a type class in one signature are two INDEPENDENT parameters, so that
## spelling would let the result type drift from the argument's.
result = n
while true:
case result.kind

View File

@@ -1357,13 +1357,13 @@ proc genBracketExpr(p: BProc; n: PNode; d: var TLoc) =
else: internalError(p.config, n.info, "expr(nkBracketExpr, " & $ty.kind & ')')
discard getTypeDesc(p.module, n.typ)
proc isSimpleExpr(n: PNode): bool =
proc isSimpleExpr(n: AnyNode): bool =
# calls all the way down --> can stay expression based
case n.kind
of nkCallKinds, nkDotExpr, nkPar, nkTupleConstr,
nkObjConstr, nkBracket, nkCurly, nkHiddenDeref, nkDerefExpr, nkHiddenAddr,
nkHiddenStdConv, nkHiddenSubConv, nkConv, nkAddr:
for c in n:
for c in sons(n):
if not isSimpleExpr(c): return false
result = true
of nkStmtListExpr:
@@ -2377,7 +2377,7 @@ proc rdSetElemLoc(conf: ConfigRef; a: TLoc, typ: PType; result: var Snippet) =
if firstOrd(conf, setType) != 0:
result = cOp(Sub, NimUint, result, cIntValue(firstOrd(conf, setType)))
proc fewCmps(conf: ConfigRef; s: PNode): bool =
proc fewCmps(conf: ConfigRef; s: AnyNode): bool =
# this function estimates whether it is better to emit code
# for constructing the set or generating a bunch of comparisons directly
if s.kind != nkCurly: return false
@@ -3257,7 +3257,7 @@ proc genTupleConstr(p: BProc, n: PNode, d: var TLoc) =
else:
genAssignment(p, d, tmp, {})
proc isConstClosure(n: PNode): bool {.inline.} =
proc isConstClosure(n: AnyNode): bool {.inline.} =
result = n.firstSon.kind == nkSym and isRoutine(n.firstSon.sym) and
n.secondSon.kind == nkNilLit

View File

@@ -31,10 +31,10 @@ proc registerTraverseProc(p: BProc, v: PSym) =
p.module.preInitProc.procSec(cpsInit).addCallStmt(fnName, traverseProc)
p.module.preInitProc.procSec(cpsInit).add("\n")
proc isAssignedImmediately(conf: ConfigRef; n: PNode): bool {.inline.} =
proc isAssignedImmediately(conf: ConfigRef; n: AnyNode): bool {.inline.} =
if n.kind == nkEmpty:
result = false
elif n.kind in nkCallKinds and n.firstSon != nil and n.firstSon.typ != nil and n.firstSon.typ.skipTypes(abstractInst).kind == tyProc:
elif n.kind in nkCallKinds and not n.firstSon.isNilNode and n.firstSon.typ != nil and n.firstSon.typ.skipTypes(abstractInst).kind == tyProc:
if n.firstSon.kind == nkSym and sfConstructor in n.firstSon.sym.flags:
result = true
elif isInvalidReturnType(conf, n.firstSon.typ, true):
@@ -1039,9 +1039,9 @@ proc genStringCase(p: BProc, t: PNode, stringKind: TTypeKind, d: var TLoc) =
cCall(eqFn, ra, rb)):
p.s(cpsStmts).addGoto(rlabel)
proc branchHasTooBigRange(b: PNode): bool =
proc branchHasTooBigRange(b: AnyNode): bool =
result = false
for it in b:
for it in sons(b):
# last son is block
if (it.kind == nkRange) and
it.secondSon.intVal - it.firstSon.intVal > RangeExpandLimit:
@@ -1354,7 +1354,7 @@ proc genTryCpp(p: BProc, t: PNode, d: var TLoc) =
linefmt(p, cpsStmts, "if (T$1_) std::rethrow_exception(T$1_);$n", [etmp])
endSimpleBlock(p, scope)
proc bodyCanRaise(p: BProc; n: PNode): bool =
proc bodyCanRaise(p: BProc; n: AnyNode): bool =
case n.kind
of nkCallKinds:
result = canRaiseDisp(p, n.firstSon)
@@ -1368,9 +1368,9 @@ proc bodyCanRaise(p: BProc; n: PNode): bool =
nkMacroDef, nkTemplateDef, nkLambda, nkDo, nkFuncDef:
result = false
else:
for i in 0 ..< safeLen(n):
if bodyCanRaise(p, n[i]): return true
result = false
for it in sons(n):
if bodyCanRaise(p, it): return true
proc genTryGoto(p: BProc; t: PNode; d: var TLoc) =
let fin = if t.lastSon.kind == nkFinally: t.lastSon else: nil

View File

@@ -1532,8 +1532,130 @@ when defined(newIcBackend):
# process on exit; a run in which `navHits` is 0 means every lookup fell
# through to the decoder and the chain is doing nothing.
var navHits, navFallbacks, navRegistered: int
# Same reasoning for the predicate grinder: "0 disagreements" is only worth
# something next to how many nodes were actually graded and how many were
# excused, so all three are counted and reported together.
var gradeGraded, gradeSkipDecl, gradeSkipTyp: int
proc grindLockstep(m: BModule; prc: PSym; c: BNode; a: PNode; path: string) =
const nkIntLits = {nkCharLit..nkUInt64Lit}
const notGradeable = {nkTypeSection, nkConstSection, nkProcDef, nkConverterDef,
nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef,
nkLambda, nkDo, nkFuncDef}
## Subtrees the predicates are not graded inside, because production never
## evaluates an expression there either — `bodyCanRaise` declares the same
## boundary and returns `false` for the whole set without looking in. The
## nodes inside carry unresolved types (a template's parameters, a generic's
## `tyGenericParam`), and asking `getSize` about one is not a disagreement
## between the two spellings, it is a question with no answer in either.
proc ordinalRanges(a: PNode): bool =
## Whether every `nkRange` directly under `a` has integer endpoints. The
## gate for `branchHasTooBigRange`, which reads `intVal` off them: a `case`
## over strings or floats has `nkOfBranch`es whose ranges hold no integer,
## and production only ever reaches that proc from the ordinal path. Computed
## from the AST side ALONE so the two spellings are gated identically — a
## gate that consulted the cursor could hide the very disagreement it is
## supposed to expose.
result = true
for it in sons(a):
if it.kind == nkRange and
(it.firstSon.kind notin nkIntLits or it.secondSon.kind notin nkIntLits):
return false
proc grindPredicates(m: BModule; p: BProc; prc: PSym; c: BNode; a: PNode;
path: string) =
## Every migrated pure predicate, run on BOTH spellings of the SAME node.
##
## The point of doing it HERE rather than once per body is coverage. A proc
## graded at the root of a body is graded on the shapes that body happens to
## start with; graded at every node it meets every shape the closure
## contains, which over a standard-library build is tens of thousands of
## nodes and effectively all of them. These predicates are pure and cheap,
## so the whole set can be run at every node for the price of the walk that
## is already happening.
##
## Only calls that are TOTAL on the node are made, and the predicates split
## in two on that question.
##
## The structural ones — `isSimpleExpr`, `bodyCanRaise`, the indirection
## walkers — read `kind`, children and (defensively) `sym`, and answer for
## any node in a body. They are graded everywhere.
##
## The type-consuming ones — `isAssignedImmediately`, `fewCmps` — hand
## `n.typ` to `getSize` / `mapType`, which are total only over types the C
## backend can lay out. Production reaches them from exactly one shape each
## (the value of a var definition; the set operand of an `in`), and away
## from that shape they meet types codegen never maps — a `tyGenericParam`,
## a `tyAnything` — and abort. That is not a disagreement between the two
## spellings, it is a question with no answer in either, so these are graded
## FROM THE PARENT at the position production calls them from. Widening a
## guard until the run goes green would be the wrong move; restricting the
## call to where it is defined is not the same thing.
template bail(what: string; cur, ast: string) =
internalError(m.config, prc.info,
"BNode/PNode disagree on " & what & " at <body>" & path & " in " &
prc.name.s & ": cursor=" & cur & " ast=" & ast)
template checkAt(what: string; cn: BNode; an: PNode; call: untyped) =
## `call` is written ONCE and instantiated twice — once with `n` bound to
## the cursor, once to the AST. Writing it twice is what would let the two
## sides drift into asking different questions.
block:
let cv = block:
let n {.inject.} = cn
call
let av = block:
let n {.inject.} = an
call
if cv != av: bail(what, $cv, $av)
template check(what: string; call: untyped) = checkAt(what, c, a, call)
# Total on any well-formed node.
check "isSimpleExpr", isSimpleExpr(n)
check "reifiedOpenArray", reifiedOpenArray(n)
check "bodyCanRaise", bodyCanRaise(p, n)
# `skipTrivialIndirections` returns a NODE, and the two spellings return
# values of different types that cannot be compared directly. Kind plus
# line info pins which node was landed on: the proc only ever walks DOWN a
# spine, so two different stopping points on the same input differ in one or
# the other unless the tree has two identical nodes at one position, which
# would make the choice immaterial anyway.
block:
let cs = skipTrivialIndirections(c)
let a2 = skipTrivialIndirections(a)
if cs.kind != a2.kind:
bail("skipTrivialIndirections kind", $cs.kind, $a2.kind)
if cs.info != a2.info:
bail("skipTrivialIndirections info",
$(m.config, cs.info), $(m.config, a2.info))
# Shape-guarded, matching the contexts production calls them from.
if a.kind in nkCallKinds and a.safeLen > 0:
check "hasNoInit", hasNoInit(n)
if a.kind in {nkClosure, nkPar, nkTupleConstr} and a.safeLen == 2:
check "isConstClosure", isConstClosure(n)
if a.kind == nkOfBranch and ordinalRanges(a):
check "branchHasTooBigRange", branchHasTooBigRange(n)
# Graded from the parent — see the note above on why these two cannot be
# asked at an arbitrary node. `genVarTuple` asks about the tuple's last
# child; `genSingleVar` about the value of an `nkIdentDefs` that defines a
# symbol; `genInOp` about the set operand of an `in`.
if a.kind == nkVarTuple and a.safeLen > 0:
checkAt "isAssignedImmediately", c.lastSon, a.lastSon,
isAssignedImmediately(m.config, n)
elif a.kind == nkIdentDefs and a.safeLen == 3 and a.firstSon.kind == nkSym:
checkAt "isAssignedImmediately", son(c, 2), son(a, 2),
isAssignedImmediately(m.config, n)
if a.kind in nkCallKinds and a.safeLen > 1 and a.secondSon.kind == nkCurly and
a.secondSon.typ != nil:
checkAt "fewCmps", c.secondSon, a.secondSon, fewCmps(m.config, n)
proc grindLockstep(m: BModule; p: BProc; prc: PSym; c: BNode; a: PNode;
path: string; gradeable: bool): bool {.discardable.} =
## Walk the `.bif` cursor and the materialised `PNode` for the SAME body in
## lockstep and require every vocabulary member to answer identically at
## every node. This grades the VOCABULARY rather than any one migrated proc,
@@ -1551,6 +1673,10 @@ when defined(newIcBackend):
"BNode/PNode disagree on " & what & " at <body>" & path & " in " &
prc.name.s & ": cursor=" & cur & " ast=" & ast)
# The result says: nothing ANYWHERE in this subtree hit the tolerated
# `(ht . <sym>)` type difference. Only a subtree that clean is handed to
# `grindPredicates` — see the descent below for why.
result = true
if a == nil:
if not c.isNilNode: bail("nil-ness", "not-nil", "nil")
return
@@ -1618,7 +1744,7 @@ when defined(newIcBackend):
a.typField == nil and a.sym != nil and at == a.sym.typ and
c.hasExplicitNilType
if htNilTyp:
discard
result = false
elif (ct == nil) != (at == nil):
bail("typ nil-ness",
(if ct == nil: "nil" else: $ct.kind) & " raw=" & c.rawDesc,
@@ -1648,15 +1774,33 @@ when defined(newIcBackend):
# That is the part being graded here: not just that the accessors agree, but
# that they still agree when the resolution context is built by the
# traversal instead of handed to it.
let gradeHere = gradeable and a.kind notin notGradeable
if a.safeLen > 0:
withNodeScope(nsBlock):
var i = 0
for child in sons(a):
let cc = son(c, i)
registerDefHere(cc)
grindLockstep(m, prc, cc, child, here & "[" & $i & "]")
if not grindLockstep(m, p, prc, cc, child, here & "[" & $i & "]",
gradeHere):
result = false
inc i
# AFTER the descent, and only on a subtree with no tolerated type difference
# anywhere in it. The predicates RECURSE, so one excused node poisons every
# ancestor's answer too: grading `bodyCanRaise` at a call whose callee is an
# `(ht . <sym>)` sym would re-report that one known difference as a fresh
# finding at every enclosing node. Excused, not ignored — the exclusions are
# counted, so a run that grades nothing cannot pass for a run that grades
# everything.
if not gradeHere:
inc gradeSkipDecl
elif not result:
inc gradeSkipTyp
else:
inc gradeGraded
grindPredicates(m, p, prc, c, a, here)
proc grindBNode(m: BModule; p: BProc; prc: PSym) =
## Differential grinding for the migrating vocabulary, opt-in via
## `NIM_IC_BNODE_GRIND`: run every proc that has moved to `AnyNode` over
@@ -1679,13 +1823,25 @@ when defined(newIcBackend):
##
## `grindLockstep` runs last and grades the vocabulary itself rather than
## these two procs; it is the check that actually covers accessors no
## migrated proc happens to call yet.
## migrated proc happens to call yet, and it carries `grindPredicates` —
## every OTHER migrated proc, run at every node of the body.
##
## WHAT THIS CANNOT SEE. Only a body that arrived as a deferred `nfLazyBody`
## placeholder can be graded, and `ast2nif` defers only bodies whose root is
## an `nkStmtList`. A one-line `proc f(x: int): int = case x ...` has an
## `nkAsgn` body, is loaded eagerly, and never reaches this proc — 652 of
## 1434 bodies on the reference target (`tools/icgrind`). Nor is the main
## module graded at all: its routines are built in-process. Both are stated
## because they are invisible from the outside — a shape added to a grind
## target can produce exactly zero coverage and no diagnostic.
if bnodeGrind < 0:
bnodeGrind = ord(existsEnv("NIM_IC_BNODE_GRIND"))
if bnodeGrind == 1:
addExitProc proc () =
stderr.writeLine "BNODEGRIND navHits=" & $navHits &
" navFallbacks=" & $navFallbacks & " navRegistered=" & $navRegistered
" navFallbacks=" & $navFallbacks & " navRegistered=" & $navRegistered &
" graded=" & $gradeGraded & " skipDecl=" & $gradeSkipDecl &
" skipTyp=" & $gradeSkipTyp
if bnodeGrind == 0: return
let ast = prc.ast
if ast == nil or ast.safeLen <= bodyPos: return
@@ -1713,7 +1869,7 @@ when defined(newIcBackend):
": cursor=" & $curPaths & " ast=" & $astPaths)
withBodyScope(scope):
grindLockstep(m, prc, viaCursor, body, "")
grindLockstep(m, p, prc, viaCursor, body, "", gradeable = true)
let (hits, fallbacks, registered) = navStats()
navHits += hits
navFallbacks += fallbacks

View File

@@ -0,0 +1,98 @@
# Shapes the predicate grinder needs, in an IMPORTED module with STATEMENT-LIST
# bodies.
#
# Two constraints, both structural, both learned by measuring rather than
# guessing:
#
# 1. The main module's routines are built in-process and never arrive as a
# deferred body, so nothing written in `grindme.nim` is graded at all.
#
# 2. `ast2nif` defers only bodies whose root is an `nkStmtList` (see the comment
# at the placeholder site: 82.5% of bodies, with one-line `nkAsgn` bodies the
# bulk of the rest). A `proc f(x: int): int = case x ...` has an `nkAsgn`
# body and is loaded eagerly, so it is invisible to the grinder. Every proc
# here therefore opens with a statement.
import std/strutils
proc risky*(x: int): int =
if x < 0: raise newException(ValueError, "neg")
result = x * 2
proc classifyChar*(c: char): string =
## `branchHasTooBigRange`, false side: char ranges are all under the limit.
var r = ""
case c
of 'a'..'z': r = "lower"
of 'A'..'Z': r = "upper"
of '0'..'9', '_': r = "wordish"
else: r = "other"
result = r
proc bigRange*(x: int): int =
## `branchHasTooBigRange`, TRUE side: 100000 > RangeExpandLimit (256).
var r = 0
case x
of 0..100000: r = 1
of 100001..200000: r = 2
else: r = 3
result = r
proc smallRange*(x: int): int =
var r = 0
case x
of 0..10: r = 1
of 11..20: r = 2
else: r = 3
result = r
proc inSets*(c: char): bool =
## `fewCmps` true side: a narrow set of an int-based element type.
discard
result = c in {'a', 'e', 'i', 'o', 'u'} and c notin {'x'..'z'}
proc bigSet*(c: char): bool =
## `fewCmps` false side: wide enough that emitting the set wins.
discard
result = c in {'a'..'z', 'A'..'Z', '0'..'9', '_', '-', '.', '+', '/', '=', '%'}
proc sumOpen*(xs: openArray[int]): int =
## `reifiedOpenArray`: an openarray PARAM is the one shape answering false.
result = 0
for x in xs: result += x
proc viaOpen*(xs: seq[int]): int =
result = 0
result += sumOpen(xs)
result += sumOpen([1, 2, 3])
result += sumOpen(xs.toOpenArray(0, 0))
proc adder*(n: int): proc (x: int): int =
## A real closure — `isConstClosure` false side.
discard
result = proc (x: int): int = x + n
proc constClosure*(): proc (x: int): int =
## `isConstClosure` TRUE side: a top-level routine as a closure value pairs
## the sym with a nil environment.
discard
result = risky
proc tuples*(): (int, string) =
discard
result = (risky(2), classifyChar('q'))
proc noInitVar*(): int =
## `hasNoInit`: a call to a `.noinit.` routine.
var t {.noinit.}: array[4, int]
t[0] = 1
result = t[0]
proc guardedLib*(x: int): string =
## `bodyCanRaise` through both a raising call and its arguments.
try:
result = $risky(x) & $risky(x + 1)
except ValueError:
result = "err"
finally:
discard

46
tools/icgrind/grindme.nim Normal file
View File

@@ -0,0 +1,46 @@
import std/[strutils, tables, algorithm]
import grindlib
type Kind = enum kA, kB, kC
type Item = object
name: string
k: Kind
vals: seq[int]
proc classify(i: Item): string =
case i.k
of kA:
if i.vals.len > 2: result = "many"
else: result = "few"
of kB:
for v in i.vals:
if v < 0: return "neg"
result = "pos"
of kC:
result = i.name.toUpperAscii
proc total(i: Item): int =
for v in i.vals: result += v
iterator pairsish(t: Table[string, int]): (string, int) =
for k, v in t: yield (k, v)
proc build(): Table[string, int] =
result = initTable[string, int]()
var items = @[Item(name: "a", k: kA, vals: @[1, 2, 3]),
Item(name: "b", k: kB, vals: @[-1]),
Item(name: "c", k: kC, vals: @[])]
items.sort(proc (x, y: Item): int = cmp(x.name, y.name))
for it in items:
result[classify(it)] = total(it)
when isMainModule:
var t = build()
var keys: seq[string] = @[]
for k, v in pairsish(t): keys.add k & "=" & $v
keys.sort()
echo keys.join(",")
echo guardedLib(5), " ", guardedLib(-5)
echo classifyChar('Q'), bigRange(150000), smallRange(5), inSets('e'), bigSet('q')
echo viaOpen(@[1, 2, 3]), adder(4)(5), constClosure()(3), noInitVar()
echo tuples()

49
tools/icgrind/readme.md Normal file
View File

@@ -0,0 +1,49 @@
# `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.