fmt/yscal.bend

cswap-bend-cli@0.1.0.0 · 349 lines · 53 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/fmt/ylex.bend as L
4
5# YAML scalars as the reader sees them: plain words, quoted strings, a flat
6# flow sequence or mapping, and a `key: value` line. Pure.
7
8# ---- comments -----------------------------------------------------------------
9
10def comment.pick(hit: Bool, n: Nat, rest: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
11  match hit:
12    case True{}:
13      Some{n}
14    case False{}:
15      rest
16
17# Where a comment starts: a `#` at the start or after a space.
18def comment.find(s: String, +n: Nat, +prev: Bool) -> Maybe<&2, Nat>:
19  match s:
20    case SNil{}:
21      None{}
22    case SCon{Chr{+c}, t}:
23      comment.pick(prev && U32.is_eq(c, 35), n, comment.find(t, Nat.add(n, 1n), L.is_space(c)))
24
25def comment.cut(m: Maybe<&2, Nat>, +s: String) -> String:
26  match m:
27    case None{}:
28      s
29    case Some{n}:
30      String.take(s, n)
31
32# The text without a trailing comment, trimmed.
33def uncomment(+s: String) -> String:
34  String.trim(comment.cut(comment.find(s, 0n, True{}), s))
35
36# ---- quoted strings -------------------------------------------------------------
37
38def sq.emit(skip: Bool, +c: U32, rest: String) -> String:
39  match skip:
40    case True{}:
41      rest
42    case False{}:
43      SCon{Chr{c}, rest}
44
45# `''` inside single quotes is one quote. `prevq`: the last character was a
46# kept quote.
47def sq.unesc(s: String, +prevq: Bool) -> String:
48  match s:
49    case SNil{}:
50      ""
51    case SCon{Chr{+c}, t}:
52      sq.emit(prevq && U32.is_eq(c, 39), c, sq.unesc(t, Bool.not(prevq) && U32.is_eq(c, 39)))
53
54def dq.val(r: Result<&2, &2, String, Json.Json>) -> L.JR():
55  match r:
56    case Done{Json.JStr{s}}:
57      Done{Json.JStr{s}}
58    case Done{x}:
59      Fail{"bad double-quoted string"}
60    case Fail{e}:
61      Fail{"bad double-quoted string: " ++ e}
62
63def first_code(s: String) -> U32:
64  match s:
65    case SNil{}:
66      0
67    case SCon{Chr{c}, t}:
68      c
69
70def unquote.q(isd: Bool, +tok: String) -> L.JR():
71  match isd:
72    case True{}:
73      dq.val(Json.parse(tok))
74    case False{}:
75      Done{Json.JStr{sq.unesc(String.take(String.drop(tok, 1n), Nat.sub(String.length(tok), 2n)), False{})}}
76
77# A quoted token (quotes included) as the string it spells.
78def unquote(+tok: String) -> L.JR():
79  unquote.q(U32.is_eq(first_code(tok), 34), tok)
80
81def str_of(r: L.JR()) -> Result<&2, &2, String, String>:
82  match r:
83    case Done{Json.JStr{s}}:
84      Done{s}
85    case Done{x}:
86      Fail{"expected a string"}
87    case Fail{e}:
88      Fail{e}
89
90# ---- one atom ----------------------------------------------------------------------
91
92def after_ok(+after: String) -> Bool:
93  String.is_empty(after) || U32.is_eq(first_code(after), 35)
94
95def quoted.fin(ok: Bool, +tok: String) -> L.JR():
96  match ok:
97    case True{}:
98      unquote(tok)
99    case False{}:
100      Fail{"unexpected text after a quoted string"}
101
102def quoted.tok(m: Maybe<&2, Nat>, +s: String) -> L.JR():
103  match m:
104    case None{}:
105      Fail{"unterminated quoted string"}
106    case Some{+n}:
107      quoted.fin(after_ok(String.trim(String.drop(s, n))), String.take(s, n))
108
109def special(+c: U32) -> Bool:
110  U32.is_eq(c, 124) || U32.is_eq(c, 62)
111
112def tagged(+c: U32) -> Bool:
113  U32.is_eq(c, 38) || U32.is_eq(c, 42) || U32.is_eq(c, 33)
114
115def atom.pick(isq: Bool, isb: Bool, ist: Bool, +s: String) -> L.JR():
116  match isq isb ist:
117    case True{} _ _:
118      quoted.tok(L.token_len(s), s)
119    case False{} True{} _:
120      Fail{"block scalars are not supported"}
121    case False{} False{} True{}:
122      Fail{"anchors, aliases and tags are not supported"}
123    case False{} False{} False{}:
124      Done{L.resolve(uncomment(s))}
125
126# A plain or quoted scalar (trimmed text).
127def atom(+s: String) -> L.JR():
128  atom.pick(U32.is_eq(first_code(s), 34) || U32.is_eq(first_code(s), 39), special(first_code(s)), tagged(first_code(s)), s)
129
130# ---- flow collections (flat: scalars only) -------------------------------------------
131
132type Sp is Data:
133  Sp{done: List<&2, String>, cur: String, q: U32}
134
135def sp.push(+c: U32, +sp: Sp) -> Sp:
136  match sp:
137    case Sp{done, cur, q}:
138      Sp{done, SCon{Chr{c}, cur}, q}
139
140def sp.cut(+sp: Sp) -> Sp:
141  match sp:
142    case Sp{done, cur, q}:
143      Sp{String.trim(String.reverse(cur)) <> done, "", q}
144
145def sp.quote(+c: U32, +sp: Sp) -> Sp:
146  match sp:
147    case Sp{done, cur, q}:
148      Sp{done, SCon{Chr{c}, cur}, c}
149
150def sp.close(+c: U32, +sp: Sp) -> Sp:
151  match sp:
152    case Sp{done, cur, q}:
153      Sp{done, SCon{Chr{c}, cur}, 0}
154
155def sp.outside(+c: U32, +sp: Sp) -> Sp:
156  Bool.pick(Sp, U32.is_eq(c, 44), sp.cut(sp),
157  Bool.pick(Sp, U32.is_eq(c, 34) || U32.is_eq(c, 39), sp.quote(c, sp), sp.push(c, sp)))
158
159def sp.q_of(sp: Sp) -> U32:
160  match sp:
161    case Sp{done, cur, q}:
162      q
163
164def sp.inside(+c: U32, +sp: Sp) -> Sp:
165  Bool.pick(Sp, U32.is_eq(c, sp.q_of(sp)), sp.close(c, sp), sp.push(c, sp))
166
167def sp.step(+c: U32, +sp: Sp) -> Sp:
168  Bool.pick(Sp, U32.is_eq(sp.q_of(sp), 0), sp.outside(c, sp), sp.inside(c, sp))
169
170def sp.go(s: String, +sp: Sp) -> Sp:
171  match s:
172    case SNil{}:
173      sp
174    case SCon{Chr{c}, t}:
175      sp.go(t, sp.step(c, sp))
176
177def sp.fin(+sp: Sp) -> List<&2, String>:
178  match sp:
179    case Sp{done, cur, q}:
180      List.reverse(&2, String, String.trim(String.reverse(cur)) <> done)
181
182# The comma-separated pieces of a flow body, quotes respected.
183def split_top(+s: String) -> List<&2, String>:
184  Bool.pick(List<&2, String>, String.is_empty(String.trim(s)), [], sp.fin(sp.go(s, Sp{[], "", 0})))
185
186def item.cons(x: L.JR(), rest: L.JR()) -> L.JR():
187  match x rest:
188    case Done{a} Done{b}:
189      Done{Json.JCons{a, b}}
190    case Fail{e} _:
191      Fail{e}
192    case _ Fail{e}:
193      Fail{e}
194
195def flow.items(ps: List<&2, String>) -> L.JR():
196  match ps:
197    case Nil{}:
198      Done{Json.JNil{}}
199    case Con{+p, r}:
200      item.cons(atom(p), flow.items(r))
201
202def wrap.arr(r: L.JR()) -> L.JR():
203  match r:
204    case Done{c}:
205      Done{Json.JArr{c}}
206    case Fail{e}:
207      Fail{e}
208
209def wrap.obj(r: L.JR()) -> L.JR():
210  match r:
211    case Done{c}:
212      Done{Json.JObj{c}}
213    case Fail{e}:
214      Fail{e}
215
216def inner(+s: String) -> String:
217  String.take(String.drop(s, 1n), Nat.sub(String.length(s), 2n))
218
219def last_code(s: String) -> U32:
220  first_code(String.reverse(s))
221
222def seq.fin(closed: Bool, +s: String) -> L.JR():
223  match closed:
224    case True{}:
225      wrap.arr(flow.items(split_top(inner(s))))
226    case False{}:
227      Fail{"unterminated flow sequence"}
228
229# ---- key: value ----------------------------------------------------------------------
230
231type KV is Data:
232  KNone{}
233  KBad{msg: String}
234  KOk{key: String, rest: String}
235
236# Index of the first `:` that ends a key (followed by a space or the end).
237def colon.pick(hit: Bool, n: Nat, rest: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
238  match hit:
239    case True{}:
240      Some{n}
241    case False{}:
242      rest
243
244def colon.find(s: String, +n: Nat) -> Maybe<&2, Nat>:
245  match s:
246    case SNil{}:
247      None{}
248    case SCon{Chr{c}, SNil{}}:
249      colon.pick(U32.is_eq(c, 58), n, None{})
250    case SCon{Chr{+c}, SCon{Chr{+d}, t}}:
251      colon.pick(U32.is_eq(c, 58) && L.is_space(d), n, colon.find(SCon{Chr{d}, t}, Nat.add(n, 1n)))
252
253def kv.plain(m: Maybe<&2, Nat>, +s: String) -> KV:
254  match m:
255    case None{}:
256      KNone{}
257    case Some{+n}:
258      KOk{String.trim(String.take(s, n)), String.trim(String.drop(s, Nat.add(n, 1n)))}
259
260def kv.colon(+k: String, +after: String) -> KV:
261  match after:
262    case SCon{Chr{c}, t}:
263      Bool.pick(KV, U32.is_eq(c, 58) && (String.is_empty(t) || L.is_space(first_code(t))), KOk{k, String.trim(t)}, KNone{})
264    case SNil{}:
265      KNone{}
266
267def kv.after(r: Result<&2, &2, String, String>, +after: String) -> KV:
268  match r:
269    case Fail{e}:
270      KBad{e}
271    case Done{k}:
272      kv.colon(k, after)
273
274def kv.quoted(m: Maybe<&2, Nat>, +s: String) -> KV:
275  match m:
276    case None{}:
277      KNone{}
278    case Some{+n}:
279      kv.after(str_of(unquote(String.take(s, n))), String.drop(s, n))
280
281def kv.pick(isq: Bool, isflow: Bool, +s: String) -> KV:
282  match isq isflow:
283    case True{} _:
284      kv.quoted(L.token_len(s), s)
285    case False{} True{}:
286      KNone{}
287    case False{} False{}:
288      kv.plain(colon.find(s, 0n), s)
289
290# The `key: rest` a line spells, when it spells one.
291def split_kv(+s: String) -> KV:
292  kv.pick(U32.is_eq(first_code(s), 34) || U32.is_eq(first_code(s), 39), U32.is_eq(first_code(s), 91) || U32.is_eq(first_code(s), 123), s)
293
294def field.cons(k: String, x: L.JR(), rest: L.JR()) -> L.JR():
295  match x rest:
296    case Done{a} Done{b}:
297      Done{Json.JField{k, a, b}}
298    case Fail{e} _:
299      Fail{e}
300    case _ Fail{e}:
301      Fail{e}
302
303def map.piece(kv: KV) -> L.JR():
304  match kv:
305    case KOk{k, rest}:
306      field.cons(k, atom(rest), Done{Json.JNil{}})
307    case KBad{m}:
308      Fail{m}
309    case KNone{}:
310      Fail{"expected key: value in a flow mapping"}
311
312# Merge one piece's field in front of the chain.
313def map.cons(x: L.JR(), rest: L.JR()) -> L.JR():
314  match x rest:
315    case Done{Json.JField{k, v, n}} Done{b}:
316      Done{Json.JField{k, v, b}}
317    case Fail{e} _:
318      Fail{e}
319    case _ Fail{e}:
320      Fail{e}
321    case _ _:
322      Fail{"bad flow mapping"}
323
324def map.items(ps: List<&2, String>) -> L.JR():
325  match ps:
326    case Nil{}:
327      Done{Json.JNil{}}
328    case Con{+p, r}:
329      map.cons(map.piece(split_kv(p)), map.items(r))
330
331def map.fin(closed: Bool, +s: String) -> L.JR():
332  match closed:
333    case True{}:
334      wrap.obj(map.items(split_top(inner(s))))
335    case False{}:
336      Fail{"unterminated flow mapping"}
337
338def flow.pick(isseq: Bool, ismap: Bool, +s: String) -> L.JR():
339  match isseq ismap:
340    case True{} _:
341      seq.fin(U32.is_eq(last_code(s), 93), s)
342    case False{} True{}:
343      map.fin(U32.is_eq(last_code(s), 125), s)
344    case False{} False{}:
345      atom(s)
346
347# A value after `key:` or `- `: flow collection, quoted or plain scalar.
348def scalar(+s: String) -> L.JR():
349  flow.pick(U32.is_eq(first_code(s), 91), U32.is_eq(first_code(s), 123), s)