diff --git a/compiler/bnode.nim b/compiler/bnode.nim index 36a5fc7dc6..776dcf562e 100644 --- a/compiler/bnode.nim +++ b/compiler/bnode.nim @@ -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 diff --git a/compiler/ccgcalls.nim b/compiler/ccgcalls.nim index 28819f607c..f0ce207932 100644 --- a/compiler/ccgcalls.nim +++ b/compiler/ccgcalls.nim @@ -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 diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index aa7b7d8c0c..b4a3f2542f 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -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 diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index 15d9c6e85b..6c13a67584 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -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 diff --git a/compiler/cgen.nim b/compiler/cgen.nim index 5f2c5a6ffc..0c2e738d3a 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -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
" & 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 " & path & " in " & prc.name.s & ": cursor=" & cur & " ast=" & ast) + # The result says: nothing ANYWHERE in this subtree hit the tolerated + # `(ht .