1 # rule fuel: a call to a def of the same file, its name then `(`, passes a Nat 2 # literal (`1000n`: digits, then `n`) where that def takes its fuel. A fuel 3 # parameter is known by its name alone (its first lowercase name before the 4 # colon): `fuel`, `gas`, `steps` or `budget`, or any name starting with 5 # `fuel`. The fuel-0 arm returns what it has, so input past the literal comes 6 # out cut short, with no error (a JSON printer that stopped at 1000 tasks). 7 # Inside a law, the checker unrolls the fixed fuel and hangs. Derive the fuel 8 # from the input's size (`Nat.mul(size, 4n)`) or take it as a parameter. A 9 # literal of any size counts, an exact repeat count such as `3n` included. 10 # Only an argument that is one literal token alone counts: `U32.to_nat(1000)`, 11 # a let-bound literal and a parenthesized `(7n)` are not seen. A def's own 12 # calls are exempt: its step passes `fuel - 1`, not a literal. 13 import Base 14 import ../../src.bend as Src 15 import ../../lazy/lazy.bend as Lazy 16 import ../../finding.bend as F 17 import ../../syntax/lex.bend as Lex 18 import ../../syntax/tree.bend as Tree 19 import ../../syntax/bind.bend as Bind 20 import ../calls.bend as Calls 21 import ../tokens.bend as T 22 23 # a def of the file and the position of a fuel parameter 24 type Fuel is Data: 25 Fuel{name: String, at: Nat} 26 27 # is it a fuel parameter's name? 28 def is_fuel(+nn: String) -> Bool: 29 Bool.or(String.starts_with(nn, "fuel"), 30 List.contains(~String, ~String.eq, ["gas", "steps", "budget"], nn)) 31 32 # the fuel parameters among a def's parameters, from position i on 33 def fuels.params(ps: List<&2, Tree.Node>, +name: String, +ii: Nat) -> List<&2, Fuel>: 34 match ps: 35 case Nil{}: 36 Nil{} 37 case Con{p, rest}: 38 +more = fuels.params(rest, name, 1n+ii) 39 Bool.pick(List<&2, Fuel>, is_fuel(Calls.param_name(p)), Fuel{name, ii} <> more, more) 40 41 # every fuel parameter of every def of the file 42 def fuels(ds: List<&2, Calls.Def>) -> List<&2, Fuel>: 43 match ds: 44 case Nil{}: 45 Nil{} 46 case Con{Calls.Def{name, sig, body}, rest}: 47 List.append(&2, Fuel, fuels.params(Calls.params(sig), name, 0n), fuels(rest)) 48 49 # an argument that is a Nat literal alone (digits, then `n`), as a finding 50 def literal(aa: Tree.Node, +path: String) -> List<&2, F.Finding>: 51 match aa: 52 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TNum{}, +t, l, c}}, Tree.NNil{}}: 53 Bool.pick(List<&2, F.Finding>, T.nat_ok(String.to_list(t)), 54 [F.Finding{path, l, c, U32.from_nat(String.length(t)), "fuel", 55 "The fuel is fixed at " ++ t ++ ", so longer input is silently cut short; derive the fuel from the input size."}], 56 []) 57 case other: 58 Nil{} 59 60 # a call to name with these arguments, against every fuel parameter 61 def call(fs: List<&2, Fuel>, +name: String, +as: List<&2, Tree.Node>, +path: String) -> List<&2, F.Finding>: 62 match fs: 63 case Nil{}: 64 Nil{} 65 case Con{Fuel{+fn, at}, rest}: 66 +more = call(rest, name, as, path) 67 Lazy.stop(List<&2, F.Finding>, Bool.not(String.eq(fn, name)), more, 68 _u => List.append(&2, F.Finding, literal(Calls.arg(as, at), path), more)) 69 70 # every call at any depth of a chain, but the def's own (self) 71 def calls(nn: Tree.Node, +fs: List<&2, Fuel>, +self: String, +path: String) -> List<&2, F.Finding>: 72 match nn: 73 case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}: 74 +more = List.concat(&2, F.Finding, [calls(kids, fs, self, path), calls(rest, fs, self, path)]) 75 +hit = Bool.and(Bool.and(Lex.is_name(k), String.eq(o, "(")), Bool.not(String.eq(t, self))) 76 Lazy.stop(List<&2, F.Finding>, Bool.not(hit), more, 77 _u => List.append(&2, F.Finding, call(fs, t, Calls.args(kids), path), more)) 78 case Tree.NCons{Tree.Group{open, kids, close}, rest}: 79 List.concat(&2, F.Finding, [calls(kids, fs, self, path), calls(rest, fs, self, path)]) 80 case Tree.NCons{Tree.Stmt{kind, kids, body}, rest}: 81 List.concat(&2, F.Finding, [calls(kids, fs, self, path), calls(body, fs, self, path), 82 calls(rest, fs, self, path)]) 83 case Tree.NCons{h, rest}: 84 calls(rest, fs, self, path) 85 case other: 86 Nil{} 87 88 # a def's (or a law's) name, "" when it has none 89 def own(mm: Maybe<&2, String>) -> String: 90 match mm: 91 case None{}: 92 "" 93 case Some{n}: 94 n 95 96 # each top-level statement, walked as its own def 97 def check.go(root: Tree.Node, +fs: List<&2, Fuel>, +path: String) -> List<&2, F.Finding>: 98 match root: 99 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}: 100 +self = own(Bind.declared(kids)) 101 List.concat(&2, F.Finding, [calls(kids, fs, self, path), calls(body, fs, self, path), check.go(rest, fs, path)]) 102 case Tree.NCons{h, rest}: 103 check.go(rest, fs, path) 104 case other: 105 Nil{} 106 107 # the rule 108 def check(ss: Src.Src) -> List<&2, F.Finding>: 109 Src.Src{path, text, toks, +tree, bound, items} = ss 110 check.go(tree, fuels(Calls.defs(tree)), path)