src/rules/suspicious/ring.bend

bolt@1.18.0.0 · 313 lines · 0 definitions

1# rule ring: a self-call replaces a binder with `List.append` of `List.drop`
2# (or `String.append` / `++`, or `tail`) by a constant count, passing it back
3# in the binder's own parameter position. Each step copies that fixed window.
4# Keep it in an Array and advance an index. A list the def matches and walks
5# (as any scrutinee of a match) is variable-length input, a window dropped
6# into another parameter's slot is not carried, and a one-shot trim is not a
7# loop: all stay quiet. Growing an accumulator by append, with no drop of
8# that same value, is `concat`. Laws and proofs do not run (a def with no
9# type at all fills a law: it is a proof).
10import Base
11import bolt@1.18.0.0/src/src.bend as Src
12import bolt@1.18.0.0/src/finding.bend as F
13import bolt@1.18.0.0/src/syntax/lex.bend as Lex
14import bolt@1.18.0.0/src/syntax/tree.bend as Tree
15import bolt@1.18.0.0/src/rules/calls.bend as Calls
16import bolt@1.18.0.0/src/lazy/lazy.bend as Lazy
17
18# the first of two maybes
19def or_tok(aa: Maybe<&2, Lex.Tok>, +bb: Maybe<&2, Lex.Tok>) -> Maybe<&2, Lex.Tok>:
20  match aa:
21    case Some{+t}:
22      Some{t}
23    case None{}:
24      bb
25
26# is the chain exactly one number?
27def is_num(nn: Tree.Node) -> Bool:
28  match nn:
29    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TNum{}, t, l, c}}, Tree.NNil{}}:
30      True{}
31    case other:
32      False{}
33
34# the chain is exactly one lowercase name; its text, or empty
35def name_text(nn: Tree.Node) -> String:
36  match nn:
37    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, Tree.NNil{}}:
38      t
39    case other:
40      ""
41
42# this argument is a window binder
43def win_name(nn: Tree.Node, +wins: List<&2, String>) -> Bool:
44  +tt = name_text(nn)
45  Bool.and(Bool.not(String.is_empty(tt)), List.contains(~String, ~String.eq, wins, tt))
46
47# does any argument name the window?
48def has_name(as: List<&2, Tree.Node>, +wins: List<&2, String>) -> Bool:
49  match as:
50    case Nil{}:
51      False{}
52    case Con{h, rest}:
53      +more = has_name(rest, wins)
54      Bool.or(win_name(h, wins), more)
55
56# is the last argument a number?
57def last_num(as: List<&2, Tree.Node>) -> Bool:
58  match as:
59    case Nil{}:
60      False{}
61    case Con{h, Nil{}}:
62      is_num(h)
63    case Con{h, rest}:
64      last_num(rest)
65
66# a fixed drop or tail of a named window
67def drop_ok(+tt: String, +as: List<&2, Tree.Node>, +wins: List<&2, String>) -> Bool:
68  +kind = List.contains(~String, ~String.eq, ["List.drop", "String.drop", "List.tail", "String.tail"], tt)
69  +tail = Bool.or(String.eq(tt, "List.tail"), String.eq(tt, "String.tail"))
70  Bool.and(kind, Bool.and(has_name(as, wins), Bool.or(tail, last_num(as))))
71
72# the callee token when this call is a fixed drop
73def drop_tok(
74  kk: Lex.TokKind,
75  +tt: String,
76  +line: U32,
77  +col: U32,
78  kids: Tree.Node,
79  +wins: List<&2, String>
80) -> Maybe<&2, Lex.Tok>:
81  Bool.pick(Maybe<&2, Lex.Tok>, drop_ok(tt, Calls.args(kids), wins), Some{Lex.Tok{kk, tt, line, col}}, None{})
82
83# the drop this call is, when the group is an application
84def here_of(
85  +open: Bool,
86  kk: Lex.TokKind,
87  +tt: String,
88  +line: U32,
89  +col: U32,
90  kids: Tree.Node,
91  +wins: List<&2, String>
92) -> Maybe<&2, Lex.Tok>:
93  match open:
94    case False{}:
95      None{}
96    case True{}:
97      drop_tok(kk, tt, line, col, kids, wins)
98
99# a drop anywhere under the node
100def drop_in(nn: Tree.Node, +wins: List<&2, String>) -> Maybe<&2, Lex.Tok>:
101  match nn:
102    case Tree.NCons{Tree.Leaf{Lex.Tok{+k, +t, +l, +c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
103      +here = here_of(String.eq(o, "("), k, t, l, c, kids, wins)
104      +inn = drop_in(kids, wins)
105      +aft = drop_in(rest, wins)
106      or_tok(here, or_tok(inn, aft))
107    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
108      or_tok(drop_in(kids, wins), drop_in(rest, wins))
109    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
110      or_tok(drop_in(kids, wins), or_tok(drop_in(body, wins), drop_in(rest, wins)))
111    case Tree.NCons{h, rest}:
112      drop_in(rest, wins)
113    case other:
114      None{}
115
116# which list an append extends
117def receiver_is(+list: Bool, +str: Bool, +as: List<&2, Tree.Node>, +wins: List<&2, String>) -> Maybe<&2, Lex.Tok>:
118  match list str:
119    case True{} b:
120      drop_in(Calls.arg(as, 2n), wins)
121    case False{} True{}:
122      drop_in(Calls.arg(as, 0n), wins)
123    case False{} False{}:
124      None{}
125
126# the drop an append copies onto, when this call is List.append or String.append
127def receiver(+tt: String, kids: Tree.Node, +wins: List<&2, String>) -> Maybe<&2, Lex.Tok>:
128  receiver_is(String.eq(tt, "List.append"), String.eq(tt, "String.append"), Calls.args(kids), wins)
129
130# a `++` whose left already dropped: that drop; otherwise nothing
131def pp_found(+is_pp: Bool, seen: Maybe<&2, Lex.Tok>) -> Maybe<&2, Lex.Tok>:
132  match is_pp seen:
133    case True{} Some{+t}:
134      Some{t}
135    case True{} None{}:
136      None{}
137    case False{} s:
138      None{}
139
140# the drop still in play after this operator (`++` starts a new left side)
141def pp_seen(+is_pp: Bool, +seen: Maybe<&2, Lex.Tok>) -> Maybe<&2, Lex.Tok>:
142  match is_pp:
143    case True{}:
144      None{}
145    case False{}:
146      seen
147
148# an application's append-receiver drop; a bracket is not an application
149def call_recv(+open: Bool, +tt: String, kids: Tree.Node, +wins: List<&2, String>) -> Maybe<&2, Lex.Tok>:
150  match open:
151    case False{}:
152      None{}
153    case True{}:
154      receiver(tt, kids, wins)
155
156# a drop this node itself is: the call when it is an application, else under a bracket
157def call_drop(
158  +open: Bool,
159  +kk: Lex.TokKind,
160  +tt: String,
161  +line: U32,
162  +col: U32,
163  +kids: Tree.Node,
164  +wins: List<&2, String>
165) -> Maybe<&2, Lex.Tok>:
166  match open:
167    case False{}:
168      drop_in(kids, wins)
169    case True{}:
170      drop_tok(kk, tt, line, col, kids, wins)
171
172# a drop then `++`, or an append onto a drop, in this chain
173def plus_scan(nn: Tree.Node, +seen: Maybe<&2, Lex.Tok>, +wins: List<&2, String>) -> Maybe<&2, Lex.Tok>:
174  match nn:
175    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TOp{}, +op, l, c}}, rest}:
176      +is_pp = String.eq(op, "++")
177      or_tok(pp_found(is_pp, seen), plus_scan(rest, pp_seen(is_pp, seen), wins))
178    case Tree.NCons{Tree.Leaf{Lex.Tok{+k, +t, +l, +c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
179      +open = String.eq(o, "(")
180      +recv = call_recv(open, t, kids, wins)
181      +inn = plus_scan(kids, None{}, wins)
182      +drop = call_drop(open, k, t, l, c, kids, wins)
183      +aft = plus_scan(rest, or_tok(seen, drop), wins)
184      or_tok(recv, or_tok(inn, aft))
185    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
186      +inn = plus_scan(kids, None{}, wins)
187      +aft = plus_scan(rest, or_tok(seen, drop_in(kids, wins)), wins)
188      or_tok(inn, aft)
189    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
190      or_tok(plus_scan(kids, None{}, wins), or_tok(plus_scan(body, None{}, wins), plus_scan(rest, seen, wins)))
191    case Tree.NCons{h, rest}:
192      plus_scan(rest, seen, wins)
193    case other:
194      None{}
195
196# a finding at the drop the scan found
197def slide_at(mm: Maybe<&2, Lex.Tok>, +path: String) -> List<&2, F.Finding>:
198  match mm:
199    case None{}:
200      Nil{}
201    case Some{Lex.Tok{k, +t, l, c}}:
202      [F.Finding{path, l, c, U32.from_nat(String.length(t)), "ring",
203        "Each step drops from and appends to a fixed-size window, which copies it; keep the window in an Array and advance an index."}]
204
205# a finding at the drop, when the argument slides a window
206def slide(aa: Tree.Node, +path: String, +wins: List<&2, String>) -> List<&2, F.Finding>:
207  slide_at(plus_scan(aa, None{}, wins), path)
208
209# the windows an argument in this parameter's slot may slide: the parameter
210# itself when it is a window, else none
211def slot_wins(+slot: String, +wins: List<&2, String>) -> List<&2, String>:
212  Bool.pick(List<&2, String>, List.contains(~String, ~String.eq, wins, slot), [slot], [])
213
214# every self-call argument that slides the window of its own slot (params,
215# from the argument's position on)
216def slides(
217  as: List<&2, Tree.Node>,
218  +params: List<&2, String>,
219  +path: String,
220  +wins: List<&2, String>
221) -> List<&2, F.Finding>:
222  match as:
223    case Nil{}:
224      Nil{}
225    case Con{h, rest}:
226      +slot = Maybe.default(&2, String, List.head(&2, String, params), "")
227      List.append(&2, F.Finding, slide(h, path, slot_wins(slot, wins)),
228        slides(rest, List.tail(&2, String, params), path, wins))
229
230# every self-call's sliding arguments
231def walk(
232  nn: Tree.Node,
233  +self: String,
234  +params: List<&2, String>,
235  +path: String,
236  +wins: List<&2, String>
237) -> List<&2, F.Finding>:
238  match nn:
239    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
240      +me = Bool.and(String.eq(o, "("), String.eq(t, self))
241      +own = Lazy.stop(List<&2, F.Finding>, Bool.not(me), [], _u => slides(Calls.args(kids), params, path, wins))
242      List.concat(&2, F.Finding, [own, walk(kids, self, params, path, wins), walk(rest, self, params, path, wins)])
243    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
244      List.concat(&2, F.Finding, [walk(kids, self, params, path, wins), walk(rest, self, params, path, wins)])
245    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
246      List.concat(&2, F.Finding, [walk(kids, self, params, path, wins), walk(body, self, params, path, wins),
247        walk(rest, self, params, path, wins)])
248    case Tree.NCons{h, rest}:
249      walk(rest, self, params, path, wins)
250    case other:
251      Nil{}
252
253# the top-level names of a chain, onto acc
254def names_in(nn: Tree.Node, acc: List<&2, String>) -> List<&2, String>:
255  match nn:
256    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, t, l, c}}, rest}:
257      t <> names_in(rest, acc)
258    case Tree.NCons{h, rest}:
259      names_in(rest, acc)
260    case other:
261      acc
262
263# every scrutinee of `match a b ..:`, onto acc (acc alone for any other chain)
264def match_names(kids: Tree.Node, +acc: List<&2, String>) -> List<&2, String>:
265  match kids:
266    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TKey{}, +kw, l, c}}, +rest}:
267      Lazy.stop(List<&2, String>, Bool.not(String.eq(kw, "match")), acc, _u => names_in(rest, acc))
268    case other:
269      acc
270
271# names the def matches, as any scrutinee: variable-length input, not a window
272def matched(nn: Tree.Node, +acc: List<&2, String>) -> List<&2, String>:
273  match nn:
274    case Tree.NCons{Tree.Stmt{Tree.STerm{}, +kids, body}, rest}:
275      +here = match_names(kids, acc)
276      matched(rest, matched(body, matched(kids, here)))
277    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
278      matched(rest, matched(body, matched(kids, acc)))
279    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
280      matched(rest, matched(kids, acc))
281    case Tree.NCons{h, rest}:
282      matched(rest, acc)
283    case other:
284      acc
285
286# parameters that are not matched as input
287def windows.go(ps: List<&2, String>, +bad: List<&2, String>, +acc: List<&2, String>) -> List<&2, String>:
288  match ps:
289    case Nil{}:
290      List.reverse(&2, String, acc)
291    case Con{+p, rest}:
292      +known = List.contains(~String, ~String.eq, bad, p)
293      +keep = Bool.and(Bool.not(String.is_empty(p)), Bool.not(known))
294      windows.go(rest, bad, Bool.pick(List<&2, String>, keep, p <> acc, acc))
295
296# parameters that are not matched as input
297def windows(sig: Tree.Node, body: Tree.Node) -> List<&2, String>:
298  windows.go(Calls.names(sig), matched(body, []), [])
299
300def check.go(ds: List<&2, Calls.Def>, +path: String, acc: List<&2, List<&2, F.Finding>>) -> List<&2, F.Finding>:
301  match ds:
302    case Nil{}:
303      List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc))
304    case Con{Calls.Def{+name, +sig, +body}, rest}:
305      check.go(rest, path,
306        Lazy.stop(List<&2, F.Finding>,
307          Bool.not(Bool.and(Calls.calls(body, name), Bool.not(Calls.exempt(path, sig)))), [],
308          _u => walk(body, name, Calls.names(sig), path, windows(sig, body))) <> acc)
309
310# the rule
311def check(ss: Src.Src) -> List<&2, F.Finding>:
312  Src.Src{path, text, toks, tree, bound, items} = ss
313  check.go(Calls.defs(tree), path, [])