~/bend-docscommunity

src/rules/pedantic/tail.bend source

src/rules/pedantic/tail.bend on the hub · documented module

# rule tail: a def whose first live parameter is a List or a String calls# itself where the call is not the whole statement: `h <> go(t)`,# `(1 + go(t) : U32)`, `+rest = go(t)`. The test is on that parameter's type# alone, never on whether the self-call shrinks it: a def that recurses on# a later Nat is reported too, and one whose list comes second is not. Each# such call holds a frame until the rest of the input is done, and the JS lane# overflows its stack at a few thousand to ~64K elements (bend-http on a 48KB# header, agora at ~4,900 entries); native is fine. Carry an accumulator and# make the self-call the whole statement (reverse once at the end if order# matters). Idiomatic code does this on purpose, so the rule is noisy: off# unless asked. Everything inside a `Bool.pick(..)` is skipped, whether or# not `pick` reports it; laws and proofs are exempt, and a def with no type# at all fills a law, so it is a proof.import Baseimport ../../src.bend as Srcimport ../../finding.bend as Fimport ../../syntax/lex.bend as Leximport ../../syntax/tree.bend as Treeimport ../../lazy/lazy.bend as Lazyimport ../calls.bend as Calls# is a statement's chain exactly a call of the name, `name(..)`?def bare(kids: Tree.Node, +name: String) -> Bool:  match kids:    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{g, Tree.NNil{}}}:      Bool.and(String.eq(t, name), Tree.opens(g, "("))    case other:      False{}# the self-calls not in tail position; skip: the chain is a bare self-calldef walk(nn: Tree.Node, +name: String, +path: String, skip: Bool) -> List<&2, F.Finding>:  match nn:    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, +l, +c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:      +call = String.eq(o, "(")      +me = Bool.and(Bool.and(call, String.eq(t, name)), Bool.not(skip))      +inside = walk(kids, name, path, False{})      +more = List.concat(&2, F.Finding, [        Bool.pick(List<&2, F.Finding>, Bool.and(call, String.eq(t, "Bool.pick")), [], inside),        walk(rest, name, path, False{})])      Bool.pick(List<&2, F.Finding>, me,        F.Finding{path, l, c, U32.from_nat(String.length(name)), "tail",          "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."}          <> more,        more)    case Tree.NCons{Tree.Group{open, kids, close}, rest}:      List.concat(&2, F.Finding, [walk(kids, name, path, False{}), walk(rest, name, path, False{})])    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:      List.concat(&2, F.Finding, [walk(kids, name, path, bare(kids, name)), walk(body, name, path, False{}),        walk(rest, name, path, False{})])    case Tree.NCons{h, rest}:      walk(rest, name, path, False{})    case other:      Nil{}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>, Bool.not(Bool.and(Calls.seq(sig), Bool.not(Calls.exempt(path, sig)))), [],          _u => walk(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, [])