src/rules/correctness/chars.bend

bolt@1.21.0.0 · 72 lines · 0 definitions

1# rule chars: a `match` with more than eight character-literal arms
2# (`case '.':`). `Char` is `Chr{code: U32}`, so such an arm is a U32 literal
3# inside a constructor pattern, and bend's C backend pays for each one out of
4# all proportion to its size -- about 90 MB, and the cost compounds through
5# every def downstream of the one holding them (eighteen arms cost 1.57 GB
6# and 8.3 s here, 0.10 GB and 0.7 s once rewritten). It is the literals, not
7# the arms: a match over eighteen constructors costs nothing measurable.
8#
9# Compare `Char.to_u32(c)` instead. Bind the fallback above the comparisons
10# so it is a value, not a call, or bolt's own `eager` rule fires on it; and
11# give the def the reusable quantity (`+c: Char`), since the code point and
12# the fallback both consume it. Where the arms carry linear values, leave
13# the match alone: a cascade would break linearity and do every branch's
14# work.
15#
16# Only a case's first match column is read, and only an arm whose pattern
17# opens with a character literal counts: `case 'x':` does, `case Con{'x', t}:`
18# and a literal in a later column of `match a b:` do not.
19import Base
20import bolt@1.21.0.0/src/src.bend as Src
21import bolt@1.21.0.0/src/finding.bend as F
22import bolt@1.21.0.0/src/syntax/lex.bend as Lex
23import bolt@1.21.0.0/src/syntax/tree.bend as Tree
24import bolt@1.21.0.0/src/lazy/lazy.bend as Lazy
25import bolt@1.21.0.0/src/rules/tokens.bend as T
26
27# one when a case pattern opens with a character literal, else zero
28def literal(kids: Tree.Node) -> U32:
29  match kids:
30    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TChar{}, t, l, c}}, rest}:
31      1
32    case other:
33      0
34
35# the character-literal arms among a match's statements
36def tally(body: Tree.Node, +nn: U32) -> U32:
37  match body:
38    case Tree.NCons{Tree.Stmt{Tree.SCase{}, kids, b}, rest}:
39      +k = literal(T.pattern(kids))
40      tally(rest, (nn + k : U32))
41    case Tree.NCons{h, rest}:
42      tally(rest, nn)
43    case other:
44      nn
45
46# a finding when the match holds more character literals than is cheap
47def report(+nn: U32, +path: String, +ll: U32, +cc: U32) -> List<&2, F.Finding>:
48  Bool.pick(List<&2, F.Finding>, U32.is_gt(nn, 8),
49    [F.Finding{path, ll, cc, 5, "chars", "This match has " ++ U32.show(nn)
50      ++ " character-literal cases, over the limit of 8, which blows up compile time and memory; compare Char.to_u32(c) instead."}],
51    [])
52
53# a statement: a match is tallied
54def on_stmt(+kids: Tree.Node, +body: Tree.Node, +path: String) -> List<&2, F.Finding>:
55  +hit = T.keyword(kids, "match")
56  Lazy.stop(List<&2, F.Finding>, Bool.not(hit), [],
57    _u => report(tally(body, 0), path, Tree.line(kids), Tree.col(kids)))
58
59# every statement, at any depth
60def walk(nn: Tree.Node, +path: String) -> List<&2, F.Finding>:
61  match nn:
62    case Tree.NCons{Tree.Stmt{kind, kids, +body}, rest}:
63      List.concat(&2, F.Finding, [on_stmt(kids, body, path), walk(body, path), walk(rest, path)])
64    case Tree.NCons{h, rest}:
65      walk(rest, path)
66    case other:
67      Nil{}
68
69# the rule
70def check(ss: Src.Src) -> List<&2, F.Finding>:
71  Src.Src{path, text, toks, tree, bound, items} = ss
72  walk(tree, path)