src/rules/correctness/pick.bend source
src/rules/correctness/pick.bend on the hub · documented module
# rule pick: a def calls itself in a branch of a `Bool.pick`. Bool.pick is a# function, so both branches are evaluated whatever the condition. In both# branches, two recursive calls a step is 2^n work where one was meant (a# per-token scan that took 20 s this way took 20 ms as one pass): bind the# call once above the pick (`+more = go(rest)`) and pick between `x <> more`# and `more`. In one branch, the recursion runs even when the other branch# was the answer: a search never stops early and walks the whole input# (portal-bend's `get`, bendoom's sorted insert). Match on the Bool instead,# in a helper that takes it as a parameter. A self-call is the def's name# followed by `(..)`: a parameter or a value named like the def is not one. A# pick nested in a branch of one already reported is not reported again; one# nested in its condition is. A# law file, a proof file and a def that is a proof never run, so they are# exempt: a def that returns a proof, or one written with no type at all# (`def f(x, y):`), which fills the law named f.import Baseimport ../../src.bend as Srcimport ../../finding.bend as Fimport ../../syntax/lex.bend as Leximport ../../syntax/tree.bend as Treeimport ../calls.bend as Callsimport ../../lazy/lazy.bend as Lazy# do both branches (the third and fourth arguments) call the def?def both(+as: List<&2, Tree.Node>, +name: String) -> Bool: Bool.and(Calls.calls(Calls.arg(as, 2n), name), Calls.calls(Calls.arg(as, 3n), name))# does either branch call the def?def once(+as: List<&2, Tree.Node>, +name: String) -> Bool: Bool.or(Calls.calls(Calls.arg(as, 2n), name), Calls.calls(Calls.arg(as, 3n), name))# is argument nth of a group quiet: the group's own quiet, or, in a branch# (the third or fourth argument) of a reported pick, husheddef mute(+quiet: Bool, +hush: Bool, nth: Nat) -> Bool: match nth: case 0n: quiet case 1n: quiet case 2n: Bool.or(quiet, hush) case 3n: Bool.or(quiet, hush) case 4n+p: quiet# the argument a token leaves the walk in: one further past a top-level commadef next(kk: Lex.TokKind, nth: Nat) -> Nat: match kk: case Lex.TComma{}: 1n+nth case other: nth# every `Bool.pick(..)` in a chain, at any depth, that recurses in both# branches or in one. The chain is argument nth of a group with quiet# (inside a branch of a reported pick) and hush (the group is a reported# pick, so its branches are quiet); a pick in the condition still counts.def walk(nn: Tree.Node, +name: String, +path: String, +quiet: Bool, +hush: Bool, +nth: Nat) -> List<&2, F.Finding>: match nn: case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, +l, +c}}, Tree.NCons{Tree.Group{open, +kids, close}, rest}}: +at = next(k, nth) +q = mute(quiet, hush, at) +as = Calls.args(kids) +ispick = String.eq(t, "Bool.pick") +two = Bool.and(Bool.and(ispick, Bool.not(q)), both(as, name)) +one = Bool.and(Bool.and(ispick, Bool.not(two)), Bool.and(Bool.not(q), once(as, name))) +more = List.concat(&2, F.Finding, [walk(kids, name, path, q, Bool.or(two, one), 0n), walk(rest, name, path, quiet, hush, at)]) Bool.pick(List<&2, F.Finding>, two, F.Finding{path, l, c, 9, "pick", name ++ " calls itself in both branches of Bool.pick, which evaluates both, so both calls run; bind the call once above the pick."} <> more, Bool.pick(List<&2, F.Finding>, one, F.Finding{path, l, c, 9, "pick", name ++ " calls itself in one branch of Bool.pick, which evaluates both, so the call always runs and never exits early; branch with `match` on the condition instead."} <> more, more)) case Tree.NCons{Tree.Leaf{Lex.Tok{k, t, l, c}}, rest}: walk(rest, name, path, quiet, hush, next(k, nth)) case Tree.NCons{Tree.Group{open, kids, close}, rest}: List.concat(&2, F.Finding, [walk(kids, name, path, mute(quiet, hush, nth), False{}, 0n), walk(rest, name, path, quiet, hush, nth)]) case Tree.NCons{Tree.Stmt{kind, kids, body}, rest}: List.concat(&2, F.Finding, [walk(kids, name, path, mute(quiet, hush, nth), False{}, 0n), walk(body, name, path, mute(quiet, hush, nth), False{}, 0n), walk(rest, name, path, quiet, hush, nth)]) case Tree.NCons{h, rest}: walk(rest, name, path, quiet, hush, nth) case other: Nil{}# every reported pick in a chain walked at quiet, outside any pick's argumentsdef picks(nn: Tree.Node, +name: String, +path: String, +quiet: Bool) -> List<&2, F.Finding>: walk(nn, name, path, quiet, False{}, 0n)def check.go(ds: List<&2, Calls.Def>, +path: String, acc: List<&2, List<&2, F.Finding>>) -> List<&2, F.Finding>: match ds: case Nil{}: List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc)) case Con{Calls.Def{+name, +sig, body}, rest}: check.go(rest, path, Lazy.stop(List<&2, F.Finding>, Calls.exempt(path, sig), [], _u => picks(body, name, path, False{})) <> acc)# the ruledef check(ss: Src.Src) -> List<&2, F.Finding>: Src.Src{path, text, toks, tree, bound, items} = ss check.go(Calls.defs(tree), path, [])