src/step.bend

bend-toon-codec@0.1.0.0 · 267 lines · 35 definitions

1import Base
2import bend-toon-codec@0.1.0.0/src/json.bend as J
3import bend-toon-codec@0.1.0.0/src/split.bend as S
4import bend-toon-codec@0.1.0.0/src/prim.bend as P
5import bend-toon-codec@0.1.0.0/src/hdr.bend as H
6import bend-toon-codec@0.1.0.0/src/entry.bend as E
7import bend-toon-codec@0.1.0.0/src/dline.bend as D
8import bend-toon-codec@0.1.0.0/src/frame.bend as F
9import bend-toon-codec@0.1.0.0/src/head.bend as Hd
10
11# One content line applied to the decoder state (spec sections 8 to 10).
12
13type Start is Data:
14  SVal{v: J.Json}
15  SOpen{fr: F.Fr}
16
17# What follows a list hyphen.
18type Item is Data:
19  IBad{}
20  IEmpty{}
21  IText{t: String}
22
23def count(+xs: J.Json) -> U32:
24  match xs:
25    case J.JCons{h, t}:
26      (1 + count(t) : U32)
27    case _:
28      0
29
30def list.len(+ks: List<&2, String>) -> U32:
31  match ks:
32    case Nil{}:
33      0
34    case Con{k, t}:
35      (1 + list.len(t) : U32)
36
37# ---- arrays after a header --------------------------------------------------------
38
39def inline.ok(+ok: Bool, +vs: J.Json) -> Result<&2, &2, String, Start>:
40  match ok:
41    case True{}:
42      Done{SVal{J.JArr{vs}}}
43    case False{}:
44      Fail{"array length does not match its header"}
45
46def inline.done(+r: Result<&2, &2, String, J.Json>, +n: U32, +strict: Bool) -> Result<&2, &2, String, Start>:
47  match r:
48    case Fail{e}:
49      Fail{e}
50    case Done{vs}:
51      inline.ok(Bool.not(strict) || U32.is_eq(count(vs), n), vs)
52
53def inline.go(+fields: List<&2, String>, +n: U32, +d: U32, +rest: String, +strict: Bool) -> Result<&2, &2, String, Start>:
54  match fields:
55    case Nil{}:
56      inline.done(P.values(S.split(rest, d)), n, strict)
57    case Con{f, t}:
58      Fail{"values after a field list"}
59
60def block(+zero: Bool, +fields: List<&2, String>, +n: U32, +d: U32, +att: F.Att, +depth: U32) -> Result<&2, &2, String, Start>:
61  match zero fields:
62    case True{} _:
63      Done{SVal{J.JArr{J.JNil{}}}}
64    case False{} Nil{}:
65      Done{SOpen{F.FLst{depth, att, n, 0, J.JNil{}}}}
66    case False{} Con{f, t}:
67      Done{SOpen{F.FTab{depth, att, n, 0, d, Con{f, t}, J.JNil{}}}}
68
69def start.pick(+noinline: Bool, +n: U32, +d: U32, +fields: List<&2, String>, +rest: String, +att: F.Att, +depth: U32, +strict: Bool) -> Result<&2, &2, String, Start>:
70  match noinline:
71    case True{}:
72      block(U32.is_zero(n), fields, n, d, att, depth)
73    case False{}:
74      inline.go(fields, n, d, rest, strict)
75
76# What a header opens: a finished array (inline values or zero length) or an
77# open frame whose child lines sit at `depth`.
78def arr_start(+att: F.Att, +depth: U32, +h: H.Hdr, +strict: Bool) -> Result<&2, &2, String, Start>:
79  match h:
80    case H.Hdr{n, d, fields, rest}:
81      start.pick(String.is_empty(String.trim(rest)), n, d, fields, rest, att, depth, strict)
82
83# ---- object lines ------------------------------------------------------------------
84
85def obj.arr(+r: Result<&2, &2, String, Start>, +k: String, +dp: U32, +a: F.Att, +fs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32, +strict: Bool) -> Result<&2, &2, String, F.St>:
86  match r:
87    case Fail{e}:
88      Fail{e}
89    case Done{SVal{v}}:
90      F.put.field(strict, dp, a, fs, k, v, rest, root, bmin)
91    case Done{SOpen{fr}}:
92      Done{F.St{Con{fr, Con{F.FObj{dp, a, fs}, rest}}, root, bmin}}
93
94# An entry applied to the object frame `FObj{dp, a, fs}` on top of `rest`.
95def obj.ent(+ent: E.Ent, +dp: U32, +a: F.Att, +fs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32, +strict: Bool) -> Result<&2, &2, String, F.St>:
96  match ent:
97    case E.ENot{}:
98      Fail{"missing colon after key"}
99    case E.EVal{k, v}:
100      F.put.field(strict, dp, a, fs, k, v, rest, root, bmin)
101    case E.EOpen{k}:
102      Done{F.St{Con{F.FObj{(dp + 1 : U32), F.AKey{k}, J.JNil{}}, Con{F.FObj{dp, a, fs}, rest}}, root, bmin}}
103    case E.EArr{Some{k}, h}:
104      obj.arr(arr_start(F.AKey{k}, (dp + 1 : U32), h, strict), k, dp, a, fs, rest, root, bmin, strict)
105    case E.EArr{None{}, h}:
106      Fail{"array header without a key"}
107
108def obj.line(+r: Result<&2, &2, String, E.Ent>, +dp: U32, +a: F.Att, +fs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32, +strict: Bool) -> Result<&2, &2, String, F.St>:
109  match r:
110    case Fail{e}:
111      Fail{e}
112    case Done{ent}:
113      obj.ent(ent, dp, a, fs, rest, root, bmin, strict)
114
115# ---- list lines --------------------------------------------------------------------
116
117def hyphen.pick(+bare: Bool, +spaced: Bool, +text: String) -> Item:
118  match bare spaced:
119    case True{} _:
120      IEmpty{}
121    case False{} True{}:
122      IText{String.trim(Hd.drop(Hd.drop(text)))}
123    case False{} False{}:
124      IBad{}
125
126def hyphen(+text: String) -> Item:
127  hyphen.pick(String.eq(text, "-"), Hd.lead2(text, 45, 32), text)
128
129def lst.push(+v: J.Json, +dp: U32, +a: F.Att, +n: U32, +c: U32, +xs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32) -> Result<&2, &2, String, F.St>:
130  Done{F.St{Con{F.FLst{dp, a, n, (c + 1 : U32), J.JCons{v, xs}}, rest}, root, bmin}}
131
132def item.prim(+r: Result<&2, &2, String, J.Json>, +dp: U32, +a: F.Att, +n: U32, +c: U32, +xs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32) -> Result<&2, &2, String, F.St>:
133  match r:
134    case Fail{e}:
135      Fail{e}
136    case Done{v}:
137      lst.push(v, dp, a, n, c, xs, rest, root, bmin)
138
139def item.arr(+r: Result<&2, &2, String, Start>, +dp: U32, +a: F.Att, +n: U32, +c: U32, +xs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32) -> Result<&2, &2, String, F.St>:
140  match r:
141    case Fail{e}:
142      Fail{e}
143    case Done{SVal{v}}:
144      lst.push(v, dp, a, n, c, xs, rest, root, bmin)
145    case Done{SOpen{fr}}:
146      Done{F.St{Con{fr, Con{F.FLst{dp, a, n, (c + 1 : U32), xs}, rest}}, root, bmin}}
147
148# A list item whose text parsed as an entry or as nothing special.
149def item.ent(+r: Result<&2, &2, String, E.Ent>, +t: String, +strict: Bool, +dp: U32, +a: F.Att, +n: U32, +c: U32, +xs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32) -> Result<&2, &2, String, F.St>:
150  match r:
151    case Fail{e}:
152      Fail{e}
153    case Done{E.ENot{}}:
154      item.prim(P.parse(t), dp, a, n, c, xs, rest, root, bmin)
155    case Done{E.EArr{None{}, h}}:
156      item.arr(arr_start(F.AItem{}, (dp + 1 : U32), h, strict), dp, a, n, c, xs, rest, root, bmin)
157    case Done{ent}:
158      obj.ent(ent, (dp + 1 : U32), F.AItem{}, J.JNil{}, Con{F.FLst{dp, a, n, (c + 1 : U32), xs}, rest}, root, bmin, strict)
159
160def lst.item(+it: Item, +strict: Bool, +dp: U32, +a: F.Att, +n: U32, +c: U32, +xs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32) -> Result<&2, &2, String, F.St>:
161  match it:
162    case IBad{}:
163      Fail{"expected a list item"}
164    case IEmpty{}:
165      lst.push(J.JObj{J.JNil{}}, dp, a, n, c, xs, rest, root, bmin)
166    case IText{t}:
167      item.ent(E.parse(strict, t), t, strict, dp, a, n, c, xs, rest, root, bmin)
168
169# ---- tabular rows ------------------------------------------------------------------
170
171def row.like.go(+di: U32, +ci: U32) -> Bool:
172  U32.is_eq(ci, S.none()) || (Bool.not(U32.is_eq(di, S.none())) && U32.is_lt(di, ci))
173
174# Section 9.3: a line with no unquoted colon, or whose first unquoted
175# delimiter comes before its first unquoted colon, is a row.
176def row.like(+text: String, +d: U32) -> Bool:
177  row.like.go(S.find_unq(text, d, 0, 0), S.find_unq(text, 58, 0, 0))
178
179def zip(+ks: List<&2, String>, +vs: J.Json) -> J.Json:
180  match ks vs:
181    case Con{k, kt} J.JCons{v, vt}:
182      J.JField{k, v, zip(kt, vt)}
183    case _ _:
184      J.JNil{}
185
186def row.push(+ok: Bool, +vs: J.Json, +dp: U32, +a: F.Att, +n: U32, +c: U32, +d: U32, +ks: List<&2, String>, +xs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32) -> Result<&2, &2, String, F.St>:
187  match ok:
188    case False{}:
189      Fail{"row width does not match the field count"}
190    case True{}:
191      Done{F.St{Con{F.FTab{dp, a, n, (c + 1 : U32), d, ks, J.JCons{J.JObj{zip(ks, vs)}, xs}}, rest}, root, bmin}}
192
193def row.add(+r: Result<&2, &2, String, J.Json>, +strict: Bool, +dp: U32, +a: F.Att, +n: U32, +c: U32, +d: U32, +ks: List<&2, String>, +xs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32) -> Result<&2, &2, String, F.St>:
194  match r:
195    case Fail{e}:
196      Fail{e}
197    case Done{vs}:
198      row.push(Bool.not(strict) || U32.is_eq(count(vs), list.len(ks)), vs, dp, a, n, c, d, ks, xs, rest, root, bmin)
199
200def tab.line(+isrow: Bool, +text: String, +strict: Bool, +dp: U32, +a: F.Att, +n: U32, +c: U32, +d: U32, +ks: List<&2, String>, +xs: J.Json, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32) -> Result<&2, &2, String, F.St>:
201  match isrow:
202    case False{}:
203      Fail{"key-value line among tabular rows"}
204    case True{}:
205      row.add(P.values(S.split(text, d)), strict, dp, a, n, c, d, ks, xs, rest, root, bmin)
206
207# ---- dispatch ----------------------------------------------------------------------
208
209def top.go(+text: String, +top: F.Fr, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32, +strict: Bool) -> Result<&2, &2, String, F.St>:
210  match top:
211    case F.FObj{dp, a, fs}:
212      obj.line(E.parse(strict, text), dp, a, fs, rest, root, bmin, strict)
213    case F.FLst{dp, a, n, c, xs}:
214      lst.item(hyphen(text), strict, dp, a, n, c, xs, rest, root, bmin)
215    case F.FTab{dp, a, n, c, d, ks, xs}:
216      tab.line(row.like(text, d), text, strict, dp, a, n, c, d, ks, xs, rest, root, bmin)
217
218def top.line(+deep: Bool, +text: String, +top: F.Fr, +rest: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32, +strict: Bool) -> Result<&2, &2, String, F.St>:
219  match deep:
220    case True{}:
221      Fail{"unexpected indentation"}
222    case False{}:
223      top.go(text, top, rest, root, bmin, strict)
224
225def dispatch.stk(+depth: U32, +text: String, +stk: List<&2, F.Fr>, +root: Maybe<&2, J.Json>, +bmin: U32, +strict: Bool) -> Result<&2, &2, String, F.St>:
226  match stk:
227    case Nil{}:
228      Fail{"unexpected content after the end of the document"}
229    case Con{top, rest}:
230      top.line(strict && U32.is_gt(depth, F.fr.depth(top)), text, top, rest, root, bmin, strict)
231
232def dispatch(+depth: U32, +text: String, +r: Result<&2, &2, String, F.St>, +strict: Bool) -> Result<&2, &2, String, F.St>:
233  match r:
234    case Fail{e}:
235      Fail{e}
236    case Done{F.St{stk, root, bmin}}:
237      dispatch.stk(depth, text, stk, root, bmin, strict)
238
239def wrap(+no: U32, +r: Result<&2, &2, String, F.St>) -> Result<&2, &2, String, F.St>:
240  match r:
241    case Fail{e}:
242      Fail{D.at(no, e)}
243    case Done{st}:
244      Done{st}
245
246def note.blank(+strict: Bool, +st: F.St) -> F.St:
247  match strict:
248    case True{}:
249      F.blank.note(st)
250    case False{}:
251      st
252
253def content(+no: U32, +depth: U32, +text: String, +st: F.St, +strict: Bool) -> Result<&2, &2, String, F.St>:
254  wrap(no, dispatch(depth, text, F.pop(List.length(&2, F.Fr, F.stk.of(st)), F.blank.check(st, depth, strict), (depth + 1 : U32), strict), strict))
255
256def step.go(+blank: Bool, +no: U32, +depth: U32, +text: String, +st: F.St, +strict: Bool) -> Result<&2, &2, String, F.St>:
257  match blank:
258    case True{}:
259      Done{note.blank(strict, st)}
260    case False{}:
261      content(no, depth, text, st, strict)
262
263# Apply one line. A blank line only updates the blank-line bookkeeping.
264def step(+l: D.DL, +st: F.St, +strict: Bool) -> Result<&2, &2, String, F.St>:
265  match l:
266    case D.DL{no, depth, text, blank}:
267      step.go(blank, no, depth, text, st, strict)