value.bend

yaml-for-bend@0.1.1.0 · 333 lines · 28 definitions

1# A YAML value: what a document holds once its scalars are resolved.
2
3import 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".
11type 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
21type Entry is Data:
22  Entry{key: Yaml, val: Yaml}
23
24def 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.
33def 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.
112def 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
139def 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
146def is_text(k: Yaml, +key: String) -> Bool:
147  match k:
148    case Str{t}:
149      String.eq(t, key)
150    case _:
151      False{}
152
153def 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.
163def 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
170def 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"}).
180def 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.
188def at(v: Yaml, key: String) -> Yaml:
189  Maybe.default(&2, Yaml, get(v, key), Null{})
190
191def items(v: Yaml) -> List<&2, Yaml>:
192  match v:
193    case Seq{xs}:
194      xs
195    case _:
196      []
197
198def entries(v: Yaml) -> List<&2, Entry>:
199  match v:
200    case Mapping{es}:
201      es
202    case _:
203      []
204
205def 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
214def keys(v: Yaml) -> List<&2, Yaml>:
215  keys.go(entries(v))
216
217# A string's text.
218def text(v: Yaml) -> Maybe<&2, String>:
219  match v:
220    case Str{t}:
221      Some{t}
222    case _:
223      None{}
224
225def is_null(v: Yaml) -> Bool:
226  match v:
227    case Null{}:
228      True{}
229    case _:
230      False{}
231
232# JSON
233# ----
234
235def 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
238def 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
254def 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.
262def json.str(s: String) -> String:
263  "\"" ++ json.chars(s) ++ "\""
264
265def 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.
282def 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.
289def key.json(str: Bool, +text: String) -> String:
290  match str:
291    case True{}:
292      text
293    case False{}:
294      json.str(text)
295
296def is_str(v: Yaml) -> Bool:
297  match v:
298    case Str{t}:
299      True{}
300    case _:
301      False{}
302
303def 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.
332def json(v: Yaml) -> String:
333  json.go(v, True{})