src/value.bend source
src/value.bend on the hub · documented module
# src/value: a JSON value. Null, bool, number, string, array, and object.# An array is a JArr of JCons cells ending in JNil; an object is a JObj of# JPair cells ending in JNil. A number keeps its source text.import Baseimport ./lazy.bend as Lazy# a JSON value; JNil, JCons and JPair are the cells of arrays and objects.# JSpan and JBind are the same string and key, as the first `n` chars of a# source suffix, so a parse does not copy that text.type Json is Data: JNull{} JBool{b: Bool} JNum{raw: String} JStr{s: String} JArr{items: Json} JObj{pairs: Json} JNil{} JCons{head: Json, tail: Json} JPair{key: String, val: Json, rest: Json} JSpan{src: String, n: U32} JBind{src: String, n: U32, val: Json, rest: Json}# where a scan of a JSON number istype Num is Data: NStart{} NInt{} NZero{} NDig{} NDot{} NFrac{} NExp{} NExpN{} NExpD{} NBad{}# the first `nn` chars of `src` as their own stringdef span.str(src: String, +nn: U32, zero: Bool) -> String: match src zero: case s True{}: "" case SNil{} False{}: "" case SCon{h, t} False{}: SCon{h, span.str(t, U32.sub(nn, 1), U32.is_zero(U32.sub(nn, 1)))}# whether the first `nn` chars of `src` equal `key`, stopping at the first miss.# A count past the end of `src` covers what there is, as span.str reads itdef span.eq.go(src: String, same: Bool, zero: Bool, key: String, +nn: U32) -> Bool: match src same zero key: case s False{} zero k: False{} case s True{} True{} SNil{}: True{} case s True{} True{} k: False{} case SCon{+a, as} True{} False{} SCon{+b, bs}: span.eq.go(as, Char.is_eq(a, b), U32.is_zero(U32.sub(nn, 1)), bs, U32.sub(nn, 1)) case SNil{} True{} False{} SNil{}: True{} case s True{} False{} k: False{}# whether the first `nn` chars of `src` are `key`def span.eq(+src: String, +nn: U32, +key: String) -> Bool: span.eq.go(src, True{}, U32.is_zero(nn), key, nn)# reverses a chain of JCons or JPair cells onto accdef reverse(cells: Json, acc: Json) -> Json: match cells: case JCons{h, t}: reverse(t, JCons{h, acc}) case JPair{k, v, r}: reverse(r, JPair{k, v, acc}) case JBind{src, n, v, r}: reverse(r, JBind{src, n, v, acc}) case _: accdef arr.go(items: List<&2, Json>) -> Json: match items: case Nil{}: JNil{} case Con{h, t}: JCons{h, arr.go(t)}# an array from a listdef arr(items: List<&2, Json>) -> Json: JArr{arr.go(items)}def obj.go(pairs: List<&1, String & Json>) -> Json: match pairs: case Nil{}: JNil{} case Con{(k, v), t}: JPair{k, v, obj.go(t)}# an object from pairsdef obj(pairs: List<&1, String & Json>) -> Json: JObj{obj.go(pairs)}# nulldef null() -> Json: JNull{}# a booleandef bool(flag: Bool) -> Json: JBool{flag}# a stringdef str(txt: String) -> Json: JStr{txt}def num.go(raw: String, ok: Bool) -> Json: match ok: case True{}: JNum{raw} case False{}: JNull{}def num.ok.digit(+cp: U32) -> Bool: Bool.and(U32.is_ge(cp, 48), U32.is_le(cp, 57))def num.ok.expmark(+cp: U32) -> Bool: Bool.or(U32.is_eq(cp, 101), U32.is_eq(cp, 69))def num.ok.lead(+cp: U32) -> Bool: Bool.and(U32.is_ge(cp, 49), U32.is_le(cp, 57))def num.ok.step.lead(ok: Bool) -> Num: match ok: case True{}: NDig{} case False{}: NBad{}def num.ok.step.int.go(+cp: U32, zero: Bool) -> Num: match zero: case True{}: NZero{} case False{}: num.ok.step.lead(num.ok.lead(cp))def num.ok.step.int(+cp: U32) -> Num: num.ok.step.int.go(cp, U32.is_eq(cp, 48))def num.ok.step.start.go(+cp: U32, minus: Bool) -> Num: match minus: case True{}: NInt{} case False{}: num.ok.step.int(cp)def num.ok.step.start(+cp: U32) -> Num: num.ok.step.start.go(cp, U32.is_eq(cp, 45))def num.ok.step.mark.go(mark: Bool) -> Num: match mark: case True{}: NExp{} case False{}: NBad{}def num.ok.step.mark(+cp: U32) -> Num: num.ok.step.mark.go(num.ok.expmark(cp))def num.ok.step.body.digit(more: Bool) -> Num: match more: case True{}: NDig{} case False{}: NBad{}def num.ok.step.body.other.go(+cp: U32, dot: Bool) -> Num: match dot: case True{}: NDot{} case False{}: num.ok.step.mark(cp)def num.ok.step.body.other(+cp: U32) -> Num: num.ok.step.body.other.go(cp, U32.is_eq(cp, 46))def num.ok.step.body.go(+cp: U32, more: Bool, digit: Bool) -> Num: match digit: case True{}: num.ok.step.body.digit(more) case False{}: num.ok.step.body.other(cp)def num.ok.step.body(+cp: U32, more: Bool) -> Num: num.ok.step.body.go(cp, more, num.ok.digit(cp))def num.ok.step.frac.other(+cp: U32, have: Bool) -> Num: match have: case True{}: num.ok.step.mark(cp) case False{}: NBad{}def num.ok.step.frac.go(+cp: U32, have: Bool, digit: Bool) -> Num: match digit: case True{}: NFrac{} case False{}: num.ok.step.frac.other(cp, have)def num.ok.step.frac(+cp: U32, have: Bool) -> Num: num.ok.step.frac.go(cp, have, num.ok.digit(cp))def num.ok.step.expd.go(digit: Bool) -> Num: match digit: case True{}: NExpD{} case False{}: NBad{}def num.ok.step.expd(+cp: U32) -> Num: num.ok.step.expd.go(num.ok.digit(cp))def num.ok.step.exps.minus.go(+cp: U32, minus: Bool) -> Num: match minus: case True{}: NExpN{} case False{}: num.ok.step.expd(cp)def num.ok.step.exps.minus(+cp: U32) -> Num: num.ok.step.exps.minus.go(cp, U32.is_eq(cp, 45))def num.ok.step.exps.go(+cp: U32, plus: Bool) -> Num: match plus: case True{}: NExpN{} case False{}: num.ok.step.exps.minus(cp)def num.ok.step.exps(+cp: U32) -> Num: num.ok.step.exps.go(cp, U32.is_eq(cp, 43))def num.ok.step(st: Num, +cp: U32) -> Num: match st: case NStart{}: num.ok.step.start(cp) case NInt{}: num.ok.step.int(cp) case NZero{}: num.ok.step.body(cp, False{}) case NDig{}: num.ok.step.body(cp, True{}) case NDot{}: num.ok.step.frac(cp, False{}) case NFrac{}: num.ok.step.frac(cp, True{}) case NExp{}: num.ok.step.exps(cp) case NExpN{}: num.ok.step.expd(cp) case NExpD{}: num.ok.step.expd(cp) case NBad{}: NBad{}def num.ok.done(st: Num) -> Bool: match st: case NZero{}: True{} case NDig{}: True{} case NFrac{}: True{} case NExpD{}: True{} case other: False{}def num.ok.go(cs: List<&2, Char>, st: Num) -> Bool: match cs: case Nil{}: num.ok.done(st) case Con{c, t}: num.ok.go(t, num.ok.step(st, Char.to_u32(c)))# whether text is a JSON numberdef num.ok(txt: String) -> Bool: num.ok.go(String.to_list(txt), NStart{})# a JSON number from its text, or null when that text is not onedef num(+raw: String) -> Json: num.go(raw, num.ok(raw))# character-for-character equality. `String.eq` rebuilds both strings and# discards the copies; a search only needs the bool. A mismatch stops.def find.eq.go(left: String, right: String, same: Bool) -> Bool: match left right same: case SNil{} SNil{} True{}: True{} case SCon{+x, xs} SCon{+y, ys} True{}: find.eq.go(xs, ys, Char.is_eq(x, y)) case l r s: False{}def find.eq(lhs: String, rhs: String) -> Bool: find.eq.go(lhs, rhs, True{})# the matched value, or the next pair when the key differsdef find.at(rest: Json, +key: String, eq: Bool, vv: Json) -> Json: match rest key eq vv: case r k True{} val: val case JPair{k2, v2, r2} k False{} val: find.at(r2, k, find.eq(k2, k), v2) case JBind{src, n, v2, r2} k False{} val: find.at(r2, k, span.eq(src, n, k), v2) case r k s val: JNull{}# the value of a key in a chain of pairs; JNull when the key is absentdef find(cells: Json, +key: String) -> Json: match cells: case JPair{k, v, r}: find.at(r, key, find.eq(k, key), v) case JBind{src, n, v, r}: find.at(r, key, span.eq(src, n, key), v) case other: JNull{}# an object's value at a key; JNull when it is not an object or has no such# key. A repeated key reads as the first value.def get(val: Json, key: String) -> Json: match val: case JObj{pairs}: find(pairs, key) case other: JNull{}def at.go(cells: Json, +idx: U32) -> Json: match cells: case JCons{h, t}: Lazy.stop(Json, U32.is_zero(idx), h, _u => at.go(t, U32.sub(idx, 1))) case other: JNull{}# an array's item at an index; JNull when it is not an array or has nonedef at(val: Json, idx: U32) -> Json: match val: case JArr{items}: at.go(items, idx) case other: JNull{}# what wf.go checks: a value, the cells of an array, or the cells of an objecttype Shape is Data: WVal{} WArr{} WObj{}# whether a value, or a chain of cells, has the shape the parser and the# builders give: arrays and objects are chains of cells ending in JNil, no# cell stands where a value belongs, and a number's text is a JSON numberdef wf.go(val: Json, shape: Shape) -> Bool: match val shape: case JNull{} WVal{}: True{} case JBool{_} WVal{}: True{} case JNum{raw} WVal{}: num.ok(raw) case JStr{_} WVal{}: True{} case JSpan{_, _} WVal{}: True{} case JArr{items} WVal{}: wf.go(items, WArr{}) case JObj{pairs} WVal{}: wf.go(pairs, WObj{}) case JNil{} WArr{}: True{} case JCons{hd, tl} WArr{}: Lazy.stop(Bool, Bool.not(wf.go(hd, WVal{})), False{}, _u => wf.go(tl, WArr{})) case JNil{} WObj{}: True{} case JPair{_, vv, rr} WObj{}: Lazy.stop(Bool, Bool.not(wf.go(vv, WVal{})), False{}, _u => wf.go(rr, WObj{})) case JBind{_, _, vv, rr} WObj{}: Lazy.stop(Bool, Bool.not(wf.go(vv, WVal{})), False{}, _u => wf.go(rr, WObj{})) case _ _: False{}# whether a value is well-formed: the shape every value from the builders and# from parse has. print writes JSON text for exactly thesedef wf(val: Json) -> Bool: wf.go(val, WVal{})# whether a chain of pairs has the key, stopping at the first matchdef has.go(cells: Json, +key: String) -> Bool: match cells: case JPair{k2, _, rr}: Lazy.stop(Bool, find.eq(k2, key), True{}, _u => has.go(rr, key)) case JBind{src, nn, _, rr}: Lazy.stop(Bool, span.eq(src, nn, key), True{}, _u => has.go(rr, key)) case _: False{}# whether an object has the key; false when the value is not an objectdef has(val: Json, key: String) -> Bool: match val: case JObj{pairs}: has.go(pairs, key) case _: False{}# how many cells a chain of array or object cells holdsdef len.go(cells: Json) -> Nat: match cells: case JCons{_, tl}: 1n+len.go(tl) case JPair{_, _, rr}: 1n+len.go(rr) case JBind{_, _, _, rr}: 1n+len.go(rr) case _: 0n# how many elements an array, or members an object, has; 0 for anything elsedef len(val: Json) -> U32: match val: case JArr{items}: U32.from_nat(len.go(items)) case JObj{pairs}: U32.from_nat(len.go(pairs)) case _: 0# a string, or none when the value is not onedef as_str(val: Json) -> Maybe<&2, String>: match val: case JStr{s}: Some{s} case JSpan{src, +n}: Some{span.str(src, n, U32.is_zero(n))} case other: None{}# a boolean, or none when the value is not onedef as_bool(val: Json) -> Maybe<&2, Bool>: match val: case JBool{b}: Some{b} case other: None{}# a number's text, or none when the value is not a numberdef as_num(val: Json) -> Maybe<&2, String>: match val: case JNum{raw}: Some{raw} case other: None{}# a number as a U32, or none when it is not one or does not fitdef as_u32(val: Json) -> Maybe<&2, U32>: match val: case JNum{raw}: U32.read(raw) case other: None{}# the largest finite F32def f32.top() -> F32: 3.4028235e38# a read that stayed finite; none when it did not. A JSON number never spells# infinity, so an infinite read is a number too large for F32def f32.fit.go(xx: F32, finite: Bool) -> Maybe<&2, F32>: match finite: case True{}: Some{xx} case False{}: None{}# a read that stayed finite, or nonedef f32.fit(got: Maybe<&2, F32>) -> Maybe<&2, F32>: match got: case Some{+xx}: f32.fit.go(xx, F32.is_le(F32.abs(xx), f32.top())) case None{}: None{}# a number as an F32, or none when it is not one or does not fitdef as_f32(val: Json) -> Maybe<&2, F32>: match val: case JNum{raw}: f32.fit(F32.read(raw)) case other: None{}