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 10 import Base 11 12 import ./value.bend as V 13 14 import ./re.bend as R 15 16 import ./num.bend as N 17 18 def yaml.tag(name: String) -> String: 19 "tag:yaml.org,2002:" ++ name 20 21 def 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 28 def 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 31 def is_true(+s: String) -> Bool: 32 Bool.or(String.eq(s, "true"), Bool.or(String.eq(s, "True"), String.eq(s, "TRUE"))) 33 34 def is_false(+s: String) -> Bool: 35 Bool.or(String.eq(s, "false"), Bool.or(String.eq(s, "False"), String.eq(s, "FALSE"))) 36 37 def dec.pat() -> List<&2, R.Pat>: 38 [R.sign(), R.digits()] 39 40 def oct.pat() -> List<&2, R.Pat>: 41 [R.c('0'), R.c('o'), R.Plus{R.One{R.Range{'0', '7'}}}] 42 43 def 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]+)? 47 def 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 51 def is_inf(+s: String) -> Bool: 52 Bool.or(String.eq(s, ".inf"), Bool.or(String.eq(s, ".Inf"), String.eq(s, ".INF"))) 53 54 def is_nan(+s: String) -> Bool: 55 Bool.or(String.eq(s, ".nan"), Bool.or(String.eq(s, ".NaN"), String.eq(s, ".NAN"))) 56 57 def 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 70 def is_neg(s: String) -> Bool: 71 String.starts_with(s, "-") 72 73 # An integer's decimal text, if +s+ is one. 74 def 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. 81 def 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". 85 def 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 91 def 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 98 def 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. 106 def 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 112 def 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 119 def 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). 127 def 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. 140 def 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. 146 def 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. 155 def 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. 163 def 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. 167 def 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 170 def 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. 178 def collection(+tag: String, v: V.Yaml) -> V.Yaml: 179 collection.or.keep(known.collection(tag), tag, v)