src/rules/suspicious/scan.bend
bolt@1.24.0.0 · 347 lines · 0 definitions
1# rule scan: a def that calls itself and, on the same step, hands a
2# parameter it carries unchanged to a walk. Every step walks that list again:
3# O(|A| * |B|), A what the def walks and B what it carries. Index it once
4# outside the loop, or merge two sorted walks. A parameter is carried when
5# every self-call passes it back as that same lone name in its own position
6# (hoist's notion). A call is a name token (plain or dotted), then a `(`
7# group, and what it walks is some of its arguments, counted from 0. A Base
8# list search walks its list: argument 2 of `List.contains`, `List.find`,
9# `List.filter` and `List.length`, 3 of `List.any` and `List.all`, 4 of
10# `List.foldl` and `List.foldr`; a Base walk that rebuilds the list
11# (`List.map`, `List.append`, ...) makes output, not a search, and is left
12# alone. A def of this file walks, when it calls itself, its first live
13# parameter that some self-call does not pass back unchanged (the one it
14# shrinks), unless that parameter is a `Nat` or a `String` (a count, such as
15# fuel, or a text); and each parameter its body, anywhere, passes as the lone
16# name of an argument a call walks, of a Base search or of a def listed
17# before it. A def of another module is not seen. A call reports when it is
18# not a self-call and an argument it walks is exactly the lone name of a
19# carried parameter: anything else there (a literal table, an expression, a
20# name the step binds) is left alone. The step is what hoist reads as hot:
21# the def's body, a case arm's body only when it calls the def, never a case
22# pattern, and never the rest of a chain after `=>` (a lambda's body, a
23# `Lazy` thunk, runs when it is called). Laws and proofs do not run (a def
24# with no type at all fills a law: it is a proof), and a law file is skipped
25# whole. One finding per call, on its callee.
26import Base
27import bolt@1.24.0.0/src/src.bend as Src
28import bolt@1.24.0.0/src/paths.bend as Paths
29import bolt@1.24.0.0/src/role.bend as Role
30import bolt@1.24.0.0/src/finding.bend as F
31import bolt@1.24.0.0/src/syntax/lex.bend as Lex
32import bolt@1.24.0.0/src/syntax/tree.bend as Tree
33import bolt@1.24.0.0/src/rules/calls.bend as Calls
34import bolt@1.24.0.0/src/lazy/lazy.bend as Lazy
35import bolt@1.24.0.0/src/rules/suspicious/hoist.bend as Hoist
36
37# a def of this file, and one argument it walks
38type Walk is Data:
39 Walk{name: String, at: Nat}
40
41# the answer as a list: none or one
42def opt(mm: Maybe<&2, Nat>) -> List<&2, Nat>:
43 match mm:
44 case None{}:
45 Nil{}
46 case Some{at}:
47 [at]
48
49# the list argument of a Base list search
50def base_at(+tt: String) -> List<&2, Nat>:
51 +two = List.contains(~String, ~String.eq, ["List.contains", "List.find", "List.filter", "List.length"], tt)
52 +three = List.contains(~String, ~String.eq, ["List.any", "List.all"], tt)
53 +four = List.contains(~String, ~String.eq, ["List.foldl", "List.foldr"], tt)
54 Bool.pick(List<&2, Nat>, two, [2n], Bool.pick(List<&2, Nat>, three, [3n], Bool.pick(List<&2, Nat>, four, [4n], [])))
55
56# the arguments the defs of this file named tt walk
57def file_at(ws: List<&2, Walk>, +tt: String) -> List<&2, Nat>:
58 match ws:
59 case Nil{}:
60 Nil{}
61 case Con{Walk{+nm, +at}, rest}:
62 +more = file_at(rest, tt)
63 Bool.pick(List<&2, Nat>, String.eq(nm, tt), at <> more, more)
64
65# the arguments a callee walks: a Base walk's list, and those the defs of this file record
66def slots(+tt: String, +ws: List<&2, Walk>) -> List<&2, Nat>:
67 List.append(&2, Nat, base_at(tt), file_at(ws, tt))
68
69# a leaf that is a plain name: its text
70def name_tok(tok: Lex.Tok) -> Maybe<&2, String>:
71 match tok:
72 case Lex.Tok{Lex.TName{}, t, l, c}:
73 Some{t}
74 case other:
75 None{}
76
77# the argument, when it is exactly one plain name
78def name_of(nn: Tree.Node) -> Maybe<&2, String>:
79 match nn:
80 case Tree.NCons{Tree.Leaf{tok}, Tree.NNil{}}:
81 name_tok(tok)
82 case other:
83 None{}
84
85# the name, when the list holds it
86def keep_in(mm: Maybe<&2, String>, +names: List<&2, String>) -> Maybe<&2, String>:
87 match mm:
88 case None{}:
89 None{}
90 case Some{+nm}:
91 Bool.pick(Maybe<&2, String>, List.contains(~String, ~String.eq, names, nm), Some{nm}, None{})
92
93# the first of two answers that has one
94def first(aa: Maybe<&2, String>, bb: Maybe<&2, String>) -> Maybe<&2, String>:
95 match aa:
96 case None{}:
97 bb
98 case Some{nm}:
99 Some{nm}
100
101# the first walked argument that is the lone name of one of the names
102def found(ss: List<&2, Nat>, +as: List<&2, Tree.Node>, +names: List<&2, String>) -> Maybe<&2, String>:
103 match ss:
104 case Nil{}:
105 None{}
106 case Con{at, rest}:
107 +more = found(rest, as, names)
108 first(keep_in(name_of(Calls.arg(as, at)), names), more)
109
110# where a name sits among the parameter names
111def index_of(ps: List<&2, String>, +nm: String, +ii: Nat) -> Maybe<&2, Nat>:
112 match ps:
113 case Nil{}:
114 None{}
115 case Con{+p, rest}:
116 Lazy.stop(Maybe<&2, Nat>, String.eq(p, nm), Some{ii}, _u => index_of(rest, nm, 1n+ii))
117
118# where the name sits among the parameter names, when there is a name
119def index_in(mm: Maybe<&2, String>, +ps: List<&2, String>) -> Maybe<&2, Nat>:
120 match mm:
121 case None{}:
122 None{}
123 case Some{+nm}:
124 index_of(ps, nm, 0n)
125
126# the parameters that sit, as lone names, at walked arguments
127def params_at(ss: List<&2, Nat>, +as: List<&2, Tree.Node>, +ps: List<&2, String>) -> List<&2, Nat>:
128 match ss:
129 case Nil{}:
130 Nil{}
131 case Con{at, rest}:
132 +more = params_at(rest, as, ps)
133 List.append(&2, Nat, opt(index_in(name_of(Calls.arg(as, at)), ps)), more)
134
135# a call's walked parameters, when its callee is a name and it is a call
136def pass_hit(
137 kk: Lex.TokKind,
138 +open: Bool,
139 +tt: String,
140 kids: Tree.Node,
141 +ps: List<&2, String>,
142 +ws: List<&2, Walk>
143) -> List<&2, Nat>:
144 match kk:
145 case Lex.TName{}:
146 Lazy.stop(List<&2, Nat>, Bool.not(open), [], _u => params_at(slots(tt, ws), Calls.args(kids), ps))
147 case Lex.TDotted{}:
148 Lazy.stop(List<&2, Nat>, Bool.not(open), [], _u => params_at(slots(tt, ws), Calls.args(kids), ps))
149 case other:
150 Nil{}
151
152# the parameters a body passes, anywhere, as an argument some call walks
153def passes(nn: Tree.Node, +ps: List<&2, String>, +ws: List<&2, Walk>) -> List<&2, Nat>:
154 match nn:
155 case Tree.NCons{Tree.Leaf{Lex.Tok{+k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
156 +own = pass_hit(k, String.eq(o, "("), t, kids, ps, ws)
157 List.concat(&2, Nat, [own, passes(kids, ps, ws), passes(rest, ps, ws)])
158 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
159 List.concat(&2, Nat, [passes(kids, ps, ws), passes(rest, ps, ws)])
160 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
161 List.concat(&2, Nat, [passes(kids, ps, ws), passes(body, ps, ws), passes(rest, ps, ws)])
162 case Tree.NCons{h, rest}:
163 passes(rest, ps, ws)
164 case other:
165 Nil{}
166
167# a live parameter that some self-call does not pass back unchanged
168def moves(+pp: Tree.Node, +carried: List<&2, String>) -> Bool:
169 Bool.and(Calls.is_live(pp), Bool.not(List.contains(~String, ~String.eq, carried, Calls.param_name(pp))))
170
171# a count or a text: a loop down one scans no list
172def scalar(+pp: Tree.Node) -> Bool:
173 +hh = Calls.type_head(pp, False{})
174 Bool.or(String.eq(hh, "Nat"), String.eq(hh, "String"))
175
176# where the parameter a def shrinks sits, unless it is a count or a text
177def place(+pp: Tree.Node, +ii: Nat) -> Maybe<&2, Nat>:
178 Bool.pick(Maybe<&2, Nat>, scalar(pp), None{}, Some{ii})
179
180def own.go(ps: List<&2, Tree.Node>, +carried: List<&2, String>, +ii: Nat) -> Maybe<&2, Nat>:
181 match ps:
182 case Nil{}:
183 None{}
184 case Con{+p, rest}:
185 +here = moves(p, carried)
186 Lazy.stop(Maybe<&2, Nat>, here, place(p, ii), _u => own.go(rest, carried, 1n+ii))
187
188# where the parameter a def shrinks sits, when it calls itself and it is no count or text
189def own(+sig: Tree.Node, +body: Tree.Node, +name: String) -> Maybe<&2, Nat>:
190 Lazy.stop(Maybe<&2, Nat>, Bool.not(Calls.calls(body, name)), None{},
191 _u => own.go(Calls.params(sig), Hoist.carried(sig, body, name), 0n))
192
193# one walk of the def for each argument
194def tag(ns: List<&2, Nat>, +name: String, +ws: List<&2, Walk>) -> List<&2, Walk>:
195 match ns:
196 case Nil{}:
197 ws
198 case Con{at, rest}:
199 Walk{name, at} <> tag(rest, name, ws)
200
201def walks.go(ds: List<&2, Calls.Def>, +ws: List<&2, Walk>) -> List<&2, Walk>:
202 match ds:
203 case Nil{}:
204 ws
205 case Con{Calls.Def{+name, +sig, +body}, rest}:
206 +mine = List.append(&2, Nat, opt(own(sig, body, name)), passes(body, Calls.names(sig), ws))
207 walks.go(rest, tag(mine, name, ws))
208
209# the defs of this file that walk, each with an argument it walks, in order
210def walks(ds: List<&2, Calls.Def>) -> List<&2, Walk>:
211 walks.go(ds, [])
212
213# the name of the parameter a def shrinks, "" for none
214def lead.go(ps: List<&2, Tree.Node>, +carried: List<&2, String>) -> String:
215 match ps:
216 case Nil{}:
217 ""
218 case Con{+p, rest}:
219 +here = moves(p, carried)
220 Lazy.stop(String, here, Calls.param_name(p), _u => lead.go(rest, carried))
221
222# a finding on the callee, naming the carried list
223def cite(
224 mm: Maybe<&2, String>,
225 +tt: String,
226 +line: U32,
227 +col: U32,
228 +self: String,
229 +lead: String,
230 +path: String
231) -> List<&2, F.Finding>:
232 match mm:
233 case None{}:
234 Nil{}
235 case Some{+pp}:
236 [F.Finding{path, line, col, U32.from_nat(String.length(tt)), "scan",
237 tt ++ " walks " ++ pp ++ " on every step of " ++ self ++ ", which carries it unchanged: that is O(|" ++ lead
238 ++ "| * |" ++ pp ++ "|). Index it once outside the loop, or merge two sorted walks."}]
239
240# a hot call of a walk other than the def, a walked argument carried
241def on_name(
242 +hot: Bool,
243 +open: Bool,
244 +tt: String,
245 +line: U32,
246 +col: U32,
247 kids: Tree.Node,
248 +self: String,
249 +ws: List<&2, Walk>,
250 +carried: List<&2, String>,
251 +lead: String,
252 +path: String
253) -> List<&2, F.Finding>:
254 Lazy.stop(List<&2, F.Finding>, Bool.not(Bool.and(hot, Bool.and(open, Bool.not(String.eq(tt, self))))), [],
255 _u => cite(found(slots(tt, ws), Calls.args(kids), carried), tt, line, col, self, lead, path))
256
257# a call's finding, when its callee is a name
258def call_hit(
259 kk: Lex.TokKind,
260 +hot: Bool,
261 +open: Bool,
262 +tt: String,
263 +line: U32,
264 +col: U32,
265 kids: Tree.Node,
266 +self: String,
267 +ws: List<&2, Walk>,
268 +carried: List<&2, String>,
269 +lead: String,
270 +path: String
271) -> List<&2, F.Finding>:
272 match kk:
273 case Lex.TName{}:
274 on_name(hot, open, tt, line, col, kids, self, ws, carried, lead, path)
275 case Lex.TDotted{}:
276 on_name(hot, open, tt, line, col, kids, self, ws, carried, lead, path)
277 case other:
278 Nil{}
279
280# the walks of carried lists on a hot path
281def walk(
282 nn: Tree.Node,
283 +self: String,
284 +ws: List<&2, Walk>,
285 +carried: List<&2, String>,
286 +lead: String,
287 +path: String,
288 +hot: Bool
289) -> List<&2, F.Finding>:
290 match nn:
291 case Tree.NCons{Tree.Leaf{Lex.Tok{+k, +t, +l, +c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
292 +heat = Hoist.warm(hot, k)
293 +own = call_hit(k, heat, String.eq(o, "("), t, l, c, kids, self, ws, carried, lead, path)
294 +inn = walk(kids, self, ws, carried, lead, path, heat)
295 +aft = walk(rest, self, ws, carried, lead, path, heat)
296 List.concat(&2, F.Finding, [own, inn, aft])
297 case Tree.NCons{Tree.Leaf{Lex.Tok{k, t, l, c}}, rest}:
298 walk(rest, self, ws, carried, lead, path, Hoist.warm(hot, k))
299 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
300 List.concat(&2, F.Finding,
301 [walk(kids, self, ws, carried, lead, path, hot), walk(rest, self, ws, carried, lead, path, hot)])
302 case Tree.NCons{Tree.Stmt{Tree.SCase{}, kids, +body}, rest}:
303 +arm = Bool.and(hot, Calls.calls(body, self))
304 List.concat(&2, F.Finding, [walk(kids, self, ws, carried, lead, path, False{}),
305 walk(body, self, ws, carried, lead, path, arm), walk(rest, self, ws, carried, lead, path, hot)])
306 case Tree.NCons{Tree.Stmt{kind, kids, body}, rest}:
307 List.concat(&2, F.Finding, [walk(kids, self, ws, carried, lead, path, hot),
308 walk(body, self, ws, carried, lead, path, hot), walk(rest, self, ws, carried, lead, path, hot)])
309 case Tree.NCons{h, rest}:
310 walk(rest, self, ws, carried, lead, path, hot)
311 case other:
312 Nil{}
313
314# one def's walk, its carried parameters and the one it shrinks read once
315def run(+sig: Tree.Node, +body: Tree.Node, +name: String, +ws: List<&2, Walk>, +path: String) -> List<&2, F.Finding>:
316 +carried = Hoist.carried(sig, body, name)
317 walk(body, name, ws, carried, lead.go(Calls.params(sig), carried), path, True{})
318
319def check.go(
320 ds: List<&2, Calls.Def>,
321 +ws: List<&2, Walk>,
322 +path: String,
323 acc: List<&2, List<&2, F.Finding>>
324) -> List<&2, F.Finding>:
325 match ds:
326 case Nil{}:
327 List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc))
328 case Con{Calls.Def{+name, +sig, +body}, rest}:
329 check.go(rest, ws, path,
330 Lazy.stop(List<&2, F.Finding>,
331 Bool.not(Bool.and(Calls.calls(body, name), Bool.not(Calls.exempt(path, sig)))), [],
332 _u => run(sig, body, name, ws, path)) <> acc)
333
334# the findings over a file's defs, their walks read once
335def check.on(+ds: List<&2, Calls.Def>, +path: String) -> List<&2, F.Finding>:
336 check.go(ds, walks(ds), path, [])
337
338# the rule; a law file is skipped whole, before its walks are read
339def check(ss: Src.Src) -> List<&2, F.Finding>:
340 Src.Src{+path, text, toks, tree, bound, items} = ss
341 Lazy.stop(List<&2, F.Finding>, Paths.is_law_file(path), [], _u => check.on(Calls.defs(tree), path))
342
343# the rule, told the file's role (src/role.bend): a law or proof file is
344# skipped whole
345def check.role(rr: Role.Role, ss: Src.Src) -> List<&2, F.Finding>:
346 Src.Src{path, text, toks, tree, bound, items} = ss
347 Lazy.stop(List<&2, F.Finding>, Role.is_law_file(rr), [], _u => check.on(Calls.defs(tree), path))