performance/eager_branch.bend
bend-lint-rules@0.2.0.0 · 307 lines · 21 definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import Base
17import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
18import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
19import bend-lint-api@0.2.0.0/shared/names.bend as Names
20import bend-lint-api@0.2.0.0/shared/sets.bend as Sets
21import bend-lint-rules@0.2.0.0/performance/deep_call_in_fork.bend as Deep
22
23
24
25
26
27type Eager is Data:
28 Eager{kind: String, value: Maybe<&2, Lint.Node>}
29
30
31
32type Owner is Data:
33 Owner{costly: Set(), target: String}
34
35
36
37
38
39def eager(kids: List<&2, Lint.Node>, +at: U32) -> String:
40 match kids:
41 case Con{Lint.Node{Lint.Reference{t}, n, s, k, f}, Con{ty, Con{c, Con{x, Con{y, rest}}}}}:
42 Bool.pick(
43 String,
44 String.eq(t, "Bool.pick") && (U32.is_eq(at, 3) || U32.is_eq(at, 4)),
45 "pick",
46 ""
47 )
48 case Con{Lint.Node{Lint.Reference{+t}, n, s, k, f}, Con{left, Con{right, Nil{}}}}:
49 Bool.pick(
50 String,
51 U32.is_eq(at, 2),
52 Bool.pick(
53 String,
54 String.eq(t, "Bool.and"),
55 "&&",
56 Bool.pick(String, String.eq(t, "Bool.or"), "||", "")
57 ),
58 ""
59 )
60 case _:
61 ""
62
63def Eager.of(+kind: String, value: Maybe<&2, Lint.Node>) -> Maybe<&2, Eager>:
64 Lists.when(Eager, Bool.not(String.eq(kind, "")), Eager{kind, value})
65
66
67
68def eagerly(path: List<&2, Lint.Above>, found: Maybe<&2, Eager>) -> Maybe<&2, Eager>:
69 match path found:
70 case _ Some{e}:
71 Some{e}
72 case Con{Lint.Above{Lint.Node{Lint.Application{}, n, s, +kids, f}, +at}, rest} None{}:
73 eagerly(rest, Eager.of(eager(kids, at), List.get(&2, Lint.Node, kids, U32.to_nat(at))))
74 case Con{Lint.Above{Lint.Node{Lint.Lambda{b, q}, n, s, k, f}, at}, rest} None{}:
75 None{}
76 case Con{Lint.Above{Lint.Node{Lint.Body{}, n, s, k, f}, at}, rest} None{}:
77 None{}
78 case Con{a, rest} None{}:
79 eagerly(rest, None{})
80 case Nil{} None{}:
81 None{}
82
83def why(+kind: String, +callee: String) -> String:
84 Bool.pick(
85 String,
86 String.eq(kind, "pick"),
87 "Bool.pick computes both of its values before it picks one, so this call of "
88 ++ callee
89 ++ " runs even when the condition picks the other value",
90 kind
91 ++ " computes its right side even when its left side decides the result ("
92 ++ Bool.pick(String, String.eq(kind, "&&"), "False", "True")
93 ++ "), so this call of "
94 ++ callee
95 ++ " runs even then"
96 )
97
98def message(kind: String, +callee: String, own: Bool) -> String:
99 why(
100 kind,
101 callee
102 )
103 ++ Bool.pick(
104 String,
105 own,
106 ": it is the loop's own call, so the loop walks to its end instead of stopping where the condition decides",
107 ""
108 )
109 ++ ". CPU and GPU: measured 5x slower than a match, for an unused call that calls itself outside a tail position. JS: computes both values too. Confidence: medium, as the cost is what the unneeded call computes. Choose with a match: pass the condition to a helper whose match runs only the needed value; inside a loop, where that helper would call the loop back (Bend refuses mutual recursion), pass the condition to the next call, and match on it there. If "
110 ++ callee
111 ++ " runs only a few steps here, expect this finding."
112
113def judged(eager: Maybe<&2, Eager>, callee: String, own: Bool) -> List<&2, Lint.Finding>:
114 match eager:
115 case Some{Eager{kind, value}}:
116 Lint.finding(Lint.Information{}, True{}, Lint.Node.span(value), message(kind, callee, own))
117 case None{}:
118 []
119
120def looping(
121 hit: Bool,
122 path: List<&2, Lint.Above>,
123 callee: String,
124 own: Bool
125) -> List<&2, Lint.Finding>:
126 match hit:
127 case True{}:
128 judged(eagerly(path, None{}), callee, own)
129 case False{}:
130 []
131
132def visit(+owner: Owner, +path: List<&2, Lint.Above>, +node: Lint.Node) -> List<&2, Lint.Finding>:
133 match owner node:
134 case Owner{
135 costly,
136 target
137 } Lint.Node{
138 Lint.Application{},
139 n,
140 s,
141 Con{Lint.Node{Lint.Reference{+called}, callee, rs, rk, rf}, args},
142 f
143 }:
144 looping(
145 Sets.has(costly, called) && Lint.is_live(path),
146 path,
147 callee,
148 String.eq(called, target)
149 )
150 case _ _:
151 []
152
153def defs(
154 nodes: List<&2, Lint.Node>,
155 +costly: Set(),
156 out: List<&2, Lint.Finding>
157) -> List<&2, Lint.Finding>:
158 match nodes:
159 case Nil{}:
160 List.reverse(&2, Lint.Finding, out)
161 case Con{Lint.Node{Lint.Definition{+t, u}, n, s, k, f}, rest}:
162 defs(
163 rest,
164 costly,
165 Lists.onto(
166 &2,
167 Lint.Finding,
168 Lint.visit(
169 ~Owner,
170 ~Lint.Finding,
171 ~visit,
172 Owner{costly, t},
173 [Lint.Node{Lint.Definition{t, u}, n, s, k, f}]
174 ),
175 out
176 )
177 )
178 case Con{node, rest}:
179 defs(rest, costly, out)
180
181
182def templates(nodes: List<&2, Lint.Node>, out: List<&2, U32>) -> List<&2, U32>:
183 match nodes:
184 case Nil{}:
185 out
186 case Con{Lint.Node{Lint.Parameter{b, Lint.Template{}}, n, s, k, f}, rest}:
187 templates(rest, b <> out)
188 case Con{node, rest}:
189 templates(rest, out)
190
191
192def applies.visit(
193 +templates: List<&2, U32>,
194 +path: List<&2, Lint.Above>,
195 +node: Lint.Node
196) -> List<&2, Unit>:
197 match node:
198 case Lint.Node{
199 Lint.Application{},
200 n,
201 s,
202 Con{Lint.Node{Lint.Variable{b}, vn, vs, vk, vf}, args},
203 f
204 }:
205 Lists.push(
206 Unit,
207 Lists.when(Unit, Bool.not(List.contains(~U32, ~U32.is_eq, templates, b)), Unit{}),
208 []
209 )
210 case _:
211 []
212
213
214def applies(+kids: List<&2, Lint.Node>, found: Bool) -> Bool:
215 match found:
216 case True{}:
217 True{}
218 case False{}:
219 Bool.not(
220 List.is_empty(
221 &2,
222 Unit,
223 Lint.visit(~List<&2, U32>, ~Unit, ~applies.visit, templates(kids, []), kids)
224 )
225 )
226
227
228
229def unflat(nodes: List<&2, Lint.Node>, out: List<&2, String>) -> List<&2, String>:
230 match nodes:
231 case Nil{}:
232 out
233 case Con{Lint.Node{Lint.Definition{t, u}, n, s, +kids, f}, rest}:
234 unflat(
235 rest,
236 Lists.push(String, Lists.when(String, applies(kids, Lint.forks(kids, False{})), t), out)
237 )
238 case Con{node, rest}:
239 unflat(rest, out)
240
241
242
243def program.unflat(sources: List<&2, Lint.Source>, out: List<&2, String>) -> List<&2, String>:
244 match sources:
245 case Nil{}:
246 out
247 case Con{Lint.Source{path, ns, text, +nodes}, rest}:
248 program.unflat(rest, unflat(nodes, Deep.deep(nodes, True{}, out)))
249
250def calls_itself(info: Maybe<&2, Lint.Def>) -> Bool:
251 match info:
252 case Some{Lint.Def{t, ps, forks, calls, r}}:
253 List.contains(~String, ~String.eq, calls, t)
254 case None{}:
255 False{}
256
257
258def loops(
259 names: List<&2, String>,
260 +defs: Map<&2, Maybe<&2, Lint.Def>>,
261 +unflat: Set(),
262 out: List<&2, String>
263) -> List<&2, String>:
264 match names:
265 case Nil{}:
266 out
267 case Con{+t, rest}:
268 loops(
269 rest,
270 defs,
271 unflat,
272 Lists.push(
273 String,
274 Lists.when(String, Sets.has(unflat, t) && calls_itself(Names.find(Lint.Def, defs, t)), t),
275 out
276 )
277 )
278
279
280
281
282def costly(+sources: List<&2, Lint.Source>, +defs: Map<&2, Maybe<&2, Lint.Def>>) -> Set():
283 Lint.Program.callers(
284 sources,
285 defs,
286 Set.from_list(
287 loops(
288 Map.keys(&2, Maybe<&2, Lint.Def>, defs),
289 defs,
290 Lint.Program.callers(sources, defs, Set.from_list(program.unflat(sources, []))),
291 []
292 )
293 )
294 )
295
296def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>):
297 +sources = Lint.Here.sources(here)
298 Lint.done(
299 defs(
300 Lint.Here.nodes(here),
301 costly(sources, Lint.Program.defs(sources, Map.new(&2, Maybe<&2, Lint.Def>))),
302 []
303 )
304 )
305
306def rule() -> Lint.Rule:
307 Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}