json.bend
bend-kit-json@0.3.0.0 · 584 lines · 0 definitions
1# JSON values, parsed and encoded as RFC 8259. Source: https://github.com/paymog/bend-kit/tree/main/json
2import Base
3
4# JSON (RFC 8259) over text; decode bytes with Utf8 first. Nesting has no
5# depth limit other than input size: the parser keeps its own stack, since a
6# Bend def cannot call itself through another def.
7# import ./json/json.bend as Json
8
9# Num keeps the number's exact text, so no precision is lost.
10type Val is Data:
11 Null{}
12 Flag{on: Bool}
13 Num{s: String}
14 Str{s: String}
15 Arr{xs: List<&2, Val>}
16 Obj{m: Map<&2, Val>}
17
18type Next is Data:
19 Next{v: Val, rest: String}
20
21# An open container: array items so far (reversed), or object fields and the
22# key whose value comes next.
23type Open is Data:
24 OArr{xs: List<&2, Val>}
25 OObj{m: Map<&2, Val>, key: String}
26
27type St is Data:
28 SValue{s: String, stack: List<&2, Open>}
29 SKey{s: String, stack: List<&2, Open>}
30 SColon{key: String, s: String, stack: List<&2, Open>}
31 SStr{s: String, acc: String, key: Bool, stack: List<&2, Open>}
32 SAfter{v: Val, s: String, stack: List<&2, Open>}
33 SDone{v: Val, s: String}
34 SFail{}
35
36# Whitespace
37# ----------
38
39def is_ws(+c: U32) -> Bool:
40 Bool.or(U32.is_eq(c, 32), Bool.or(U32.is_eq(c, 9), Bool.or(U32.is_eq(c, 10), U32.is_eq(c, 13))))
41
42# Bool.pick runs both arms, so loops peek: the next char's class is a Bool
43# param and only the matching arm recurses.
44def ws.go(t: String, +c: U32, ws: Bool) -> String:
45 match t:
46 case SNil{}:
47 match ws:
48 case True{}:
49 SNil{}
50 case False{}:
51 SCon{Chr{c}, SNil{}}
52 case SCon{Chr{+d}, u}:
53 match ws:
54 case True{}:
55 ws.go(u, d, is_ws(d))
56 case False{}:
57 SCon{Chr{c}, SCon{Chr{d}, u}}
58
59def skip_ws(s: String) -> String:
60 match s:
61 case SNil{}:
62 SNil{}
63 case SCon{Chr{+c}, t}:
64 ws.go(t, c, is_ws(c))
65
66# Numbers (§6)
67# ------------
68# States: 0 start, 1 after "-", 2 after a leading "0", 3 int digits, 4 after
69# ".", 5 fraction digits, 6 after "e", 7 after the exponent sign, 8 exponent
70# digits. 99 means the char does not continue the number.
71
72def digit(+c: U32) -> Bool:
73 Bool.and(U32.is_le(48, c), U32.is_le(c, 57))
74
75def num.trans(+st: U32, +c: U32) -> U32:
76 +d = digit(c)
77 +z = U32.is_eq(c, 48)
78 +dot = U32.is_eq(c, 46)
79 +e = Bool.or(U32.is_eq(c, 101), U32.is_eq(c, 69))
80 +sign = Bool.or(U32.is_eq(c, 43), U32.is_eq(c, 45))
81 Bool.pick(U32, U32.is_eq(st, 0), Bool.pick(U32, U32.is_eq(c, 45), 1, Bool.pick(U32, z, 2, Bool.pick(U32, d, 3, 99))),
82 Bool.pick(U32, U32.is_eq(st, 1), Bool.pick(U32, z, 2, Bool.pick(U32, d, 3, 99)),
83 Bool.pick(U32, Bool.or(U32.is_eq(st, 2), U32.is_eq(st, 3)), Bool.pick(U32, Bool.and(d, U32.is_eq(st, 3)), 3, Bool.pick(U32, dot, 4, Bool.pick(U32, e, 6, 99))),
84 Bool.pick(U32, Bool.or(U32.is_eq(st, 4), U32.is_eq(st, 5)), Bool.pick(U32, d, 5, Bool.pick(U32, Bool.and(e, U32.is_eq(st, 5)), 6, 99)),
85 Bool.pick(U32, U32.is_eq(st, 6), Bool.pick(U32, sign, 7, Bool.pick(U32, d, 8, 99)),
86 Bool.pick(U32, Bool.or(U32.is_eq(st, 7), U32.is_eq(st, 8)), Bool.pick(U32, d, 8, 99), 99))))))
87
88def num.accepts(+st: U32) -> Bool:
89 Bool.or(Bool.or(U32.is_eq(st, 2), U32.is_eq(st, 3)), Bool.or(U32.is_eq(st, 5), U32.is_eq(st, 8)))
90
91def num.end(+st: U32, acc: String, rest: String) -> Maybe<&2, Next>:
92 Bool.pick(Maybe<&2, Next>, num.accepts(st), Some{Next{Num{String.reverse(acc)}, rest}}, None{})
93
94def num.go(t: String, +c: U32, +st: U32, acc: String, stop: Bool) -> Maybe<&2, Next>:
95 match t:
96 case SNil{}:
97 match stop:
98 case True{}:
99 num.end(st, acc, SCon{Chr{c}, SNil{}})
100 case False{}:
101 num.end(num.trans(st, c), SCon{Chr{c}, acc}, SNil{})
102 case SCon{Chr{+d}, u}:
103 match stop:
104 case True{}:
105 num.end(st, acc, SCon{Chr{c}, SCon{Chr{d}, u}})
106 case False{}:
107 +n = num.trans(st, c)
108 num.go(u, d, n, SCon{Chr{c}, acc}, U32.is_eq(num.trans(n, d), 99))
109
110def num(s: String) -> Maybe<&2, Next>:
111 match s:
112 case SNil{}:
113 None{}
114 case SCon{Chr{+c}, t}:
115 num.go(t, c, 0, SNil{}, U32.is_eq(num.trans(0, c), 99))
116
117# Literals
118# --------
119
120def lit.if(+s: String, v: Val, n: Nat, ok: Bool) -> Maybe<&2, Next>:
121 match ok:
122 case False{}:
123 None{}
124 case True{}:
125 Some{Next{v, String.drop(s, n)}}
126
127def lit(+s: String, word: String, v: Val, +n: Nat) -> Maybe<&2, Next>:
128 lit.if(s, v, n, String.starts_with(s, word))
129
130# Strings (§7)
131# ------------
132
133type Esc is Data:
134 Esc{cp: U32, rest: String}
135
136def hexd(+c: U32) -> U32:
137 Bool.pick(U32, digit(c), (c - 48 : U32), Bool.pick(U32, Bool.and(U32.is_le(97, c), U32.is_le(c, 102)), (c - 87 : U32), Bool.pick(U32, Bool.and(U32.is_le(65, c), U32.is_le(c, 70)), (c - 55 : U32), 99)))
138
139def hex4.go(s: String, +acc: U32, n: Nat) -> Maybe<&2, Esc>:
140 match s:
141 case SNil{}:
142 match n:
143 case 0n:
144 Some{Esc{acc, SNil{}}}
145 case 1n+k:
146 None{}
147 case SCon{Chr{+c}, t}:
148 match n:
149 case 0n:
150 Some{Esc{acc, SCon{Chr{c}, t}}}
151 case 1n+k:
152 +h = hexd(c)
153 Bool.pick(Maybe<&2, Esc>, U32.is_eq(h, 99), None{}, hex4.go(t, (acc * 16 + h : U32), k))
154
155def hex4(s: String) -> Maybe<&2, Esc>:
156 hex4.go(s, 0, 4n)
157
158def is_hi(+v: U32) -> Bool:
159 Bool.and(U32.is_le(55296, v), U32.is_le(v, 56319))
160
161def is_lo(+v: U32) -> Bool:
162 Bool.and(U32.is_le(56320, v), U32.is_le(v, 57343))
163
164def pair.lo(+hi: U32, +rest: String, lo: Maybe<&2, Esc>) -> Esc:
165 match lo:
166 case None{}:
167 Esc{65533, rest}
168 case Some{Esc{+v, r}}:
169 Bool.pick(Esc, is_lo(v), Esc{(65536 + (hi - 55296 : U32) * 1024 + (v - 56320 : U32) : U32), r}, Esc{65533, rest})
170
171# §8.2: a high surrogate pairs with a following \uDC00-\uDFFF; a lone one is U+FFFD.
172def pair(+hi: U32, +rest: String) -> Esc:
173 Bool.pick(Esc, String.starts_with(rest, "\\u"), pair.lo(hi, rest, hex4(String.drop(rest, 2n))), Esc{65533, rest})
174
175def esc.u(e: Maybe<&2, Esc>) -> Maybe<&2, Esc>:
176 match e:
177 case None{}:
178 None{}
179 case Some{Esc{+v, +r}}:
180 Some{Bool.pick(Esc, is_hi(v), pair(v, r), Bool.pick(Esc, is_lo(v), Esc{65533, r}, Esc{v, r}))}
181
182def esc.simple(+e: U32) -> U32:
183 Bool.pick(U32, U32.is_eq(e, 34), 34, Bool.pick(U32, U32.is_eq(e, 92), 92, Bool.pick(U32, U32.is_eq(e, 47), 47,
184 Bool.pick(U32, U32.is_eq(e, 98), 8, Bool.pick(U32, U32.is_eq(e, 102), 12, Bool.pick(U32, U32.is_eq(e, 110), 10,
185 Bool.pick(U32, U32.is_eq(e, 114), 13, Bool.pick(U32, U32.is_eq(e, 116), 9, 99))))))))
186
187# One escape after the backslash.
188def esc.one(t: String) -> Maybe<&2, Esc>:
189 match t:
190 case SNil{}:
191 None{}
192 case SCon{Chr{+e}, +u}:
193 +v = esc.simple(e)
194 Bool.pick(Maybe<&2, Esc>, U32.is_eq(e, 117), esc.u(hex4(u)), Bool.pick(Maybe<&2, Esc>, U32.is_eq(v, 99), None{}, Some{Esc{v, u}}))
195
196def str.esc(e: Maybe<&2, Esc>, acc: String, key: Bool, stack: List<&2, Open>) -> St:
197 match e:
198 case None{}:
199 SFail{}
200 case Some{Esc{cp, r}}:
201 SStr{r, SCon{Chr{cp}, acc}, key, stack}
202
203def str.close(+acc: String, t: String, key: Bool, stack: List<&2, Open>) -> St:
204 match key:
205 case True{}:
206 SColon{String.reverse(acc), t, stack}
207 case False{}:
208 SAfter{Str{String.reverse(acc)}, t, stack}
209
210def str.c(quote: Bool, ctl: Bool, bs: Bool, c: U32, t: String, acc: String, key: Bool, stack: List<&2, Open>) -> St:
211 match quote:
212 case True{}:
213 str.close(acc, t, key, stack)
214 case False{}:
215 match ctl:
216 case True{}:
217 SFail{}
218 case False{}:
219 match bs:
220 case True{}:
221 str.esc(esc.one(t), acc, key, stack)
222 case False{}:
223 SStr{t, SCon{Chr{c}, acc}, key, stack}
224
225# §7: unescaped chars below U+0020 are not allowed in a string.
226def str.step(s: String, acc: String, key: Bool, stack: List<&2, Open>) -> St:
227 match s:
228 case SNil{}:
229 SFail{}
230 case SCon{Chr{+c}, t}:
231 str.c(U32.is_eq(c, 34), U32.is_lt(c, 32), U32.is_eq(c, 92), c, t, acc, key, stack)
232
233# Machine
234# -------
235
236def got(m: Maybe<&2, Next>, stack: List<&2, Open>) -> St:
237 match m:
238 case None{}:
239 SFail{}
240 case Some{Next{v, r}}:
241 SAfter{v, r, stack}
242
243def open.close(r: String, close: Val, stack: List<&2, Open>, empty: Bool, open: Open) -> St:
244 match empty:
245 case True{}:
246 SAfter{close, String.drop(r, 1n), stack}
247 case False{}:
248 match open:
249 case OArr{xs}:
250 SValue{r, Con{OArr{xs}, stack}}
251 case OObj{m, k}:
252 SKey{r, Con{OObj{m, k}, stack}}
253
254def value.step(+s: String, +c: U32, +t: String, +stack: List<&2, Open>) -> St:
255 +r = skip_ws(t)
256 Bool.pick(St, U32.is_eq(c, 91), open.close(r, Arr{Nil{}}, stack, String.starts_with(r, "]"), OArr{Nil{}}),
257 Bool.pick(St, U32.is_eq(c, 123), open.close(r, Obj{Map.new(&2, Val)}, stack, String.starts_with(r, "}"), OObj{Map.new(&2, Val), ""}),
258 Bool.pick(St, U32.is_eq(c, 34), SStr{t, SNil{}, False{}, stack},
259 Bool.pick(St, U32.is_eq(c, 116), got(lit(s, "true", Flag{True{}}, 4n), stack),
260 Bool.pick(St, U32.is_eq(c, 102), got(lit(s, "false", Flag{False{}}, 5n), stack),
261 Bool.pick(St, U32.is_eq(c, 110), got(lit(s, "null", Null{}, 4n), stack),
262 got(num(s), stack)))))))
263
264def value(s: String, stack: List<&2, Open>) -> St:
265 match s:
266 case SNil{}:
267 SFail{}
268 case SCon{Chr{+c}, +t}:
269 value.step(SCon{Chr{c}, t}, c, t, stack)
270
271def key(s: String, stack: List<&2, Open>) -> St:
272 match s:
273 case SNil{}:
274 SFail{}
275 case SCon{Chr{+c}, t}:
276 Bool.pick(St, U32.is_eq(c, 34), SStr{t, SNil{}, True{}, stack}, SFail{})
277
278def colon.top(k: String, r: String, stack: List<&2, Open>) -> St:
279 match stack:
280 case Nil{}:
281 SFail{}
282 case Con{OArr{xs}, up}:
283 SFail{}
284 case Con{OObj{m, old}, up}:
285 SValue{r, Con{OObj{m, k}, up}}
286
287def colon(k: String, s: String, stack: List<&2, Open>) -> St:
288 match s:
289 case SNil{}:
290 SFail{}
291 case SCon{Chr{+c}, t}:
292 Bool.pick(St, U32.is_eq(c, 58), colon.top(k, t, stack), SFail{})
293
294# A finished value joins its container; then "," or the closing bracket.
295def after.arr.c(comma: Bool, close: Bool, t: String, xs: List<&2, Val>, up: List<&2, Open>) -> St:
296 match comma:
297 case True{}:
298 SValue{t, Con{OArr{xs}, up}}
299 case False{}:
300 match close:
301 case True{}:
302 SAfter{Arr{List.reverse(&2, Val, xs)}, t, up}
303 case False{}:
304 SFail{}
305
306def after.arr(xs: List<&2, Val>, r: String, up: List<&2, Open>) -> St:
307 match r:
308 case SNil{}:
309 SFail{}
310 case SCon{Chr{+c}, t}:
311 after.arr.c(U32.is_eq(c, 44), U32.is_eq(c, 93), t, xs, up)
312
313def after.obj.c(comma: Bool, close: Bool, t: String, m: Map<&2, Val>, up: List<&2, Open>) -> St:
314 match comma:
315 case True{}:
316 SKey{t, Con{OObj{m, ""}, up}}
317 case False{}:
318 match close:
319 case True{}:
320 SAfter{Obj{m}, t, up}
321 case False{}:
322 SFail{}
323
324def after.obj(m: Map<&2, Val>, r: String, up: List<&2, Open>) -> St:
325 match r:
326 case SNil{}:
327 SFail{}
328 case SCon{Chr{+c}, t}:
329 after.obj.c(U32.is_eq(c, 44), U32.is_eq(c, 125), t, m, up)
330
331def after(v: Val, s: String, stack: List<&2, Open>) -> St:
332 match stack:
333 case Nil{}:
334 SDone{v, s}
335 case Con{OArr{xs}, up}:
336 after.arr(Con{v, xs}, skip_ws(s), up)
337 case Con{OObj{m, k}, up}:
338 after.obj(Map.set(&2, Val, m, k, v), skip_ws(s), up)
339
340def step(st: St) -> St:
341 match st:
342 case SValue{s, stack}:
343 value(skip_ws(s), stack)
344 case SKey{s, stack}:
345 key(skip_ws(s), stack)
346 case SColon{k, s, stack}:
347 colon(k, skip_ws(s), stack)
348 case SStr{s, acc, k, stack}:
349 str.step(s, acc, k, stack)
350 case SAfter{v, s, stack}:
351 after(v, s, stack)
352 case SDone{v, s}:
353 SDone{v, s}
354 case SFail{}:
355 SFail{}
356
357def done(v: Val, rest: String) -> Maybe<&2, Val>:
358 match rest:
359 case SNil{}:
360 Some{v}
361 case SCon{h, t}:
362 None{}
363
364# fuel: every step but the last eats at least one char.
365def run(fuel: Nat, st: St) -> Maybe<&2, Val>:
366 match fuel:
367 case 0n:
368 None{}
369 case 1n+f:
370 match st:
371 case SDone{v, s}:
372 done(v, skip_ws(s))
373 case SFail{}:
374 None{}
375 case SValue{s, stack}:
376 run(f, step(SValue{s, stack}))
377 case SKey{s, stack}:
378 run(f, step(SKey{s, stack}))
379 case SColon{k, s, stack}:
380 run(f, step(SColon{k, s, stack}))
381 case SStr{s, acc, k, stack}:
382 run(f, step(SStr{s, acc, k, stack}))
383 case SAfter{v, s, stack}:
384 run(f, step(SAfter{v, s, stack}))
385
386# One JSON text: a value with optional whitespace around it.
387def parse(+s: String) -> Maybe<&2, Val>:
388 run(Nat.add(String.length(s), 2n), SValue{s, Nil{}})
389
390# Access
391# ------
392
393def get.hit(r: Map<&2, Val> & Bool) -> Bool:
394 (m, b) = r
395 b
396
397def get.some(r: Map<&2, Val> & Val) -> Maybe<&2, Val>:
398 (m, x) = r
399 Some{x}
400
401def get.has(+m: Map<&2, Val>, +k: String, hit: Bool) -> Maybe<&2, Val>:
402 match hit:
403 case False{}:
404 None{}
405 case True{}:
406 get.some(Map.get(Val, Null{}, m, k))
407
408def get.of(+m: Map<&2, Val>, +k: String) -> Maybe<&2, Val>:
409 get.has(m, k, get.hit(Map.has(&2, Val, m, k)))
410
411# The value under key k of an object.
412def get(v: Val, k: String) -> Maybe<&2, Val>:
413 match v:
414 case Obj{m}:
415 get.of(m, k)
416 case Null{}:
417 None{}
418 case Flag{on}:
419 None{}
420 case Num{n}:
421 None{}
422 case Str{s}:
423 None{}
424 case Arr{xs}:
425 None{}
426
427# Encode
428# ------
429
430def hexc(+n: U32) -> Char:
431 Chr{Bool.pick(U32, U32.is_lt(n, 10), (48 + n : U32), (87 + n : U32))}
432
433# The escaped form of one char, reversed (it is pushed onto a reversed acc).
434def esc.put(+c: U32, +acc: String) -> String:
435 Bool.pick(String, U32.is_eq(c, 34), SCon{Chr{34}, SCon{Chr{92}, acc}},
436 Bool.pick(String, U32.is_eq(c, 92), SCon{Chr{92}, SCon{Chr{92}, acc}},
437 Bool.pick(String, U32.is_eq(c, 10), SCon{Chr{110}, SCon{Chr{92}, acc}},
438 Bool.pick(String, U32.is_eq(c, 13), SCon{Chr{114}, SCon{Chr{92}, acc}},
439 Bool.pick(String, U32.is_eq(c, 9), SCon{Chr{116}, SCon{Chr{92}, acc}},
440 Bool.pick(String, U32.is_lt(c, 32), SCon{hexc(U32.and(c, 15)), SCon{hexc(U32.shrn(c, 4n)), SCon{Chr{48}, SCon{Chr{48}, SCon{Chr{117}, SCon{Chr{92}, acc}}}}}},
441 SCon{Chr{c}, acc}))))))
442
443def esc.go(s: String, acc: String) -> String:
444 match s:
445 case SNil{}:
446 String.reverse(acc)
447 case SCon{Chr{+c}, t}:
448 esc.go(t, esc.put(c, acc))
449
450def esc(s: String) -> String:
451 esc.go(s, SNil{})
452
453type Item is Data:
454 ILit{s: String}
455 IVal{v: Val}
456
457def items.sep(first: Bool, tail: List<&2, Item>) -> List<&2, Item>:
458 match first:
459 case True{}:
460 tail
461 case False{}:
462 Con{ILit{","}, tail}
463
464def items.arr(xs: List<&2, Val>, first: Bool, rest: List<&2, Item>) -> List<&2, Item>:
465 match xs:
466 case Nil{}:
467 Con{ILit{"]"}, rest}
468 case Con{x, t}:
469 items.sep(first, Con{IVal{x}, items.arr(t, False{}, rest)})
470
471def items.obj(kvs: List<&2, Sigma<&2, &2, String, _ => Val>>, first: Bool, rest: List<&2, Item>) -> List<&2, Item>:
472 match kvs:
473 case Nil{}:
474 Con{ILit{"}"}, rest}
475 case (k, v) <> t:
476 items.sep(first, Con{ILit{"\"" ++ esc(k) ++ "\":"}, Con{IVal{v}, items.obj(t, False{}, rest)}})
477
478def put(s: String, racc: String) -> String:
479 String.reverse(s) ++ racc
480
481# ponytail: @unsafe because it walks a work list, not the Val; each step
482# emits text or swaps a container for its strictly smaller parts.
483@unsafe
484def enc.go(items: List<&2, Item>, racc: String) -> String:
485 match items:
486 case Nil{}:
487 String.reverse(racc)
488 case Con{ILit{s}, t}:
489 enc.go(t, put(s, racc))
490 case Con{IVal{Null{}}, t}:
491 enc.go(t, put("null", racc))
492 case Con{IVal{Flag{True{}}}, t}:
493 enc.go(t, put("true", racc))
494 case Con{IVal{Flag{False{}}}, t}:
495 enc.go(t, put("false", racc))
496 case Con{IVal{Num{n}}, t}:
497 enc.go(t, put(n, racc))
498 case Con{IVal{Str{s}}, t}:
499 enc.go(t, put("\"" ++ esc(s) ++ "\"", racc))
500 case Con{IVal{Arr{xs}}, t}:
501 enc.go(Con{ILit{"["}, items.arr(xs, True{}, t)}, racc)
502 case Con{IVal{Obj{m}}, t}:
503 enc.go(Con{ILit{"{"}, items.obj(Map.to_list(&2, Val, m), True{}, t)}, racc)
504
505# Compact JSON; object keys come out sorted.
506def encode(v: Val) -> String:
507 enc.go(Con{IVal{v}, Nil{}}, "")
508
509def at.go(xs: List<&2, Val>, n: Nat) -> Maybe<&2, Val>:
510 match xs:
511 case Nil{}:
512 None{}
513 case Con{v, t}:
514 match n:
515 case 0n:
516 Some{v}
517 case 1n+p:
518 at.go(t, p)
519
520
521def at(v: Val, n: Nat) -> Maybe<&2, Val>:
522 match v:
523 case Arr{xs}:
524 at.go(xs, n)
525 case Null{}:
526 None{}
527 case Flag{on}:
528 None{}
529 case Num{s}:
530 None{}
531 case Str{s}:
532 None{}
533 case Obj{m}:
534 None{}
535
536def u32.over(+acc: U32, +c: U32) -> Bool:
537 Bool.or(U32.is_lt(429496729, acc), Bool.and(U32.is_eq(acc, 429496729), U32.is_lt(53, c)))
538
539def u32.dig(s: String, +acc: U32, bad: Bool) -> Maybe<&2, U32>:
540 match s:
541 case SNil{}:
542 match bad:
543 case True{}:
544 None{}
545 case False{}:
546 Some{acc}
547 case SCon{Chr{+c}, t}:
548 u32.dig(t, (acc * 10 + (c - 48 : U32) : U32), Bool.or(bad, Bool.or(Bool.not(digit(c)), u32.over(acc, c))))
549
550def u32.zero(t: String) -> Maybe<&2, U32>:
551 match t:
552 case SNil{}:
553 Some{0}
554 case SCon{d, u}:
555 None{}
556
557def u32.lead(+c: U32, t: String, zero: Bool) -> Maybe<&2, U32>:
558 match zero:
559 case True{}:
560 u32.zero(t)
561 case False{}:
562 u32.dig(t, (c - 48 : U32), Bool.or(Bool.not(digit(c)), u32.over(0, c)))
563
564def u32.num(s: String) -> Maybe<&2, U32>:
565 match s:
566 case SNil{}:
567 None{}
568 case SCon{Chr{+c}, t}:
569 u32.lead(c, t, U32.is_eq(c, 48))
570
571def u32(v: Val) -> Maybe<&2, U32>:
572 match v:
573 case Num{s}:
574 u32.num(s)
575 case Null{}:
576 None{}
577 case Flag{on}:
578 None{}
579 case Str{s}:
580 None{}
581 case Arr{xs}:
582 None{}
583 case Obj{m}:
584 None{}