1 # rule strict: a def calls itself inside an operand of `Bool.and(..)` or 2 # `Bool.or(..)`, or in a comma-separated stretch holding `&&` or `||` (either 3 # side of the operator), matched by that exact text 4 # (a qualified `Base.Bool.or` is not seen). They are functions, not 5 # short-circuits: both sides always run (timed: `False{} && f(x)` costs all of 6 # f), so `ok(h) && go(rest)` walks the whole input after the answer is known, 7 # and the pending operator keeps a frame per step (the JS lane overflowed at 8 # ~64K this way). Match on the Bool in a helper, or carry it as an argument: 9 # `go(rest, ok(h))`. A self-call bound by a let above is not flagged: binding 10 # it says both run. Nor is one in a lambda body (`_u => go(rest)`, as 11 # `Lazy.or_else` takes): a thunk does not run eagerly, unless the body itself 12 # holds `&&` or `||`. A `=>` ends the stretch on its left, so a body's `&&` or 13 # `||` never reaches back past it: `go(x) <> _u => a && b` is not flagged. 14 import Base 15 import ../../src.bend as Src 16 import ../../finding.bend as F 17 import ../../syntax/lex.bend as Lex 18 import ../../syntax/tree.bend as Tree 19 import ../calls.bend as Calls 20 import ../../lazy/lazy.bend as Lazy 21 22 # is the operator `&&` or `||`? 23 def logic(+tt: String) -> Bool: 24 Bool.or(String.eq(tt, "&&"), String.eq(tt, "||")) 25 26 # a comma or a `=>`: the end of a stretch 27 def seg_op.ends(kk: Lex.TokKind) -> Bool: 28 match kk: 29 case Lex.TComma{}: 30 True{} 31 case Lex.TLam{}: 32 True{} 33 case other: 34 False{} 35 36 # this leaf's `&&` or `||`, or one further on in the stretch 37 def seg_op.or(+here: Bool, more: Bool) -> Bool: 38 Bool.or(here, more) 39 40 # does the chain, up to its first top-level comma or `=>`, hold `&&` or `||`? 41 def seg_op(nn: Tree.Node) -> Bool: 42 match nn: 43 case Tree.NCons{+h, rest}: 44 Lazy.stop(Bool, Calls.kind.leaf(~seg_op.ends, h), False{}, 45 _u => seg_op.or(Bool.and(Calls.kind.leaf(~Calls.kind.oper, h), logic(Calls.leaf.text(h))), seg_op(rest))) 46 case other: 47 False{} 48 49 # is the group after a name an operand: the stretch is hot, or it is the `(` 50 # of `Bool.and` or `Bool.or` 51 def inner(+hot: Bool, +oo: String, +tt: String) -> Bool: 52 Bool.or(hot, Bool.and(String.eq(oo, "("), Bool.or(String.eq(tt, "Bool.and"), String.eq(tt, "Bool.or")))) 53 54 # a name, then a group: a finding when the name is the def, the group a `(` 55 # call and the stretch hot, then what the group and the rest report 56 def call( 57 +tt: String, 58 +ll: U32, 59 +cc: U32, 60 +oo: String, 61 +name: String, 62 +path: String, 63 +hot: Bool, 64 kids: List<&2, F.Finding>, 65 rest: List<&2, F.Finding> 66 ) -> List<&2, F.Finding>: 67 +more = List.concat(&2, F.Finding, [kids, rest]) 68 Bool.pick(List<&2, F.Finding>, Bool.and(Bool.and(String.eq(oo, "("), String.eq(tt, name)), hot), 69 F.Finding{path, ll, cc, U32.from_nat(String.length(name)), "strict", 70 name ++ " calls itself inside Bool.and or Bool.or, which evaluate both sides, so there is no short-circuit; match on the Bool in a helper."} <> more, 71 more) 72 73 # a leaf read by its kind: past a comma, past a `=>`, or past any other 74 # token; only the branch taken runs 75 def leaf( 76 kk: Lex.TokKind, 77 comma: Unit -> List<&2, F.Finding>, 78 lam: Unit -> List<&2, F.Finding>, 79 plain: Unit -> List<&2, F.Finding> 80 ) -> List<&2, F.Finding>: 81 match kk: 82 case Lex.TComma{}: 83 comma(Unit{}) 84 case Lex.TLam{}: 85 lam(Unit{}) 86 case _k: 87 plain(Unit{}) 88 89 # the self-calls in operands: outer is whether the enclosing group is an 90 # operand, hot whether the current comma segment is; a `=>` starts a lambda 91 # body, hot only by its own `&&`/`||`. A leaf is read by its kind in `leaf`, 92 # once the cell after it is known 93 def walk(nn: Tree.Node, +name: String, +path: String, +outer: Bool, +hot: Bool) -> List<&2, F.Finding>: 94 match nn: 95 case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, +l, +c}}, +rest}: 96 match rest: 97 case Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, r2}: 98 leaf(k, _u => walk(rest, name, path, outer, Bool.or(outer, seg_op(rest))), 99 _v => walk(rest, name, path, outer, seg_op(rest)), 100 _w => call(t, l, c, o, name, path, hot, 101 walk(kids, name, path, inner(hot, o, t), Bool.or(inner(hot, o, t), seg_op(kids))), 102 walk(r2, name, path, outer, hot))) 103 case _r: 104 leaf(k, _u => walk(rest, name, path, outer, Bool.or(outer, seg_op(rest))), 105 _v => walk(rest, name, path, outer, seg_op(rest)), _w => walk(rest, name, path, outer, hot)) 106 case Tree.NCons{Tree.Group{open, +kids, close}, rest}: 107 List.concat(&2, F.Finding, [walk(kids, name, path, hot, Bool.or(hot, seg_op(kids))), 108 walk(rest, name, path, outer, hot)]) 109 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}: 110 List.concat(&2, F.Finding, [walk(kids, name, path, False{}, seg_op(kids)), 111 walk(body, name, path, False{}, False{}), 112 walk(rest, name, path, outer, hot)]) 113 case Tree.NCons{h, rest}: 114 walk(rest, name, path, outer, hot) 115 case other: 116 Nil{} 117 118 def check.go(ds: List<&2, Calls.Def>, +path: String, acc: List<&2, List<&2, F.Finding>>) -> List<&2, F.Finding>: 119 match ds: 120 case Nil{}: 121 List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc)) 122 case Con{Calls.Def{+name, +sig, body}, rest}: 123 check.go(rest, path, 124 Lazy.stop(List<&2, F.Finding>, Calls.exempt(path, sig), [], 125 _u => walk(body, name, path, False{}, False{})) <> acc) 126 127 # the rule 128 def check(ss: Src.Src) -> List<&2, F.Finding>: 129 Src.Src{path, text, toks, tree, bound, items} = ss 130 check.go(Calls.defs(tree), path, [])