core.bend

yaml-for-bend@0.1.1.0 · 179 lines · 29 definitions

1# The YAML 1.2 Core schema: how a scalar's text and tag become a value.
2#
3# An untagged plain scalar is null (null, Null, NULL, ~ or nothing), a
4# boolean (true/True/TRUE, false/False/FALSE), an integer (decimal, 0o
5# octal, 0x hex), a float (decimal, .inf, -.inf, .nan) or else a string.
6# A quoted or block scalar, or one tagged "!", is a string. A standard tag
7# (!!str, !!null, !!bool, !!int, !!float) reads the text as that type, and
8# fails when it can't; any other tag is kept as a Tagged value.
9
10import Base
11
12import yaml-for-bend@0.1.1.0/value.bend as V
13
14import yaml-for-bend@0.1.1.0/re.bend as R
15
16import yaml-for-bend@0.1.1.0/num.bend as N
17
18def yaml.tag(name: String) -> String:
19  "tag:yaml.org,2002:" ++ name
20
21def when(-A: Type, hit: Bool, yes: Unit -> A, no: Unit -> A) -> A:
22  match hit:
23    case True{}:
24      yes(Unit{})
25    case False{}:
26      no(Unit{})
27
28def is_null(+s: String) -> Bool:
29  Bool.or(Bool.or(String.is_empty(s), String.eq(s, "~")), Bool.or(String.eq(s, "null"), Bool.or(String.eq(s, "Null"), String.eq(s, "NULL"))))
30
31def is_true(+s: String) -> Bool:
32  Bool.or(String.eq(s, "true"), Bool.or(String.eq(s, "True"), String.eq(s, "TRUE")))
33
34def is_false(+s: String) -> Bool:
35  Bool.or(String.eq(s, "false"), Bool.or(String.eq(s, "False"), String.eq(s, "FALSE")))
36
37def dec.pat() -> List<&2, R.Pat>:
38  [R.sign(), R.digits()]
39
40def oct.pat() -> List<&2, R.Pat>:
41  [R.c('0'), R.c('o'), R.Plus{R.One{R.Range{'0', '7'}}}]
42
43def hex.pat() -> List<&2, R.Pat>:
44  [R.c('0'), R.c('x'), R.Plus{R.One{R.Hex{}}}]
45
46# [-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?
47def float.pat() -> List<&2, R.Pat>:
48  [R.sign(), R.Alt{[[R.c('.'), R.digits()], [R.digits(), R.Opt{R.seq([R.c('.'), R.Star{R.digit()}])}]]},
49   R.Opt{R.seq([R.of("eE"), R.sign(), R.digits()])}]
50
51def is_inf(+s: String) -> Bool:
52  Bool.or(String.eq(s, ".inf"), Bool.or(String.eq(s, ".Inf"), String.eq(s, ".INF")))
53
54def is_nan(+s: String) -> Bool:
55  Bool.or(String.eq(s, ".nan"), Bool.or(String.eq(s, ".NaN"), String.eq(s, ".NAN")))
56
57def unsigned(s: String) -> String:
58  match s:
59    case SCon{c, t}:
60      match c:
61        case '-':
62          t
63        case '+':
64          t
65        case _:
66          SCon{c, t}
67    case SNil{}:
68      ""
69
70def is_neg(s: String) -> Bool:
71  String.starts_with(s, "-")
72
73# An integer's decimal text, if +s+ is one.
74def int.of(+s: String) -> Maybe<&2, String>:
75  when(Maybe<&2, String>, R.matches(dec.pat(), s), u => Some{N.int.text(is_neg(s), unsigned(s), 10n)},
76    u => when(Maybe<&2, String>, R.matches(oct.pat(), s), u => Some{N.int.text(False{}, String.drop(s, 2n), 8n)},
77      u => when(Maybe<&2, String>, R.matches(hex.pat(), s), u => Some{N.int.text(False{}, String.drop(s, 2n), 16n)}, u => None{})))
78
79# Whether +s+ is an integer in a base other than ten, which takes time
80# that grows with the square of its length to read.
81def in.base(+s: String) -> Bool:
82  Bool.or(R.matches(oct.pat(), s), R.matches(hex.pat(), s))
83
84# A float's text, if +s+ is one: as written, or ".inf", "-.inf", ".nan".
85def float.of(+s: String) -> Maybe<&2, String>:
86  +u = unsigned(s)
87  Bool.pick(Maybe<&2, String>, is_inf(u), Some{Bool.pick(String, is_neg(s), "-.inf", ".inf")},
88    Bool.pick(Maybe<&2, String>, is_nan(s), Some{".nan"},
89      Bool.pick(Maybe<&2, String>, R.matches(float.pat(), s), Some{s}, None{})))
90
91def or.int(m: Maybe<&2, String>, s: String, rest: Unit -> V.Yaml) -> V.Yaml:
92  match m:
93    case Some{t}:
94      V.Int{t}
95    case None{}:
96      rest(Unit{})
97
98def or.float(m: Maybe<&2, String>, rest: Unit -> V.Yaml) -> V.Yaml:
99  match m:
100    case Some{t}:
101      V.Float{t}
102    case None{}:
103      rest(Unit{})
104
105# An untagged plain scalar.
106def plain(+s: String) -> V.Yaml:
107  when(V.Yaml, is_null(s), u => V.Null{},
108    u => when(V.Yaml, is_true(s), u => V.Boolean{True{}},
109      u => when(V.Yaml, is_false(s), u => V.Boolean{False{}},
110        u => or.int(int.of(s), s, u => or.float(float.of(s), u => V.Str{s})))))
111
112def must(m: Maybe<&2, V.Yaml>, +tag: String, +s: String) -> Result<&2, &2, String, V.Yaml>:
113  match m:
114    case Some{v}:
115      Done{v}
116    case None{}:
117      Fail{"found a value that isn't a " ++ tag ++ ": " ++ s}
118
119def int.yaml(m: Maybe<&2, String>) -> Maybe<&2, V.Yaml>:
120  match m:
121    case Some{t}:
122      Some{V.Int{t}}
123    case None{}:
124      None{}
125
126# !!float: a float, or an integer written as one ("3" is 3.0).
127def float.yaml(m: Maybe<&2, String>, i: Maybe<&2, String>) -> Maybe<&2, V.Yaml>:
128  match m:
129    case Some{t}:
130      Some{V.Float{t}}
131    case None{}:
132      match i:
133        case Some{t}:
134          Some{V.Float{t ++ ".0"}}
135        case None{}:
136          None{}
137
138# The tags whose text the Core schema reads: "!", !!str, !!null, !!bool,
139# !!int and !!float.
140def known(+tag: String) -> Bool:
141  Bool.or(Bool.or(String.eq(tag, "!"), String.eq(tag, yaml.tag("str"))),
142    Bool.or(Bool.or(String.eq(tag, yaml.tag("null")), String.eq(tag, yaml.tag("bool"))), Bool.or(String.eq(tag, yaml.tag("int")), String.eq(tag, yaml.tag("float")))))
143
144# A scalar's text read by a known tag: "!" and !!str are a string, and
145# !!null, !!bool, !!int and !!float must be one.
146def read(+tag: String, +s: String) -> Result<&2, &2, String, V.Yaml>:
147  when(Result<&2, &2, String, V.Yaml>, Bool.or(String.eq(tag, "!"), String.eq(tag, yaml.tag("str"))), u => Done{V.Str{s}},
148    u => when(Result<&2, &2, String, V.Yaml>, String.eq(tag, yaml.tag("null")), u => must(Bool.pick(Maybe<&2, V.Yaml>, is_null(s), Some{V.Null{}}, None{}), "!!null", s),
149      u => when(Result<&2, &2, String, V.Yaml>, String.eq(tag, yaml.tag("bool")),
150        u => must(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{})), "!!bool", s),
151        u => when(Result<&2, &2, String, V.Yaml>, String.eq(tag, yaml.tag("int")), u => must(int.yaml(int.of(s)), "!!int", s),
152          u => must(float.yaml(float.of(s), int.of(s)), "!!float", s)))))
153
154# A tag that isn't known is kept with the text, as data: no tag runs code.
155def read.or.keep(hit: Bool, +tag: String, +s: String) -> Result<&2, &2, String, V.Yaml>:
156  match hit:
157    case True{}:
158      read(tag, s)
159    case False{}:
160      Done{V.Tagged{tag, V.Str{s}}}
161
162# A scalar with a tag.
163def tagged(+tag: String, +s: String) -> Result<&2, &2, String, V.Yaml>:
164  read.or.keep(known(tag), tag, s)
165
166# The tags that leave a collection as it is: "!", !!seq and !!map.
167def known.collection(+tag: String) -> Bool:
168  Bool.or(String.eq(tag, "!"), Bool.or(String.eq(tag, yaml.tag("seq")), String.eq(tag, yaml.tag("map"))))
169
170def collection.or.keep(hit: Bool, +tag: String, v: V.Yaml) -> V.Yaml:
171  match hit:
172    case True{}:
173      v
174    case False{}:
175      V.Tagged{tag, v}
176
177# A collection with a tag: another tag is kept with it, as data.
178def collection(+tag: String, v: V.Yaml) -> V.Yaml:
179  collection.or.keep(known.collection(tag), tag, v)