mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-01 19:33:42 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
98
tools/icgrind/grindlib.nim
Normal file
98
tools/icgrind/grindlib.nim
Normal 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
46
tools/icgrind/grindme.nim
Normal 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
49
tools/icgrind/readme.md
Normal 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.
|
||||
Reference in New Issue
Block a user