yaml11.bend

yaml-for-bend@0.1.1.0 · 252 lines · 41 definitions

1# The YAML 1.1 types (yaml.org/type), for YAML written for 1.1 parsers.
2#
3# An untagged plain scalar is null (~, null, Null, NULL or nothing), a
4# boolean (yes, no, true, false, on, off, each lowercase, capitalized or
5# uppercase), an integer (decimal, 0b binary, 0 octal, 0x hex, or base 60
6# as in 1:30:00, with "_" anywhere among the digits), a float (decimal,
7# base 60 with a fraction, .inf, -.inf, .nan), a timestamp, or a string.
8# A timestamp is Tagged{"tag:yaml.org,2002:timestamp", Str{text}}, the text
9# as written. "<<" is the merge key, which the composer handles.
10#
11# y, Y, n and N are strings, as in most 1.1 parsers, though the 1.1 type
12# list has them as booleans.
13
14import Base
15
16import yaml-for-bend@0.1.1.0/value.bend as V
17
18import yaml-for-bend@0.1.1.0/re.bend as R
19
20import yaml-for-bend@0.1.1.0/num.bend as N
21
22import yaml-for-bend@0.1.1.0/core.bend as C
23
24def when(-A: Type, hit: Bool, yes: Unit -> A, no: Unit -> A) -> A:
25  match hit:
26    case True{}:
27      yes(Unit{})
28    case False{}:
29      no(Unit{})
30
31def timestamp() -> String:
32  C.yaml.tag("timestamp")
33
34def is_true(+s: String) -> Bool:
35  List.contains(~String, ~String.eq, ["yes", "Yes", "YES", "true", "True", "TRUE", "on", "On", "ON"], s)
36
37def is_false(+s: String) -> Bool:
38  List.contains(~String, ~String.eq, ["no", "No", "NO", "false", "False", "FALSE", "off", "Off", "OFF"], s)
39
40# Patterns
41# --------
42
43def d() -> R.Pat:
44  R.digit()
45
46def digits_() -> R.Pat:
47  R.Star{R.of("0123456789_")}
48
49def bin.pat() -> List<&2, R.Pat>:
50  [R.sign(), R.c('0'), R.c('b'), R.Plus{R.of("01_")}]
51
52def oct.pat() -> List<&2, R.Pat>:
53  [R.sign(), R.c('0'), R.Plus{R.of("01234567_")}]
54
55def dec.pat() -> List<&2, R.Pat>:
56  [R.sign(), R.Alt{[[R.c('0')], [R.One{R.Range{'1', '9'}}, digits_()]]}]
57
58def hex.pat() -> List<&2, R.Pat>:
59  [R.sign(), R.c('0'), R.c('x'), R.Plus{R.Alt{[[R.One{R.Hex{}}], [R.c('_')]]}}]
60
61def sexa.part() -> R.Pat:
62  R.seq([R.c(':'), R.Opt{R.One{R.Range{'0', '5'}}}, d()])
63
64def sexa.int.pat() -> List<&2, R.Pat>:
65  [R.sign(), R.One{R.Range{'1', '9'}}, digits_(), R.Plus{sexa.part()}]
66
67def float.pat() -> List<&2, R.Pat>:
68  [R.sign(), R.Opt{R.seq([d(), digits_()])}, R.c('.'), digits_(), R.Opt{R.seq([R.of("eE"), R.of("-+"), R.digits()])}]
69
70def sexa.float.pat() -> List<&2, R.Pat>:
71  [R.sign(), d(), digits_(), R.Plus{sexa.part()}, R.c('.'), digits_()]
72
73def ymd.pat() -> List<&2, R.Pat>:
74  [d(), d(), d(), d(), R.c('-'), d(), d(), R.c('-'), d(), d()]
75
76def blank() -> R.Pat:
77  R.of(" \t")
78
79def time.pat() -> List<&2, R.Pat>:
80  [d(), d(), d(), d(), R.c('-'), d(), R.Opt{d()}, R.c('-'), d(), R.Opt{d()},
81   R.Alt{[[R.of("Tt")], [R.Plus{blank()}]]}, d(), R.Opt{d()}, R.c(':'), d(), d(), R.c(':'), d(), d(),
82   R.Opt{R.seq([R.c('.'), R.Star{d()}])},
83   R.Opt{R.seq([R.Star{blank()}, R.Alt{[[R.c('Z')], [R.of("-+"), d(), R.Opt{d()}, R.Opt{R.seq([R.c(':'), d(), d()])}]]}])}]
84
85def is_timestamp(+s: String) -> Bool:
86  Bool.or(R.matches(ymd.pat(), s), R.matches(time.pat(), s))
87
88# Values
89# ------
90
91def no.seps(s: String) -> String:
92  match s:
93    case SNil{}:
94      ""
95    case SCon{c, t}:
96      match c:
97        case '_':
98          no.seps(t)
99        case _:
100          SCon{c, no.seps(t)}
101
102def has_digit(s: String) -> Bool:
103  match s:
104    case SNil{}:
105      False{}
106    case SCon{+c, t}:
107      Bool.or(Char.is_digit(c), has_digit(t))
108
109# Base 60: each part is the next digit, "1:30" is 90.
110def sexa.go(parts: List<&2, String>, acc: List<&2, Nat>) -> List<&2, Nat>:
111  match parts:
112    case Nil{}:
113      acc
114    case Con{p, rest}:
115      sexa.go(rest, N.big.mul.add(acc, 60n, N.big.small(p, 0n)))
116
117def sexa.big(parts: List<&2, String>) -> List<&2, Nat>:
118  match parts:
119    case Nil{}:
120      []
121    case Con{p, rest}:
122      sexa.go(rest, N.big.read(p, 10n, []))
123
124def int.of.digits(+ds: String) -> String:
125  when(String, String.starts_with(ds, "0b"), u => N.big.show(N.big.read(String.drop(ds, 2n), 2n, [])),
126    u => when(String, String.starts_with(ds, "0x"), u => N.big.show(N.big.read(String.drop(ds, 2n), 16n, [])),
127      u => when(String, String.contains(ds, ":"), u => N.big.show(sexa.big(String.split(ds, ':'))),
128        u => when(String, String.starts_with(ds, "0"), u => N.big.show(N.big.read(ds, 8n, [])), u => N.decimal(ds)))))
129
130# An integer's decimal text; +s+ already matched one of the integer forms.
131def int.text(+s: String) -> String:
132  +t = no.seps(s)
133  N.signed(C.is_neg(t), int.of.digits(C.unsigned(t)))
134
135def is_int(+s: String) -> Bool:
136  Bool.or(Bool.or(R.matches(dec.pat(), s), R.matches(oct.pat(), s)),
137    Bool.or(Bool.or(R.matches(hex.pat(), s), R.matches(bin.pat(), s)), R.matches(sexa.int.pat(), s)))
138
139# Whether +s+ is an integer or float in a base other than ten, which takes
140# time that grows with the square of its length to read.
141def in.base(+s: String) -> Bool:
142  Bool.or(Bool.or(R.matches(oct.pat(), s), R.matches(hex.pat(), s)),
143    Bool.or(Bool.or(R.matches(bin.pat(), s), R.matches(sexa.int.pat(), s)), R.matches(sexa.float.pat(), s)))
144
145def before.dot(s: String) -> String:
146  match s:
147    case SNil{}:
148      ""
149    case SCon{c, t}:
150      match c:
151        case '.':
152          ""
153        case _:
154          SCon{c, before.dot(t)}
155
156def after.dot(s: String) -> String:
157  match s:
158    case SNil{}:
159      ""
160    case SCon{c, t}:
161      match c:
162        case '.':
163          t
164        case _:
165          after.dot(t)
166
167# A base-60 float: the whole part in base 60, then the fraction.
168def sexa.float(+s: String) -> String:
169  +t = no.seps(s)
170  +body = C.unsigned(t)
171  N.dec.exact(Bool.pick(String, C.is_neg(t), "-", "") ++ N.big.show(sexa.big(String.split(before.dot(body), ':'))) ++ "." ++ after.dot(body))
172
173def float.of(+s: String) -> Maybe<&2, String>:
174  +body = C.unsigned(s)
175  when(Maybe<&2, String>, C.is_inf(body), u => Some{Bool.pick(String, C.is_neg(s), "-.inf", ".inf")},
176    u => when(Maybe<&2, String>, C.is_nan(s), u => Some{".nan"},
177      u => when(Maybe<&2, String>, Bool.and(R.matches(float.pat(), s), has_digit(s)), u => Some{N.dec.exact(no.seps(s))},
178        u => when(Maybe<&2, String>, R.matches(sexa.float.pat(), s), u => Some{sexa.float(s)}, u => None{}))))
179
180def int.of(+s: String) -> Maybe<&2, String>:
181  when(Maybe<&2, String>, is_int(s), u => Some{int.text(s)}, u => None{})
182
183def or.int(m: Maybe<&2, String>, rest: Unit -> V.Yaml) -> V.Yaml:
184  match m:
185    case Some{t}:
186      V.Int{t}
187    case None{}:
188      rest(Unit{})
189
190def or.float(m: Maybe<&2, String>, rest: Unit -> V.Yaml) -> V.Yaml:
191  match m:
192    case Some{t}:
193      V.Float{t}
194    case None{}:
195      rest(Unit{})
196
197# An untagged plain scalar.
198def plain(+s: String) -> V.Yaml:
199  when(V.Yaml, C.is_null(s), u => V.Null{},
200    u => when(V.Yaml, is_true(s), u => V.Boolean{True{}},
201      u => when(V.Yaml, is_false(s), u => V.Boolean{False{}},
202        u => or.int(int.of(s), u => or.float(float.of(s),
203          u => when(V.Yaml, is_timestamp(s), u => V.Tagged{timestamp(), V.Str{s}}, u => V.Str{s}))))))
204
205def int.yaml(m: Maybe<&2, String>) -> Maybe<&2, V.Yaml>:
206  match m:
207    case Some{t}:
208      Some{V.Int{t}}
209    case None{}:
210      None{}
211
212# !!float: a float, or an integer written as one ("3" is 3.0).
213def float.yaml(m: Maybe<&2, String>, i: Maybe<&2, String>) -> Maybe<&2, V.Yaml>:
214  match m:
215    case Some{t}:
216      Some{V.Float{t}}
217    case None{}:
218      match i:
219        case Some{t}:
220          Some{V.Float{N.dec.exact(t)}}
221        case None{}:
222          None{}
223
224def bool.yaml(+s: String) -> Maybe<&2, V.Yaml>:
225  Bool.pick(Maybe<&2, V.Yaml>, is_true(s), Some{V.Boolean{True{}}}, Bool.pick(Maybe<&2, V.Yaml>, is_false(s), Some{V.Boolean{False{}}}, None{}))
226
227# The tags whose text YAML 1.1 reads: "!", !!str, !!merge, !!null, !!bool,
228# !!int and !!float.
229def known(+tag: String) -> Bool:
230  Bool.or(Bool.or(Bool.or(String.eq(tag, "!"), String.eq(tag, C.yaml.tag("str"))), String.eq(tag, C.yaml.tag("merge"))),
231    Bool.or(Bool.or(String.eq(tag, C.yaml.tag("null")), String.eq(tag, C.yaml.tag("bool"))), Bool.or(String.eq(tag, C.yaml.tag("int")), String.eq(tag, C.yaml.tag("float")))))
232
233# A scalar's text read by a known tag, in its 1.1 forms: !!merge is the
234# merge key, "!" and !!str are a string.
235def read(+tag: String, +s: String) -> Result<&2, &2, String, V.Yaml>:
236  when(Result<&2, &2, String, V.Yaml>, Bool.or(Bool.or(String.eq(tag, "!"), String.eq(tag, C.yaml.tag("str"))), String.eq(tag, C.yaml.tag("merge"))), u => Done{V.Str{s}},
237    u => when(Result<&2, &2, String, V.Yaml>, String.eq(tag, C.yaml.tag("null")), u => C.must(Bool.pick(Maybe<&2, V.Yaml>, C.is_null(s), Some{V.Null{}}, None{}), "!!null", s),
238      u => when(Result<&2, &2, String, V.Yaml>, String.eq(tag, C.yaml.tag("bool")), u => C.must(bool.yaml(s), "!!bool", s),
239        u => when(Result<&2, &2, String, V.Yaml>, String.eq(tag, C.yaml.tag("int")), u => C.must(int.yaml(int.of(s)), "!!int", s),
240          u => C.must(float.yaml(float.of(s), int.of(s)), "!!float", s)))))
241
242# A tag that isn't known is kept with the text, as data: no tag runs code.
243def read.or.keep(hit: Bool, +tag: String, +s: String) -> Result<&2, &2, String, V.Yaml>:
244  match hit:
245    case True{}:
246      read(tag, s)
247    case False{}:
248      Done{V.Tagged{tag, V.Str{s}}}
249
250# A scalar with a tag.
251def tagged(+tag: String, +s: String) -> Result<&2, &2, String, V.Yaml>:
252  read.or.keep(known(tag), tag, s)