src/rules/correctness/escape.bend source
src/rules/correctness/escape.bend on the hub · documented module
# rule escape: a string or char literal with `\0` then a digit, as in# `"\033[1m"`. Bend's escapes are \n \t \r \0 \\ \' \" and \u{..}; it has no# octal, so `\033` is NUL and then the chars "33" (nohzafk esc_bad: the bytes# 00 33 33, not ESC), with no error. Write the code point, `\u{1B}`. Other# unknown escapes (\x1b, \e) the checker rejects on its own.import Baseimport ../../src.bend as Srcimport ../../finding.bend as Fimport ../../syntax/lex.bend as Leximport ../../lazy/lazy.bend as Lazy# a literal's char as the rule reads it: a backslash, a `0`, another digit,# or anything else. Read once, so the scan below matches constructors, not# char literals.type Sym is Data: SSlash{} SZero{} SDigit{c: Char} SOther{c: Char}# a char as a Symdef sym(cc: Char) -> Sym: match cc: case Chr{+x}: Bool.pick(Sym, U32.is_eq(x, 92), SSlash{}, Bool.pick(Sym, U32.is_eq(x, 48), SZero{}, Bool.pick(Sym, Bool.and(U32.is_ge(x, 49), U32.is_le(x, 57)), SDigit{Chr{x}}, SOther{Chr{x}})))# a literal's chars as Symsdef syms(cs: List<&2, Char>) -> List<&2, Sym>: match cs: case Nil{}: Nil{} case Con{c, t}: sym(c) <> syms(t)# the Sym's char when it is an octal digitdef octal(ss: Sym) -> Maybe<&2, Char>: match ss: case SZero{}: Some{'0'} case SDigit{Chr{+x}}: Bool.pick(Maybe<&2, Char>, U32.is_le(x, 55), Some{Chr{x}}, None{}) case other: None{}# keep the octal digit, and those after itdef run.if(mm: Maybe<&2, Char>, rest: List<&2, Char>) -> List<&2, Char>: match mm: case None{}: Nil{} case Some{c}: c <> rest# the octal digits (at most two) right after the `\0`, in orderdef run(ss: List<&2, Sym>, nn: Nat) -> List<&2, Char>: match ss nn: case Nil{} m: Nil{} case Con{s, t} 0n: Nil{} case Con{s, t} 1n+p: run.if(octal(s), run(t, p))# the digits' octal valuedef value(cs: List<&2, Char>, +acc: U32) -> U32: match cs: case Nil{}: acc case Con{Chr{+x}, t}: value(t, ((acc * 8) + (x - 48) : U32))# a hex digitdef hexd(+nn: U32) -> String: String.from_list([Chr{Bool.pick(U32, U32.is_lt(nn, 10), (nn + 48 : U32), (nn + 55 : U32))}])# a value under 256 in hex, without leading zerosdef hex(+nn: U32) -> String: Bool.pick(String, U32.is_lt(nn, 16), hexd(nn), hexd((nn / 16 : U32)) ++ hexd((nn % 16 : U32)))# the finding for `\0` at a column, the digits after it in dsdef finding(+ds: List<&2, Char>, +path: String, +line: U32, +col: U32) -> F.Finding: +digits = String.from_list(ds) Lazy.stop(F.Finding, List.is_empty(&2, Char, ds), F.Finding{path, line, col, 3, "escape", "\\0 followed by digits is a NUL character and then those digits, not an octal escape; write \\u{..} instead."}, _u => F.Finding{path, line, col, (U32.from_nat(String.length(digits)) + 2 : U32), "escape", "\\0" ++ digits ++ " is a NUL character followed by \"" ++ digits ++ "\", not an octal escape; write \\u{" ++ hex(value(ds, 0)) ++ "}."})# after a `\0` at a column: a finding when a digit followsdef after(ss: List<&2, Sym>, +path: String, +line: U32, +col: U32, more: List<&2, F.Finding>) -> List<&2, F.Finding>: match ss: case Con{SZero{}, t}: finding(run(SZero{} <> t, 2n), path, line, col) <> more case Con{SDigit{d}, t}: finding(run(SDigit{d} <> t, 2n), path, line, col) <> more case other: more# a literal's Syms from a column; an escape is its backslash and one chardef scan(ss: List<&2, Sym>, +path: String, +line: U32, +col: U32) -> List<&2, F.Finding>: match ss: case Con{SSlash{}, Con{SZero{}, +rest}}: after(rest, path, line, col, scan(rest, path, line, (col + 2 : U32))) case Con{SSlash{}, Con{e, rest}}: scan(rest, path, line, (col + 2 : U32)) case Con{s, rest}: scan(rest, path, line, (col + 1 : U32)) case Nil{}: Nil{}def check.go(toks: List<&2, Lex.Tok>, +path: String) -> List<&2, F.Finding>: match toks: case Con{Lex.Tok{Lex.TStr{}, t, l, c}, rest}: List.append(&2, F.Finding, scan(syms(String.to_list(t)), path, l, c), check.go(rest, path)) case Con{Lex.Tok{Lex.TChar{}, t, l, c}, rest}: List.append(&2, F.Finding, scan(syms(String.to_list(t)), path, l, c), check.go(rest, path)) case Con{h, rest}: check.go(rest, path) case Nil{}: Nil{}# the ruledef check(ss: Src.Src) -> List<&2, F.Finding>: Src.Src{path, text, toks, tree, bound, items} = ss check.go(toks, path)