fmt/yamlr.bend
cswap-bend-cli@0.1.0.0 · 326 lines · 52 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
4import cswap-bend-cli@0.1.0.0/fmt/yscal.bend as S
5
6
7
8
9
10
11
12
13
14
15
16
17
18type Ln is Data:
19 Ln{ind: Nat, text: String}
20
21
22type Plan is Data:
23 PRes{r: Result<&2, &2, String, Json.Json>}
24 PBlk{ls: List<&2, Ln>}
25 PArr{es: List<&2, List<&2, Ln>>}
26 PObj{es: List<&2, List<&2, Ln>>}
27 PItems{b: List<&2, Ln>, r: List<&2, List<&2, Ln>>}
28 PFieldI{k: String, v: Result<&2, &2, String, Json.Json>, r: List<&2, List<&2, Ln>>}
29 PFieldB{k: String, b: List<&2, Ln>, r: List<&2, List<&2, Ln>>}
30
31
32
33
34
35def lead.count(s: String, +n: Nat) -> Nat:
36 match s:
37 case SCon{Chr{c}, t}:
38 Bool.pick(Nat, U32.is_eq(c, 32), lead.count(t, Nat.add(n, 1n)), n)
39 case SNil{}:
40 n
41
42def tab_lead(s: String) -> Bool:
43 match s:
44 case SCon{Chr{+c}, t}:
45 Bool.pick(Bool, U32.is_eq(c, 32), tab_lead(t), U32.is_eq(c, 9))
46 case SNil{}:
47 False{}
48
49def is_comment(+t: String) -> Bool:
50 String.is_empty(t) || U32.is_eq(S.first_code(t), 35)
51
52type Acc is Data:
53 Acc{ls: List<&2, Ln>, seen: Bool, err: String}
54
55def acc.add(+a: Acc, +raw: String) -> Acc:
56 match a:
57 case Acc{ls, seen, err}:
58 Acc{Ln{lead.count(raw, 0n), String.trim_start(raw)} <> ls, True{}, err}
59
60def acc.fail(+a: Acc, msg: String) -> Acc:
61 match a:
62 case Acc{ls, seen, err}:
63 Acc{ls, seen, Bool.pick(String, String.is_empty(err), msg, err)}
64
65def acc.skip(+a: Acc) -> Acc:
66 a
67
68def acc.marker(+a: Acc, +seen: Bool) -> Acc:
69 Bool.pick(Acc, seen, acc.fail(a, "several documents are not supported"), a)
70
71def acc.seen(+a: Acc) -> Bool:
72 match a:
73 case Acc{ls, seen, err}:
74 seen
75
76def acc.line2(blank: Bool, marker: Bool, tab: Bool, +a: Acc, +raw: String) -> Acc:
77 match blank marker tab:
78 case True{} _ _:
79 a
80 case False{} True{} _:
81 acc.marker(a, acc.seen(a))
82 case False{} False{} True{}:
83 acc.fail(a, "tabs are not allowed for indentation")
84 case False{} False{} False{}:
85 acc.add(a, raw)
86
87def acc.line(+a: Acc, +raw: String) -> Acc:
88 acc.line2(is_comment(String.trim_start(raw)), String.eq(String.trim_end(raw), "---"), tab_lead(raw), a, String.trim_end(raw))
89
90def acc.go(rs: List<&2, String>, +a: Acc) -> Acc:
91 match rs:
92 case Nil{}:
93 a
94 case Con{r, t}:
95 acc.go(t, acc.line(a, r))
96
97def lines.fin(+a: Acc) -> Result<&2, &2, String, List<&2, Ln>>:
98 match a:
99 case Acc{ls, seen, err}:
100 Bool.pick(Result<&2, &2, String, List<&2, Ln>>, String.is_empty(err), Done{List.reverse(&2, Ln, ls)}, Fail{err})
101
102def lines_of(+text: String) -> Result<&2, &2, String, List<&2, Ln>>:
103 lines.fin(acc.go(String.split(text, '\n'), Acc{[], False{}, ""}))
104
105
106
107def all_ge(ls: List<&2, Ln>, +n: Nat) -> Bool:
108 match ls:
109 case Nil{}:
110 True{}
111 case Con{Ln{i, t}, r}:
112 Nat.is_ge(i, n) && all_ge(r, n)
113
114def is_dash(+t: String) -> Bool:
115 String.eq(t, "-") || String.starts_with(t, "- ")
116
117def key_empty.of(kv: S.KV) -> Bool:
118 match kv:
119 case S.KOk{k, rest}:
120 String.is_empty(rest)
121 case _:
122 False{}
123
124def key_empty(+t: String) -> Bool:
125 key_empty.of(S.split_kv(t))
126
127type Sp is Data:
128 Sp{done: List<&2, List<&2, Ln>>, cur: List<&2, Ln>, hold: Bool}
129
130def sp.start(+sp: Sp, +l: Ln, hold: Bool) -> Sp:
131 match sp:
132 case Sp{done, cur, h}:
133 Sp{Bool.pick(List<&2, List<&2, Ln>>, List.is_empty(&2, Ln, cur), done, List.reverse(&2, Ln, cur) <> done), [l], hold}
134
135def sp.more(+sp: Sp, +l: Ln) -> Sp:
136 match sp:
137 case Sp{done, cur, h}:
138 Sp{done, l <> cur, h}
139
140def sp.hold(+sp: Sp) -> Bool:
141 match sp:
142 case Sp{done, cur, h}:
143 h
144
145
146
147def sp.step2(new: Bool, +sp: Sp, +l: Ln, +t: String) -> Sp:
148 match new:
149 case True{}:
150 sp.start(sp, l, key_empty(t))
151 case False{}:
152 sp.more(sp, l)
153
154def sp.step(+n: Nat, +map: Bool, +sp: Sp, +l: Ln) -> Sp:
155 match l:
156 case Ln{i, t}:
157 sp.step2(Nat.is_eq(i, n) && Bool.not(map && sp.hold(sp) && is_dash(t)), sp, Ln{i, t}, t)
158
159def sp.go(ls: List<&2, Ln>, +n: Nat, +map: Bool, +sp: Sp) -> Sp:
160 match ls:
161 case Nil{}:
162 sp
163 case Con{l, t}:
164 sp.go(t, n, map, sp.step(n, map, sp, l))
165
166def sp.fin(+sp: Sp) -> List<&2, List<&2, Ln>>:
167 match sp:
168 case Sp{done, cur, h}:
169 List.reverse(&2, List<&2, Ln>, Bool.pick(List<&2, List<&2, Ln>>, List.is_empty(&2, Ln, cur), done, List.reverse(&2, Ln, cur) <> done))
170
171def split(ls: List<&2, Ln>, +n: Nat, +map: Bool) -> List<&2, List<&2, Ln>>:
172 sp.fin(sp.go(ls, n, map, Sp{[], [], False{}}))
173
174
175
176def bad(msg: String) -> Plan:
177 PRes{Fail{msg}}
178
179def item.text(+t: String) -> String:
180 Bool.pick(String, String.eq(t, "-"), "", String.trim_start(String.drop(t, 1n)))
181
182def item.rest(+r: String) -> String:
183 Bool.pick(String, String.is_empty(S.uncomment(r)), "", r)
184
185def item.block(+n: Nat, +t: String, +rest: String, +body: List<&2, Ln>) -> List<&2, Ln>:
186 Bool.pick(List<&2, Ln>, String.is_empty(rest), body, Ln{Nat.add(n, Nat.sub(String.length(t), String.length(rest))), rest} <> body)
187
188def items.head(dash: Bool, +n: Nat, +t: String, body: List<&2, Ln>, r: List<&2, List<&2, Ln>>) -> Plan:
189 match dash:
190 case True{}:
191 PItems{item.block(n, t, item.rest(item.text(t)), body), r}
192 case False{}:
193 bad("expected '- ' to start a sequence item")
194
195def plan.items(es: List<&2, List<&2, Ln>>) -> Plan:
196 match es:
197 case Nil{}:
198 PRes{Done{Json.JNil{}}}
199 case Con{Con{Ln{n, +t}, body}, r}:
200 items.head(is_dash(t), n, t, body, r)
201 case Con{Nil{}, r}:
202 bad("empty sequence entry")
203
204def field.extra(v: Result<&2, &2, String, Json.Json>, +k: String) -> Plan:
205 match v:
206 case Fail{e}:
207 bad(e)
208 case Done{x}:
209 bad("unexpected indented lines after the value of '" ++ k ++ "'")
210
211def field.both(no_body: Bool, +k: String, +rest: String, r: List<&2, List<&2, Ln>>) -> Plan:
212 match no_body:
213 case True{}:
214 PFieldI{k, S.scalar(rest), r}
215 case False{}:
216 field.extra(S.scalar(rest), k)
217
218def field.inline(empty: Bool, +k: String, +rest: String, body: List<&2, Ln>, r: List<&2, List<&2, Ln>>) -> Plan:
219 match empty:
220 case True{}:
221 PFieldB{k, body, r}
222 case False{}:
223 field.both(List.is_empty(&2, Ln, body), k, rest, r)
224
225def field.kv(kv: S.KV, body: List<&2, Ln>, r: List<&2, List<&2, Ln>>) -> Plan:
226 match kv:
227 case S.KOk{k, +rest}:
228 field.inline(String.is_empty(S.uncomment(rest)), k, rest, body, r)
229 case S.KBad{m}:
230 bad(m)
231 case S.KNone{}:
232 bad("expected 'key: value'")
233
234def plan.fields(es: List<&2, List<&2, Ln>>) -> Plan:
235 match es:
236 case Nil{}:
237 PRes{Done{Json.JNil{}}}
238 case Con{Con{Ln{n, t}, body}, r}:
239 field.kv(S.split_kv(t), body, r)
240 case Con{Nil{}, r}:
241 bad("empty mapping entry")
242
243type BKind is Data:
244 KSeq{}
245 KMap{}
246 KScalar{}
247
248def kind.of2(dash: Bool, kv: S.KV) -> BKind:
249 match dash kv:
250 case True{} _:
251 KSeq{}
252 case False{} S.KNone{}:
253 KScalar{}
254 case False{} _:
255 KMap{}
256
257def kind.of(+t: String) -> BKind:
258 kind.of2(is_dash(t), S.split_kv(t))
259
260def blk.scalar(single: Bool, +t: String) -> Plan:
261 match single:
262 case True{}:
263 PRes{S.scalar(t)}
264 case False{}:
265 bad("unexpected indentation")
266
267def blk.kind(k: BKind, +n: Nat, +t: String, ls: List<&2, Ln>, +all: List<&2, Ln>) -> Plan:
268 match k:
269 case KSeq{}:
270 PArr{split(all, n, False{})}
271 case KMap{}:
272 PObj{split(all, n, True{})}
273 case KScalar{}:
274 blk.scalar(List.is_empty(&2, Ln, ls), t)
275
276def blk.fit(ok: Bool, +n: Nat, +t: String, ls: List<&2, Ln>, +all: List<&2, Ln>) -> Plan:
277 match ok:
278 case True{}:
279 blk.kind(kind.of(t), n, t, ls, all)
280 case False{}:
281 bad("inconsistent indentation")
282
283def plan.blk(+ls: List<&2, Ln>) -> Plan:
284 match ls:
285 case Nil{}:
286 PRes{Done{Json.JNull{}}}
287 case Con{Ln{n, t}, rest}:
288 blk.fit(all_ge(rest, n), n, t, rest, Con{Ln{n, t}, rest})
289
290
291
292def item.cons(x: Result<&2, &2, String, Json.Json>, rest: Result<&2, &2, String, Json.Json>) -> Result<&2, &2, String, Json.Json>:
293 S.item.cons(x, rest)
294
295def val(fuel: Nat, pl: Plan) -> Result<&2, &2, String, Json.Json>:
296 match fuel pl:
297 case 0n _:
298 Fail{"nesting too deep"}
299 case 1n++p PRes{r}:
300 r
301 case 1n++p PBlk{ls}:
302 val(p, plan.blk(ls))
303 case 1n++p PArr{es}:
304 S.wrap.arr(val(p, plan.items(es)))
305 case 1n++p PObj{es}:
306 S.wrap.obj(val(p, plan.fields(es)))
307 case 1n++p PItems{b, r}:
308 item.cons(val(p, PBlk{b}), val(p, plan.items(r)))
309 case 1n++p PFieldI{k, v, r}:
310 S.field.cons(k, v, val(p, plan.fields(r)))
311 case 1n++p PFieldB{k, b, r}:
312 S.field.cons(k, val(p, PBlk{b}), val(p, plan.fields(r)))
313
314def parse.run(ls: List<&2, Ln>, +fuel: Nat) -> Result<&2, &2, String, Json.Json>:
315 val(fuel, PBlk{ls})
316
317def parse.lines(r: Result<&2, &2, String, List<&2, Ln>>) -> Result<&2, &2, String, Json.Json>:
318 match r:
319 case Fail{e}:
320 Fail{e}
321 case Done{+ls}:
322 parse.run(ls, Nat.add(Nat.mul(4n, List.length(&2, Ln, ls)), 8n))
323
324
325def parse(+text: String) -> Result<&2, &2, String, Json.Json>:
326 parse.lines(lines_of(text))