src/yaml.bend

bend-kube-runtime@0.2.0.0 · 1,157 lines · 150 definitions

1import Base
2import bend-kube-runtime@0.2.0.0/src/json.bend as J
3
4# YAML
5# ====
6#
7# The block YAML that kubeconfigs are written in, read into JSON, so a
8# kubeconfig needs no kubectl to load. Covered: block mappings and
9# sequences (a sequence may sit at its parent key's indentation, as
10# kubectl writes it), "- key: value" items, plain, 'single' and "double"
11# quoted scalars, null/~/true/false, comments, "---", and flow values that
12# are JSON ({}, [], ["a", "b"]). Not covered: anchors, tags, block
13# scalars (| >), multi-document streams, and non-JSON flow collections.
14#
15# A pushdown automaton over lines: open containers sit on a stack, each
16# with the indentation it was opened at, and a line first closes every
17# container deeper than itself.
18
19type Line is Data:
20  Line{indent: U32, text: String}
21
22# An open container: a mapping (members reversed, and the key whose
23# value is still to come) or a sequence (items reversed, and, after a bare
24# "- ", the anchor of the item whose block is still to come).
25type YFrame is Data:
26  YMap{indent: U32, members: List<&2, J.KV>, pend: Maybe<&2, String>}
27  YSeq{indent: U32, items: List<&2, J.Json>, pend: Maybe<&2, String>}
28  YRoot{value: J.Json}
29
30# mode: 0 between tokens, 1 in "..", 2 in '..', 3 in a plain scalar;
31# buf: the plain scalar so far (reversed); out: JSON text (reversed).
32type Flow is Data:
33  Flow{mode: U32, buf: String, out: String, esc: Bool}
34
35# A block scalar being read: its key line's indent and text before the
36# indicator, literal (|) or folded (>), chomping (0 clip, 1 strip -, 2
37# keep +), the content's indent (0 until its first line), and its lines
38# so far (reversed, the content indent removed).
39type Blk is Data:
40  Blk{ki: U32, prefix: String, folded: Bool, chomp: U32, bi: U32, acc: List<&2, String>}
41
42type BH is Data:
43  BH{prefix: String, folded: Bool, chomp: U32}
44
45# The line fold: lines out (reversed), and the block scalar, if inside one.
46type LS is Data:
47  LS{out: List<&2, Line>, blk: Maybe<&2, Blk>}
48
49# A value with its anchor (&name) split off.
50type AV is Data:
51  AV{anchor: String, rest: String}
52
53type YR is Data:
54  YR{json: J.Json, anchors: Map<&2, J.Json>}
55
56# Lines
57# -----
58
59def Yaml.indent(s: String, +n: U32) -> U32:
60  match s:
61    case SCon{' ', t}:
62      Yaml.indent(t, (n + 1 : U32))
63    case _:
64      n
65
66def Yaml.quote.go(+q: U32, +k: U32, none: Bool, same: Bool) -> U32:
67  match none same:
68    case True{} _:
69      k
70    case False{} True{}:
71      0
72    case _ _:
73      q
74
75# Entering or leaving a quote of kind k (1 single, 2 double).
76def Yaml.quote(+q: U32, +k: U32) -> U32:
77  Yaml.quote.go(q, k, U32.is_eq(q, 0), U32.is_eq(q, k))
78
79# The quote state after char c.
80def Yaml.nq(+c: Char, +q: U32) -> U32:
81  match c:
82    case '"':
83      Yaml.quote(q, 2)
84    case '\'':
85      Yaml.quote(q, 1)
86    case _:
87      q
88
89# Whether a comment starts at the head of s: a '#' after a space, outside
90# quotes.
91def Yaml.cut_at(s: String, +q: U32, prev: Bool) -> Bool:
92  match s:
93    case SCon{'#', t}:
94      Bool.and(prev, U32.is_eq(q, 0))
95    case _:
96      False{}
97
98# Drops a comment, to the end of the line.
99def Yaml.uncomment(s: String, +q: U32, cut: Bool) -> String:
100  match s cut:
101    case _ True{}:
102      SNil{}
103    case SNil{} False{}:
104      SNil{}
105    case SCon{+c, +t} False{}:
106      SCon{c, Yaml.uncomment(t, Yaml.nq(c, q), Yaml.cut_at(t, Yaml.nq(c, q), Char.is_space(c)))}
107
108def Yaml.keep(+t: String) -> Bool:
109  Bool.and(Bool.not(String.is_empty(t)), Bool.not(String.eq(t, "---")))
110
111def Yaml.lines.put.go(+i: U32, text: String, rest: List<&2, Line>, keep: Bool) -> List<&2, Line>:
112  match keep:
113    case True{}:
114      Line{i, text} <> rest
115    case False{}:
116      rest
117
118def Yaml.lines.put(+i: U32, +text: String, rest: List<&2, Line>) -> List<&2, Line>:
119  Yaml.lines.put.go(i, text, rest, Yaml.keep(text))
120
121# A block scalar's header ("key: |", "- >-", ...): the text before the
122# indicator, its style and chomping.
123def Yaml.head.go(r: String) -> Maybe<&2, BH>:
124  match r:
125    case SCon{'|', SCon{' ', t}}:
126      Some{BH{String.reverse(t) ++ " ", False{}, 0}}
127    case SCon{'>', SCon{' ', t}}:
128      Some{BH{String.reverse(t) ++ " ", True{}, 0}}
129    case SCon{'-', SCon{'|', SCon{' ', t}}}:
130      Some{BH{String.reverse(t) ++ " ", False{}, 1}}
131    case SCon{'-', SCon{'>', SCon{' ', t}}}:
132      Some{BH{String.reverse(t) ++ " ", True{}, 1}}
133    case SCon{'+', SCon{'|', SCon{' ', t}}}:
134      Some{BH{String.reverse(t) ++ " ", False{}, 2}}
135    case SCon{'+', SCon{'>', SCon{' ', t}}}:
136      Some{BH{String.reverse(t) ++ " ", True{}, 2}}
137    case _:
138      None{}
139
140def Yaml.head(t: String) -> Maybe<&2, BH>:
141  Yaml.head.go(String.reverse(t))
142
143def Yaml.clean(+l: String) -> String:
144  String.trim(Yaml.uncomment(l, 0, Yaml.cut_at(l, 0, True{})))
145
146# Trailing blank lines (of a reversed list): how many, and the rest.
147def Yaml.blanks(xs: List<&2, String>, +n: U32) -> U32:
148  match xs:
149    case Con{SNil{}, t}:
150      Yaml.blanks(t, (n + 1 : U32))
151    case _:
152      n
153
154def Yaml.unblank(xs: List<&2, String>) -> List<&2, String>:
155  match xs:
156    case Con{SNil{}, t}:
157      Yaml.unblank(t)
158    case x:
159      x
160
161def Yaml.fold.sep(prev: Bool) -> String:
162  match prev:
163    case True{}:
164      " "
165    case False{}:
166      ""
167
168# Folded lines: a line break between two text lines becomes a space; blank
169# lines stay line breaks.
170def Yaml.fold(xs: List<&2, String>, prev: Bool) -> String:
171  match xs:
172    case Nil{}:
173      ""
174    case Con{SNil{}, t}:
175      "\n" ++ Yaml.fold(t, False{})
176    case Con{l, t}:
177      Yaml.fold.sep(prev) ++ l ++ Yaml.fold(t, True{})
178
179def Yaml.block.body(lines: List<&2, String>, folded: Bool) -> String:
180  match folded:
181    case True{}:
182      Yaml.fold(lines, False{})
183    case False{}:
184      String.join(lines, "\n")
185
186def Yaml.block.tail.go(+chomp: U32, +n: U32, strip: Bool, keep: Bool) -> String:
187  match strip keep:
188    case True{} _:
189      ""
190    case False{} True{}:
191      String.repeat("\n", U32.to_nat((n + 1 : U32)))
192    case _ _:
193      "\n"
194
195def Yaml.block.tail(+chomp: U32, +n: U32, empty: Bool) -> String:
196  match empty:
197    case True{}:
198      ""
199    case False{}:
200      Yaml.block.tail.go(chomp, n, U32.is_eq(chomp, 1), U32.is_eq(chomp, 2))
201
202# A finished block scalar, as the one line "prefix <JSON string>".
203def Yaml.block.line(b: Blk) -> Line:
204  match b:
205    case Blk{ki, prefix, folded, +chomp, bi, +acc}:
206      Line{ki, prefix ++ J.Json.show(J.JStr{
207        Yaml.block.body(List.reverse(&2, String, Yaml.unblank(acc)), folded)
208          ++ Yaml.block.tail(chomp, Yaml.blanks(acc, 0), List.is_empty(&2, String, Yaml.unblank(acc)))})}
209
210def Yaml.normal.go(out: List<&2, Line>, +i: U32, +t: String, h: Maybe<&2, BH>) -> LS:
211  match h:
212    case Some{BH{p, f, c}}:
213      LS{out, Some{Blk{i, p, f, c, 0, Nil{}}}}
214    case None{}:
215      LS{Yaml.lines.put(i, t, out), None{}}
216
217# An ordinary line: kept if meaningful, or the start of a block scalar.
218def Yaml.normal(out: List<&2, Line>, +raw: String) -> LS:
219  Yaml.normal.go(out, Yaml.indent(raw, 0), Yaml.clean(raw), Yaml.head(Yaml.clean(raw)))
220
221def Yaml.in_block.go(out: List<&2, Line>, b: Blk, +raw: String, blank: Bool, deeper: Bool,
222  first: Bool) -> LS:
223  match b blank deeper first:
224    case Blk{ki, p, f, c, bi, acc} True{} _ _:
225      LS{out, Some{Blk{ki, p, f, c, bi, SNil{} <> acc}}}
226    case Blk{ki, p, f, c, bi, acc} False{} True{} True{}:
227      LS{out, Some{Blk{ki, p, f, c, Yaml.indent(raw, 0), String.drop(raw, U32.to_nat(Yaml.indent(raw, 0))) <> acc}}}
228    case Blk{ki, p, f, c, +bi, acc} False{} True{} False{}:
229      LS{out, Some{Blk{ki, p, f, c, bi, String.drop(raw, U32.to_nat(bi)) <> acc}}}
230    case +x False{} False{} _:
231      Yaml.normal(Yaml.block.line(x) <> out, raw)
232
233# A raw line inside a block scalar: content while deeper than the key (or
234# blank), else the block ends and the line is read as ordinary.
235def Yaml.in_block(out: List<&2, Line>, b: Blk, +raw: String) -> LS:
236  match b:
237    case Blk{+ki, p, f, c, +bi, acc}:
238      Yaml.in_block.go(out, Blk{ki, p, f, c, bi, acc}, raw, String.is_empty(String.trim(raw)),
239        U32.is_gt(Yaml.indent(raw, 0), ki), U32.is_eq(bi, 0))
240
241def Yaml.step_line(st: LS, +raw: String) -> LS:
242  match st:
243    case LS{out, None{}}:
244      Yaml.normal(out, raw)
245    case LS{out, Some{b}}:
246      Yaml.in_block(out, b, raw)
247
248def Yaml.lines.fold(ls: List<&2, String>, st: LS) -> LS:
249  match ls:
250    case Nil{}:
251      st
252    case Con{l, t}:
253      Yaml.lines.fold(t, Yaml.step_line(st, l))
254
255def Yaml.lines.fin(st: LS) -> List<&2, Line>:
256  match st:
257    case LS{out, None{}}:
258      List.reverse(&2, Line, out)
259    case LS{out, Some{b}}:
260      List.reverse(&2, Line, Yaml.block.line(b) <> out)
261
262# The meaningful lines: indentation and text, comments and blanks gone,
263# block scalars (| and >) folded into one line each.
264def Yaml.lines(s: String) -> List<&2, Line>:
265  Yaml.lines.fin(Yaml.lines.fold(String.lines(s), LS{Nil{}, None{}}))
266
267# Scalars
268# -------
269
270def Yaml.unsingle(s: String) -> String:
271  match s:
272    case SNil{}:
273      SNil{}
274    case SCon{'\'', SCon{'\'', t}}:
275      SCon{'\'', Yaml.unsingle(t)}
276    case SCon{c, t}:
277      SCon{c, Yaml.unsingle(t)}
278
279def Yaml.strip_ends(+s: String) -> String:
280  String.take(String.drop(s, 1n), Nat.sub(String.length(s), 2n))
281
282def Yaml.json_or(r: Result<&2, &2, String, J.Json>, s: String) -> J.Json:
283  match r:
284    case Done{j}:
285      j
286    case Fail{e}:
287      J.JStr{s}
288
289# Numbers, as YAML 1.2's core schema reads them:
290# [-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?, read a character
291# at a time. The digits so far are kept reversed.
292type Num is Data:
293  NStart{}
294  NSign{neg: Bool}
295  NInt{neg: Bool, int: List<&2, Char>}
296  NDot0{neg: Bool}
297  NFrac{neg: Bool, int: List<&2, Char>, frac: List<&2, Char>}
298  NExp0{neg: Bool, int: List<&2, Char>, frac: List<&2, Char>, exp: List<&2, Char>}
299  NExp1{neg: Bool, int: List<&2, Char>, frac: List<&2, Char>, exp: List<&2, Char>}
300  NExpD{neg: Bool, int: List<&2, Char>, frac: List<&2, Char>, exp: List<&2, Char>}
301  NBad{}
302
303def Num.step(st: Num, +c: Char, dig: Bool, dot: Bool, ex: Bool, sg: Bool) -> Num:
304  match st dig dot ex sg:
305    case NStart{} True{} d e s:
306      NInt{False{}, [c]}
307    case NStart{} False{} True{} e s:
308      NDot0{False{}}
309    case NStart{} False{} False{} e True{}:
310      NSign{Char.is_eq(c, '-')}
311    case NStart{} False{} False{} e False{}:
312      NBad{}
313    case NSign{n} True{} d e s:
314      NInt{n, [c]}
315    case NSign{n} False{} True{} e s:
316      NDot0{n}
317    case NSign{n} False{} False{} e s:
318      NBad{}
319    case NInt{n, i} True{} d e s:
320      NInt{n, Con{c, i}}
321    case NInt{n, i} False{} True{} e s:
322      NFrac{n, i, Nil{}}
323    case NInt{n, i} False{} False{} True{} s:
324      NExp0{n, i, Nil{}, [c]}
325    case NInt{n, i} False{} False{} False{} s:
326      NBad{}
327    case NDot0{n} True{} d e s:
328      NFrac{n, Nil{}, [c]}
329    case NDot0{n} False{} d e s:
330      NBad{}
331    case NFrac{n, i, f} True{} d e s:
332      NFrac{n, i, Con{c, f}}
333    case NFrac{n, i, f} False{} d True{} s:
334      NExp0{n, i, f, [c]}
335    case NFrac{n, i, f} False{} d False{} s:
336      NBad{}
337    case NExp0{n, i, f, x} True{} d e s:
338      NExpD{n, i, f, Con{c, x}}
339    case NExp0{n, i, f, x} False{} d e True{}:
340      NExp1{n, i, f, Con{c, x}}
341    case NExp0{n, i, f, x} False{} d e False{}:
342      NBad{}
343    case NExp1{n, i, f, x} True{} d e s:
344      NExpD{n, i, f, Con{c, x}}
345    case NExp1{n, i, f, x} False{} d e s:
346      NBad{}
347    case NExpD{n, i, f, x} True{} d e s:
348      NExpD{n, i, f, Con{c, x}}
349    case NExpD{n, i, f, x} False{} d e s:
350      NBad{}
351    case NBad{} g d e s:
352      NBad{}
353
354def Num.go(s: String, st: Num) -> Num:
355  match s:
356    case SNil{}:
357      st
358    case SCon{+c, t}:
359      Num.go(t, Num.step(st, c, Char.is_digit(c), Char.is_eq(c, '.'),
360        Bool.or(Char.is_eq(c, 'e'), Char.is_eq(c, 'E')),
361        Bool.or(Char.is_eq(c, '-'), Char.is_eq(c, '+'))))
362
363def Num.sign(neg: Bool) -> String:
364  match neg:
365    case True{}:
366      "-"
367    case False{}:
368      ""
369
370# The integer digits without leading zeros, "0" for none.
371def Num.int(cs: List<&2, Char>) -> String:
372  match cs:
373    case Nil{}:
374      "0"
375    case Con{'0', t}:
376      Num.int(t)
377    case Con{c, t}:
378      String.from_list(Con{c, t})
379
380def Num.frac(f: List<&2, Char>) -> String:
381  match f:
382    case Nil{}:
383      ""
384    case Con{c, t}:
385      "." ++ String.from_list(List.reverse(&2, Char, Con{c, t}))
386
387# The number as JSON writes it, or None when s is not one.
388def Num.fin(st: Num) -> Maybe<&2, String>:
389  match st:
390    case NInt{n, i}:
391      Some{Num.sign(n) ++ Num.int(List.reverse(&2, Char, i))}
392    case NFrac{n, i, f}:
393      Some{Num.sign(n) ++ Num.int(List.reverse(&2, Char, i)) ++ Num.frac(f)}
394    case NExpD{n, i, f, x}:
395      Some{Num.sign(n) ++ Num.int(List.reverse(&2, Char, i)) ++ Num.frac(f) ++
396        String.from_list(List.reverse(&2, Char, x))}
397    case _:
398      None{}
399
400def Num.parse(s: String) -> Maybe<&2, String>:
401  Num.fin(Num.go(s, NStart{}))
402
403def Yaml.plain.num(x: String, n: Maybe<&2, String>) -> J.Json:
404  match n:
405    case Some{t}:
406      J.JNum{t}
407    case None{}:
408      J.JStr{x}
409
410# A plain scalar: null, ~, true, false, or a string.
411def Yaml.plain(s: String) -> J.Json:
412  match s:
413    case "null":
414      J.JNull{}
415    case "~":
416      J.JNull{}
417    case "true":
418      J.JBool{True{}}
419    case "false":
420      J.JBool{False{}}
421    case +x:
422      Yaml.plain.num(x, Num.parse(x))
423
424def Flow.peek(s: String) -> Char:
425  match s:
426    case SCon{c, t}:
427      c
428    case SNil{}:
429      ' '
430
431# "key: rest" or "key:": the key (unquoted) and the rest, if this is a
432# mapping entry.
433type KR is Data:
434  KR{key: String, rest: String}
435
436def Flow.punct(+c: Char) -> Bool:
437  match c:
438    case '{':
439      True{}
440    case '}':
441      True{}
442    case '[':
443      True{}
444    case ']':
445      True{}
446    case ',':
447      True{}
448    case ':':
449      True{}
450    case _:
451      False{}
452
453def Flow.out0.go(+c: Char, out: String, punct: Bool, space: Bool) -> Flow:
454  match punct space:
455    case True{} _:
456      Flow{0, "", SCon{c, out}, False{}}
457    case False{} True{}:
458      Flow{0, "", out, False{}}
459    case False{} False{}:
460      Flow{3, SCon{c, SNil{}}, out, False{}}
461
462def Flow.out0(+c: Char, out: String) -> Flow:
463  match c:
464    case '"':
465      Flow{1, "", SCon{'"', out}, False{}}
466    case '\'':
467      Flow{2, "", SCon{'"', out}, False{}}
468    case x:
469      Flow.out0.go(x, out, Flow.punct(x), Char.is_space(x))
470
471def Flow.dq(esc: Bool, +c: Char, out: String) -> Flow:
472  match esc c:
473    case True{} x:
474      Flow{1, "", SCon{x, out}, False{}}
475    case False{} '\\':
476      Flow{1, "", SCon{'\\', out}, True{}}
477    case False{} '"':
478      Flow{0, "", SCon{'"', out}, False{}}
479    case False{} x:
480      Flow{1, "", SCon{x, out}, False{}}
481
482def Flow.sq.quote(out: String, doubled: Bool) -> Flow:
483  match doubled:
484    case True{}:
485      Flow{2, "", out, True{}}
486    case False{}:
487      Flow{0, "", SCon{'"', out}, False{}}
488
489def Flow.sq(esc: Bool, +c: Char, +nxt: Char, out: String) -> Flow:
490  match esc c:
491    case True{} _:
492      Flow{2, "", SCon{'\'', out}, False{}}
493    case False{} '\'':
494      Flow.sq.quote(out, Char.is_eq(nxt, '\''))
495    case False{} '"':
496      Flow{2, "", SCon{'"', SCon{'\\', out}}, False{}}
497    case False{} '\\':
498      Flow{2, "", SCon{'\\', SCon{'\\', out}}, False{}}
499    case False{} x:
500      Flow{2, "", SCon{x, out}, False{}}
501
502def Flow.closer(+c: Char) -> Bool:
503  match c:
504    case '}':
505      True{}
506    case ']':
507      True{}
508    case ',':
509      True{}
510    case _:
511      False{}
512
513# A plain scalar ends at a closer, or at ':' before a space or a closer.
514def Flow.ends(+c: Char, +nxt: Char) -> Bool:
515  Bool.or(Flow.closer(c), Bool.and(Char.is_eq(c, ':'), Bool.or(Char.is_space(nxt), Flow.closer(nxt))))
516
517def Flow.emit(buf: String, out: String) -> String:
518  String.reverse(J.Json.show(Yaml.plain(String.trim(String.reverse(buf))))) ++ out
519
520def Flow.plain.go(+c: Char, buf: String, out: String, ends: Bool) -> Flow:
521  match ends:
522    case True{}:
523      Flow{0, "", SCon{c, Flow.emit(buf, out)}, False{}}
524    case False{}:
525      Flow{3, SCon{c, buf}, out, False{}}
526
527def Flow.plain(+c: Char, +nxt: Char, buf: String, out: String) -> Flow:
528  Flow.plain.go(c, buf, out, Flow.ends(c, nxt))
529
530def Flow.step.go(+m: U32, b: String, o: String, e: Bool, +c: Char, +nxt: Char, m0: Bool, m1: Bool,
531  m2: Bool) -> Flow:
532  match m0 m1 m2:
533    case True{} _ _:
534      Flow.out0(c, o)
535    case False{} True{} _:
536      Flow.dq(e, c, o)
537    case False{} False{} True{}:
538      Flow.sq(e, c, nxt, o)
539    case _ _ _:
540      Flow.plain(c, nxt, b, o)
541
542def Flow.step(st: Flow, +c: Char, +nxt: Char) -> Flow:
543  match st:
544    case Flow{+m, b, o, e}:
545      Flow.step.go(m, b, o, e, c, nxt, U32.is_eq(m, 0), U32.is_eq(m, 1), U32.is_eq(m, 2))
546
547def Flow.go(s: String, st: Flow) -> Flow:
548  match s:
549    case SNil{}:
550      st
551    case SCon{+c, +t}:
552      Flow.go(t, Flow.step(st, c, Flow.peek(t)))
553
554def Flow.fin.go(b: String, o: String, plain: Bool) -> String:
555  match plain:
556    case True{}:
557      Flow.emit(b, o)
558    case False{}:
559      o
560
561def Flow.fin(st: Flow) -> String:
562  match st:
563    case Flow{+m, b, o, e}:
564      String.reverse(Flow.fin.go(b, o, U32.is_eq(m, 3)))
565
566# A flow value's JSON text.
567def Yaml.flow(s: String) -> String:
568  Flow.fin(Flow.go(s, Flow{0, "", "", False{}}))
569
570# Anchors, aliases, tags
571# ----------------------
572#
573# The line parser leaves markers: an anchored value is the object
574# {"\u0000&name": value}, an alias the string "\u0000*name", and a key
575# whose anchored value is still to come is "\u0001name\u0001key". After
576# parsing, Yaml.resolve walks the document in order, recording anchors and
577# replacing aliases (an alias names an earlier anchor), and applies merge
578# keys (<<: *base), whose members fill in keys the mapping lacks.
579
580def Yaml.split_word.go(s: String, acc: String) -> AV:
581  match s:
582    case SNil{}:
583      AV{String.reverse(acc), ""}
584    case SCon{' ', t}:
585      AV{String.reverse(acc), String.trim(t)}
586    case SCon{c, t}:
587      Yaml.split_word.go(t, SCon{c, acc})
588
589# The first word and the rest.
590def Yaml.split_word(s: String) -> AV:
591  Yaml.split_word.go(s, "")
592
593def Yaml.scalar.untagged(+s: String) -> J.Json:
594  match s:
595    case SCon{'"', t}:
596      Yaml.json_or(J.Json.parse(s), s)
597    case SCon{'\'', t}:
598      J.JStr{Yaml.unsingle(Yaml.strip_ends(s))}
599    case x:
600      Yaml.plain(x)
601
602# A tagged scalar: !!str keeps the text a string; other tags are dropped.
603def Yaml.tagged(av: AV) -> J.Json:
604  match av:
605    case AV{"!str", r}:
606      J.JStr{r}
607    case AV{t, +r}:
608      Yaml.scalar.untagged(r)
609
610# A scalar or flow value; an alias (*name) or a tag (!!str x) first.
611def Yaml.scalar(+s: String) -> J.Json:
612  match s:
613    case SCon{'*', t}:
614      J.JStr{SCon{Chr{0}, SCon{'*', t}}}
615    case SCon{'!', t}:
616      Yaml.tagged(Yaml.split_word(t))
617    case SCon{'"', t}:
618      Yaml.json_or(J.Json.parse(s), s)
619    case SCon{'\'', t}:
620      J.JStr{Yaml.unsingle(Yaml.strip_ends(s))}
621    case SCon{'[', t}:
622      Yaml.json_or(J.Json.parse(Yaml.flow(s)), s)
623    case SCon{'{', t}:
624      Yaml.json_or(J.Json.parse(Yaml.flow(s)), s)
625    case x:
626      Yaml.plain(x)
627
628def Yaml.key.go(s: String, acc: String) -> Maybe<&2, KR>:
629  match s:
630    case SNil{}:
631      None{}
632    case SCon{':', SNil{}}:
633      Some{KR{String.reverse(acc), ""}}
634    case SCon{':', SCon{' ', t}}:
635      Some{KR{String.reverse(acc), String.trim(t)}}
636    case SCon{c, t}:
637      Yaml.key.go(t, SCon{c, acc})
638
639def Yaml.unquote_key(+k: String) -> String:
640  match k:
641    case SCon{'"', t}:
642      Yaml.strip_ends(k)
643    case SCon{'\'', t}:
644      Yaml.strip_ends(k)
645    case x:
646      x
647
648def Yaml.key.fin(m: Maybe<&2, KR>) -> Maybe<&2, KR>:
649  match m:
650    case Some{KR{k, r}}:
651      Some{KR{Yaml.unquote_key(String.trim(k)), r}}
652    case None{}:
653      None{}
654
655def Yaml.key(+s: String) -> Maybe<&2, KR>:
656  match s:
657    case SCon{'"', t}:
658      Yaml.key.fin(Yaml.key.go(s, ""))
659    case SCon{'[', t}:
660      None{}
661    case SCon{'{', t}:
662      None{}
663    case x:
664      Yaml.key.fin(Yaml.key.go(x, ""))
665
666# The stack
667# ---------
668
669def Yaml.rev_kv(xs: List<&2, J.KV>, acc: J.Json) -> J.Json:
670  match xs:
671    case Nil{}:
672      acc
673    case Con{J.KV{k, v}, t}:
674      Yaml.rev_kv(t, J.JMem{k, v, acc})
675
676def Yaml.rev_items(xs: List<&2, J.Json>, acc: J.Json) -> J.Json:
677  match xs:
678    case Nil{}:
679      acc
680    case Con{h, t}:
681      Yaml.rev_items(t, J.JCons{h, acc})
682
683def Yaml.pend_null(p: Maybe<&2, String>, ms: List<&2, J.KV>) -> List<&2, J.KV>:
684  match p:
685    case Some{k}:
686      J.KV{k, J.JNull{}} <> ms
687    case None{}:
688      ms
689
690def Yaml.wrap.go(a: String, v: J.Json, none: Bool) -> J.Json:
691  match none:
692    case True{}:
693      v
694    case False{}:
695      J.JObj{J.JMem{SCon{Chr{0}, SCon{'&', a}}, v, J.JNil{}}}
696
697def Yaml.wrap(+a: String, v: J.Json) -> J.Json:
698  Yaml.wrap.go(a, v, String.is_empty(a))
699
700# A finished container's value.
701def Yaml.value(f: YFrame) -> J.Json:
702  match f:
703    case YMap{i, ms, p}:
704      J.JObj{Yaml.rev_kv(Yaml.pend_null(p, ms), J.JNil{})}
705    case YSeq{i, xs, None{}}:
706      J.JArr{Yaml.rev_items(xs, J.JNil{})}
707    case YSeq{i, xs, Some{a}}:
708      J.JArr{Yaml.rev_items(Yaml.wrap(a, J.JNull{}) <> xs, J.JNil{})}
709    case YRoot{v}:
710      v
711
712# Whether s starts with the control char of code n.
713def Yaml.ctl(s: String, +n: U32) -> Bool:
714  match s:
715    case SCon{Chr{+c}, t}:
716      U32.is_eq(c, n)
717    case _:
718      False{}
719
720def Yaml.member.split(parts: List<&2, String>, v: J.Json) -> J.KV:
721  match parts:
722    case Con{a, Con{k, rest}}:
723      J.KV{String.join(k <> rest, SCon{Chr{1}, SNil{}}), Yaml.wrap(a, v)}
724    case _:
725      J.KV{"", v}
726
727def Yaml.member.go(k: String, v: J.Json, marked: Bool) -> J.KV:
728  match marked:
729    case True{}:
730      Yaml.member.split(String.split(String.drop(k, 1n), Chr{1}), v)
731    case False{}:
732      J.KV{k, v}
733
734# A pending key's member, its anchor (if marked) wrapped around v.
735def Yaml.member(+k: String, v: J.Json) -> J.KV:
736  Yaml.member.go(k, v, Yaml.ctl(k, 1))
737
738# Hands v to the container on top: a mapping's pending key, a sequence's
739# next item, or the document itself.
740def Yaml.give(v: J.Json, st: List<&2, YFrame>) -> List<&2, YFrame>:
741  match st:
742    case Con{YMap{i, ms, Some{k}}, t}:
743      YMap{i, Yaml.member(k, v) <> ms, None{}} <> t
744    case Con{YSeq{i, xs, None{}}, t}:
745      YSeq{i, v <> xs, None{}} <> t
746    case Con{YSeq{i, xs, Some{a}}, t}:
747      YSeq{i, Yaml.wrap(a, v) <> xs, None{}} <> t
748    case Con{YMap{i, ms, None{}}, t}:
749      YMap{i, ms, None{}} <> t
750    case Con{YRoot{x}, t}:
751      YRoot{v} <> t
752    case Nil{}:
753      [YRoot{v}]
754
755def Yaml.top_indent(st: List<&2, YFrame>) -> U32:
756  match st:
757    case Con{YMap{i, ms, p}, t}:
758      i
759    case Con{YSeq{i, xs, p}, t}:
760      i
761    case _:
762      0
763
764# Whether the top container ends before a line at indent i: it is deeper,
765# or it is a sequence at i and the line is no item of it.
766def Yaml.ends(+st: List<&2, YFrame>, +i: U32, dash: Bool) -> Bool:
767  match st:
768    case Con{YMap{+j, ms, p}, t}:
769      U32.is_gt(j, i)
770    case Con{YSeq{+j, xs, p}, t}:
771      Bool.or(U32.is_gt(j, i), Bool.and(U32.is_eq(j, i), Bool.not(dash)))
772    case _:
773      False{}
774
775def Yaml.pop(st: List<&2, YFrame>) -> List<&2, YFrame>:
776  match st:
777    case Con{f, t}:
778      Yaml.give(Yaml.value(f), t)
779    case Nil{}:
780      Nil{}
781
782def Yaml.close(fuel: Nat, +st: List<&2, YFrame>, +i: U32, +dash: Bool, ends: Bool) -> List<&2, YFrame>:
783  match fuel ends:
784    case 1n+f True{}:
785      Yaml.close(f, Yaml.pop(st), i, dash, Yaml.ends(Yaml.pop(st), i, dash))
786    case _ _:
787      st
788
789def Yaml.map_at(+st: List<&2, YFrame>, +i: U32) -> Bool:
790  match st:
791    case Con{YMap{+j, ms, p}, t}:
792      U32.is_eq(j, i)
793    case _:
794      False{}
795
796def Yaml.mark.go(a: String, k: String, none: Bool) -> String:
797  match none:
798    case True{}:
799      k
800    case False{}:
801      SCon{Chr{1}, a ++ SCon{Chr{1}, k}}
802
803def Yaml.mark(+a: String, k: String) -> String:
804  Yaml.mark.go(a, k, String.is_empty(a))
805
806def Yaml.entry.put.go(st: List<&2, YFrame>, k: String, +a: String, +r: String, empty: Bool) ->
807  List<&2, YFrame>:
808  match st empty:
809    case Con{YMap{i, ms, p}, t} True{}:
810      YMap{i, ms, Some{Yaml.mark(a, k)}} <> t
811    case Con{YMap{i, ms, p}, t} False{}:
812      YMap{i, J.KV{k, Yaml.wrap(a, Yaml.scalar(r))} <> ms, None{}} <> t
813    case s _:
814      s
815
816def Yaml.entry.put.av(st: List<&2, YFrame>, k: String, av: AV) -> List<&2, YFrame>:
817  match av:
818    case AV{+a, +r}:
819      Yaml.entry.put.go(st, k, a, r, String.is_empty(r))
820
821def Yaml.split_anchor(+s: String) -> AV:
822  match s:
823    case SCon{'&', t}:
824      Yaml.split_word(t)
825    case x:
826      AV{"", x}
827
828def Yaml.entry.put(st: List<&2, YFrame>, k: String, +r: String, empty: Bool) -> List<&2, YFrame>:
829  Yaml.entry.put.av(st, k, Yaml.split_anchor(r))
830
831# Flow collections
832# ----------------
833#
834# A flow value ({a: b, c: [d, 'e']}) becomes JSON text: quoted scalars are
835# re-quoted as JSON strings, plain ones read as scalars, punctuation kept.
836
837def Yaml.entry.go(st: List<&2, YFrame>, +i: U32, kr: KR, here: Bool) -> List<&2, YFrame>:
838  match kr here:
839    case KR{k, +r} True{}:
840      Yaml.entry.put(st, k, r, String.is_empty(r))
841    case KR{k, +r} False{}:
842      Yaml.entry.put(YMap{i, Nil{}, None{}} <> st, k, r, String.is_empty(r))
843
844# A mapping entry at indent i: into the open mapping at i, or a new one.
845def Yaml.entry(+st: List<&2, YFrame>, +i: U32, kr: KR) -> List<&2, YFrame>:
846  Yaml.entry.go(st, i, kr, Yaml.map_at(st, i))
847
848def Yaml.seq_at(+st: List<&2, YFrame>, +i: U32) -> Bool:
849  match st:
850    case Con{YSeq{+j, xs, p}, t}:
851      U32.is_eq(j, i)
852    case _:
853      False{}
854
855# A new item: an item still waiting for its block got none, so it is null.
856def Yaml.item.seq.go(st: List<&2, YFrame>, +i: U32, here: Bool) -> List<&2, YFrame>:
857  match st here:
858    case Con{YSeq{j, xs, Some{a}}, t} True{}:
859      YSeq{j, Yaml.wrap(a, J.JNull{}) <> xs, None{}} <> t
860    case x True{}:
861      x
862    case x False{}:
863      YSeq{i, Nil{}, None{}} <> x
864
865def Yaml.item.seq(+st: List<&2, YFrame>, +i: U32) -> List<&2, YFrame>:
866  Yaml.item.seq.go(st, i, Yaml.seq_at(st, i))
867
868def Yaml.item.value.go(st: List<&2, YFrame>, +a: String, +r: String, empty: Bool) -> List<&2, YFrame>:
869  match st empty:
870    case Con{YSeq{i, xs, p}, t} True{}:
871      YSeq{i, xs, Some{a}} <> t
872    case x _:
873      Yaml.give(Yaml.wrap(a, Yaml.scalar(r)), x)
874
875# "- &a value", "- *a", "- " (a block follows), or "- &a" (an anchored
876# block follows).
877def Yaml.item.value(st: List<&2, YFrame>, av: AV) -> List<&2, YFrame>:
878  match av:
879    case AV{+a, +r}:
880      Yaml.item.value.go(st, a, r, String.is_empty(r))
881
882# The item is a mapping when it reads "key: ..." (its keys then sit at
883# i + 2), else a scalar.
884def Yaml.item.go(st: List<&2, YFrame>, +i: U32, rest: String, kr: Maybe<&2, KR>) -> List<&2, YFrame>:
885  match kr:
886    case Some{x}:
887      Yaml.entry.go(st, (i + 2 : U32), x, False{})
888    case None{}:
889      Yaml.item.value(st, Yaml.split_anchor(rest))
890
891# A "- item" line at indent i: into the open sequence at i, or a new one.
892def Yaml.item(+st: List<&2, YFrame>, +i: U32, +rest: String) -> List<&2, YFrame>:
893  Yaml.item.go(Yaml.item.seq(st, i), i, rest, Yaml.key(rest))
894
895def Yaml.is_dash(t: String) -> Bool:
896  match t:
897    case SCon{'-', SCon{' ', r}}:
898      True{}
899    case "-":
900      True{}
901    case _:
902      False{}
903
904def Yaml.after_dash(t: String) -> String:
905  String.trim(String.drop(t, 1n))
906
907def Yaml.step.entry(+st: List<&2, YFrame>, +i: U32, +t: String, kr: Maybe<&2, KR>) -> List<&2, YFrame>:
908  match kr:
909    case Some{x}:
910      Yaml.entry(st, i, x)
911    case None{}:
912      Yaml.give(Yaml.scalar(t), st)
913
914def Yaml.step.go(+st: List<&2, YFrame>, +i: U32, +t: String, dash: Bool) -> List<&2, YFrame>:
915  match dash:
916    case True{}:
917      Yaml.item(st, i, Yaml.after_dash(t))
918    case False{}:
919      Yaml.step.entry(st, i, t, Yaml.key(t))
920
921# One line.
922def Yaml.step(+st: List<&2, YFrame>, l: Line) -> List<&2, YFrame>:
923  match l:
924    case Line{+i, +t}:
925      Yaml.step.go(Yaml.close(64n, st, i, Yaml.is_dash(t), Yaml.ends(st, i, Yaml.is_dash(t))), i, t,
926        Yaml.is_dash(t))
927
928def Yaml.feed(ls: List<&2, Line>, +st: List<&2, YFrame>) -> List<&2, YFrame>:
929  match ls:
930    case Nil{}:
931      st
932    case Con{l, t}:
933      Yaml.feed(t, Yaml.step(st, l))
934
935def Yaml.result(st: List<&2, YFrame>) -> J.Json:
936  match st:
937    case Con{YRoot{v}, t}:
938      v
939    case Con{f, t}:
940      Yaml.value(f)
941    case Nil{}:
942      J.JNull{}
943
944def Yaml.is_open(st: List<&2, YFrame>) -> Bool:
945  match st:
946    case Con{YRoot{v}, t}:
947      False{}
948    case Con{f, t}:
949      True{}
950    case Nil{}:
951      False{}
952
953def Yaml.close_all.go(fuel: Nat, +st: List<&2, YFrame>, open: Bool) -> List<&2, YFrame>:
954  match fuel open:
955    case 1n+f True{}:
956      Yaml.close_all.go(f, Yaml.pop(st), Yaml.is_open(Yaml.pop(st)))
957    case _ _:
958      st
959
960# Closes every open container, into the document.
961def Yaml.close_all(fuel: Nat, +st: List<&2, YFrame>) -> List<&2, YFrame>:
962  Yaml.close_all.go(fuel, st, Yaml.is_open(st))
963
964def YR.json(r: YR) -> J.Json:
965  match r:
966    case YR{j, a}:
967      j
968
969# Resolution
970# ----------
971
972
973def YR.anchors(r: YR) -> Map<&2, J.Json>:
974  match r:
975    case YR{j, a}:
976      a
977
978def Yaml.marker.go(n: String, hit: Bool) -> Maybe<&2, String>:
979  match hit:
980    case True{}:
981      Some{n}
982    case False{}:
983      None{}
984
985def Yaml.marker(s: String, +kind: Char) -> Maybe<&2, String>:
986  match s:
987    case SCon{Chr{+c}, SCon{+k, n}}:
988      Yaml.marker.go(n, Bool.and(U32.is_eq(c, 0), Char.is_eq(k, kind)))
989    case _:
990      None{}
991
992def Yaml.anchor_name(k: String) -> Maybe<&2, String>:
993  Yaml.marker(k, '&')
994
995def Yaml.res.mem(k: String, h: YR, t: YR) -> YR:
996  match h t:
997    case YR{v, a} YR{rest, a2}:
998      YR{J.JMem{k, v, rest}, a2}
999
1000def Yaml.res.cons(h: YR, t: YR) -> YR:
1001  match h t:
1002    case YR{v, a} YR{rest, a2}:
1003      YR{J.JCons{v, rest}, a2}
1004
1005def Yaml.merge.take(+ms: J.Json) -> J.Json:
1006  J.Json.lookup(ms, "<<")
1007
1008def Yaml.merge.missing.put(k: String, v: J.Json, rest: J.Json, h: Bool, has: Bool) -> J.Json:
1009  match has:
1010    case True{}:
1011      rest
1012    case False{}:
1013      J.JMem{k, v, rest}
1014
1015def Yaml.has(ms: J.Json, +k: String) -> Bool:
1016  Bool.not(J.Json.is_null(J.Json.lookup(ms, k)))
1017
1018# Members whose keys the chain ms lacks, from the merged object's chain.
1019def Yaml.merge.missing(src: J.Json, +ms: J.Json) -> J.Json:
1020  match src:
1021    case J.JMem{+k, v, t}:
1022      Yaml.merge.missing.put(k, v, Yaml.merge.missing(t, ms), J.Json.head_is(ms, k), Yaml.has(ms, k))
1023    case _:
1024      J.JNil{}
1025
1026def Yaml.append_mem(ms: J.Json, extra: J.Json) -> J.Json:
1027  match ms:
1028    case J.JMem{k, v, t}:
1029      J.JMem{k, v, Yaml.append_mem(t, extra)}
1030    case _:
1031      extra
1032
1033def Yaml.merge.go(src: J.Json, +rest: J.Json) -> J.Json:
1034  match src:
1035    case J.JObj{m}:
1036      Yaml.append_mem(rest, Yaml.merge.missing(m, rest))
1037    case _:
1038      rest
1039
1040# Applies a "<<" member: its object's members join where keys are absent.
1041def Yaml.merge(+ms: J.Json) -> J.Json:
1042  Yaml.merge.go(Yaml.merge.take(ms), J.Json.del_mem(ms, "<<"))
1043
1044def Yaml.res.obj(r: YR) -> YR:
1045  match r:
1046    case YR{ms, a}:
1047      YR{J.JObj{Yaml.merge(ms)}, a}
1048
1049def Yaml.res.arr(r: YR) -> YR:
1050  match r:
1051    case YR{xs, a}:
1052      YR{J.JArr{xs}, a}
1053
1054def Yaml.is_alias(s: String) -> Maybe<&2, String>:
1055  Yaml.marker(s, '*')
1056
1057def Yaml.lookup.fin(r: Map<&2, J.Json> & J.Json) -> J.Json:
1058  match r:
1059    case (m, v):
1060      v
1061
1062def Yaml.alias(+name: String, +anchors: Map<&2, J.Json>) -> J.Json:
1063  Yaml.lookup.fin(Map.get(J.Json, J.JNull{}, anchors, name))
1064
1065def Yaml.res.str(s: String, +a: Map<&2, J.Json>, alias: Maybe<&2, String>) -> YR:
1066  match alias:
1067    case Some{n}:
1068      YR{Yaml.alias(n, a), a}
1069    case None{}:
1070      YR{J.JStr{s}, a}
1071
1072def Yaml.res.anchor(+n: String, r: YR) -> YR:
1073  match r:
1074    case YR{+v, a}:
1075      YR{v, Map.set(&2, J.Json, a, n, v)}
1076
1077# A one-member object: an anchor marker, or a plain object.
1078def Yaml.res.single(+k: String, n: Maybe<&2, String>, +rv: YR, tail: J.Json, a: Map<&2, J.Json>) -> YR:
1079  match n:
1080    case Some{name}:
1081      Yaml.res.anchor(name, rv)
1082    case None{}:
1083      Yaml.res.obj(Yaml.res.mem(k, rv, YR{tail, YR.anchors(rv)}))
1084
1085# The document with anchors recorded, aliases replaced and merges applied.
1086def Yaml.res(j: J.Json, +a: Map<&2, J.Json>) -> YR:
1087  match j:
1088    case J.JStr{+s}:
1089      Yaml.res.str(s, a, Yaml.is_alias(s))
1090    case J.JObj{J.JMem{+k, v, J.JNil{}}}:
1091      Yaml.res.single(k, Yaml.anchor_name(k), Yaml.res(v, a), J.JNil{}, a)
1092    case J.JObj{ms}:
1093      Yaml.res.obj(Yaml.res(ms, a))
1094    case J.JArr{xs}:
1095      Yaml.res.arr(Yaml.res(xs, a))
1096    case J.JCons{h, t}:
1097      +rh = Yaml.res(h, a)
1098      Yaml.res.cons(rh, Yaml.res(t, YR.anchors(rh)))
1099    case J.JMem{k, v, t}:
1100      +rv = Yaml.res(v, a)
1101      Yaml.res.mem(k, rv, Yaml.res(t, YR.anchors(rv)))
1102    case x:
1103      YR{x, a}
1104
1105def Yaml.resolve(j: J.Json) -> J.Json:
1106  YR.json(Yaml.res(j, Map.new(&2, J.Json)))
1107
1108# The (first) document as JSON, anchors and aliases resolved.
1109def Yaml.parse(s: String) -> J.Json:
1110  Yaml.resolve(Yaml.result(Yaml.close_all(64n, Yaml.feed(Yaml.lines(s), Nil{}))))
1111
1112# Documents
1113# ---------
1114
1115def Yaml.is_sep(+l: String) -> Bool:
1116  Bool.or(String.starts_with(l, "---"), String.eq(String.trim_end(l), "..."))
1117
1118def Yaml.docs.cur(+l: String, cur: List<&2, String>, sep: Bool) -> List<&2, String>:
1119  match sep:
1120    case True{}:
1121      Nil{}
1122    case False{}:
1123      l <> cur
1124
1125def Yaml.docs.done(+cur: List<&2, String>, done: List<&2, String>, sep: Bool) -> List<&2, String>:
1126  match sep:
1127    case True{}:
1128      String.join(List.reverse(&2, String, cur), "\n") <> done
1129    case False{}:
1130      done
1131
1132# Splits a stream at "---" and "..." lines; documents come reversed.
1133def Yaml.docs.go(ls: List<&2, String>, +cur: List<&2, String>, done: List<&2, String>) -> List<&2, String>:
1134  match ls:
1135    case Nil{}:
1136      String.join(List.reverse(&2, String, cur), "\n") <> done
1137    case Con{+l, t}:
1138      Yaml.docs.go(t, Yaml.docs.cur(l, cur, Yaml.is_sep(l)), Yaml.docs.done(cur, done, Yaml.is_sep(l)))
1139
1140def Yaml.docs.keep(d: String, acc: List<&2, J.Json>, empty: Bool) -> List<&2, J.Json>:
1141  match empty:
1142    case True{}:
1143      acc
1144    case False{}:
1145      Yaml.parse(d) <> acc
1146
1147def Yaml.docs.parse(ds: List<&2, String>, acc: List<&2, J.Json>) -> List<&2, J.Json>:
1148  match ds:
1149    case Nil{}:
1150      acc
1151    case Con{+d, t}:
1152      Yaml.docs.parse(t, Yaml.docs.keep(d, acc, List.is_empty(&2, Line, Yaml.lines(d))))
1153
1154# Every document of a stream (kubectl's multi-document files), in order;
1155# empty documents are skipped.
1156def Yaml.parse_all(s: String) -> List<&2, J.Json>:
1157  Yaml.docs.parse(Yaml.docs.go(String.lines(s), Nil{}, Nil{}), Nil{})