src/rules/suspicious/hoist.bend

bolt@1.22.0.0 · 653 lines · 0 definitions

1# rule hoist: a fixed table built inside a def that calls itself, from inputs
2# that do not change, and then indexed. The build runs again on every step.
3# Build it once, outside the recursion. A build is a list or array literal,
4# `List.replicate` / `List.range` / `List.map` / `Array.new` / `Array.map`, or
5# a call to a def of this file whose body is itself one fixed table of more
6# than eight cells; a def whose body cannot be seen (another module's) is not
7# a build. A table of eight cells or fewer is left alone, and so is a build
8# whose arguments depend on the step. A name that is not a carried parameter
9# (a pattern binder, a value computed on the step) keeps the build; a case arm
10# that does not recurse is cold; laws and proofs do not run (a def with no
11# type at all fills a law: it is a proof). `List.get` itself stays with
12# `index`.
13import Base
14import bolt@1.22.0.0/src/src.bend as Src
15import bolt@1.22.0.0/src/finding.bend as F
16import bolt@1.22.0.0/src/syntax/lex.bend as Lex
17import bolt@1.22.0.0/src/syntax/tree.bend as Tree
18import bolt@1.22.0.0/src/rules/calls.bend as Calls
19import bolt@1.22.0.0/src/lazy/lazy.bend as Lazy
20import bolt@1.22.0.0/src/rules/suspicious/table.bend as Table
21
22# where an accessor reads its collection, when it is one
23type Slot is Data:
24  Slot{on: Bool, at: Nat}
25
26# a `name = expr` whose left side is one plain name
27type Got is Data:
28  Got{nm: String, expr: Tree.Node}
29
30# which argument a get or set indexes
31def slot_pick(+list: Bool, +str: Bool, +arr: Bool) -> Slot:
32  match list str arr:
33    case True{} b c:
34      Slot{True{}, 2n}
35    case False{} True{} c:
36      Slot{True{}, 0n}
37    case False{} False{} True{}:
38      Slot{True{}, 1n}
39    case False{} False{} False{}:
40      Slot{False{}, 0n}
41
42# List.get / List.set read argument 2, String.get argument 0, Array.get / Array.set argument 1
43def slot_of(+tt: String) -> Slot:
44  slot_pick(
45    Bool.or(String.eq(tt, "List.get"), String.eq(tt, "List.set")),
46    String.eq(tt, "String.get"),
47    Bool.or(String.eq(tt, "Array.get"), String.eq(tt, "Array.set")))
48
49# the self-calls' argument lists
50def pushes(nn: Tree.Node, +self: String) -> List<&2, List<&2, Tree.Node>>:
51  match nn:
52    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
53      +me = Bool.and(String.eq(o, "("), String.eq(t, self))
54      +more = List.concat(&2, List<&2, Tree.Node>, [pushes(kids, self), pushes(rest, self)])
55      Bool.pick(List<&2, List<&2, Tree.Node>>, me, Calls.args(kids) <> more, more)
56    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
57      List.concat(&2, List<&2, Tree.Node>, [pushes(kids, self), pushes(rest, self)])
58    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
59      List.concat(&2, List<&2, Tree.Node>, [pushes(kids, self), pushes(body, self), pushes(rest, self)])
60    case Tree.NCons{h, rest}:
61      pushes(rest, self)
62    case other:
63      Nil{}
64
65# is the chain exactly this name?
66def lone_eq(nn: Tree.Node, +name: String) -> Bool:
67  match nn:
68    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, Tree.NNil{}}:
69      String.eq(t, name)
70    case other:
71      False{}
72
73# every self-call passes this same name at this argument
74def all_same(cs: List<&2, List<&2, Tree.Node>>, +ii: Nat, +name: String) -> Bool:
75  match cs:
76    case Nil{}:
77      True{}
78    case Con{as, rest}:
79      +here = lone_eq(Calls.arg(as, ii), name)
80      +more = all_same(rest, ii, name)
81      Bool.and(here, more)
82
83# parameters every self-call passes unchanged, when there is a self-call
84def carried.go(
85  ps: List<&2, Tree.Node>,
86  +cs: List<&2, List<&2, Tree.Node>>,
87  +ii: Nat,
88  +acc: List<&2, String>
89) -> List<&2, String>:
90  match ps:
91    case Nil{}:
92      List.reverse(&2, String, acc)
93    case Con{p, rest}:
94      +nm = Calls.param_name(p)
95      +keep = Bool.and(Bool.not(String.is_empty(nm)), all_same(cs, ii, nm))
96      carried.go(rest, cs, (ii + 1n : Nat), Bool.pick(List<&2, String>, keep, nm <> acc, acc))
97
98# parameters passed through unchanged; empty when nothing recurses
99def carried_of(cs: List<&2, List<&2, Tree.Node>>, +sig: Tree.Node) -> List<&2, String>:
100  match cs:
101    case Nil{}:
102      Nil{}
103    case Con{h, t}:
104      carried.go(Calls.params(sig), h <> t, 0n, [])
105
106# parameters passed through unchanged; empty when nothing recurses
107def carried(sig: Tree.Node, body: Tree.Node, +self: String) -> List<&2, String>:
108  carried_of(pushes(body, self), sig)
109
110# a plain name is closed only when it is a carried parameter; a type or a dotted name is
111def name_ok(kk: Lex.TokKind, +tt: String, +carried: List<&2, String>) -> Bool:
112  match kk:
113    case Lex.TName{}:
114      List.contains(~String, ~String.eq, carried, tt)
115    case other:
116      True{}
117
118# a token after an operator: a template argument is not a value, anything else is
119def leaf_ok(+skip: Bool, tok: Lex.Tok, +carried: List<&2, String>) -> Bool:
120  match skip:
121    case True{}:
122      True{}
123    case False{}:
124      Lex.Tok{+k, +t, l, c} = tok
125      name_ok(k, t, carried)
126
127# an application is closed in its arguments; a bracketed name is a value
128def value_ok(+app: Bool, kk: Lex.TokKind, +tt: String, +carried: List<&2, String>) -> Bool:
129  match app:
130    case True{}:
131      True{}
132    case False{}:
133      name_ok(kk, tt, carried)
134
135# every value name under the node is a carried parameter (callees and templates are not values)
136def closed(nn: Tree.Node, +carried: List<&2, String>) -> Bool:
137  match nn:
138    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TOp{}, +op, l, c}}, Tree.NCons{Tree.Leaf{+tok}, tail}}:
139      +ok = leaf_ok(String.eq(op, "~"), tok, carried)
140      +aft = closed(tail, carried)
141      Bool.and(ok, aft)
142    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TOp{}, op, l, c}}, rest}:
143      closed(rest, carried)
144    case Tree.NCons{Tree.Leaf{Lex.Tok{+k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
145      +ok = value_ok(String.eq(o, "("), k, t, carried)
146      +inn = closed(kids, carried)
147      +aft = closed(rest, carried)
148      Bool.and(ok, Bool.and(inn, aft))
149    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, rest}:
150      +ok = List.contains(~String, ~String.eq, carried, t)
151      +aft = closed(rest, carried)
152      Bool.and(ok, aft)
153    case Tree.NCons{Tree.Leaf{tok}, rest}:
154      closed(rest, carried)
155    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
156      +inn = closed(kids, carried)
157      +aft = closed(rest, carried)
158      Bool.and(inn, aft)
159    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
160      +inn = closed(kids, carried)
161      +bod = closed(body, carried)
162      +aft = closed(rest, carried)
163      Bool.and(inn, Bool.and(bod, aft))
164    case Tree.NCons{h, rest}:
165      closed(rest, carried)
166    case Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}:
167      List.contains(~String, ~String.eq, carried, t)
168    case other:
169      True{}
170
171# a top-level token of this text, not one nested in a group
172def top_mark(nn: Tree.Node, +want: String) -> Bool:
173  match nn:
174    case Tree.NCons{Tree.Group{open, kids, close}, rest}:
175      top_mark(rest, want)
176    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, rest}:
177      +more = top_mark(rest, want)
178      Bool.or(String.eq(t, want), more)
179    case Tree.NCons{h, rest}:
180      top_mark(rest, want)
181    case other:
182      False{}
183
184# `[v : T*n]` or `[v : T^d]`: an array, not a list
185def is_array(+kids: Tree.Node) -> Bool:
186  +col = top_mark(kids, ":")
187  +star = top_mark(kids, "*")
188  +hat = top_mark(kids, "^")
189  Bool.and(col, Bool.or(star, hat))
190
191# how many top-level copies of this token
192def top_count(nn: Tree.Node, +want: String, +got: Nat) -> Nat:
193  match nn:
194    case Tree.NCons{Tree.Group{open, kids, close}, rest}:
195      top_count(rest, want, got)
196    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, rest}:
197      +next = Bool.pick(Nat, String.eq(t, want), (got + 1n : Nat), got)
198      top_count(rest, want, next)
199    case Tree.NCons{h, rest}:
200      top_count(rest, want, got)
201    case other:
202      got
203
204# a number literal of eight or fewer
205def small_num(+tt: String) -> Bool:
206  List.contains(~String, ~String.eq,
207    ["0", "1", "2", "3", "4", "5", "6", "7", "8", "0n", "1n", "2n", "3n", "4n", "5n", "6n", "7n", "8n"], tt)
208
209# the size after `*` is a literal of eight or fewer, or the depth after `^` (2^d slots) three or fewer
210def small_size(nn: Tree.Node) -> Bool:
211  match nn:
212    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TOp{}, +op, l, c}},
213        Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TNum{}, +t, l2, c2}}, rest}}:
214      +here = Bool.or(Bool.and(String.eq(op, "*"), small_num(t)),
215        Bool.and(String.eq(op, "^"), List.contains(~String, ~String.eq, ["0", "1", "2", "3", "0n", "1n", "2n", "3n"], t)))
216      +more = small_size(rest)
217      Bool.or(here, more)
218    case Tree.NCons{Tree.Group{open, kids, close}, rest}:
219      small_size(rest)
220    case Tree.NCons{h, rest}:
221      small_size(rest)
222    case other:
223      False{}
224
225# more than eight cells: nine or more list elements, or an array whose size is not that small
226def wide_shape(+arr: Bool, +kids: Tree.Node) -> Bool:
227  match arr:
228    case True{}:
229      Bool.not(small_size(kids))
230    case False{}:
231      Nat.is_gt(top_count(kids, ",", 0n), 7n)
232
233# constructors that build a fixed table rather than walk one
234def allowed(+tt: String) -> Bool:
235  List.contains(~String, ~String.eq,
236    ["List.replicate", "List.range", "List.map", "Array.new", "Array.map"], tt)
237
238# a Base constructor on the allow list, or a def of this file whose body is
239# itself a wide table (`wides`); a def whose body cannot be seen is not one
240def builder(kk: Lex.TokKind, +tt: String, +self: String, +wides: List<&2, String>) -> Bool:
241  match kk:
242    case Lex.TName{}:
243      Bool.and(Bool.not(String.eq(tt, self)), List.contains(~String, ~String.eq, wides, tt))
244    case Lex.TDotted{}:
245      Bool.or(allowed(tt), List.contains(~String, ~String.eq, wides, tt))
246    case other:
247      False{}
248
249# a bracket group that is a closed list or array of more than eight cells
250def group_yes(+arr: Bool, +kids: Tree.Node, +carried: List<&2, String>) -> Bool:
251  +shape = Bool.or(top_mark(kids, ","), arr)
252  +ok = closed(kids, carried)
253  +wide = wide_shape(arr, kids)
254  Bool.and(shape, Bool.and(ok, wide))
255
256# a bracket group that is a closed list or array
257def table_group(+brack: Bool, +kids: Tree.Node, +carried: List<&2, String>) -> Bool:
258  match brack:
259    case False{}:
260      False{}
261    case True{}:
262      group_yes(is_array(kids), kids, carried)
263
264# the chain is exactly one number; its text, or empty
265def num_text(nn: Tree.Node) -> String:
266  match nn:
267    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TNum{}, +t, l, c}}, Tree.NNil{}}:
268      t
269    case other:
270      ""
271
272# a count is wide when it is not a literal of eight or fewer (a name may be large)
273def count_wide(aa: Tree.Node) -> Bool:
274  Bool.not(small_num(num_text(aa)))
275
276# the last argument
277def last_node(as: List<&2, Tree.Node>) -> Tree.Node:
278  match as:
279    case Nil{}:
280      Tree.NNil{}
281    case Con{h, Nil{}}:
282      h
283    case Con{h, rest}:
284      last_node(rest)
285
286# `List.replicate` / `Array.new` take argument 2; anything else is wide unless it is `List.range`
287def call_arm(rep: Bool, +as: List<&2, Tree.Node>, +end: Tree.Node, +range: Bool) -> Bool:
288  match rep:
289    case True{}:
290      count_wide(Calls.arg(as, 2n))
291    case False{}:
292      Bool.pick(Bool, range, count_wide(end), True{})
293
294# `List.replicate` / `Array.new` / `List.range` are wide unless their count is a small literal
295def call_wide(+tt: String, kids: Tree.Node) -> Bool:
296  +as = Calls.args(kids)
297  call_arm(Bool.or(String.eq(tt, "List.replicate"), String.eq(tt, "Array.new")), as, last_node(as), String.eq(tt, "List.range"))
298
299# a call that builds a table from closed arguments, and is not a small literal count
300def table_built(+ok: Bool, +wide: Bool, kids: Tree.Node, +carried: List<&2, String>) -> Bool:
301  match ok wide:
302    case True{} True{}:
303      closed(kids, carried)
304    case True{} False{}:
305      False{}
306    case False{} b:
307      False{}
308
309# a call that builds a table from closed arguments
310def table_call(
311  +app: Bool,
312  kk: Lex.TokKind,
313  +tt: String,
314  +kids: Tree.Node,
315  +self: String,
316  +wides: List<&2, String>,
317  +carried: List<&2, String>
318) -> Bool:
319  match app:
320    case False{}:
321      False{}
322    case True{}:
323      table_built(builder(kk, tt, self, wides), call_wide(tt, kids), kids, carried)
324
325# a fixed table of more than eight cells, whatever its cells read
326def wide_of(nn: Tree.Node) -> Bool:
327  match nn:
328    case Tree.NCons{Tree.Group{Lex.Tok{k, +o, l, c}, +kids, close}, Tree.NNil{}}:
329      wide_shape(is_array(kids), kids)
330    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}},
331        Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, Tree.NNil{}}}:
332      call_wide(t, kids)
333    case other:
334      False{}
335
336# a def whose body is one wide fixed table (`Table.built`, and more than eight cells)
337def wide_one(body: Tree.Node, +name: String, +acc: List<&2, String>) -> List<&2, String>:
338  match body:
339    case Tree.NCons{Tree.Stmt{kind, +kids, inn}, Tree.NNil{}}:
340      Bool.pick(List<&2, String>, Bool.and(Table.built(kids), wide_of(kids)), name <> acc, acc)
341    case other:
342      acc
343
344# defs of this file whose body is one wide fixed table
345def wide_defs(ds: List<&2, Calls.Def>, acc: List<&2, String>) -> List<&2, String>:
346  match ds:
347    case Nil{}:
348      acc
349    case Con{Calls.Def{+name, sig, +body}, rest}:
350      wide_defs(rest, wide_one(body, name, acc))
351
352# a list, an array, or a builder call, closed over carried parameters
353def is_table(nn: Tree.Node, +self: String, +wides: List<&2, String>, +carried: List<&2, String>) -> Bool:
354  match nn:
355    case Tree.NCons{Tree.Group{Lex.Tok{k, +o, l, c}, +kids, close}, Tree.NNil{}}:
356      table_group(String.eq(o, "["), kids, carried)
357    case Tree.NCons{Tree.Leaf{Lex.Tok{+k, +t, l, c}},
358        Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, Tree.NNil{}}}:
359      table_call(String.eq(o, "("), k, t, kids, self, wides, carried)
360    case other:
361      False{}
362
363# drop the right-hand side
364def skip_expr(nn: Tree.Node) -> Maybe<&2, Got>:
365  match nn:
366    case other:
367      None{}
368
369# the chain after `=`, when the left side is one plain name and nothing else
370def got_bad(+bad: Bool, +nm: String, expr: Tree.Node) -> Maybe<&2, Got>:
371  match bad:
372    case True{}:
373      skip_expr(expr)
374    case False{}:
375      Some{Got{nm, expr}}
376
377# the chain after `=`, when exactly one plain name stands before it
378def got_at(+seen: Bool, +bad: Bool, +nm: String, expr: Tree.Node) -> Maybe<&2, Got>:
379  match seen:
380    case False{}:
381      skip_expr(expr)
382    case True{}:
383      got_bad(bad, nm, expr)
384
385# a second name, a constructor or a group on the left is not a plain binder
386def eat(nn: Tree.Node, +seen: Bool, +bad: Bool, +nm: String) -> Maybe<&2, Got>:
387  match nn:
388    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TEq{}, t, l, c}}, rest}:
389      got_at(seen, bad, nm, rest)
390    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, rest}:
391      eat(rest, True{}, Bool.or(bad, seen), t)
392    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TUpper{}, t, l, c}}, rest}:
393      eat(rest, seen, True{}, nm)
394    case Tree.NCons{Tree.Group{open, kids, close}, rest}:
395      eat(rest, seen, True{}, nm)
396    case Tree.NCons{h, rest}:
397      eat(rest, seen, bad, nm)
398    case other:
399      None{}
400
401# a finding on the table expression
402def cite(+nn: Tree.Node, +path: String) -> List<&2, F.Finding>:
403  [F.Finding{path, Tree.line(nn), Tree.col(nn), U32.from_nat(String.length(Tree.text(nn))), "hoist",
404    "This table is rebuilt on every step from inputs that do not change; build it once, outside the recursion."}]
405
406# a name, and nothing after it
407def wrapped_inside(nn: Tree.Node, +binder: String) -> Bool:
408  match nn:
409    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, Tree.NNil{}}:
410      String.eq(t, binder)
411    case other:
412      False{}
413
414# inside one pair of parentheses
415def wrapped_open(+paren: Bool, kids: Tree.Node, +binder: String) -> Bool:
416  match paren:
417    case True{}:
418      wrapped_inside(kids, binder)
419    case False{}:
420      False{}
421
422# the chain is exactly this name, or that name in parentheses
423def wrapped(nn: Tree.Node, +binder: String) -> Bool:
424  match nn:
425    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TName{}, +t, l, c}}, Tree.NNil{}}:
426      String.eq(t, binder)
427    case Tree.NCons{Tree.Group{Lex.Tok{k, +o, l, c}, +kids, close}, Tree.NNil{}}:
428      wrapped_open(String.eq(o, "("), kids, binder)
429    case other:
430      False{}
431
432# the collection argument is this name
433def uses_arg(ss: Slot, +as: List<&2, Tree.Node>, +binder: String) -> Bool:
434  Slot{on, at} = ss
435  match on:
436    case False{}:
437      False{}
438    case True{}:
439      wrapped(Calls.arg(as, at), binder)
440
441# `name[` is an index; a call is an index when its collection argument is the name
442def uses_here_call(+call: Bool, +tt: String, kids: Tree.Node, +binder: String) -> Bool:
443  match call:
444    case False{}:
445      False{}
446    case True{}:
447      uses_arg(slot_of(tt), Calls.args(kids), binder)
448
449# `name[` is an index; a call is an index when its collection argument is the name
450def uses_here(+brack: Bool, +call: Bool, +tt: String, kids: Tree.Node, +binder: String) -> Bool:
451  match brack:
452    case True{}:
453      String.eq(tt, binder)
454    case False{}:
455      uses_here_call(call, tt, kids, binder)
456
457# the name is indexed later: `name[`, or the collection of a get or set
458def uses_name(nn: Tree.Node, +binder: String) -> Bool:
459  match nn:
460    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
461      +here = uses_here(String.eq(o, "["), String.eq(o, "("), t, kids, binder)
462      +inn = uses_name(kids, binder)
463      +aft = uses_name(rest, binder)
464      Bool.or(here, Bool.or(inn, aft))
465    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
466      +inn = uses_name(kids, binder)
467      +aft = uses_name(rest, binder)
468      Bool.or(inn, aft)
469    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
470      +inn = uses_name(kids, binder)
471      +bod = uses_name(body, binder)
472      +aft = uses_name(rest, binder)
473      Bool.or(inn, Bool.or(bod, aft))
474    case Tree.NCons{h, rest}:
475      uses_name(rest, binder)
476    case other:
477      False{}
478
479# indexed in the let's body or in a following statement
480def uses_or(body: Tree.Node, rest: Tree.Node, +binder: String) -> Bool:
481  +inn = uses_name(body, binder)
482  +aft = uses_name(rest, binder)
483  Bool.or(inn, aft)
484
485# a finding when the table is also indexed
486def let_used(+used: Bool, +expr: Tree.Node, +path: String) -> List<&2, F.Finding>:
487  match used:
488    case True{}:
489      cite(expr, path)
490    case False{}:
491      Nil{}
492
493# a finding when the right-hand side is a closed table that is indexed
494def let_use(+tab: Bool, +used: Bool, +expr: Tree.Node, +path: String) -> List<&2, F.Finding>:
495  match tab:
496    case False{}:
497      Nil{}
498    case True{}:
499      let_used(used, expr, path)
500
501# a plain let of a table
502def let_of(
503  mm: Maybe<&2, Got>,
504  +body: Tree.Node,
505  +rest: Tree.Node,
506  +self: String,
507  +wides: List<&2, String>,
508  +carried: List<&2, String>,
509  +path: String
510) -> List<&2, F.Finding>:
511  match mm:
512    case None{}:
513      Nil{}
514    case Some{Got{+nm, +expr}}:
515      let_use(is_table(expr, self, wides, carried), uses_or(body, rest, nm), expr, path)
516
517# a hot plain let of a table that is indexed afterwards
518def let_hit(
519  +hot: Bool,
520  kids: Tree.Node,
521  +body: Tree.Node,
522  +rest: Tree.Node,
523  +self: String,
524  +wides: List<&2, String>,
525  +carried: List<&2, String>,
526  +path: String
527) -> List<&2, F.Finding>:
528  match hot:
529    case False{}:
530      Nil{}
531    case True{}:
532      let_of(eat(kids, False{}, False{}, ""), body, rest, self, wides, carried, path)
533
534# a finding when this argument is itself a closed table
535def one_yes(+yes: Bool, +aa: Tree.Node, +path: String) -> List<&2, F.Finding>:
536  match yes:
537    case True{}:
538      cite(aa, path)
539    case False{}:
540      Nil{}
541
542# the collection argument, when this call indexes one
543def arg_table(
544  ss: Slot,
545  +as: List<&2, Tree.Node>,
546  +self: String,
547  +wides: List<&2, String>,
548  +carried: List<&2, String>,
549  +path: String
550) -> List<&2, F.Finding>:
551  Slot{on, at} = ss
552  match on:
553    case False{}:
554      Nil{}
555    case True{}:
556      +aa = Calls.arg(as, at)
557      one_yes(is_table(aa, self, wides, carried), aa, path)
558
559# an inline table passed straight to a get or set, when the group is a call
560def call_open(
561  +open: Bool,
562  +tt: String,
563  kids: Tree.Node,
564  +self: String,
565  +wides: List<&2, String>,
566  +carried: List<&2, String>,
567  +path: String
568) -> List<&2, F.Finding>:
569  match open:
570    case False{}:
571      Nil{}
572    case True{}:
573      arg_table(slot_of(tt), Calls.args(kids), self, wides, carried, path)
574
575# an inline table passed straight to a get or set
576def call_table(
577  +hot: Bool,
578  +open: Bool,
579  +tt: String,
580  kids: Tree.Node,
581  +self: String,
582  +wides: List<&2, String>,
583  +carried: List<&2, String>,
584  +path: String
585) -> List<&2, F.Finding>:
586  match hot:
587    case False{}:
588      Nil{}
589    case True{}:
590      call_open(open, tt, kids, self, wides, carried, path)
591
592# a `=>` cools the rest of its chain: a lambda's body runs when it is called, not per step
593def warm(+hot: Bool, kk: Lex.TokKind) -> Bool:
594  match kk:
595    case Lex.TLam{}:
596      False{}
597    case other:
598      hot
599
600# tables built in a hot region
601def walk(
602  nn: Tree.Node,
603  +self: String,
604  +wides: List<&2, String>,
605  +carried: List<&2, String>,
606  +path: String,
607  +hot: Bool
608) -> List<&2, F.Finding>:
609  match nn:
610    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:
611      +heat = warm(hot, k)
612      +own = call_table(heat, String.eq(o, "("), t, kids, self, wides, carried, path)
613      +inn = walk(kids, self, wides, carried, path, heat)
614      +aft = walk(rest, self, wides, carried, path, heat)
615      List.concat(&2, F.Finding, [own, inn, aft])
616    case Tree.NCons{Tree.Leaf{Lex.Tok{k, t, l, c}}, rest}:
617      walk(rest, self, wides, carried, path, warm(hot, k))
618    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
619      List.concat(&2, F.Finding,
620        [walk(kids, self, wides, carried, path, hot), walk(rest, self, wides, carried, path, hot)])
621    case Tree.NCons{Tree.Stmt{Tree.SCase{}, kids, +body}, rest}:
622      +arm = Bool.and(hot, Calls.calls(body, self))
623      List.concat(&2, F.Finding, [walk(kids, self, wides, carried, path, False{}),
624        walk(body, self, wides, carried, path, arm), walk(rest, self, wides, carried, path, hot)])
625    case Tree.NCons{Tree.Stmt{kind, +kids, +body}, +rest}:
626      +own = let_hit(hot, kids, body, rest, self, wides, carried, path)
627      List.concat(&2, F.Finding, [own, walk(kids, self, wides, carried, path, hot),
628        walk(body, self, wides, carried, path, hot), walk(rest, self, wides, carried, path, hot)])
629    case Tree.NCons{h, rest}:
630      walk(rest, self, wides, carried, path, hot)
631    case other:
632      Nil{}
633
634def check.go(
635  ds: List<&2, Calls.Def>,
636  +wide: List<&2, String>,
637  +path: String,
638  acc: List<&2, List<&2, F.Finding>>
639) -> List<&2, F.Finding>:
640  match ds:
641    case Nil{}:
642      List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc))
643    case Con{Calls.Def{+name, +sig, +body}, rest}:
644      check.go(rest, wide, path,
645        Lazy.stop(List<&2, F.Finding>,
646          Bool.not(Bool.and(Calls.calls(body, name), Bool.not(Calls.exempt(path, sig)))), [],
647          _u => walk(body, name, wide, carried(sig, body, name), path, True{})) <> acc)
648
649# the rule
650def check(ss: Src.Src) -> List<&2, F.Finding>:
651  Src.Src{path, text, toks, tree, bound, items} = ss
652  +ds = Calls.defs(tree)
653  check.go(ds, wide_defs(ds, []), path, [])