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. 6 import Base 7 import ../../src.bend as Src 8 import ../../finding.bend as F 9 import ../../syntax/lex.bend as Lex 10 import ../../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. 15 type Sym is Data: 16 SSlash{} 17 SZero{} 18 SDigit{c: Char} 19 SOther{c: Char} 20 21 # a char as a Sym 22 def 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 30 def 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 38 def 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 48 def 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 56 def 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 66 def 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 74 def 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 78 def 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 82 def 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 91 def 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 101 def 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 112 def 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 124 def check(ss: Src.Src) -> List<&2, F.Finding>: 125 Src.Src{path, text, toks, tree, bound, items} = ss 126 check.go(toks, path)