1 # rule tail: a def whose first live parameter is a List or a String calls 2 # itself where the call is not the whole statement: `h <> go(t)`, 3 # `(1 + go(t) : U32)`, `+rest = go(t)`. The test is on that parameter's type 4 # alone, never on whether the self-call shrinks it: a def that recurses on 5 # a later Nat is reported too, and one whose list comes second is not. Each 6 # such call holds a frame until the rest of the input is done, and the JS lane 7 # overflows its stack at a few thousand to ~64K elements (bend-http on a 48KB 8 # header, agora at ~4,900 entries); native is fine. Carry an accumulator and 9 # make the self-call the whole statement (reverse once at the end if order 10 # matters). Idiomatic code does this on purpose, so the rule is noisy: off 11 # unless asked. Everything inside a `Bool.pick(..)` is skipped, whether or 12 # not `pick` reports it; laws and proofs are exempt, and a def with no type 13 # at all fills a law, so it is a proof. 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 ../../lazy/lazy.bend as Lazy 20 import ../calls.bend as Calls 21 22 # is a statement's chain exactly a call of the name, `name(..)`? 23 def bare(kids: Tree.Node, +name: String) -> Bool: 24 match kids: 25 case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{g, Tree.NNil{}}}: 26 Bool.and(String.eq(t, name), Tree.opens(g, "(")) 27 case other: 28 False{} 29 30 # the self-calls not in tail position; skip: the chain is a bare self-call 31 def walk(nn: Tree.Node, +name: String, +path: String, skip: Bool) -> List<&2, F.Finding>: 32 match nn: 33 case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, +l, +c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}: 34 +call = String.eq(o, "(") 35 +me = Bool.and(Bool.and(call, String.eq(t, name)), Bool.not(skip)) 36 +inside = walk(kids, name, path, False{}) 37 +more = List.concat(&2, F.Finding, [ 38 Bool.pick(List<&2, F.Finding>, Bool.and(call, String.eq(t, "Bool.pick")), [], inside), 39 walk(rest, name, path, False{})]) 40 Bool.pick(List<&2, F.Finding>, me, 41 F.Finding{path, l, c, U32.from_nat(String.length(name)), "tail", 42 "This call to " ++ name ++ " is not a tail call, so a long list or string overflows the stack on the JS lane; carry an accumulator."} 43 <> more, 44 more) 45 case Tree.NCons{Tree.Group{open, kids, close}, rest}: 46 List.concat(&2, F.Finding, [walk(kids, name, path, False{}), walk(rest, name, path, False{})]) 47 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}: 48 List.concat(&2, F.Finding, [walk(kids, name, path, bare(kids, name)), walk(body, name, path, False{}), 49 walk(rest, name, path, False{})]) 50 case Tree.NCons{h, rest}: 51 walk(rest, name, path, False{}) 52 case other: 53 Nil{} 54 55 def check.go(ds: List<&2, Calls.Def>, +path: String, acc: List<&2, List<&2, F.Finding>>) -> List<&2, F.Finding>: 56 match ds: 57 case Nil{}: 58 List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc)) 59 case Con{Calls.Def{+name, +sig, body}, rest}: 60 check.go(rest, path, 61 Lazy.stop(List<&2, F.Finding>, Bool.not(Bool.and(Calls.seq(sig), Bool.not(Calls.exempt(path, sig)))), [], 62 _u => walk(body, name, path, False{})) <> acc) 63 64 # the rule 65 def check(ss: Src.Src) -> List<&2, F.Finding>: 66 Src.Src{path, text, toks, tree, bound, items} = ss 67 check.go(Calls.defs(tree), path, [])