fmt/ylex.bend

cswap-bend-cli@0.1.0.0 · 231 lines · 38 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3
4# YAML pieces shared by the writer (yamlw.bend) and the reader (yamlr.bend):
5# which strings may be written plain, quoted tokens, and how a plain word
6# resolves to null, a boolean, a number or a string. Pure.
7
8def JR() -> Type:
9  Result<&2, &2, String, Json.Json>
10
11def ge(+c: U32, lo: U32) -> Bool:
12  U32.is_ge(c, lo)
13
14def between(+c: U32, lo: U32, hi: U32) -> Bool:
15  U32.is_ge(c, lo) && U32.is_le(c, hi)
16
17def is_digit(+c: U32) -> Bool:
18  between(c, 48, 57)
19
20def is_space(+c: U32) -> Bool:
21  U32.is_eq(c, 32)
22
23# ---- numbers (the JSON grammar: -?(0|[1-9]d*)(.d+)?([eE][+-]?d+)?) ---------
24
25type NS is Data:
26  NStart{}
27  NMinus{}
28  NZero{}
29  NInt{}
30  NDot{}
31  NFrac{}
32  NExp{}
33  NESign{}
34  NEDig{}
35  NBad{}
36
37def ns.lead(+c: U32) -> NS:
38  Bool.pick(NS, U32.is_eq(c, 48), NZero{}, Bool.pick(NS, between(c, 49, 57), NInt{}, NBad{}))
39
40def ns.start(+c: U32) -> NS:
41  Bool.pick(NS, U32.is_eq(c, 45), NMinus{}, ns.lead(c))
42
43def ns.tail(+c: U32) -> NS:
44  Bool.pick(NS, U32.is_eq(c, 46), NDot{}, Bool.pick(NS, U32.is_eq(c, 101) || U32.is_eq(c, 69), NExp{}, NBad{}))
45
46def ns.int(+c: U32) -> NS:
47  Bool.pick(NS, is_digit(c), NInt{}, ns.tail(c))
48
49def ns.frac(+c: U32) -> NS:
50  Bool.pick(NS, is_digit(c), NFrac{}, Bool.pick(NS, U32.is_eq(c, 101) || U32.is_eq(c, 69), NExp{}, NBad{}))
51
52def ns.exp(+c: U32) -> NS:
53  Bool.pick(NS, U32.is_eq(c, 43) || U32.is_eq(c, 45), NESign{}, Bool.pick(NS, is_digit(c), NEDig{}, NBad{}))
54
55def ns.digits(+c: U32, next: NS) -> NS:
56  Bool.pick(NS, is_digit(c), next, NBad{})
57
58def ns.step(s: NS, +c: U32) -> NS:
59  match s:
60    case NStart{}:
61      ns.start(c)
62    case NMinus{}:
63      ns.lead(c)
64    case NZero{}:
65      ns.tail(c)
66    case NInt{}:
67      ns.int(c)
68    case NDot{}:
69      ns.digits(c, NFrac{})
70    case NFrac{}:
71      ns.frac(c)
72    case NExp{}:
73      ns.exp(c)
74    case NESign{}:
75      ns.digits(c, NEDig{})
76    case NEDig{}:
77      ns.digits(c, NEDig{})
78    case NBad{}:
79      NBad{}
80
81def ns.run(s: String, st: NS) -> NS:
82  match s:
83    case SNil{}:
84      st
85    case SCon{Chr{c}, t}:
86      ns.run(t, ns.step(st, c))
87
88def ns.accept(s: NS) -> Bool:
89  match s:
90    case NZero{}:
91      True{}
92    case NInt{}:
93      True{}
94    case NFrac{}:
95      True{}
96    case NEDig{}:
97      True{}
98    case _:
99      False{}
100
101# Is the whole text a JSON number?
102def is_number(s: String) -> Bool:
103  ns.accept(ns.run(s, NStart{}))
104
105# ---- plain words -------------------------------------------------------------
106
107def word_is(+low: String, w: String) -> Bool:
108  String.eq(low, w)
109
110def is_null_word(+low: String) -> Bool:
111  String.is_empty(low) || word_is(low, "~") || word_is(low, "null")
112
113def is_bool_word(+low: String) -> Bool:
114  word_is(low, "true") || word_is(low, "false")
115
116# Words YAML 1.1 and 1.2 readers turn into another type.
117def reserved(+low: String) -> Bool:
118  is_null_word(low) || is_bool_word(low) || word_is(low, "yes") || word_is(low, "no") || word_is(low, "on") || word_is(low, "off") || word_is(low, "y") || word_is(low, "n")
119
120# A plain word as the value it stands for.
121def resolve.low(+low: String, +s: String) -> Json.Json:
122  Bool.pick(Json.Json, is_null_word(low), Json.JNull{},
123  Bool.pick(Json.Json, word_is(low, "true"), Json.JBool{True{}},
124  Bool.pick(Json.Json, word_is(low, "false"), Json.JBool{False{}},
125  Bool.pick(Json.Json, is_number(s), Json.JNum{s}, Json.JStr{s}))))
126
127def resolve(+s: String) -> Json.Json:
128  resolve.low(String.to_lower(s), s)
129
130# ---- may a string be written plain? -------------------------------------------
131
132# Characters a plain scalar must not start with: indicators, quotes, and
133# anything a reader could take for a number, a date, a tag or a merge key.
134def bad_first(+c: U32) -> Bool:
135  U32.is_eq(c, 45) || U32.is_eq(c, 63) || U32.is_eq(c, 58) || U32.is_eq(c, 44) || U32.is_eq(c, 91) || U32.is_eq(c, 93) || U32.is_eq(c, 123) || U32.is_eq(c, 125)
136    || U32.is_eq(c, 35) || U32.is_eq(c, 38) || U32.is_eq(c, 42) || U32.is_eq(c, 33) || U32.is_eq(c, 124) || U32.is_eq(c, 62) || U32.is_eq(c, 39) || U32.is_eq(c, 34)
137    || U32.is_eq(c, 37) || U32.is_eq(c, 64) || U32.is_eq(c, 96) || U32.is_eq(c, 43) || U32.is_eq(c, 46) || U32.is_eq(c, 126) || U32.is_eq(c, 61) || U32.is_eq(c, 60)
138    || is_digit(c) || is_space(c)
139
140def bad_char(+c: U32) -> Bool:
141  U32.is_lt(c, 32) || between(c, 127, 159) || between(c, 8232, 8233) || U32.is_eq(c, 65279)
142
143# Scan the rest: no control characters, no ": " or trailing ":", no " #".
144def rest_ok(s: String, +prev: U32) -> Bool:
145  match s:
146    case SNil{}:
147      Bool.not(U32.is_eq(prev, 58)) && Bool.not(is_space(prev))
148    case SCon{Chr{+c}, t}:
149      Bool.not(bad_char(c)) && Bool.not(U32.is_eq(prev, 58) && is_space(c)) && Bool.not(is_space(prev) && U32.is_eq(c, 35)) && rest_ok(t, c)
150
151def first_ok(s: String) -> Bool:
152  match s:
153    case SNil{}:
154      False{}
155    case SCon{Chr{+c}, t}:
156      Bool.not(bad_first(c)) && Bool.not(bad_char(c)) && rest_ok(t, c)
157
158def plain_ok(+s: String) -> Bool:
159  first_ok(s) && Bool.not(reserved(String.to_lower(s)))
160
161# ---- quoted tokens ------------------------------------------------------------
162
163def dq.pick(done: Bool, n: Nat, rest: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
164  match done:
165    case True{}:
166      Some{n}
167    case False{}:
168      rest
169
170# `n` characters of a double-quoted body (after the opening quote) up to and
171# including its closing quote, or None when it never closes.
172def dq.end(s: String, +n: Nat, +esc: Bool) -> Maybe<&2, Nat>:
173  match s:
174    case SNil{}:
175      None{}
176    case SCon{Chr{+c}, t}:
177      dq.pick(Bool.not(esc) && U32.is_eq(c, 34), Nat.add(n, 1n), dq.end(t, Nat.add(n, 1n), Bool.not(esc) && U32.is_eq(c, 92)))
178
179def sq.eos(prevq: Bool, n: Nat) -> Maybe<&2, Nat>:
180  match prevq:
181    case True{}:
182      Some{n}
183    case False{}:
184      None{}
185
186def sq.pick(done: Bool, n: Nat, rest: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
187  match done:
188    case True{}:
189      Some{n}
190    case False{}:
191      rest
192
193# The same for a single-quoted body: `''` is a quote. `prevq`: the last
194# character was a quote that may close the string.
195def sq.end(s: String, +n: Nat, +prevq: Bool) -> Maybe<&2, Nat>:
196  match s:
197    case SNil{}:
198      sq.eos(prevq, n)
199    case SCon{Chr{+c}, t}:
200      sq.pick(prevq && Bool.not(U32.is_eq(c, 39)), n, sq.end(t, Nat.add(n, 1n), Bool.not(prevq) && U32.is_eq(c, 39)))
201
202def first_code(s: String) -> U32:
203  match s:
204    case SNil{}:
205      0
206    case SCon{Chr{c}, t}:
207      c
208
209def tl.plus(m: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
210  match m:
211    case None{}:
212      None{}
213    case Some{n}:
214      Some{Nat.add(n, 1n)}
215
216def tl.pick(isd: Bool, iss: Bool, d: Maybe<&2, Nat>, s: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
217  match isd iss:
218    case True{} _:
219      d
220    case False{} True{}:
221      s
222    case False{} False{}:
223      None{}
224
225def token_len.q(+s: String, +c: U32) -> Maybe<&2, Nat>:
226  tl.pick(U32.is_eq(c, 34), U32.is_eq(c, 39), tl.plus(dq.end(String.drop(s, 1n), 0n, False{})), tl.plus(sq.end(String.drop(s, 1n), 0n, False{})))
227
228# Length of the quoted token at the start of `s` (quotes included), when `s`
229# starts with a quote and the token closes.
230def token_len(+s: String) -> Maybe<&2, Nat>:
231  token_len.q(s, first_code(s))