1 import Base 2 import ../json.bend as Json 3 import ./ylex.bend as L 4 5 # YAML scalars as the reader sees them: plain words, quoted strings, a flat 6 # flow sequence or mapping, and a `key: value` line. Pure. 7 8 # ---- comments ----------------------------------------------------------------- 9 10 def comment.pick(hit: Bool, n: Nat, rest: Maybe<&2, Nat>) -> Maybe<&2, Nat>: 11 match hit: 12 case True{}: 13 Some{n} 14 case False{}: 15 rest 16 17 # Where a comment starts: a `#` at the start or after a space. 18 def comment.find(s: String, +n: Nat, +prev: Bool) -> Maybe<&2, Nat>: 19 match s: 20 case SNil{}: 21 None{} 22 case SCon{Chr{+c}, t}: 23 comment.pick(prev && U32.is_eq(c, 35), n, comment.find(t, Nat.add(n, 1n), L.is_space(c))) 24 25 def comment.cut(m: Maybe<&2, Nat>, +s: String) -> String: 26 match m: 27 case None{}: 28 s 29 case Some{n}: 30 String.take(s, n) 31 32 # The text without a trailing comment, trimmed. 33 def uncomment(+s: String) -> String: 34 String.trim(comment.cut(comment.find(s, 0n, True{}), s)) 35 36 # ---- quoted strings ------------------------------------------------------------- 37 38 def sq.emit(skip: Bool, +c: U32, rest: String) -> String: 39 match skip: 40 case True{}: 41 rest 42 case False{}: 43 SCon{Chr{c}, rest} 44 45 # `''` inside single quotes is one quote. `prevq`: the last character was a 46 # kept quote. 47 def sq.unesc(s: String, +prevq: Bool) -> String: 48 match s: 49 case SNil{}: 50 "" 51 case SCon{Chr{+c}, t}: 52 sq.emit(prevq && U32.is_eq(c, 39), c, sq.unesc(t, Bool.not(prevq) && U32.is_eq(c, 39))) 53 54 def dq.val(r: Result<&2, &2, String, Json.Json>) -> L.JR(): 55 match r: 56 case Done{Json.JStr{s}}: 57 Done{Json.JStr{s}} 58 case Done{x}: 59 Fail{"bad double-quoted string"} 60 case Fail{e}: 61 Fail{"bad double-quoted string: " ++ e} 62 63 def first_code(s: String) -> U32: 64 match s: 65 case SNil{}: 66 0 67 case SCon{Chr{c}, t}: 68 c 69 70 def unquote.q(isd: Bool, +tok: String) -> L.JR(): 71 match isd: 72 case True{}: 73 dq.val(Json.parse(tok)) 74 case False{}: 75 Done{Json.JStr{sq.unesc(String.take(String.drop(tok, 1n), Nat.sub(String.length(tok), 2n)), False{})}} 76 77 # A quoted token (quotes included) as the string it spells. 78 def unquote(+tok: String) -> L.JR(): 79 unquote.q(U32.is_eq(first_code(tok), 34), tok) 80 81 def str_of(r: L.JR()) -> Result<&2, &2, String, String>: 82 match r: 83 case Done{Json.JStr{s}}: 84 Done{s} 85 case Done{x}: 86 Fail{"expected a string"} 87 case Fail{e}: 88 Fail{e} 89 90 # ---- one atom ---------------------------------------------------------------------- 91 92 def after_ok(+after: String) -> Bool: 93 String.is_empty(after) || U32.is_eq(first_code(after), 35) 94 95 def quoted.fin(ok: Bool, +tok: String) -> L.JR(): 96 match ok: 97 case True{}: 98 unquote(tok) 99 case False{}: 100 Fail{"unexpected text after a quoted string"} 101 102 def quoted.tok(m: Maybe<&2, Nat>, +s: String) -> L.JR(): 103 match m: 104 case None{}: 105 Fail{"unterminated quoted string"} 106 case Some{+n}: 107 quoted.fin(after_ok(String.trim(String.drop(s, n))), String.take(s, n)) 108 109 def special(+c: U32) -> Bool: 110 U32.is_eq(c, 124) || U32.is_eq(c, 62) 111 112 def tagged(+c: U32) -> Bool: 113 U32.is_eq(c, 38) || U32.is_eq(c, 42) || U32.is_eq(c, 33) 114 115 def atom.pick(isq: Bool, isb: Bool, ist: Bool, +s: String) -> L.JR(): 116 match isq isb ist: 117 case True{} _ _: 118 quoted.tok(L.token_len(s), s) 119 case False{} True{} _: 120 Fail{"block scalars are not supported"} 121 case False{} False{} True{}: 122 Fail{"anchors, aliases and tags are not supported"} 123 case False{} False{} False{}: 124 Done{L.resolve(uncomment(s))} 125 126 # A plain or quoted scalar (trimmed text). 127 def atom(+s: String) -> L.JR(): 128 atom.pick(U32.is_eq(first_code(s), 34) || U32.is_eq(first_code(s), 39), special(first_code(s)), tagged(first_code(s)), s) 129 130 # ---- flow collections (flat: scalars only) ------------------------------------------- 131 132 type Sp is Data: 133 Sp{done: List<&2, String>, cur: String, q: U32} 134 135 def sp.push(+c: U32, +sp: Sp) -> Sp: 136 match sp: 137 case Sp{done, cur, q}: 138 Sp{done, SCon{Chr{c}, cur}, q} 139 140 def sp.cut(+sp: Sp) -> Sp: 141 match sp: 142 case Sp{done, cur, q}: 143 Sp{String.trim(String.reverse(cur)) <> done, "", q} 144 145 def sp.quote(+c: U32, +sp: Sp) -> Sp: 146 match sp: 147 case Sp{done, cur, q}: 148 Sp{done, SCon{Chr{c}, cur}, c} 149 150 def sp.close(+c: U32, +sp: Sp) -> Sp: 151 match sp: 152 case Sp{done, cur, q}: 153 Sp{done, SCon{Chr{c}, cur}, 0} 154 155 def sp.outside(+c: U32, +sp: Sp) -> Sp: 156 Bool.pick(Sp, U32.is_eq(c, 44), sp.cut(sp), 157 Bool.pick(Sp, U32.is_eq(c, 34) || U32.is_eq(c, 39), sp.quote(c, sp), sp.push(c, sp))) 158 159 def sp.q_of(sp: Sp) -> U32: 160 match sp: 161 case Sp{done, cur, q}: 162 q 163 164 def sp.inside(+c: U32, +sp: Sp) -> Sp: 165 Bool.pick(Sp, U32.is_eq(c, sp.q_of(sp)), sp.close(c, sp), sp.push(c, sp)) 166 167 def sp.step(+c: U32, +sp: Sp) -> Sp: 168 Bool.pick(Sp, U32.is_eq(sp.q_of(sp), 0), sp.outside(c, sp), sp.inside(c, sp)) 169 170 def sp.go(s: String, +sp: Sp) -> Sp: 171 match s: 172 case SNil{}: 173 sp 174 case SCon{Chr{c}, t}: 175 sp.go(t, sp.step(c, sp)) 176 177 def sp.fin(+sp: Sp) -> List<&2, String>: 178 match sp: 179 case Sp{done, cur, q}: 180 List.reverse(&2, String, String.trim(String.reverse(cur)) <> done) 181 182 # The comma-separated pieces of a flow body, quotes respected. 183 def split_top(+s: String) -> List<&2, String>: 184 Bool.pick(List<&2, String>, String.is_empty(String.trim(s)), [], sp.fin(sp.go(s, Sp{[], "", 0}))) 185 186 def item.cons(x: L.JR(), rest: L.JR()) -> L.JR(): 187 match x rest: 188 case Done{a} Done{b}: 189 Done{Json.JCons{a, b}} 190 case Fail{e} _: 191 Fail{e} 192 case _ Fail{e}: 193 Fail{e} 194 195 def flow.items(ps: List<&2, String>) -> L.JR(): 196 match ps: 197 case Nil{}: 198 Done{Json.JNil{}} 199 case Con{+p, r}: 200 item.cons(atom(p), flow.items(r)) 201 202 def wrap.arr(r: L.JR()) -> L.JR(): 203 match r: 204 case Done{c}: 205 Done{Json.JArr{c}} 206 case Fail{e}: 207 Fail{e} 208 209 def wrap.obj(r: L.JR()) -> L.JR(): 210 match r: 211 case Done{c}: 212 Done{Json.JObj{c}} 213 case Fail{e}: 214 Fail{e} 215 216 def inner(+s: String) -> String: 217 String.take(String.drop(s, 1n), Nat.sub(String.length(s), 2n)) 218 219 def last_code(s: String) -> U32: 220 first_code(String.reverse(s)) 221 222 def seq.fin(closed: Bool, +s: String) -> L.JR(): 223 match closed: 224 case True{}: 225 wrap.arr(flow.items(split_top(inner(s)))) 226 case False{}: 227 Fail{"unterminated flow sequence"} 228 229 # ---- key: value ---------------------------------------------------------------------- 230 231 type KV is Data: 232 KNone{} 233 KBad{msg: String} 234 KOk{key: String, rest: String} 235 236 # Index of the first `:` that ends a key (followed by a space or the end). 237 def colon.pick(hit: Bool, n: Nat, rest: Maybe<&2, Nat>) -> Maybe<&2, Nat>: 238 match hit: 239 case True{}: 240 Some{n} 241 case False{}: 242 rest 243 244 def colon.find(s: String, +n: Nat) -> Maybe<&2, Nat>: 245 match s: 246 case SNil{}: 247 None{} 248 case SCon{Chr{c}, SNil{}}: 249 colon.pick(U32.is_eq(c, 58), n, None{}) 250 case SCon{Chr{+c}, SCon{Chr{+d}, t}}: 251 colon.pick(U32.is_eq(c, 58) && L.is_space(d), n, colon.find(SCon{Chr{d}, t}, Nat.add(n, 1n))) 252 253 def kv.plain(m: Maybe<&2, Nat>, +s: String) -> KV: 254 match m: 255 case None{}: 256 KNone{} 257 case Some{+n}: 258 KOk{String.trim(String.take(s, n)), String.trim(String.drop(s, Nat.add(n, 1n)))} 259 260 def kv.colon(+k: String, +after: String) -> KV: 261 match after: 262 case SCon{Chr{c}, t}: 263 Bool.pick(KV, U32.is_eq(c, 58) && (String.is_empty(t) || L.is_space(first_code(t))), KOk{k, String.trim(t)}, KNone{}) 264 case SNil{}: 265 KNone{} 266 267 def kv.after(r: Result<&2, &2, String, String>, +after: String) -> KV: 268 match r: 269 case Fail{e}: 270 KBad{e} 271 case Done{k}: 272 kv.colon(k, after) 273 274 def kv.quoted(m: Maybe<&2, Nat>, +s: String) -> KV: 275 match m: 276 case None{}: 277 KNone{} 278 case Some{+n}: 279 kv.after(str_of(unquote(String.take(s, n))), String.drop(s, n)) 280 281 def kv.pick(isq: Bool, isflow: Bool, +s: String) -> KV: 282 match isq isflow: 283 case True{} _: 284 kv.quoted(L.token_len(s), s) 285 case False{} True{}: 286 KNone{} 287 case False{} False{}: 288 kv.plain(colon.find(s, 0n), s) 289 290 # The `key: rest` a line spells, when it spells one. 291 def split_kv(+s: String) -> KV: 292 kv.pick(U32.is_eq(first_code(s), 34) || U32.is_eq(first_code(s), 39), U32.is_eq(first_code(s), 91) || U32.is_eq(first_code(s), 123), s) 293 294 def field.cons(k: String, x: L.JR(), rest: L.JR()) -> L.JR(): 295 match x rest: 296 case Done{a} Done{b}: 297 Done{Json.JField{k, a, b}} 298 case Fail{e} _: 299 Fail{e} 300 case _ Fail{e}: 301 Fail{e} 302 303 def map.piece(kv: KV) -> L.JR(): 304 match kv: 305 case KOk{k, rest}: 306 field.cons(k, atom(rest), Done{Json.JNil{}}) 307 case KBad{m}: 308 Fail{m} 309 case KNone{}: 310 Fail{"expected key: value in a flow mapping"} 311 312 # Merge one piece's field in front of the chain. 313 def map.cons(x: L.JR(), rest: L.JR()) -> L.JR(): 314 match x rest: 315 case Done{Json.JField{k, v, n}} Done{b}: 316 Done{Json.JField{k, v, b}} 317 case Fail{e} _: 318 Fail{e} 319 case _ Fail{e}: 320 Fail{e} 321 case _ _: 322 Fail{"bad flow mapping"} 323 324 def map.items(ps: List<&2, String>) -> L.JR(): 325 match ps: 326 case Nil{}: 327 Done{Json.JNil{}} 328 case Con{+p, r}: 329 map.cons(map.piece(split_kv(p)), map.items(r)) 330 331 def map.fin(closed: Bool, +s: String) -> L.JR(): 332 match closed: 333 case True{}: 334 wrap.obj(map.items(split_top(inner(s)))) 335 case False{}: 336 Fail{"unterminated flow mapping"} 337 338 def flow.pick(isseq: Bool, ismap: Bool, +s: String) -> L.JR(): 339 match isseq ismap: 340 case True{} _: 341 seq.fin(U32.is_eq(last_code(s), 93), s) 342 case False{} True{}: 343 map.fin(U32.is_eq(last_code(s), 125), s) 344 case False{} False{}: 345 atom(s) 346 347 # A value after `key:` or `- `: flow collection, quoted or plain scalar. 348 def scalar(+s: String) -> L.JR(): 349 flow.pick(U32.is_eq(first_code(s), 91), U32.is_eq(first_code(s), 123), s)