src/rules/correctness/escape.bend

bolt@1.17.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.17.0.0/src/src.bend as Src
8import bolt@1.17.0.0/src/finding.bend as F
9import bolt@1.17.0.0/src/syntax/lex.bend as Lex
10import bolt@1.17.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)