# Json v1 — leaf-only JSON AST + encode/decode for Bend. # Publish entry for this package. Depends only on Base (no Parse hub import). # # Number representation: U32 (unsigned 32-bit integers). No floats, signs, or # exponents in v1. # # AST constructors: JNull | JBool | JNum(U32) | JStr. # Arrays/objects (nested encode/decode) are deferred to json-nested / v2. # # Supported decode subset (leaves only): # - null, true, false # - numbers: one or more decimal digits → U32 (via U32.read) # - strings: "..." with escapes \", \\, \/, \n, \t, \r (other \x fail) # - optional JSON whitespace (space/tab/CR/LF) around a single leaf value # Not in v1: arrays, objects, floats, negatives, unicode \uXXXX. import Base type Json is Data: JNull{} JBool{b: Bool} JNum{n: U32} JStr{s: String} # ---- constructors / sugar ------------------------------------------------- def Json.null() -> Json: JNull{} def Json.bool(b: Bool) -> Json: JBool{b} def Json.num(n: U32) -> Json: JNum{n} def Json.str(s: String) -> Json: JStr{s} # ---- encode / show -------------------------------------------------------- def Json.encode.bool(b: Bool) -> String: match b: case False{}: "false" case True{}: "true" # Escape " and \ only when printing; other chars pass through (valid if already # JSON-safe). Decode accepts a slightly richer escape set (see decode.str). def Json.encode.esc.char(+c: Char) -> String: match c: case Chr{+x}: Bool.pick(String, U32.is_eq(x, 34), "\\\"", Bool.pick(String, U32.is_eq(x, 92), "\\\\", SCon{Chr{x}, SNil{}})) def Json.encode.esc.go(s: String) -> String: match s: case SNil{}: SNil{} case SCon{+h, t}: String.append(Json.encode.esc.char(h), Json.encode.esc.go(t)) def Json.encode.str(+s: String) -> String: String.append("\"", String.append(Json.encode.esc.go(s), "\"")) def Json.encode(j: Json) -> String: match j: case JNull{}: "null" case JBool{b}: Json.encode.bool(b) case JNum{n}: U32.show(n) case JStr{s}: Json.encode.str(s) def Json.show(j: Json) -> String: Json.encode(j) # ---- decode helpers (Base-only mini parser; not the Parse package) -------- def Json.skip_ws(s: String) -> String: String.trim_start(s) def Json.starts(+p: String, +s: String) -> Bool: String.starts_with(s, p) def Json.drop(+n: Nat, s: String) -> String: String.drop(s, n) # Span leading digits. Nat fuel + step flag (same shape as Parse.take_while). def Json.take_digits.go( n: Nat, s: String, acc: String, step: Bool, +h: Char, ok: Bool ) -> String & String: match n s step ok: case 0n s2 _ _: (String.reverse(acc), s2) case 1n+p SNil{} False{} _: (String.reverse(acc), SNil{}) case 1n+p SCon{+h2, t} False{} _: Json.take_digits.go(p, t, acc, True{}, h2, Char.is_digit(h2)) case 1n+p s2 True{} False{}: (String.reverse(acc), SCon{h, s2}) case 1n+p s2 True{} True{}: Json.take_digits.go(p, s2, SCon{h, acc}, False{}, h, False{}) case 1n+p s2 False{} _: (String.reverse(acc), s2) def Json.take_digits(+s: String) -> String & String: Json.take_digits.go( Nat.add(String.length(s), String.length(s)), s, SNil{}, False{}, Char.from_u32(0), False{} ) # String body with escapes. Nat fuel (2 steps/char) + esc flag; +fuel clones # Nat for Bool.pick arms (affine otherwise). def Json.decode.str.go( n: Nat, esc: Bool, s: String, +acc: String ) -> Maybe<&1, String & String>: match n esc s: case 0n _ _: None{} case 1n+p False{} SNil{}: None{} case 1n+p False{} SCon{Chr{+x}, +t}: +fuel = p Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 34), Some{(String.reverse(acc), t)}, Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 92), Json.decode.str.go(fuel, True{}, t, acc), Json.decode.str.go(fuel, False{}, t, SCon{Chr{x}, acc}))) case 1n+p True{} SNil{}: None{} case 1n+p True{} SCon{Chr{+x}, +t}: +fuel = p Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 34), Json.decode.str.go(fuel, False{}, t, SCon{Chr{34}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 92), Json.decode.str.go(fuel, False{}, t, SCon{Chr{92}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 47), Json.decode.str.go(fuel, False{}, t, SCon{Chr{47}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 110), Json.decode.str.go(fuel, False{}, t, SCon{Chr{10}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 116), Json.decode.str.go(fuel, False{}, t, SCon{Chr{9}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 114), Json.decode.str.go(fuel, False{}, t, SCon{Chr{13}, acc}), None{})))))) def Json.decode.str.after_quote(+s: String) -> Maybe<&1, String & String>: Json.decode.str.go( Nat.add(String.length(s), String.length(s)), False{}, s, SNil{} ) def Json.decode.lit(+rest: String, v: Json, +s: String) -> Maybe<&1, Json & String>: Bool.pick( Maybe<&1, Json & String>, Json.starts(rest, s), Some{(v, Json.drop(String.length(rest), s))}, None{} ) def Json.decode.string_val.cont( m: Maybe<&1, String & String> ) -> Maybe<&1, Json & String>: match m: case None{}: None{} case Some{(body, rest)}: Some{(JStr{body}, rest)} def Json.decode.string_val(s: String) -> Maybe<&1, Json & String>: Json.decode.string_val.cont(Json.decode.str.after_quote(s)) def Json.decode.number.read( rest: String, m: Maybe<&2, U32> ) -> Maybe<&1, Json & String>: match m: case None{}: None{} case Some{n}: Some{(JNum{n}, rest)} def Json.decode.number.go( pair: String & String ) -> Maybe<&1, Json & String>: (digits, rest) = pair match digits: case SNil{}: None{} case SCon{h, t}: Json.decode.number.read(rest, U32.read(SCon{h, t})) def Json.decode.number(s: String) -> Maybe<&1, Json & String>: Json.decode.number.go(Json.take_digits(s)) # Leaf value only (no '[' / '{'). def Json.decode.value(s: String) -> Maybe<&1, Json & String>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&1, Json & String>, U32.is_eq(x, 110), Json.decode.lit("ull", JNull{}, t), Bool.pick(Maybe<&1, Json & String>, U32.is_eq(x, 116), Json.decode.lit("rue", JBool{True{}}, t), Bool.pick(Maybe<&1, Json & String>, U32.is_eq(x, 102), Json.decode.lit("alse", JBool{False{}}, t), Bool.pick(Maybe<&1, Json & String>, U32.is_eq(x, 34), Json.decode.string_val(t), Bool.pick(Maybe<&1, Json & String>, Char.is_digit(Chr{x}), Json.decode.number(SCon{Chr{x}, t}), None{}))))) def Json.decode.finish.rest(v: Json, rest: String) -> Maybe<&2, Json>: match rest: case SNil{}: Some{v} case SCon{h, t}: None{} def Json.decode.finish.go(m: Maybe<&1, Json & String>) -> Maybe<&2, Json>: match m: case None{}: None{} case Some{(v, rest)}: Json.decode.finish.rest(v, Json.skip_ws(rest)) def Json.decode.finish(m: Maybe<&1, Json & String>) -> Maybe<&2, Json>: Json.decode.finish.go(m) def Json.decode(s: String) -> Maybe<&2, Json>: Json.decode.finish(Json.decode.value(Json.skip_ws(s))) def Json.parse(s: String) -> Maybe<&2, Json>: Json.decode(s) def Json.decode_prefix(s: String) -> Maybe<&1, Json & String>: Json.decode.value(Json.skip_ws(s))