src/rules/suspicious/rewalk.bend
bolt@1.21.0.0 · 905 lines · 0 definitions
1# rule rewalk: one straight piece of a def calls the same walk twice on the
2# same argument, and one result is used only for a single value (a get of one
3# index, one field, or a let whose name is only read that way) while the other
4# result is kept whole. Take the value from that other result. The same
5# argument is the same text with no let of any name it uses (`=` or `<-`,
6# taking effect at the end of the let) between the two calls. A different
7# case arm is a different path, and two narrow reads are left alone. A walk is
8# a def of this file that loops, or a Base walk (`List.map` and the like).
9# Laws and proofs do not run (a def with no type at all fills a law: it is a
10# proof). This is not `twice`, which is duplicate literals in a list pattern.
11import Base
12import bolt@1.21.0.0/src/src.bend as Src
13import bolt@1.21.0.0/src/finding.bend as F
14import bolt@1.21.0.0/src/syntax/lex.bend as Lex
15import bolt@1.21.0.0/src/syntax/tree.bend as Tree
16import bolt@1.21.0.0/src/rules/calls.bend as Calls
17import bolt@1.21.0.0/src/lazy/lazy.bend as Lazy
18
19# how the result of a call is used
20type How is Data:
21 HWide{}
22 HSlot{}
23 HLet{binder: String}
24 HField{}
25
26# one expensive call
27type Site is Data:
28 Site{name: String, args: String, line: U32, col: U32, len: U32, how: How}
29
30# a narrow read and a wide read of one name
31type Hit is Data:
32 Hit{wide: Bool, slot: Bool}
33
34# where an accessor reads its collection
35type Slot is Data:
36 Slot{on: Bool, at: Nat}
37
38# a position a mark pass recorded
39type Pos is Data:
40 Pos{line: U32, col: U32}
41
42# the left of `=` : one name, and whether a group makes it a field
43type Lhs is Data:
44 Lhs{seen: Bool, extra: Bool, field: Bool, nm: String}
45
46# a let of one name, and where it takes effect
47type Bind is Data:
48 Bind{nm: String, line: U32, col: U32}
49
50# a let whose right-hand side is one call
51type Note is Data:
52 Note{line: U32, col: U32, how: How}
53
54# neither read
55def none_hit() -> Hit:
56 Hit{False{}, False{}}
57
58# either read from two results
59def either(aa: Hit, +bb: Hit) -> Hit:
60 Hit{+w1, +s1} = aa
61 Hit{+w2, +s2} = bb
62 Hit{Bool.or(w1, w2), Bool.or(s1, s2)}
63
64# a mention of the name: wide, unless this position is a narrow slot
65def name_slot(+slot: Bool) -> Hit:
66 match slot:
67 case True{}:
68 Hit{False{}, True{}}
69 case False{}:
70 Hit{True{}, False{}}
71
72# a mention of the name, or nothing when it is a different name
73def name_hit(+same: Bool, +slot: Bool) -> Hit:
74 match same:
75 case False{}:
76 none_hit()
77 case True{}:
78 name_slot(slot)
79
80# which argument a narrow accessor reads
81def slot_pick(+list: Bool, +str: Bool, +arr: Bool) -> Slot:
82 match list str arr:
83 case True{} b c:
84 Slot{True{}, 2n}
85 case False{} True{} c:
86 Slot{True{}, 0n}
87 case False{} False{} True{}:
88 Slot{True{}, 1n}
89 case False{} False{} False{}:
90 Slot{False{}, 0n}
91
92# List.get / head / last / length, String.get / length, Array.get; not List.set
93def slot_of(+tt: String) -> Slot:
94 slot_pick(
95 List.contains(~String, ~String.eq, ["List.get", "List.head", "List.last", "List.length"], tt),
96 Bool.or(String.eq(tt, "String.get"), String.eq(tt, "String.length")),
97 String.eq(tt, "Array.get"))
98
99# after a comma, still inside the slot argument?
100def step_is_zero(pp: Nat) -> Bool:
101 match pp:
102 case 0n:
103 True{}
104 case 1n+k:
105 False{}
106
107# after a comma, is the next argument the slot?
108def step_hot_live(left: Nat) -> Bool:
109 match left:
110 case 0n:
111 False{}
112 case 1n+p:
113 step_is_zero(p)
114
115# the slot flag after a comma
116def step_hot(+hot: Bool, +left: Nat, +live: Bool) -> Bool:
117 match live:
118 case False{}:
119 hot
120 case True{}:
121 step_hot_live(left)
122
123# the arguments still to skip after a comma
124def step_left_live(left: Nat) -> Nat:
125 match left:
126 case 0n:
127 0n
128 case 1n+p:
129 p
130
131# the arguments still to skip after a comma
132def step_left(+left: Nat, +live: Bool) -> Nat:
133 match live:
134 case False{}:
135 left
136 case True{}:
137 step_left_live(left)
138
139# the chain is exactly one call
140def as_open(+open: Bool, tok: Lex.Tok) -> Maybe<&2, Lex.Tok>:
141 match open:
142 case True{}:
143 Some{tok}
144 case False{}:
145 None{}
146
147# the inside of one pair of parentheses, when it is exactly a call
148def as_inside(nn: Tree.Node) -> Maybe<&2, Lex.Tok>:
149 match nn:
150 case Tree.NCons{Tree.Leaf{tok}, Tree.NCons{Tree.Group{Lex.Tok{k, +o, l, c}, kids, close}, Tree.NNil{}}}:
151 as_open(String.eq(o, "("), tok)
152 case other:
153 None{}
154
155# a parenthesized call, or nothing
156def as_wrapped(+paren: Bool, kids: Tree.Node) -> Maybe<&2, Lex.Tok>:
157 match paren:
158 case True{}:
159 as_inside(kids)
160 case False{}:
161 None{}
162
163# the chain is exactly one call, or one pair of parentheses around one: its callee
164def as_call(nn: Tree.Node) -> Maybe<&2, Lex.Tok>:
165 match nn:
166 case Tree.NCons{Tree.Group{Lex.Tok{k, +o, l, c}, +kids, close}, Tree.NNil{}}:
167 as_wrapped(String.eq(o, "("), kids)
168 case Tree.NCons{Tree.Leaf{tok}, Tree.NCons{Tree.Group{Lex.Tok{k, +o, l, c}, kids, close}, Tree.NNil{}}}:
169 as_open(String.eq(o, "("), tok)
170 case other:
171 None{}
172
173# the callee, when this argument is exactly a call
174def pos_of(mm: Maybe<&2, Lex.Tok>) -> List<&2, Pos>:
175 match mm:
176 case None{}:
177 Nil{}
178 case Some{Lex.Tok{k, t, l, c}}:
179 [Pos{l, c}]
180
181# the collection argument's call, when this is a narrow accessor
182def mark_slot(ss: Slot, +as: List<&2, Tree.Node>) -> List<&2, Pos>:
183 Slot{on, at} = ss
184 match on:
185 case False{}:
186 Nil{}
187 case True{}:
188 pos_of(as_call(Calls.arg(as, at)))
189
190# calls that sit in a narrow accessor's collection argument
191def mark(nn: Tree.Node) -> List<&2, Pos>:
192 match nn:
193 case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
194 +here = Lazy.stop(List<&2, Pos>, Bool.not(String.eq(o, "(")), [], _u => mark_slot(slot_of(t), Calls.args(kids)))
195 List.concat(&2, Pos, [here, mark(kids), mark(rest)])
196 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
197 List.concat(&2, Pos, [mark(kids), mark(rest)])
198 case Tree.NCons{Tree.Stmt{Tree.SCase{}, +kids, body}, rest}:
199 List.concat(&2, Pos, [mark(kids), mark(rest)])
200 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
201 List.concat(&2, Pos, [mark(kids), mark(body), mark(rest)])
202 case Tree.NCons{h, rest}:
203 mark(rest)
204 case other:
205 Nil{}
206
207# the name before a group is a narrow read, a callee, or an ordinary mention
208def here_call(+call: Bool, +tt: String, +name: String, +hot: Bool) -> Hit:
209 match call:
210 case True{}:
211 name_hit(String.eq(tt, name), False{})
212 case False{}:
213 name_hit(String.eq(tt, name), hot)
214
215# the name before a group is a narrow read, a callee, or an ordinary mention
216def here_hit(+brack: Bool, +call: Bool, +tt: String, +name: String, +hot: Bool) -> Hit:
217 match brack:
218 case True{}:
219 name_hit(String.eq(tt, name), True{})
220 case False{}:
221 here_call(call, tt, name, hot)
222
223# a bracket's inside is cold; a call's arguments start before the slot
224def inner_hot_call(+call: Bool, +hot: Bool) -> Bool:
225 match call:
226 case True{}:
227 False{}
228 case False{}:
229 hot
230
231# a bracket's inside is cold; a call's arguments start before the slot
232def inner_hot(+brack: Bool, +call: Bool, +hot: Bool) -> Bool:
233 match brack:
234 case True{}:
235 False{}
236 case False{}:
237 inner_hot_call(call, hot)
238
239# the argument index a slot reads
240def slot_at(ss: Slot) -> Nat:
241 Slot{on, at} = ss
242 at
243
244# the slot index of a call; a bracket has none
245def inner_left_call(+call: Bool, +tt: String) -> Nat:
246 match call:
247 case True{}:
248 slot_at(slot_of(tt))
249 case False{}:
250 0n
251
252# the slot index of a call; a bracket has none
253def inner_left(+brack: Bool, +call: Bool, +tt: String) -> Nat:
254 match brack:
255 case True{}:
256 0n
257 case False{}:
258 inner_left_call(call, tt)
259
260# whether an accessor has a collection argument
261def slot_on(ss: Slot) -> Bool:
262 Slot{on, at} = ss
263 on
264
265# only a call's arguments count commas as separators
266def inner_live_call(+call: Bool, +tt: String) -> Bool:
267 match call:
268 case True{}:
269 slot_on(slot_of(tt))
270 case False{}:
271 False{}
272
273# only a call's arguments count commas as separators
274def inner_live(+brack: Bool, +call: Bool, +tt: String) -> Bool:
275 match brack:
276 case True{}:
277 False{}
278 case False{}:
279 inner_live_call(call, tt)
280
281# does the chain bind?
282def has_eq(nn: Tree.Node) -> Bool:
283 match nn:
284 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TEq{}, t, l, c}}, rest}:
285 True{}
286 case Tree.NCons{h, rest}:
287 has_eq(rest)
288 case other:
289 False{}
290
291# a read counts only once the left of `=` is over
292def hit_keep(skip: Bool, hh: Hit) -> Hit:
293 match skip:
294 case True{}:
295 none_hit()
296 case False{}:
297 hh
298
299# inside a call, unless this chain is still the left of a let
300def pick_hot(+skip: Bool, +brack: Bool, +call: Bool, +hot: Bool) -> Bool:
301 match skip:
302 case True{}:
303 False{}
304 case False{}:
305 inner_hot(brack, call, hot)
306
307# inside a call, unless this chain is still the left of a let
308def pick_left(+skip: Bool, +brack: Bool, +call: Bool, +tt: String) -> Nat:
309 match skip:
310 case True{}:
311 0n
312 case False{}:
313 inner_left(brack, call, tt)
314
315# inside a call, unless this chain is still the left of a let
316def pick_live(+skip: Bool, +brack: Bool, +call: Bool, +tt: String) -> Bool:
317 match skip:
318 case True{}:
319 False{}
320 case False{}:
321 inner_live(brack, call, tt)
322
323# reads of the name. hot: this chain is a collection argument. live/left: commas
324# count toward that argument. skip: ignore names until `=` (the left of a let)
325def hits(nn: Tree.Node, +name: String, +hot: Bool, +left: Nat, +live: Bool, +skip: Bool) -> Hit:
326 match nn:
327 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TEq{}, t, l, c}}, rest}:
328 hits(rest, name, hot, left, live, False{})
329 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TComma{}, t, l, c}}, rest}:
330 hits(rest, name, step_hot(hot, left, live), step_left(left, live), live, skip)
331 case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
332 +brack = String.eq(o, "[")
333 +call = String.eq(o, "(")
334 +here = hit_keep(skip, here_hit(brack, call, t, name, hot))
335 +inn = hits(kids, name, pick_hot(skip, brack, call, hot), pick_left(skip, brack, call, t),
336 pick_live(skip, brack, call, t), skip)
337 +aft = hits(rest, name, hot, left, live, skip)
338 either(here, either(inn, aft))
339 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, rest}:
340 either(hit_keep(skip, name_hit(String.eq(t, name), hot)), hits(rest, name, hot, left, live, skip))
341 case Tree.NCons{Tree.Leaf{tok}, rest}:
342 hits(rest, name, hot, left, live, skip)
343 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
344 either(hits(kids, name, hot, 0n, False{}, skip), hits(rest, name, hot, left, live, skip))
345 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
346 +eq = has_eq(kids)
347 +inn = hits(kids, name, False{}, 0n, False{}, eq)
348 +bod = hits(body, name, False{}, 0n, False{}, False{})
349 +aft = hits(rest, name, hot, left, live, skip)
350 either(inn, either(bod, aft))
351 case Tree.NCons{h, rest}:
352 hits(rest, name, hot, left, live, skip)
353 case other:
354 none_hit()
355
356# a let of one name is narrow when that name is only read narrowly
357def narrow_let(hh: Hit) -> Bool:
358 Hit{wide, slot} = hh
359 Bool.and(slot, Bool.not(wide))
360
361# a slot or a field is narrow; a wide use is not; a let depends on the name's reads
362def narrow(hh: How, +body: Tree.Node) -> Bool:
363 match hh:
364 case HWide{}:
365 False{}
366 case HSlot{}:
367 True{}
368 case HField{}:
369 True{}
370 case HLet{+binder}:
371 narrow_let(hits(body, binder, False{}, 0n, False{}, False{}))
372
373# every name a node calls
374def called(nn: Tree.Node) -> List<&2, String>:
375 match nn:
376 case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
377 +more = List.concat(&2, String, [called(kids), called(rest)])
378 Bool.pick(List<&2, String>, String.eq(o, "("), t <> more, more)
379 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
380 List.concat(&2, String, [called(kids), called(rest)])
381 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
382 List.concat(&2, String, [called(kids), called(body), called(rest)])
383 case Tree.NCons{h, rest}:
384 called(rest)
385 case other:
386 Nil{}
387
388# does the list hold a name the set has?
389def any_of(cs: List<&2, String>, +set: List<&2, String>) -> Bool:
390 match cs:
391 case Nil{}:
392 False{}
393 case Con{c, t}:
394 Lazy.or_else(List.contains(~String, ~String.eq, set, c), _u => any_of(t, set))
395
396def loops.go(ds: List<&2, Calls.Def>, +acc: List<&2, String>) -> List<&2, String>:
397 match ds:
398 case Nil{}:
399 acc
400 case Con{Calls.Def{+name, sig, body}, rest}:
401 +cs = called(body)
402 +deep = Bool.or(List.contains(~String, ~String.eq, cs, name), any_of(cs, acc))
403 loops.go(rest, Bool.pick(List<&2, String>, deep, name <> acc, acc))
404
405# defs of this file that loop
406def loops(ds: List<&2, Calls.Def>) -> List<&2, String>:
407 loops.go(ds, [])
408
409# Base walks a rewalk cares about
410def heavy(+tt: String) -> Bool:
411 List.contains(~String, ~String.eq, [
412 "List.map", "List.filter", "List.sort", "List.foldl", "List.foldr", "List.reverse", "List.concat",
413 "Array.map", "Array.to_list",
414 "String.reverse", "String.to_upper", "String.to_lower", "String.to_list", "String.from_list"], tt)
415
416# a loop of this file, or a Base walk, and not this def itself
417def expensive(+tt: String, +self: String, +lp: List<&2, String>) -> Bool:
418 Bool.and(Bool.not(String.eq(tt, self)), Bool.or(heavy(tt), List.contains(~String, ~String.eq, lp, tt)))
419
420# the rest of the chain indexes the call
421def indexed(rest: Tree.Node) -> Bool:
422 match rest:
423 case Tree.NCons{Tree.Group{Lex.Tok{k, +o, l, c}, kids, close}, tail}:
424 String.eq(o, "[")
425 case other:
426 False{}
427
428# a slot when the call is indexed, otherwise wide, when the call is expensive
429def how_of(+nar: Bool) -> How:
430 match nar:
431 case True{}:
432 HSlot{}
433 case False{}:
434 HWide{}
435
436# a slot when the call is indexed, otherwise wide, when the call is expensive
437def site_how(+mine: Bool, +nar: Bool) -> Maybe<&2, How>:
438 match mine:
439 case False{}:
440 None{}
441 case True{}:
442 Some{how_of(nar)}
443
444# the call as a site, when it is one
445def site_list(mm: Maybe<&2, How>, +name: String, +args: String, +line: U32, +col: U32, +len: U32) -> List<&2, Site>:
446 match mm:
447 case None{}:
448 Nil{}
449 case Some{how}:
450 [Site{name, args, line, col, len, how}]
451
452# one site when this application is an expensive call
453def open_site(
454 +open: Bool,
455 +indexed: Bool,
456 +tt: String,
457 +args: String,
458 +line: U32,
459 +col: U32,
460 +len: U32,
461 +self: String,
462 +lp: List<&2, String>
463) -> List<&2, Site>:
464 match open:
465 case False{}:
466 Nil{}
467 case True{}:
468 site_list(site_how(expensive(tt, self, lp), indexed), tt, args, line, col, len)
469
470# every name under the node
471def names(nn: Tree.Node, +acc: List<&2, String>) -> List<&2, String>:
472 match nn:
473 case Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}:
474 t <> acc
475 case Tree.Group{open, kids, close}:
476 names(kids, acc)
477 case Tree.Stmt{kind, kids, body}:
478 names(body, names(kids, acc))
479 case Tree.NCons{h, rest}:
480 names(rest, names(h, acc))
481 case other:
482 acc
483
484# the names a let binds: every name left of its `=` or `<-`
485def lhs_names(nn: Tree.Node, +acc: List<&2, String>) -> List<&2, String>:
486 match nn:
487 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TEq{}, t, l, c}}, rest}:
488 acc
489 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TBind{}, t, l, c}}, rest}:
490 acc
491 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, rest}:
492 lhs_names(rest, t <> acc)
493 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
494 lhs_names(rest, names(kids, acc))
495 case Tree.NCons{h, rest}:
496 lhs_names(rest, acc)
497 case other:
498 acc
499
500# where a let's names take effect: its last token, after the right-hand side
501def last_pos(ts: List<&2, Lex.Tok>) -> Pos:
502 match ts:
503 case Nil{}:
504 Pos{0, 0}
505 case Con{Lex.Tok{k, t, +l, +c}, rest}:
506 Pos{l, c}
507
508# one binding per name, at the let's end
509def bind_all(ns: List<&2, String>, +line: U32, +col: U32, +acc: List<&2, Bind>) -> List<&2, Bind>:
510 match ns:
511 case Nil{}:
512 acc
513 case Con{+nm, rest}:
514 bind_all(rest, line, col, Bind{nm, line, col} <> acc)
515
516# one binding per name, at this position
517def bind_at(ns: List<&2, String>, pp: Pos, +acc: List<&2, Bind>) -> List<&2, Bind>:
518 Pos{line, col} = pp
519 bind_all(ns, line, col, acc)
520
521# the lets of one straight piece; a case arm is not part of it
522def binds(nn: Tree.Node, +acc: List<&2, Bind>) -> List<&2, Bind>:
523 match nn:
524 case Tree.NCons{Tree.Stmt{Tree.SCase{}, kids, body}, rest}:
525 binds(rest, acc)
526 case Tree.NCons{Tree.Stmt{Tree.SLet{}, +kids, +body}, rest}:
527 +here = bind_at(lhs_names(kids, []), last_pos(Tree.leaves.go(body, Tree.leaves.go(kids, []))), acc)
528 binds(rest, binds(body, here))
529 case Tree.NCons{Tree.Stmt{kind, kids, +body}, rest}:
530 binds(rest, binds(body, acc))
531 case Tree.NCons{h, rest}:
532 binds(rest, acc)
533 case other:
534 acc
535
536# (l1, c1) comes before (l2, c2)
537def before(+l1: U32, +c1: U32, +l2: U32, +c2: U32) -> Bool:
538 Bool.or(U32.is_lt(l1, l2), Bool.and(U32.is_eq(l1, l2), U32.is_lt(c1, c2)))
539
540# how many lets of this name take effect before this position
541def count(bs: List<&2, Bind>, +name: String, +line: U32, +col: U32) -> U32:
542 match bs:
543 case Nil{}:
544 0
545 case Con{Bind{+nm, +l, +c}, rest}:
546 +more = count(rest, name, line, col)
547 Bool.pick(U32, Bool.and(String.eq(nm, name), before(l, c, line, col)), (more + 1 : U32), more)
548
549# which binding of each name an argument reads: two calls with the same text
550# read the same values only when no let of those names sits between them
551def stamp(ns: List<&2, String>, +bs: List<&2, Bind>, +line: U32, +col: U32) -> String:
552 match ns:
553 case Nil{}:
554 ""
555 case Con{+nm, rest}:
556 "|" ++ nm ++ "=" ++ U32.show(count(bs, nm, line, col)) ++ stamp(rest, bs, line, col)
557
558# every expensive call, wide unless the call is indexed on the spot; its
559# arguments are their text and which let of each name they read
560def gather(nn: Tree.Node, +self: String, +lp: List<&2, String>, +bs: List<&2, Bind>) -> List<&2, Site>:
561 match nn:
562 case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, +l, +c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, +rest}}:
563 +args = Tree.show(kids) ++ stamp(names(kids, []), bs, l, c)
564 +here = open_site(String.eq(o, "("), indexed(rest), t, args, l, c,
565 U32.from_nat(String.length(t)), self, lp)
566 List.concat(&2, Site, [here, gather(kids, self, lp, bs), gather(rest, self, lp, bs)])
567 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
568 List.concat(&2, Site, [gather(kids, self, lp, bs), gather(rest, self, lp, bs)])
569 case Tree.NCons{Tree.Stmt{Tree.SCase{}, +kids, body}, rest}:
570 List.concat(&2, Site, [gather(kids, self, lp, bs), gather(rest, self, lp, bs)])
571 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
572 List.concat(&2, Site, [gather(kids, self, lp, bs), gather(body, self, lp, bs), gather(rest, self, lp, bs)])
573 case Tree.NCons{h, rest}:
574 gather(rest, self, lp, bs)
575 case other:
576 Nil{}
577
578# a second name on the left
579def add_seen(+seen: Bool, +extra: Bool, +field: Bool, +nm: String, +tt: String) -> Lhs:
580 match seen:
581 case False{}:
582 Lhs{True{}, extra, field, tt}
583 case True{}:
584 Lhs{True{}, True{}, field, nm}
585
586# names inside a field group, which makes the let a field read
587def fold_names(nn: Tree.Node, st: Lhs) -> Lhs:
588 match nn:
589 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, rest}:
590 Lhs{seen, extra, field, nm} = st
591 fold_names(rest, add_seen(seen, extra, True{}, nm, t))
592 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
593 fold_names(rest, fold_names(kids, st))
594 case Tree.NCons{h, rest}:
595 fold_names(rest, st)
596 case other:
597 st
598
599# the left of `=`
600def eat(nn: Tree.Node, st: Lhs) -> Lhs:
601 match nn:
602 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TEq{}, t, l, c}}, rest}:
603 st
604 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, rest}:
605 Lhs{seen, extra, field, nm} = st
606 eat(rest, add_seen(seen, extra, field, nm, t))
607 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
608 eat(rest, fold_names(kids, st))
609 case Tree.NCons{h, rest}:
610 eat(rest, st)
611 case other:
612 st
613
614# a field when the left has a group, otherwise a let of that name
615def note_how(+field: Bool, +nm: String) -> How:
616 match field:
617 case True{}:
618 HField{}
619 case False{}:
620 HLet{nm}
621
622# nothing, and the maybe is consumed
623def note_none(mm: Maybe<&2, Lex.Tok>) -> Maybe<&2, Note>:
624 match mm:
625 case other:
626 None{}
627
628# the note when the right-hand side is a call
629def note_some(+field: Bool, +nm: String, mm: Maybe<&2, Lex.Tok>) -> Maybe<&2, Note>:
630 match mm:
631 case Some{Lex.Tok{k, t, l, c}}:
632 Some{Note{l, c, note_how(field, nm)}}
633 case None{}:
634 None{}
635
636# the note for a call, when the left is exactly one name
637def note_rhs(+one: Bool, +field: Bool, +nm: String, mm: Maybe<&2, Lex.Tok>) -> Maybe<&2, Note>:
638 match one:
639 case False{}:
640 note_none(mm)
641 case True{}:
642 note_some(field, nm, mm)
643
644# the tokens after `=`
645def rhs_of(nn: Tree.Node) -> Tree.Node:
646 match nn:
647 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TEq{}, t, l, c}}, rest}:
648 rest
649 case Tree.NCons{h, rest}:
650 rhs_of(rest)
651 case other:
652 Tree.NNil{}
653
654# a let or a field binding of exactly one call
655def note_from(st: Lhs, +rhs: Tree.Node) -> Maybe<&2, Note>:
656 Lhs{seen, extra, field, +nm} = st
657 note_rhs(Bool.and(seen, Bool.not(extra)), field, nm, as_call(rhs))
658
659# a let or a field binding of exactly one call
660def note_st(+kids: Tree.Node) -> Maybe<&2, Note>:
661 note_from(eat(kids, Lhs{False{}, False{}, False{}, ""}), rhs_of(kids))
662
663# one site, retagged when it is the let's call
664def retag_one(+hit: Bool, how: How, +note: How, +name: String, +args: String, +line: U32, +col: U32, +len: U32) -> Site:
665 match hit:
666 case True{}:
667 Site{name, args, line, col, len, note}
668 case False{}:
669 Site{name, args, line, col, len, how}
670
671# a note's line
672def note_line(nn: Note) -> U32:
673 Note{line, col, how} = nn
674 line
675
676# a note's column
677def note_col(nn: Note) -> U32:
678 Note{line, col, how} = nn
679 col
680
681# a note's how
682def note_how_of(nn: Note) -> How:
683 Note{line, col, how} = nn
684 how
685
686# sites whose callee is this let's call take its how
687def retag(sites: List<&2, Site>, +note: Note) -> List<&2, Site>:
688 match sites:
689 case Nil{}:
690 Nil{}
691 case Con{Site{+name, +args, +line, +col, +len, how}, rest}:
692 +nl = note_line(note)
693 +nc = note_col(note)
694 +nh = note_how_of(note)
695 +hit = Bool.and(U32.is_eq(line, nl), U32.is_eq(col, nc))
696 retag_one(hit, how, nh, name, args, line, col, len) <> retag(rest, note)
697
698# retag when the statement is a let of a call
699def retag_may(mm: Maybe<&2, Note>, sites: List<&2, Site>) -> List<&2, Site>:
700 match mm:
701 case None{}:
702 sites
703 case Some{note}:
704 retag(sites, note)
705
706# lets of a call retag that call
707def apply(nn: Tree.Node, sites: List<&2, Site>) -> List<&2, Site>:
708 match nn:
709 case Tree.NCons{Tree.Stmt{Tree.SCase{}, kids, body}, rest}:
710 apply(rest, sites)
711 case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
712 apply(rest, apply(body, retag_may(note_st(kids), sites)))
713 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
714 apply(rest, apply(kids, sites))
715 case Tree.NCons{h, rest}:
716 apply(rest, sites)
717 case other:
718 sites
719
720# this position was marked a collection argument
721def pinned(+line: U32, +col: U32, ps: List<&2, Pos>) -> Bool:
722 match ps:
723 case Nil{}:
724 False{}
725 case Con{Pos{l, c}, rest}:
726 +here = Bool.and(U32.is_eq(l, line), U32.is_eq(c, col))
727 +more = pinned(line, col, rest)
728 Bool.or(here, more)
729
730# a wide call at a marked position becomes a slot; a let or a field stays
731def pin_wide(how: How, +name: String, +args: String, +line: U32, +col: U32, +len: U32) -> Site:
732 match how:
733 case HWide{}:
734 Site{name, args, line, col, len, HSlot{}}
735 case other:
736 Site{name, args, line, col, len, other}
737
738# a wide call at a marked position becomes a slot
739def pin_how(+hit: Bool, how: How, +name: String, +args: String, +line: U32, +col: U32, +len: U32) -> Site:
740 match hit:
741 case False{}:
742 Site{name, args, line, col, len, how}
743 case True{}:
744 pin_wide(how, name, args, line, col, len)
745
746# one site
747def pin_one(ps: List<&2, Pos>, ss: Site) -> Site:
748 Site{+name, +args, +line, +col, +len, how} = ss
749 pin_how(pinned(line, col, ps), how, name, args, line, col, len)
750
751# marked collection arguments become slots
752def pin(+ps: List<&2, Pos>, sites: List<&2, Site>) -> List<&2, Site>:
753 match sites:
754 case Nil{}:
755 Nil{}
756 case Con{s, rest}:
757 pin_one(ps, s) <> pin(ps, rest)
758
759# a site, with whether its result is read for a single value over the piece
760type Rated is Data:
761 Rated{site: Site, nar: Bool}
762
763# one site, rated: the piece is walked for it here, once
764def rate_one(ss: Site, +body: Tree.Node) -> Rated:
765 Site{+name, +args, +line, +col, +len, +how} = ss
766 Rated{Site{name, args, line, col, len, how}, narrow(how, body)}
767
768# each site, rated once, so the reports below compare ratings and never walk
769# the piece again: a walk per site, not one per pair of sites
770def rate(sites: List<&2, Site>, +body: Tree.Node) -> List<&2, Rated>:
771 match sites:
772 case Nil{}:
773 Nil{}
774 case Con{s, rest}:
775 rate_one(s, body) <> rate(rest, body)
776
777# another site has the same callee and the same arguments, and keeps the whole result
778def other_go(rs: List<&2, Rated>, +name: String, +args: String, +line: U32, +col: U32) -> Bool:
779 match rs:
780 case Nil{}:
781 False{}
782 case Con{Rated{Site{+nm, +as, +l, +c, len, how}, +nar}, rest}:
783 +same = Bool.and(String.eq(nm, name), String.eq(as, args))
784 +diff = Bool.not(Bool.and(U32.is_eq(l, line), U32.is_eq(c, col)))
785 +here = Bool.and(Bool.and(same, diff), Bool.not(nar))
786 +more = other_go(rest, name, args, line, col)
787 Bool.or(here, more)
788
789# a narrow site of this call already reported
790def earlier(seen: List<&2, Rated>, +name: String, +args: String, +line: U32, +col: U32) -> Bool:
791 match seen:
792 case Nil{}:
793 False{}
794 case Con{Rated{Site{+nm, +as, +l, +c, len, how}, +nar}, rest}:
795 +same = Bool.and(String.eq(nm, name), String.eq(as, args))
796 +diff = Bool.not(Bool.and(U32.is_eq(l, line), U32.is_eq(c, col)))
797 +here = Bool.and(Bool.and(same, diff), nar)
798 +more = earlier(rest, name, args, line, col)
799 Bool.or(here, more)
800
801# the finding when no earlier narrow site took this call
802def report_fresh(
803 +prior: Bool,
804 +name: String,
805 +line: U32,
806 +col: U32,
807 +len: U32,
808 +path: String
809) -> List<&2, F.Finding>:
810 match prior:
811 case False{}:
812 [F.Finding{path, line, col, len, "rewalk",
813 name ++ " is called twice on the same argument, and one result is used only for a single value; take that value from the other result."}]
814 case True{}:
815 Nil{}
816
817# the finding, when this is the first narrow site of a duplicated call
818def report_nar(
819 +nar: Bool,
820 +prior: Bool,
821 +name: String,
822 +line: U32,
823 +col: U32,
824 +len: U32,
825 +path: String
826) -> List<&2, F.Finding>:
827 match nar:
828 case False{}:
829 Nil{}
830 case True{}:
831 report_fresh(prior, name, line, col, len, path)
832
833# the finding when a duplicate exists and this site is the first narrow one
834def report_dup(
835 +dup: Bool,
836 +nar: Bool,
837 +name: String,
838 +args: String,
839 +line: U32,
840 +col: U32,
841 +len: U32,
842 +path: String,
843 +seen: List<&2, Rated>
844) -> List<&2, F.Finding>:
845 match dup:
846 case False{}:
847 Nil{}
848 case True{}:
849 report_nar(nar, earlier(seen, name, args, line, col), name, line, col, len, path)
850
851# one site
852def report_one(rr: Rated, +all: List<&2, Rated>, +path: String, +seen: List<&2, Rated>) -> List<&2, F.Finding>:
853 Rated{ss, +nar} = rr
854 Site{+name, +args, +line, +col, +len, how} = ss
855 report_dup(other_go(all, name, args, line, col), nar, name, args, line, col, len, path, seen)
856
857# the first narrow site of each duplicated call
858def report(rs: List<&2, Rated>, +all: List<&2, Rated>, +path: String, +seen: List<&2, Rated>) -> List<&2, F.Finding>:
859 match rs:
860 case Nil{}:
861 Nil{}
862 case Con{+r, rest}:
863 +mine = report_one(r, all, path, seen)
864 List.append(&2, F.Finding, mine, report(rest, all, path, r <> seen))
865
866# findings in one straight region; a case arm is not part of it
867def local(+nn: Tree.Node, +self: String, +lp: List<&2, String>, +path: String) -> List<&2, F.Finding>:
868 +rs = rate(pin(mark(nn), apply(nn, gather(nn, self, lp, binds(nn, [])))), nn)
869 report(rs, rs, path, [])
870
871# each case arm on its own, so two arms are not one path
872def visit(nn: Tree.Node, +self: String, +lp: List<&2, String>, +path: String) -> List<&2, F.Finding>:
873 match nn:
874 case Tree.NCons{Tree.Stmt{Tree.SCase{}, kids, +body}, rest}:
875 +here = local(body, self, lp, path)
876 List.concat(&2, F.Finding, [here, visit(body, self, lp, path), visit(rest, self, lp, path)])
877 case Tree.NCons{Tree.Stmt{kind, +kids, +body}, rest}:
878 List.concat(&2, F.Finding, [visit(kids, self, lp, path), visit(body, self, lp, path),
879 visit(rest, self, lp, path)])
880 case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
881 List.concat(&2, F.Finding, [visit(kids, self, lp, path), visit(rest, self, lp, path)])
882 case Tree.NCons{h, rest}:
883 visit(rest, self, lp, path)
884 case other:
885 Nil{}
886
887def check.go(
888 ds: List<&2, Calls.Def>,
889 +lp: List<&2, String>,
890 +path: String,
891 acc: List<&2, List<&2, F.Finding>>
892) -> List<&2, F.Finding>:
893 match ds:
894 case Nil{}:
895 List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc))
896 case Con{Calls.Def{+name, +sig, +body}, rest}:
897 check.go(rest, lp, path,
898 Lazy.stop(List<&2, F.Finding>, Calls.exempt(path, sig), [],
899 _u => List.concat(&2, F.Finding, [local(body, name, lp, path), visit(body, name, lp, path)])) <> acc)
900
901# the rule
902def check(ss: Src.Src) -> List<&2, F.Finding>:
903 Src.Src{path, text, toks, tree, bound, items} = ss
904 +ds = Calls.defs(tree)
905 check.go(ds, loops(ds), path, [])