1 import Base 2 import ../json.bend as Json 3 4 # YAML pieces shared by the writer (yamlw.bend) and the reader (yamlr.bend): 5 # which strings may be written plain, quoted tokens, and how a plain word 6 # resolves to null, a boolean, a number or a string. Pure. 7 8 def JR() -> Type: 9 Result<&2, &2, String, Json.Json> 10 11 def ge(+c: U32, lo: U32) -> Bool: 12 U32.is_ge(c, lo) 13 14 def between(+c: U32, lo: U32, hi: U32) -> Bool: 15 U32.is_ge(c, lo) && U32.is_le(c, hi) 16 17 def is_digit(+c: U32) -> Bool: 18 between(c, 48, 57) 19 20 def is_space(+c: U32) -> Bool: 21 U32.is_eq(c, 32) 22 23 # ---- numbers (the JSON grammar: -?(0|[1-9]d*)(.d+)?([eE][+-]?d+)?) --------- 24 25 type NS is Data: 26 NStart{} 27 NMinus{} 28 NZero{} 29 NInt{} 30 NDot{} 31 NFrac{} 32 NExp{} 33 NESign{} 34 NEDig{} 35 NBad{} 36 37 def ns.lead(+c: U32) -> NS: 38 Bool.pick(NS, U32.is_eq(c, 48), NZero{}, Bool.pick(NS, between(c, 49, 57), NInt{}, NBad{})) 39 40 def ns.start(+c: U32) -> NS: 41 Bool.pick(NS, U32.is_eq(c, 45), NMinus{}, ns.lead(c)) 42 43 def ns.tail(+c: U32) -> NS: 44 Bool.pick(NS, U32.is_eq(c, 46), NDot{}, Bool.pick(NS, U32.is_eq(c, 101) || U32.is_eq(c, 69), NExp{}, NBad{})) 45 46 def ns.int(+c: U32) -> NS: 47 Bool.pick(NS, is_digit(c), NInt{}, ns.tail(c)) 48 49 def ns.frac(+c: U32) -> NS: 50 Bool.pick(NS, is_digit(c), NFrac{}, Bool.pick(NS, U32.is_eq(c, 101) || U32.is_eq(c, 69), NExp{}, NBad{})) 51 52 def ns.exp(+c: U32) -> NS: 53 Bool.pick(NS, U32.is_eq(c, 43) || U32.is_eq(c, 45), NESign{}, Bool.pick(NS, is_digit(c), NEDig{}, NBad{})) 54 55 def ns.digits(+c: U32, next: NS) -> NS: 56 Bool.pick(NS, is_digit(c), next, NBad{}) 57 58 def ns.step(s: NS, +c: U32) -> NS: 59 match s: 60 case NStart{}: 61 ns.start(c) 62 case NMinus{}: 63 ns.lead(c) 64 case NZero{}: 65 ns.tail(c) 66 case NInt{}: 67 ns.int(c) 68 case NDot{}: 69 ns.digits(c, NFrac{}) 70 case NFrac{}: 71 ns.frac(c) 72 case NExp{}: 73 ns.exp(c) 74 case NESign{}: 75 ns.digits(c, NEDig{}) 76 case NEDig{}: 77 ns.digits(c, NEDig{}) 78 case NBad{}: 79 NBad{} 80 81 def ns.run(s: String, st: NS) -> NS: 82 match s: 83 case SNil{}: 84 st 85 case SCon{Chr{c}, t}: 86 ns.run(t, ns.step(st, c)) 87 88 def ns.accept(s: NS) -> Bool: 89 match s: 90 case NZero{}: 91 True{} 92 case NInt{}: 93 True{} 94 case NFrac{}: 95 True{} 96 case NEDig{}: 97 True{} 98 case _: 99 False{} 100 101 # Is the whole text a JSON number? 102 def is_number(s: String) -> Bool: 103 ns.accept(ns.run(s, NStart{})) 104 105 # ---- plain words ------------------------------------------------------------- 106 107 def word_is(+low: String, w: String) -> Bool: 108 String.eq(low, w) 109 110 def is_null_word(+low: String) -> Bool: 111 String.is_empty(low) || word_is(low, "~") || word_is(low, "null") 112 113 def is_bool_word(+low: String) -> Bool: 114 word_is(low, "true") || word_is(low, "false") 115 116 # Words YAML 1.1 and 1.2 readers turn into another type. 117 def reserved(+low: String) -> Bool: 118 is_null_word(low) || is_bool_word(low) || word_is(low, "yes") || word_is(low, "no") || word_is(low, "on") || word_is(low, "off") || word_is(low, "y") || word_is(low, "n") 119 120 # A plain word as the value it stands for. 121 def resolve.low(+low: String, +s: String) -> Json.Json: 122 Bool.pick(Json.Json, is_null_word(low), Json.JNull{}, 123 Bool.pick(Json.Json, word_is(low, "true"), Json.JBool{True{}}, 124 Bool.pick(Json.Json, word_is(low, "false"), Json.JBool{False{}}, 125 Bool.pick(Json.Json, is_number(s), Json.JNum{s}, Json.JStr{s})))) 126 127 def resolve(+s: String) -> Json.Json: 128 resolve.low(String.to_lower(s), s) 129 130 # ---- may a string be written plain? ------------------------------------------- 131 132 # Characters a plain scalar must not start with: indicators, quotes, and 133 # anything a reader could take for a number, a date, a tag or a merge key. 134 def bad_first(+c: U32) -> Bool: 135 U32.is_eq(c, 45) || U32.is_eq(c, 63) || U32.is_eq(c, 58) || U32.is_eq(c, 44) || U32.is_eq(c, 91) || U32.is_eq(c, 93) || U32.is_eq(c, 123) || U32.is_eq(c, 125) 136 || U32.is_eq(c, 35) || U32.is_eq(c, 38) || U32.is_eq(c, 42) || U32.is_eq(c, 33) || U32.is_eq(c, 124) || U32.is_eq(c, 62) || U32.is_eq(c, 39) || U32.is_eq(c, 34) 137 || U32.is_eq(c, 37) || U32.is_eq(c, 64) || U32.is_eq(c, 96) || U32.is_eq(c, 43) || U32.is_eq(c, 46) || U32.is_eq(c, 126) || U32.is_eq(c, 61) || U32.is_eq(c, 60) 138 || is_digit(c) || is_space(c) 139 140 def bad_char(+c: U32) -> Bool: 141 U32.is_lt(c, 32) || between(c, 127, 159) || between(c, 8232, 8233) || U32.is_eq(c, 65279) 142 143 # Scan the rest: no control characters, no ": " or trailing ":", no " #". 144 def rest_ok(s: String, +prev: U32) -> Bool: 145 match s: 146 case SNil{}: 147 Bool.not(U32.is_eq(prev, 58)) && Bool.not(is_space(prev)) 148 case SCon{Chr{+c}, t}: 149 Bool.not(bad_char(c)) && Bool.not(U32.is_eq(prev, 58) && is_space(c)) && Bool.not(is_space(prev) && U32.is_eq(c, 35)) && rest_ok(t, c) 150 151 def first_ok(s: String) -> Bool: 152 match s: 153 case SNil{}: 154 False{} 155 case SCon{Chr{+c}, t}: 156 Bool.not(bad_first(c)) && Bool.not(bad_char(c)) && rest_ok(t, c) 157 158 def plain_ok(+s: String) -> Bool: 159 first_ok(s) && Bool.not(reserved(String.to_lower(s))) 160 161 # ---- quoted tokens ------------------------------------------------------------ 162 163 def dq.pick(done: Bool, n: Nat, rest: Maybe<&2, Nat>) -> Maybe<&2, Nat>: 164 match done: 165 case True{}: 166 Some{n} 167 case False{}: 168 rest 169 170 # `n` characters of a double-quoted body (after the opening quote) up to and 171 # including its closing quote, or None when it never closes. 172 def dq.end(s: String, +n: Nat, +esc: Bool) -> Maybe<&2, Nat>: 173 match s: 174 case SNil{}: 175 None{} 176 case SCon{Chr{+c}, t}: 177 dq.pick(Bool.not(esc) && U32.is_eq(c, 34), Nat.add(n, 1n), dq.end(t, Nat.add(n, 1n), Bool.not(esc) && U32.is_eq(c, 92))) 178 179 def sq.eos(prevq: Bool, n: Nat) -> Maybe<&2, Nat>: 180 match prevq: 181 case True{}: 182 Some{n} 183 case False{}: 184 None{} 185 186 def sq.pick(done: Bool, n: Nat, rest: Maybe<&2, Nat>) -> Maybe<&2, Nat>: 187 match done: 188 case True{}: 189 Some{n} 190 case False{}: 191 rest 192 193 # The same for a single-quoted body: `''` is a quote. `prevq`: the last 194 # character was a quote that may close the string. 195 def sq.end(s: String, +n: Nat, +prevq: Bool) -> Maybe<&2, Nat>: 196 match s: 197 case SNil{}: 198 sq.eos(prevq, n) 199 case SCon{Chr{+c}, t}: 200 sq.pick(prevq && Bool.not(U32.is_eq(c, 39)), n, sq.end(t, Nat.add(n, 1n), Bool.not(prevq) && U32.is_eq(c, 39))) 201 202 def first_code(s: String) -> U32: 203 match s: 204 case SNil{}: 205 0 206 case SCon{Chr{c}, t}: 207 c 208 209 def tl.plus(m: Maybe<&2, Nat>) -> Maybe<&2, Nat>: 210 match m: 211 case None{}: 212 None{} 213 case Some{n}: 214 Some{Nat.add(n, 1n)} 215 216 def tl.pick(isd: Bool, iss: Bool, d: Maybe<&2, Nat>, s: Maybe<&2, Nat>) -> Maybe<&2, Nat>: 217 match isd iss: 218 case True{} _: 219 d 220 case False{} True{}: 221 s 222 case False{} False{}: 223 None{} 224 225 def token_len.q(+s: String, +c: U32) -> Maybe<&2, Nat>: 226 tl.pick(U32.is_eq(c, 34), U32.is_eq(c, 39), tl.plus(dq.end(String.drop(s, 1n), 0n, False{})), tl.plus(sq.end(String.drop(s, 1n), 0n, False{}))) 227 228 # Length of the quoted token at the start of `s` (quotes included), when `s` 229 # starts with a quote and the token closes. 230 def token_len(+s: String) -> Maybe<&2, Nat>: 231 token_len.q(s, first_code(s))