1 # A YAML value: what a document holds once its scalars are resolved. 2 3 import Base 4 5 # A value. An integer is its decimal text ("-26"), so any size fits. A float 6 # is decimal text too, since Bend has no 64-bit floats: as written under the 7 # Core schema ("1e3", "+.5"), and exact under YAML 1.1 ("1_000.50" is 8 # "1000.5"); infinity and NaN are ".inf", "-.inf" and ".nan". A mapping keeps its entries in order. 9 # Tagged is a value whose tag the schema doesn't resolve: "!point", or 10 # "tag:yaml.org,2002:binary". 11 type Yaml is Data: 12 Null{} 13 Boolean{on: Bool} 14 Int{text: String} 15 Float{text: String} 16 Str{text: String} 17 Seq{items: List<&2, Yaml>} 18 Mapping{entries: List<&2, Entry>} 19 Tagged{tag: String, val: Yaml} 20 21 type Entry is Data: 22 Entry{key: Yaml, val: Yaml} 23 24 def same.bool(a: Bool, b: Bool) -> Bool: 25 match a: 26 case True{}: 27 b 28 case False{}: 29 Bool.not(b) 30 31 # Whether two values are the same: same kinds, same texts, same items and 32 # entries in the same order. 33 def eq(a: Yaml, b: Yaml) -> Bool: 34 match a: 35 case Null{}: 36 match b: 37 case Null{}: 38 True{} 39 case _: 40 False{} 41 case Boolean{x}: 42 match b: 43 case Boolean{y}: 44 same.bool(x, y) 45 case _: 46 False{} 47 case Int{x}: 48 match b: 49 case Int{y}: 50 String.eq(x, y) 51 case _: 52 False{} 53 case Float{x}: 54 match b: 55 case Float{y}: 56 String.eq(x, y) 57 case _: 58 False{} 59 case Str{x}: 60 match b: 61 case Str{y}: 62 String.eq(x, y) 63 case _: 64 False{} 65 case Seq{xs}: 66 match xs: 67 case Nil{}: 68 match b: 69 case Seq{ys}: 70 List.is_empty(&2, Yaml, ys) 71 case _: 72 False{} 73 case Con{x, xt}: 74 match b: 75 case Seq{ys}: 76 match ys: 77 case Nil{}: 78 False{} 79 case Con{y, yt}: 80 Bool.and(eq(x, y), eq(Seq{xt}, Seq{yt})) 81 case _: 82 False{} 83 case Mapping{xs}: 84 match xs: 85 case Nil{}: 86 match b: 87 case Mapping{ys}: 88 List.is_empty(&2, Entry, ys) 89 case _: 90 False{} 91 case Con{Entry{xk, xv}, xt}: 92 match b: 93 case Mapping{ys}: 94 match ys: 95 case Nil{}: 96 False{} 97 case Con{Entry{yk, yv}, yt}: 98 Bool.and(Bool.and(eq(xk, yk), eq(xv, yv)), eq(Mapping{xt}, Mapping{yt})) 99 case _: 100 False{} 101 case Tagged{s, x}: 102 match b: 103 case Tagged{t, y}: 104 Bool.and(String.eq(s, t), eq(x, y)) 105 case _: 106 False{} 107 108 # A value as text that two values share only when they are the same, to 109 # look keys up by. A string is its length and text; "[" goes before each 110 # item of a sequence and "]" after them, and "{" before each entry of a 111 # mapping and "}" after them. 112 def canon(v: Yaml) -> String: 113 match v: 114 case Null{}: 115 "~" 116 case Boolean{on}: 117 Bool.pick(String, on, "t", "f") 118 case Int{t}: 119 "i" ++ t ++ ";" 120 case Float{t}: 121 "d" ++ t ++ ";" 122 case Str{+t}: 123 "s" ++ Nat.show(String.length(t)) ++ ":" ++ t 124 case Seq{xs}: 125 match xs: 126 case Nil{}: 127 "]" 128 case Con{x, rest}: 129 "[" ++ canon(x) ++ canon(Seq{rest}) 130 case Mapping{es}: 131 match es: 132 case Nil{}: 133 "}" 134 case Con{Entry{k, x}, rest}: 135 "{" ++ canon(k) ++ canon(x) ++ canon(Mapping{rest}) 136 case Tagged{+tag, x}: 137 "!" ++ Nat.show(String.length(tag)) ++ ":" ++ tag ++ canon(x) 138 139 def get.hit(hit: Bool, v: Yaml, more: Unit -> Maybe<&2, Yaml>) -> Maybe<&2, Yaml>: 140 match hit: 141 case True{}: 142 Some{v} 143 case False{}: 144 more(Unit{}) 145 146 def is_text(k: Yaml, +key: String) -> Bool: 147 match k: 148 case Str{t}: 149 String.eq(t, key) 150 case _: 151 False{} 152 153 def get.go(es: List<&2, Entry>, +key: String) -> Maybe<&2, Yaml>: 154 match es: 155 case Nil{}: 156 None{} 157 case Con{e, rest}: 158 match e: 159 case Entry{k, v}: 160 get.hit(is_text(k, key), v, u => get.go(rest, key)) 161 162 # The value under the string key +key+, if +v+ is a mapping that has it. 163 def get(v: Yaml, key: String) -> Maybe<&2, Yaml>: 164 match v: 165 case Mapping{es}: 166 get.go(es, key) 167 case _: 168 None{} 169 170 def find.go(es: List<&2, Entry>, +key: Yaml) -> Maybe<&2, Yaml>: 171 match es: 172 case Nil{}: 173 None{} 174 case Con{e, rest}: 175 match e: 176 case Entry{k, v}: 177 get.hit(eq(k, key), v, u => find.go(rest, key)) 178 179 # The value under any key: get(v, "a") is find(v, Str{"a"}). 180 def find(v: Yaml, key: Yaml) -> Maybe<&2, Yaml>: 181 match v: 182 case Mapping{es}: 183 find.go(es, key) 184 case _: 185 None{} 186 187 # The value under +key+, or null. 188 def at(v: Yaml, key: String) -> Yaml: 189 Maybe.default(&2, Yaml, get(v, key), Null{}) 190 191 def items(v: Yaml) -> List<&2, Yaml>: 192 match v: 193 case Seq{xs}: 194 xs 195 case _: 196 [] 197 198 def entries(v: Yaml) -> List<&2, Entry>: 199 match v: 200 case Mapping{es}: 201 es 202 case _: 203 [] 204 205 def keys.go(es: List<&2, Entry>) -> List<&2, Yaml>: 206 match es: 207 case Nil{}: 208 [] 209 case Con{e, rest}: 210 match e: 211 case Entry{k, v}: 212 k <> keys.go(rest) 213 214 def keys(v: Yaml) -> List<&2, Yaml>: 215 keys.go(entries(v)) 216 217 # A string's text. 218 def text(v: Yaml) -> Maybe<&2, String>: 219 match v: 220 case Str{t}: 221 Some{t} 222 case _: 223 None{} 224 225 def is_null(v: Yaml) -> Bool: 226 match v: 227 case Null{}: 228 True{} 229 case _: 230 False{} 231 232 # JSON 233 # ---- 234 235 def hex.digit(+n: Nat) -> Char: 236 Chr{U32.from_nat(Bool.pick(Nat, Nat.is_lt(n, 10n), Nat.add(n, 48n), Nat.add(n, 87n)))} 237 238 def json.char(c: Char) -> String: 239 match c: 240 case '"': 241 "\\\"" 242 case '\\': 243 "\\\\" 244 case '\n': 245 "\\n" 246 case '\t': 247 "\\t" 248 case '\r': 249 "\\r" 250 case Chr{+x}: 251 Bool.pick(String, U32.is_lt(x, 32), "\\u00" ++ SCon{hex.digit(U32.to_nat(U32.div(x, 16))), SCon{hex.digit(U32.to_nat(U32.mod(x, 16))), SNil{}}}, 252 SCon{Chr{x}, SNil{}}) 253 254 def json.chars(s: String) -> String: 255 match s: 256 case SNil{}: 257 "" 258 case SCon{c, t}: 259 json.char(c) ++ json.chars(t) 260 261 # A JSON string. 262 def json.str(s: String) -> String: 263 "\"" ++ json.chars(s) ++ "\"" 264 265 def zero.point(s: String) -> String: 266 match s: 267 case SNil{}: 268 "" 269 case SCon{c, t}: 270 match c: 271 case '.': 272 match t: 273 case SNil{}: 274 ".0" 275 case SCon{+d, +u}: 276 Bool.pick(String, Char.is_digit(d), SCon{'.', SCon{d, u}}, ".0" ++ SCon{d, u}) 277 case _: 278 SCon{c, zero.point(t)} 279 280 # A float in JSON's form: "+.5e3" is 0.5e3; infinity and NaN stay as YAML 281 # writes them, since JSON has no word for them. 282 def json.float(+t: String) -> String: 283 +neg = String.starts_with(t, "-") 284 +body = Bool.pick(String, Bool.or(neg, String.starts_with(t, "+")), String.drop(t, 1n), t) 285 Bool.pick(String, String.starts_with(body, "."), Bool.pick(String, Char.is_digit(Maybe.default(&2, Char, String.get(body, 1n), ' ')), Bool.pick(String, neg, "-0", "0") ++ zero.point(body), t), 286 Bool.pick(String, neg, "-", "") ++ zero.point(body)) 287 288 # A key's JSON: a string as it is, anything else as a string of its JSON. 289 def key.json(str: Bool, +text: String) -> String: 290 match str: 291 case True{}: 292 text 293 case False{}: 294 json.str(text) 295 296 def is_str(v: Yaml) -> Bool: 297 match v: 298 case Str{t}: 299 True{} 300 case _: 301 False{} 302 303 def json.go(v: Yaml, +first: Bool) -> String: 304 match v: 305 case Null{}: 306 "null" 307 case Boolean{on}: 308 Bool.pick(String, on, "true", "false") 309 case Int{t}: 310 t 311 case Float{t}: 312 json.float(t) 313 case Str{t}: 314 json.str(t) 315 case Seq{xs}: 316 match xs: 317 case Nil{}: 318 Bool.pick(String, first, "[]", "]") 319 case Con{x, rest}: 320 Bool.pick(String, first, "[", ",") ++ json.go(x, True{}) ++ json.go(Seq{rest}, False{}) 321 case Mapping{es}: 322 match es: 323 case Nil{}: 324 Bool.pick(String, first, "{}", "}") 325 case Con{Entry{+k, x}, rest}: 326 Bool.pick(String, first, "{", ",") ++ key.json(is_str(k), json.go(k, True{})) ++ ":" ++ json.go(x, True{}) ++ json.go(Mapping{rest}, False{}) 327 case Tagged{tag, x}: 328 json.go(x, True{}) 329 330 # A value as JSON text, on one line. A key that isn't a string is written 331 # as a string of its JSON, and a tag is dropped. 332 def json(v: Yaml) -> String: 333 json.go(v, True{})