src/rules/correctness/escape.bend
bolt@1.12.0.0 · 126 lines · 0 definitions
1# rule escape: a string or char literal with `\0` then a digit, as in
2# `"\033[1m"`. Bend's escapes are \n \t \r \0 \\ \' \" and \u{..}; it has no
3# octal, so `\033` is NUL and then the chars "33" (nohzafk esc_bad: the bytes
4# 00 33 33, not ESC), with no error. Write the code point, `\u{1B}`. Other
5# unknown escapes (\x1b, \e) the checker rejects on its own.
6import Base
7import bolt@1.12.0.0/src/src.bend as Src
8import bolt@1.12.0.0/src/finding.bend as F
9import bolt@1.12.0.0/src/syntax/lex.bend as Lex
10import bolt@1.12.0.0/src/lazy/lazy.bend as Lazy
11
12# a literal's char as the rule reads it: a backslash, a `0`, another digit,
13# or anything else. Read once, so the scan below matches constructors, not
14# char literals.
15type Sym is Data:
16 SSlash{}
17 SZero{}
18 SDigit{c: Char}
19 SOther{c: Char}
20
21# a char as a Sym
22def sym(cc: Char) -> Sym:
23 match cc:
24 case Chr{+x}:
25 Bool.pick(Sym, U32.is_eq(x, 92), SSlash{},
26 Bool.pick(Sym, U32.is_eq(x, 48), SZero{},
27 Bool.pick(Sym, Bool.and(U32.is_ge(x, 49), U32.is_le(x, 57)), SDigit{Chr{x}}, SOther{Chr{x}})))
28
29# a literal's chars as Syms
30def syms(cs: List<&2, Char>) -> List<&2, Sym>:
31 match cs:
32 case Nil{}:
33 Nil{}
34 case Con{c, t}:
35 sym(c) <> syms(t)
36
37# the Sym's char when it is an octal digit
38def octal(ss: Sym) -> Maybe<&2, Char>:
39 match ss:
40 case SZero{}:
41 Some{'0'}
42 case SDigit{Chr{+x}}:
43 Bool.pick(Maybe<&2, Char>, U32.is_le(x, 55), Some{Chr{x}}, None{})
44 case other:
45 None{}
46
47# keep the octal digit, and those after it
48def run.if(mm: Maybe<&2, Char>, rest: List<&2, Char>) -> List<&2, Char>:
49 match mm:
50 case None{}:
51 Nil{}
52 case Some{c}:
53 c <> rest
54
55# the octal digits (at most two) right after the `\0`, in order
56def run(ss: List<&2, Sym>, nn: Nat) -> List<&2, Char>:
57 match ss nn:
58 case Nil{} m:
59 Nil{}
60 case Con{s, t} 0n:
61 Nil{}
62 case Con{s, t} 1n+p:
63 run.if(octal(s), run(t, p))
64
65# the digits' octal value
66def value(cs: List<&2, Char>, +acc: U32) -> U32:
67 match cs:
68 case Nil{}:
69 acc
70 case Con{Chr{+x}, t}:
71 value(t, ((acc * 8) + (x - 48) : U32))
72
73# a hex digit
74def hexd(+nn: U32) -> String:
75 String.from_list([Chr{Bool.pick(U32, U32.is_lt(nn, 10), (nn + 48 : U32), (nn + 55 : U32))}])
76
77# a value under 256 in hex, without leading zeros
78def hex(+nn: U32) -> String:
79 Bool.pick(String, U32.is_lt(nn, 16), hexd(nn), hexd((nn / 16 : U32)) ++ hexd((nn % 16 : U32)))
80
81# the finding for `\0` at a column, the digits after it in ds
82def finding(+ds: List<&2, Char>, +path: String, +line: U32, +col: U32) -> F.Finding:
83 +digits = String.from_list(ds)
84 Lazy.stop(F.Finding, List.is_empty(&2, Char, ds),
85 F.Finding{path, line, col, 3, "escape",
86 "\\0 followed by digits is a NUL character and then those digits, not an octal escape; write \\u{..} instead."},
87 _u => F.Finding{path, line, col, (U32.from_nat(String.length(digits)) + 2 : U32), "escape",
88 "\\0" ++ digits ++ " is a NUL character followed by \"" ++ digits ++ "\", not an octal escape; write \\u{" ++ hex(value(ds, 0)) ++ "}."})
89
90# after a `\0` at a column: a finding when a digit follows
91def after(ss: List<&2, Sym>, +path: String, +line: U32, +col: U32, more: List<&2, F.Finding>) -> List<&2, F.Finding>:
92 match ss:
93 case Con{SZero{}, t}:
94 finding(run(SZero{} <> t, 2n), path, line, col) <> more
95 case Con{SDigit{d}, t}:
96 finding(run(SDigit{d} <> t, 2n), path, line, col) <> more
97 case other:
98 more
99
100# a literal's Syms from a column; an escape is its backslash and one char
101def scan(ss: List<&2, Sym>, +path: String, +line: U32, +col: U32) -> List<&2, F.Finding>:
102 match ss:
103 case Con{SSlash{}, Con{SZero{}, +rest}}:
104 after(rest, path, line, col, scan(rest, path, line, (col + 2 : U32)))
105 case Con{SSlash{}, Con{e, rest}}:
106 scan(rest, path, line, (col + 2 : U32))
107 case Con{s, rest}:
108 scan(rest, path, line, (col + 1 : U32))
109 case Nil{}:
110 Nil{}
111
112def check.go(toks: List<&2, Lex.Tok>, +path: String) -> List<&2, F.Finding>:
113 match toks:
114 case Con{Lex.Tok{Lex.TStr{}, t, l, c}, rest}:
115 List.append(&2, F.Finding, scan(syms(String.to_list(t)), path, l, c), check.go(rest, path))
116 case Con{Lex.Tok{Lex.TChar{}, t, l, c}, rest}:
117 List.append(&2, F.Finding, scan(syms(String.to_list(t)), path, l, c), check.go(rest, path))
118 case Con{h, rest}:
119 check.go(rest, path)
120 case Nil{}:
121 Nil{}
122
123# the rule
124def check(ss: Src.Src) -> List<&2, F.Finding>:
125 Src.Src{path, text, toks, tree, bound, items} = ss
126 check.go(toks, path)