src/rules/perf/strings.bend

bolt@1.26.0.0 · 113 lines · 0 definitions

1# rule strings: a `match` whose arms are string literals (`case "foo":`) with
2# more than 64 literal characters in all. Each string arm lowers into nested
3# per-char tests, and the checker's time and memory grow with the characters
4# (bend 2.0.16: 45 characters take 0.8 s and 0.35 GB, 100 take 2 s and 1 GB,
5# 480 take 12 s and 4.8 GB; pi-bend BEND-001/016 was 16 long event names).
6# The number of arms barely matters, so only the characters count. Classify
7# the string once through a lookup table into a sum type, and match on that.
8# Only a case's first match column is read: string literals in a later
9# column of `match a b:` do not count. A literal with no closing quote (an
10# unterminated string) is not a string arm.
11import Base
12import bolt@1.26.0.0/src/src.bend as Src
13import bolt@1.26.0.0/src/finding.bend as F
14import bolt@1.26.0.0/src/syntax/lex.bend as Lex
15import bolt@1.26.0.0/src/syntax/tree.bend as Tree
16import bolt@1.26.0.0/src/lazy/lazy.bend as Lazy
17import bolt@1.26.0.0/src/rules/tokens.bend as T
18
19# the string arms of a match, and their characters
20type Tally is Data:
21  Tally{arms: U32, chars: U32}
22
23# does the text end on a closing quote, its escapes read as a backslash and
24# the one char after it? esc: the char before was an unescaped backslash;
25# shut: it was an unescaped quote
26def closes(cs: List<&2, Char>, +esc: Bool, +shut: Bool) -> Bool:
27  match cs:
28    case Nil{}:
29      shut
30    case Con{ch, rest}:
31      +code = Char.to_u32(ch)
32      closes(rest, Bool.and(Bool.not(esc), U32.is_eq(code, 92)), Bool.and(Bool.not(esc), U32.is_eq(code, 34)))
33
34# past its opening char, do the chars end on a closing quote?
35def opened(cs: List<&2, Char>) -> Bool:
36  match cs:
37    case Nil{}:
38      False{}
39    case Con{_q, rest}:
40      closes(rest, False{}, False{})
41
42# is a string token closed: past its opening quote, does it end on a quote no
43# backslash escapes? `"\"`, an unclosed string at the end of the file, is not
44def terminated(+tt: String) -> Bool:
45  opened(String.to_list(tt))
46
47# the characters of the string a case pattern opens with, if it does and the
48# string is closed
49def literal(kids: Tree.Node) -> Maybe<&2, U32>:
50  match kids:
51    case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TStr{}, +t, l, c}}, rest}:
52      +w = T.width(t)
53      Bool.pick(Maybe<&2, U32>, terminated(t), Some{(w - 2 : U32)}, None{})
54    case other:
55      None{}
56
57# one for a string arm, else zero
58def one(mm: Maybe<&2, U32>) -> U32:
59  match mm:
60    case None{}:
61      0
62    case Some{n}:
63      1
64
65# a string arm's characters, else zero
66def size(mm: Maybe<&2, U32>) -> U32:
67  match mm:
68    case None{}:
69      0
70    case Some{n}:
71      n
72
73# the string arms among a match's statements
74def tally(body: Tree.Node, +nn: U32, +chars: U32) -> Tally:
75  match body:
76    case Tree.NCons{Tree.Stmt{Tree.SCase{}, kids, b}, rest}:
77      +m = literal(T.pattern(kids))
78      +k = one(m)
79      +ch = size(m)
80      tally(rest, (nn + k : U32), (chars + ch : U32))
81    case Tree.NCons{h, rest}:
82      tally(rest, nn, chars)
83    case other:
84      Tally{nn, chars}
85
86# a finding when the match's strings are too long in all
87def report(tt: Tally, +path: String, +ll: U32, +cc: U32) -> List<&2, F.Finding>:
88  Tally{+n, +chars} = tt
89  Bool.pick(List<&2, F.Finding>, U32.is_gt(chars, 64),
90    [F.Finding{path, ll, cc, 5, "strings", "This match has " ++ U32.show(n) ++ " string-literal cases totalling " ++ U32.show(chars)
91      ++ " characters, over the limit of 64, which blows up compile time and memory; map the string to a sum type once and match on that."}],
92    [])
93
94# a statement: a match is tallied
95def on_stmt(+kids: Tree.Node, +body: Tree.Node, +path: String) -> List<&2, F.Finding>:
96  +hit = T.keyword(kids, "match")
97  Lazy.stop(List<&2, F.Finding>, Bool.not(hit), [],
98    _u => report(tally(body, 0, 0), path, Tree.line(kids), Tree.col(kids)))
99
100# every statement, at any depth
101def walk(nn: Tree.Node, +path: String) -> List<&2, F.Finding>:
102  match nn:
103    case Tree.NCons{Tree.Stmt{kind, kids, +body}, rest}:
104      List.concat(&2, F.Finding, [on_stmt(kids, body, path), walk(body, path), walk(rest, path)])
105    case Tree.NCons{h, rest}:
106      walk(rest, path)
107    case other:
108      Nil{}
109
110# the rule
111def check(ss: Src.Src) -> List<&2, F.Finding>:
112  Src.Src{path, text, toks, tree, bound, items} = ss
113  walk(tree, path)