src/yaml.bend

bend-kube-runtime@0.1.1.0 · 1,051 lines · 0 definitions

1import Base
2import bend-kube-runtime@0.1.1.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
289def Yaml.digits(s: String) -> Bool:
290  match s:
291    case SNil{}:
292      True{}
293    case SCon{+c, t}:
294      Bool.and(Char.is_digit(c), Yaml.digits(t))
295
296# Plain integers are numbers, as YAML 1.2's core schema reads them.
297def Yaml.plain.num(x: String, int: Bool) -> J.Json:
298  match int:
299    case True{}:
300      J.JNum{x}
301    case False{}:
302      J.JStr{x}
303
304# A plain scalar: null, ~, true, false, or a string.
305def Yaml.plain(s: String) -> J.Json:
306  match s:
307    case "null":
308      J.JNull{}
309    case "~":
310      J.JNull{}
311    case "true":
312      J.JBool{True{}}
313    case "false":
314      J.JBool{False{}}
315    case +x:
316      Yaml.plain.num(x, Bool.and(Bool.not(String.is_empty(x)), Yaml.digits(x)))
317
318def Flow.peek(s: String) -> Char:
319  match s:
320    case SCon{c, t}:
321      c
322    case SNil{}:
323      ' '
324
325# "key: rest" or "key:": the key (unquoted) and the rest, if this is a
326# mapping entry.
327type KR is Data:
328  KR{key: String, rest: String}
329
330def Flow.punct(+c: Char) -> Bool:
331  match c:
332    case '{':
333      True{}
334    case '}':
335      True{}
336    case '[':
337      True{}
338    case ']':
339      True{}
340    case ',':
341      True{}
342    case ':':
343      True{}
344    case _:
345      False{}
346
347def Flow.out0.go(+c: Char, out: String, punct: Bool, space: Bool) -> Flow:
348  match punct space:
349    case True{} _:
350      Flow{0, "", SCon{c, out}, False{}}
351    case False{} True{}:
352      Flow{0, "", out, False{}}
353    case False{} False{}:
354      Flow{3, SCon{c, SNil{}}, out, False{}}
355
356def Flow.out0(+c: Char, out: String) -> Flow:
357  match c:
358    case '"':
359      Flow{1, "", SCon{'"', out}, False{}}
360    case '\'':
361      Flow{2, "", SCon{'"', out}, False{}}
362    case x:
363      Flow.out0.go(x, out, Flow.punct(x), Char.is_space(x))
364
365def Flow.dq(esc: Bool, +c: Char, out: String) -> Flow:
366  match esc c:
367    case True{} x:
368      Flow{1, "", SCon{x, out}, False{}}
369    case False{} '\\':
370      Flow{1, "", SCon{'\\', out}, True{}}
371    case False{} '"':
372      Flow{0, "", SCon{'"', out}, False{}}
373    case False{} x:
374      Flow{1, "", SCon{x, out}, False{}}
375
376def Flow.sq.quote(out: String, doubled: Bool) -> Flow:
377  match doubled:
378    case True{}:
379      Flow{2, "", out, True{}}
380    case False{}:
381      Flow{0, "", SCon{'"', out}, False{}}
382
383def Flow.sq(esc: Bool, +c: Char, +nxt: Char, out: String) -> Flow:
384  match esc c:
385    case True{} _:
386      Flow{2, "", SCon{'\'', out}, False{}}
387    case False{} '\'':
388      Flow.sq.quote(out, Char.is_eq(nxt, '\''))
389    case False{} '"':
390      Flow{2, "", SCon{'"', SCon{'\\', out}}, False{}}
391    case False{} '\\':
392      Flow{2, "", SCon{'\\', SCon{'\\', out}}, False{}}
393    case False{} x:
394      Flow{2, "", SCon{x, out}, False{}}
395
396def Flow.closer(+c: Char) -> Bool:
397  match c:
398    case '}':
399      True{}
400    case ']':
401      True{}
402    case ',':
403      True{}
404    case _:
405      False{}
406
407# A plain scalar ends at a closer, or at ':' before a space or a closer.
408def Flow.ends(+c: Char, +nxt: Char) -> Bool:
409  Bool.or(Flow.closer(c), Bool.and(Char.is_eq(c, ':'), Bool.or(Char.is_space(nxt), Flow.closer(nxt))))
410
411def Flow.emit(buf: String, out: String) -> String:
412  String.reverse(J.Json.show(Yaml.plain(String.trim(String.reverse(buf))))) ++ out
413
414def Flow.plain.go(+c: Char, buf: String, out: String, ends: Bool) -> Flow:
415  match ends:
416    case True{}:
417      Flow{0, "", SCon{c, Flow.emit(buf, out)}, False{}}
418    case False{}:
419      Flow{3, SCon{c, buf}, out, False{}}
420
421def Flow.plain(+c: Char, +nxt: Char, buf: String, out: String) -> Flow:
422  Flow.plain.go(c, buf, out, Flow.ends(c, nxt))
423
424def Flow.step.go(+m: U32, b: String, o: String, e: Bool, +c: Char, +nxt: Char, m0: Bool, m1: Bool,
425  m2: Bool) -> Flow:
426  match m0 m1 m2:
427    case True{} _ _:
428      Flow.out0(c, o)
429    case False{} True{} _:
430      Flow.dq(e, c, o)
431    case False{} False{} True{}:
432      Flow.sq(e, c, nxt, o)
433    case _ _ _:
434      Flow.plain(c, nxt, b, o)
435
436def Flow.step(st: Flow, +c: Char, +nxt: Char) -> Flow:
437  match st:
438    case Flow{+m, b, o, e}:
439      Flow.step.go(m, b, o, e, c, nxt, U32.is_eq(m, 0), U32.is_eq(m, 1), U32.is_eq(m, 2))
440
441def Flow.go(s: String, st: Flow) -> Flow:
442  match s:
443    case SNil{}:
444      st
445    case SCon{+c, +t}:
446      Flow.go(t, Flow.step(st, c, Flow.peek(t)))
447
448def Flow.fin.go(b: String, o: String, plain: Bool) -> String:
449  match plain:
450    case True{}:
451      Flow.emit(b, o)
452    case False{}:
453      o
454
455def Flow.fin(st: Flow) -> String:
456  match st:
457    case Flow{+m, b, o, e}:
458      String.reverse(Flow.fin.go(b, o, U32.is_eq(m, 3)))
459
460# A flow value's JSON text.
461def Yaml.flow(s: String) -> String:
462  Flow.fin(Flow.go(s, Flow{0, "", "", False{}}))
463
464# Anchors, aliases, tags
465# ----------------------
466#
467# The line parser leaves markers: an anchored value is the object
468# {"\u0000&name": value}, an alias the string "\u0000*name", and a key
469# whose anchored value is still to come is "\u0001name\u0001key". After
470# parsing, Yaml.resolve walks the document in order, recording anchors and
471# replacing aliases (an alias names an earlier anchor), and applies merge
472# keys (<<: *base), whose members fill in keys the mapping lacks.
473
474def Yaml.split_word.go(s: String, acc: String) -> AV:
475  match s:
476    case SNil{}:
477      AV{String.reverse(acc), ""}
478    case SCon{' ', t}:
479      AV{String.reverse(acc), String.trim(t)}
480    case SCon{c, t}:
481      Yaml.split_word.go(t, SCon{c, acc})
482
483# The first word and the rest.
484def Yaml.split_word(s: String) -> AV:
485  Yaml.split_word.go(s, "")
486
487def Yaml.scalar.untagged(+s: String) -> J.Json:
488  match s:
489    case SCon{'"', t}:
490      Yaml.json_or(J.Json.parse(s), s)
491    case SCon{'\'', t}:
492      J.JStr{Yaml.unsingle(Yaml.strip_ends(s))}
493    case x:
494      Yaml.plain(x)
495
496# A tagged scalar: !!str keeps the text a string; other tags are dropped.
497def Yaml.tagged(av: AV) -> J.Json:
498  match av:
499    case AV{"!str", r}:
500      J.JStr{r}
501    case AV{t, +r}:
502      Yaml.scalar.untagged(r)
503
504# A scalar or flow value; an alias (*name) or a tag (!!str x) first.
505def Yaml.scalar(+s: String) -> J.Json:
506  match s:
507    case SCon{'*', t}:
508      J.JStr{SCon{Chr{0}, SCon{'*', t}}}
509    case SCon{'!', t}:
510      Yaml.tagged(Yaml.split_word(t))
511    case SCon{'"', t}:
512      Yaml.json_or(J.Json.parse(s), s)
513    case SCon{'\'', t}:
514      J.JStr{Yaml.unsingle(Yaml.strip_ends(s))}
515    case SCon{'[', t}:
516      Yaml.json_or(J.Json.parse(Yaml.flow(s)), s)
517    case SCon{'{', t}:
518      Yaml.json_or(J.Json.parse(Yaml.flow(s)), s)
519    case x:
520      Yaml.plain(x)
521
522def Yaml.key.go(s: String, acc: String) -> Maybe<&2, KR>:
523  match s:
524    case SNil{}:
525      None{}
526    case SCon{':', SNil{}}:
527      Some{KR{String.reverse(acc), ""}}
528    case SCon{':', SCon{' ', t}}:
529      Some{KR{String.reverse(acc), String.trim(t)}}
530    case SCon{c, t}:
531      Yaml.key.go(t, SCon{c, acc})
532
533def Yaml.unquote_key(+k: String) -> String:
534  match k:
535    case SCon{'"', t}:
536      Yaml.strip_ends(k)
537    case SCon{'\'', t}:
538      Yaml.strip_ends(k)
539    case x:
540      x
541
542def Yaml.key.fin(m: Maybe<&2, KR>) -> Maybe<&2, KR>:
543  match m:
544    case Some{KR{k, r}}:
545      Some{KR{Yaml.unquote_key(String.trim(k)), r}}
546    case None{}:
547      None{}
548
549def Yaml.key(+s: String) -> Maybe<&2, KR>:
550  match s:
551    case SCon{'"', t}:
552      Yaml.key.fin(Yaml.key.go(s, ""))
553    case SCon{'[', t}:
554      None{}
555    case SCon{'{', t}:
556      None{}
557    case x:
558      Yaml.key.fin(Yaml.key.go(x, ""))
559
560# The stack
561# ---------
562
563def Yaml.rev_kv(xs: List<&2, J.KV>, acc: J.Json) -> J.Json:
564  match xs:
565    case Nil{}:
566      acc
567    case Con{J.KV{k, v}, t}:
568      Yaml.rev_kv(t, J.JMem{k, v, acc})
569
570def Yaml.rev_items(xs: List<&2, J.Json>, acc: J.Json) -> J.Json:
571  match xs:
572    case Nil{}:
573      acc
574    case Con{h, t}:
575      Yaml.rev_items(t, J.JCons{h, acc})
576
577def Yaml.pend_null(p: Maybe<&2, String>, ms: List<&2, J.KV>) -> List<&2, J.KV>:
578  match p:
579    case Some{k}:
580      J.KV{k, J.JNull{}} <> ms
581    case None{}:
582      ms
583
584def Yaml.wrap.go(a: String, v: J.Json, none: Bool) -> J.Json:
585  match none:
586    case True{}:
587      v
588    case False{}:
589      J.JObj{J.JMem{SCon{Chr{0}, SCon{'&', a}}, v, J.JNil{}}}
590
591def Yaml.wrap(+a: String, v: J.Json) -> J.Json:
592  Yaml.wrap.go(a, v, String.is_empty(a))
593
594# A finished container's value.
595def Yaml.value(f: YFrame) -> J.Json:
596  match f:
597    case YMap{i, ms, p}:
598      J.JObj{Yaml.rev_kv(Yaml.pend_null(p, ms), J.JNil{})}
599    case YSeq{i, xs, None{}}:
600      J.JArr{Yaml.rev_items(xs, J.JNil{})}
601    case YSeq{i, xs, Some{a}}:
602      J.JArr{Yaml.rev_items(Yaml.wrap(a, J.JNull{}) <> xs, J.JNil{})}
603    case YRoot{v}:
604      v
605
606# Whether s starts with the control char of code n.
607def Yaml.ctl(s: String, +n: U32) -> Bool:
608  match s:
609    case SCon{Chr{+c}, t}:
610      U32.is_eq(c, n)
611    case _:
612      False{}
613
614def Yaml.member.split(parts: List<&2, String>, v: J.Json) -> J.KV:
615  match parts:
616    case Con{a, Con{k, rest}}:
617      J.KV{String.join(k <> rest, SCon{Chr{1}, SNil{}}), Yaml.wrap(a, v)}
618    case _:
619      J.KV{"", v}
620
621def Yaml.member.go(k: String, v: J.Json, marked: Bool) -> J.KV:
622  match marked:
623    case True{}:
624      Yaml.member.split(String.split(String.drop(k, 1n), Chr{1}), v)
625    case False{}:
626      J.KV{k, v}
627
628# A pending key's member, its anchor (if marked) wrapped around v.
629def Yaml.member(+k: String, v: J.Json) -> J.KV:
630  Yaml.member.go(k, v, Yaml.ctl(k, 1))
631
632# Hands v to the container on top: a mapping's pending key, a sequence's
633# next item, or the document itself.
634def Yaml.give(v: J.Json, st: List<&2, YFrame>) -> List<&2, YFrame>:
635  match st:
636    case Con{YMap{i, ms, Some{k}}, t}:
637      YMap{i, Yaml.member(k, v) <> ms, None{}} <> t
638    case Con{YSeq{i, xs, None{}}, t}:
639      YSeq{i, v <> xs, None{}} <> t
640    case Con{YSeq{i, xs, Some{a}}, t}:
641      YSeq{i, Yaml.wrap(a, v) <> xs, None{}} <> t
642    case Con{YMap{i, ms, None{}}, t}:
643      YMap{i, ms, None{}} <> t
644    case Con{YRoot{x}, t}:
645      YRoot{v} <> t
646    case Nil{}:
647      [YRoot{v}]
648
649def Yaml.top_indent(st: List<&2, YFrame>) -> U32:
650  match st:
651    case Con{YMap{i, ms, p}, t}:
652      i
653    case Con{YSeq{i, xs, p}, t}:
654      i
655    case _:
656      0
657
658# Whether the top container ends before a line at indent i: it is deeper,
659# or it is a sequence at i and the line is no item of it.
660def Yaml.ends(+st: List<&2, YFrame>, +i: U32, dash: Bool) -> Bool:
661  match st:
662    case Con{YMap{+j, ms, p}, t}:
663      U32.is_gt(j, i)
664    case Con{YSeq{+j, xs, p}, t}:
665      Bool.or(U32.is_gt(j, i), Bool.and(U32.is_eq(j, i), Bool.not(dash)))
666    case _:
667      False{}
668
669def Yaml.pop(st: List<&2, YFrame>) -> List<&2, YFrame>:
670  match st:
671    case Con{f, t}:
672      Yaml.give(Yaml.value(f), t)
673    case Nil{}:
674      Nil{}
675
676def Yaml.close(fuel: Nat, +st: List<&2, YFrame>, +i: U32, +dash: Bool, ends: Bool) -> List<&2, YFrame>:
677  match fuel ends:
678    case 1n+f True{}:
679      Yaml.close(f, Yaml.pop(st), i, dash, Yaml.ends(Yaml.pop(st), i, dash))
680    case _ _:
681      st
682
683def Yaml.map_at(+st: List<&2, YFrame>, +i: U32) -> Bool:
684  match st:
685    case Con{YMap{+j, ms, p}, t}:
686      U32.is_eq(j, i)
687    case _:
688      False{}
689
690def Yaml.mark.go(a: String, k: String, none: Bool) -> String:
691  match none:
692    case True{}:
693      k
694    case False{}:
695      SCon{Chr{1}, a ++ SCon{Chr{1}, k}}
696
697def Yaml.mark(+a: String, k: String) -> String:
698  Yaml.mark.go(a, k, String.is_empty(a))
699
700def Yaml.entry.put.go(st: List<&2, YFrame>, k: String, +a: String, +r: String, empty: Bool) ->
701  List<&2, YFrame>:
702  match st empty:
703    case Con{YMap{i, ms, p}, t} True{}:
704      YMap{i, ms, Some{Yaml.mark(a, k)}} <> t
705    case Con{YMap{i, ms, p}, t} False{}:
706      YMap{i, J.KV{k, Yaml.wrap(a, Yaml.scalar(r))} <> ms, None{}} <> t
707    case s _:
708      s
709
710def Yaml.entry.put.av(st: List<&2, YFrame>, k: String, av: AV) -> List<&2, YFrame>:
711  match av:
712    case AV{+a, +r}:
713      Yaml.entry.put.go(st, k, a, r, String.is_empty(r))
714
715def Yaml.split_anchor(+s: String) -> AV:
716  match s:
717    case SCon{'&', t}:
718      Yaml.split_word(t)
719    case x:
720      AV{"", x}
721
722def Yaml.entry.put(st: List<&2, YFrame>, k: String, +r: String, empty: Bool) -> List<&2, YFrame>:
723  Yaml.entry.put.av(st, k, Yaml.split_anchor(r))
724
725# Flow collections
726# ----------------
727#
728# A flow value ({a: b, c: [d, 'e']}) becomes JSON text: quoted scalars are
729# re-quoted as JSON strings, plain ones read as scalars, punctuation kept.
730
731def Yaml.entry.go(st: List<&2, YFrame>, +i: U32, kr: KR, here: Bool) -> List<&2, YFrame>:
732  match kr here:
733    case KR{k, +r} True{}:
734      Yaml.entry.put(st, k, r, String.is_empty(r))
735    case KR{k, +r} False{}:
736      Yaml.entry.put(YMap{i, Nil{}, None{}} <> st, k, r, String.is_empty(r))
737
738# A mapping entry at indent i: into the open mapping at i, or a new one.
739def Yaml.entry(+st: List<&2, YFrame>, +i: U32, kr: KR) -> List<&2, YFrame>:
740  Yaml.entry.go(st, i, kr, Yaml.map_at(st, i))
741
742def Yaml.seq_at(+st: List<&2, YFrame>, +i: U32) -> Bool:
743  match st:
744    case Con{YSeq{+j, xs, p}, t}:
745      U32.is_eq(j, i)
746    case _:
747      False{}
748
749# A new item: an item still waiting for its block got none, so it is null.
750def Yaml.item.seq.go(st: List<&2, YFrame>, +i: U32, here: Bool) -> List<&2, YFrame>:
751  match st here:
752    case Con{YSeq{j, xs, Some{a}}, t} True{}:
753      YSeq{j, Yaml.wrap(a, J.JNull{}) <> xs, None{}} <> t
754    case x True{}:
755      x
756    case x False{}:
757      YSeq{i, Nil{}, None{}} <> x
758
759def Yaml.item.seq(+st: List<&2, YFrame>, +i: U32) -> List<&2, YFrame>:
760  Yaml.item.seq.go(st, i, Yaml.seq_at(st, i))
761
762def Yaml.item.value.go(st: List<&2, YFrame>, +a: String, +r: String, empty: Bool) -> List<&2, YFrame>:
763  match st empty:
764    case Con{YSeq{i, xs, p}, t} True{}:
765      YSeq{i, xs, Some{a}} <> t
766    case x _:
767      Yaml.give(Yaml.wrap(a, Yaml.scalar(r)), x)
768
769# "- &a value", "- *a", "- " (a block follows), or "- &a" (an anchored
770# block follows).
771def Yaml.item.value(st: List<&2, YFrame>, av: AV) -> List<&2, YFrame>:
772  match av:
773    case AV{+a, +r}:
774      Yaml.item.value.go(st, a, r, String.is_empty(r))
775
776# The item is a mapping when it reads "key: ..." (its keys then sit at
777# i + 2), else a scalar.
778def Yaml.item.go(st: List<&2, YFrame>, +i: U32, rest: String, kr: Maybe<&2, KR>) -> List<&2, YFrame>:
779  match kr:
780    case Some{x}:
781      Yaml.entry.go(st, (i + 2 : U32), x, False{})
782    case None{}:
783      Yaml.item.value(st, Yaml.split_anchor(rest))
784
785# A "- item" line at indent i: into the open sequence at i, or a new one.
786def Yaml.item(+st: List<&2, YFrame>, +i: U32, +rest: String) -> List<&2, YFrame>:
787  Yaml.item.go(Yaml.item.seq(st, i), i, rest, Yaml.key(rest))
788
789def Yaml.is_dash(t: String) -> Bool:
790  match t:
791    case SCon{'-', SCon{' ', r}}:
792      True{}
793    case "-":
794      True{}
795    case _:
796      False{}
797
798def Yaml.after_dash(t: String) -> String:
799  String.trim(String.drop(t, 1n))
800
801def Yaml.step.entry(+st: List<&2, YFrame>, +i: U32, +t: String, kr: Maybe<&2, KR>) -> List<&2, YFrame>:
802  match kr:
803    case Some{x}:
804      Yaml.entry(st, i, x)
805    case None{}:
806      Yaml.give(Yaml.scalar(t), st)
807
808def Yaml.step.go(+st: List<&2, YFrame>, +i: U32, +t: String, dash: Bool) -> List<&2, YFrame>:
809  match dash:
810    case True{}:
811      Yaml.item(st, i, Yaml.after_dash(t))
812    case False{}:
813      Yaml.step.entry(st, i, t, Yaml.key(t))
814
815# One line.
816def Yaml.step(+st: List<&2, YFrame>, l: Line) -> List<&2, YFrame>:
817  match l:
818    case Line{+i, +t}:
819      Yaml.step.go(Yaml.close(64n, st, i, Yaml.is_dash(t), Yaml.ends(st, i, Yaml.is_dash(t))), i, t,
820        Yaml.is_dash(t))
821
822def Yaml.feed(ls: List<&2, Line>, +st: List<&2, YFrame>) -> List<&2, YFrame>:
823  match ls:
824    case Nil{}:
825      st
826    case Con{l, t}:
827      Yaml.feed(t, Yaml.step(st, l))
828
829def Yaml.result(st: List<&2, YFrame>) -> J.Json:
830  match st:
831    case Con{YRoot{v}, t}:
832      v
833    case Con{f, t}:
834      Yaml.value(f)
835    case Nil{}:
836      J.JNull{}
837
838def Yaml.is_open(st: List<&2, YFrame>) -> Bool:
839  match st:
840    case Con{YRoot{v}, t}:
841      False{}
842    case Con{f, t}:
843      True{}
844    case Nil{}:
845      False{}
846
847def Yaml.close_all.go(fuel: Nat, +st: List<&2, YFrame>, open: Bool) -> List<&2, YFrame>:
848  match fuel open:
849    case 1n+f True{}:
850      Yaml.close_all.go(f, Yaml.pop(st), Yaml.is_open(Yaml.pop(st)))
851    case _ _:
852      st
853
854# Closes every open container, into the document.
855def Yaml.close_all(fuel: Nat, +st: List<&2, YFrame>) -> List<&2, YFrame>:
856  Yaml.close_all.go(fuel, st, Yaml.is_open(st))
857
858def YR.json(r: YR) -> J.Json:
859  match r:
860    case YR{j, a}:
861      j
862
863# Resolution
864# ----------
865
866
867def YR.anchors(r: YR) -> Map<&2, J.Json>:
868  match r:
869    case YR{j, a}:
870      a
871
872def Yaml.marker.go(n: String, hit: Bool) -> Maybe<&2, String>:
873  match hit:
874    case True{}:
875      Some{n}
876    case False{}:
877      None{}
878
879def Yaml.marker(s: String, +kind: Char) -> Maybe<&2, String>:
880  match s:
881    case SCon{Chr{+c}, SCon{+k, n}}:
882      Yaml.marker.go(n, Bool.and(U32.is_eq(c, 0), Char.is_eq(k, kind)))
883    case _:
884      None{}
885
886def Yaml.anchor_name(k: String) -> Maybe<&2, String>:
887  Yaml.marker(k, '&')
888
889def Yaml.res.mem(k: String, h: YR, t: YR) -> YR:
890  match h t:
891    case YR{v, a} YR{rest, a2}:
892      YR{J.JMem{k, v, rest}, a2}
893
894def Yaml.res.cons(h: YR, t: YR) -> YR:
895  match h t:
896    case YR{v, a} YR{rest, a2}:
897      YR{J.JCons{v, rest}, a2}
898
899def Yaml.merge.take(+ms: J.Json) -> J.Json:
900  J.Json.lookup(ms, "<<")
901
902def Yaml.merge.missing.put(k: String, v: J.Json, rest: J.Json, h: Bool, has: Bool) -> J.Json:
903  match has:
904    case True{}:
905      rest
906    case False{}:
907      J.JMem{k, v, rest}
908
909def Yaml.has(ms: J.Json, +k: String) -> Bool:
910  Bool.not(J.Json.is_null(J.Json.lookup(ms, k)))
911
912# Members whose keys the chain ms lacks, from the merged object's chain.
913def Yaml.merge.missing(src: J.Json, +ms: J.Json) -> J.Json:
914  match src:
915    case J.JMem{+k, v, t}:
916      Yaml.merge.missing.put(k, v, Yaml.merge.missing(t, ms), J.Json.head_is(ms, k), Yaml.has(ms, k))
917    case _:
918      J.JNil{}
919
920def Yaml.append_mem(ms: J.Json, extra: J.Json) -> J.Json:
921  match ms:
922    case J.JMem{k, v, t}:
923      J.JMem{k, v, Yaml.append_mem(t, extra)}
924    case _:
925      extra
926
927def Yaml.merge.go(src: J.Json, +rest: J.Json) -> J.Json:
928  match src:
929    case J.JObj{m}:
930      Yaml.append_mem(rest, Yaml.merge.missing(m, rest))
931    case _:
932      rest
933
934# Applies a "<<" member: its object's members join where keys are absent.
935def Yaml.merge(+ms: J.Json) -> J.Json:
936  Yaml.merge.go(Yaml.merge.take(ms), J.Json.del_mem(ms, "<<"))
937
938def Yaml.res.obj(r: YR) -> YR:
939  match r:
940    case YR{ms, a}:
941      YR{J.JObj{Yaml.merge(ms)}, a}
942
943def Yaml.res.arr(r: YR) -> YR:
944  match r:
945    case YR{xs, a}:
946      YR{J.JArr{xs}, a}
947
948def Yaml.is_alias(s: String) -> Maybe<&2, String>:
949  Yaml.marker(s, '*')
950
951def Yaml.lookup.fin(r: Map<&2, J.Json> & J.Json) -> J.Json:
952  match r:
953    case (m, v):
954      v
955
956def Yaml.alias(+name: String, +anchors: Map<&2, J.Json>) -> J.Json:
957  Yaml.lookup.fin(Map.get(J.Json, J.JNull{}, anchors, name))
958
959def Yaml.res.str(s: String, +a: Map<&2, J.Json>, alias: Maybe<&2, String>) -> YR:
960  match alias:
961    case Some{n}:
962      YR{Yaml.alias(n, a), a}
963    case None{}:
964      YR{J.JStr{s}, a}
965
966def Yaml.res.anchor(+n: String, r: YR) -> YR:
967  match r:
968    case YR{+v, a}:
969      YR{v, Map.set(&2, J.Json, a, n, v)}
970
971# A one-member object: an anchor marker, or a plain object.
972def Yaml.res.single(+k: String, n: Maybe<&2, String>, +rv: YR, tail: J.Json, a: Map<&2, J.Json>) -> YR:
973  match n:
974    case Some{name}:
975      Yaml.res.anchor(name, rv)
976    case None{}:
977      Yaml.res.obj(Yaml.res.mem(k, rv, YR{tail, YR.anchors(rv)}))
978
979# The document with anchors recorded, aliases replaced and merges applied.
980def Yaml.res(j: J.Json, +a: Map<&2, J.Json>) -> YR:
981  match j:
982    case J.JStr{+s}:
983      Yaml.res.str(s, a, Yaml.is_alias(s))
984    case J.JObj{J.JMem{+k, v, J.JNil{}}}:
985      Yaml.res.single(k, Yaml.anchor_name(k), Yaml.res(v, a), J.JNil{}, a)
986    case J.JObj{ms}:
987      Yaml.res.obj(Yaml.res(ms, a))
988    case J.JArr{xs}:
989      Yaml.res.arr(Yaml.res(xs, a))
990    case J.JCons{h, t}:
991      +rh = Yaml.res(h, a)
992      Yaml.res.cons(rh, Yaml.res(t, YR.anchors(rh)))
993    case J.JMem{k, v, t}:
994      +rv = Yaml.res(v, a)
995      Yaml.res.mem(k, rv, Yaml.res(t, YR.anchors(rv)))
996    case x:
997      YR{x, a}
998
999def Yaml.resolve(j: J.Json) -> J.Json:
1000  YR.json(Yaml.res(j, Map.new(&2, J.Json)))
1001
1002# The (first) document as JSON, anchors and aliases resolved.
1003def Yaml.parse(s: String) -> J.Json:
1004  Yaml.resolve(Yaml.result(Yaml.close_all(64n, Yaml.feed(Yaml.lines(s), Nil{}))))
1005
1006# Documents
1007# ---------
1008
1009def Yaml.is_sep(+l: String) -> Bool:
1010  Bool.or(String.starts_with(l, "---"), String.eq(String.trim_end(l), "..."))
1011
1012def Yaml.docs.cur(+l: String, cur: List<&2, String>, sep: Bool) -> List<&2, String>:
1013  match sep:
1014    case True{}:
1015      Nil{}
1016    case False{}:
1017      l <> cur
1018
1019def Yaml.docs.done(+cur: List<&2, String>, done: List<&2, String>, sep: Bool) -> List<&2, String>:
1020  match sep:
1021    case True{}:
1022      String.join(List.reverse(&2, String, cur), "\n") <> done
1023    case False{}:
1024      done
1025
1026# Splits a stream at "---" and "..." lines; documents come reversed.
1027def Yaml.docs.go(ls: List<&2, String>, +cur: List<&2, String>, done: List<&2, String>) -> List<&2, String>:
1028  match ls:
1029    case Nil{}:
1030      String.join(List.reverse(&2, String, cur), "\n") <> done
1031    case Con{+l, t}:
1032      Yaml.docs.go(t, Yaml.docs.cur(l, cur, Yaml.is_sep(l)), Yaml.docs.done(cur, done, Yaml.is_sep(l)))
1033
1034def Yaml.docs.keep(d: String, acc: List<&2, J.Json>, empty: Bool) -> List<&2, J.Json>:
1035  match empty:
1036    case True{}:
1037      acc
1038    case False{}:
1039      Yaml.parse(d) <> acc
1040
1041def Yaml.docs.parse(ds: List<&2, String>, acc: List<&2, J.Json>) -> List<&2, J.Json>:
1042  match ds:
1043    case Nil{}:
1044      acc
1045    case Con{+d, t}:
1046      Yaml.docs.parse(t, Yaml.docs.keep(d, acc, List.is_empty(&2, Line, Yaml.lines(d))))
1047
1048# Every document of a stream (kubectl's multi-document files), in order;
1049# empty documents are skipped.
1050def Yaml.parse_all(s: String) -> List<&2, J.Json>:
1051  Yaml.docs.parse(Yaml.docs.go(String.lines(s), Nil{}, Nil{}), Nil{})