mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 08:54:25 +00:00
import json import path import time import proc -- found in std/ after the program's own files, so a program's own json.fch always wins. path: join, base, dir, ext, stem, parts, clean, is_abs. time: now, millis, sleep, date, local, format, days_in_month (clock_gettime, strftime through C import). proc: run, output (an error with the command's output when it fails), env, pid, cwd. json: parse (errors with line and column), stringify, pretty, new_*, get/at/set/push/has/names and str_or/int_or/float_or/bool_or; \u escapes with surrogate pairs. The C importer now leaves a plain (non-const) char * parameter a ptr[char], a buffer C writes into; only const char * is text going in. str(ptr) and str([]u8) are in the self-hosted compiler. boot.sh: 40 passed, 0 failed.
518 lines
13 KiB
Text
518 lines
13 KiB
Text
// JSON: read it, build it, write it.
|
|
//
|
|
// import json
|
|
//
|
|
// doc := try json.parse("{\"name\": \"Finch\", \"tags\": [\"fast\", \"small\"], \"stars\": 5}")
|
|
// print(doc.get("name").str_or("")) Finch
|
|
// print(doc.get("tags").at(1).str_or("")) small
|
|
// print(doc.get("stars").int_or(0)) 5
|
|
// print(doc.get("missing").is_null()) true (a missing key is null, so chains never fail)
|
|
//
|
|
// out := json.new_object()
|
|
// out.set("ok", json.new_bool(true))
|
|
// list := json.new_list()
|
|
// list.push(json.new_number(1.5))
|
|
// out.set("values", list)
|
|
// print(json.stringify(out)) {"ok":true,"values":[1.5]}
|
|
// print(json.pretty(out)) the same, over several lines
|
|
|
|
enum Kind { Null, Bool, Number, Text, List, Object }
|
|
|
|
// One JSON value. `kind` says which of the other fields it uses: flag for Bool, num for Number,
|
|
// text for Text, items for List, and items with keys (the same length, in the same order) for Object.
|
|
struct Json {
|
|
Kind kind
|
|
bool flag
|
|
float num
|
|
str text
|
|
[]Json items
|
|
[]str keys
|
|
}
|
|
|
|
fn new_null() -> Json {
|
|
return Json(kind: Kind.Null)
|
|
}
|
|
|
|
fn new_bool(bool b) -> Json {
|
|
return Json(kind: Kind.Bool, flag: b)
|
|
}
|
|
|
|
fn new_number(float n) -> Json {
|
|
return Json(kind: Kind.Number, num: n)
|
|
}
|
|
|
|
fn new_text(str s) -> Json {
|
|
return Json(kind: Kind.Text, text: s)
|
|
}
|
|
|
|
fn new_list() -> Json {
|
|
return Json(kind: Kind.List)
|
|
}
|
|
|
|
fn new_object() -> Json {
|
|
return Json(kind: Kind.Object)
|
|
}
|
|
|
|
// ---------- looking inside ----------
|
|
|
|
fn Json.is_null() -> bool {
|
|
return self.kind == Kind.Null
|
|
}
|
|
|
|
// the number of elements of a list, of keys of an object, of bytes of a text; 0 for the rest
|
|
fn Json.len() -> int {
|
|
return match self.kind {
|
|
List, Object -> self.items.len
|
|
Text -> self.text.len
|
|
else -> 0
|
|
}
|
|
}
|
|
|
|
fn Json.has(str key) -> bool {
|
|
return self.kind == Kind.Object && self.keys.contains(key)
|
|
}
|
|
|
|
// the value of a key of an object; null if there is none (or this is not an object)
|
|
fn Json.get(str key) -> Json {
|
|
if self.kind != Kind.Object {
|
|
return new_null()
|
|
}
|
|
i := self.keys.find(key)
|
|
if i < 0 {
|
|
return new_null()
|
|
}
|
|
return self.items[i]
|
|
}
|
|
|
|
// the element of a list; null if there is none
|
|
fn Json.at(int i) -> Json {
|
|
if self.kind != Kind.List || i < 0 || i >= self.items.len {
|
|
return new_null()
|
|
}
|
|
return self.items[i]
|
|
}
|
|
|
|
// the value if it is of that kind, else what was given
|
|
fn Json.str_or(str fallback) -> str {
|
|
if self.kind == Kind.Text {
|
|
return self.text
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
fn Json.float_or(float fallback) -> float {
|
|
if self.kind == Kind.Number {
|
|
return self.num
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
fn Json.int_or(int fallback) -> int {
|
|
if self.kind == Kind.Number {
|
|
return int(self.num)
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
fn Json.bool_or(bool fallback) -> bool {
|
|
if self.kind == Kind.Bool {
|
|
return self.flag
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// the keys of an object, in the order they were written
|
|
fn Json.names() -> []str {
|
|
return self.keys
|
|
}
|
|
|
|
// ---------- building ----------
|
|
|
|
// add to the end of a list
|
|
fn Json.push(Json v) {
|
|
self.items.push(v)
|
|
}
|
|
|
|
// give a key a value in an object; a key that is already there is replaced
|
|
fn Json.set(str key, Json v) {
|
|
i := self.keys.find(key)
|
|
if i >= 0 {
|
|
self.items[i] = v
|
|
return
|
|
}
|
|
self.keys.push(key)
|
|
self.items.push(v)
|
|
}
|
|
|
|
// ---------- writing ----------
|
|
|
|
fn quote(str s) -> str {
|
|
[]str parts = ["\""]
|
|
for c in s {
|
|
n := int(c)
|
|
if c == '"' {
|
|
parts.push("\\\"")
|
|
} else if c == '\\' {
|
|
parts.push("\\\\")
|
|
} else if c == '\n' {
|
|
parts.push("\\n")
|
|
} else if c == '\r' {
|
|
parts.push("\\r")
|
|
} else if c == '\t' {
|
|
parts.push("\\t")
|
|
} else if n < 32 {
|
|
parts.push("\\u00" + hexdigit(n / 16) + hexdigit(n % 16))
|
|
} else {
|
|
parts.push(str(c))
|
|
}
|
|
}
|
|
parts.push("\"")
|
|
return parts.join("")
|
|
}
|
|
|
|
fn hexdigit(int n) -> str {
|
|
digits := "0123456789abcdef"
|
|
return str(digits[n])
|
|
}
|
|
|
|
fn number_text(float n) -> str {
|
|
if n == float(int(n)) && n > -1000000000000000.0 && n < 1000000000000000.0 {
|
|
return str(int(n))
|
|
}
|
|
return str(n)
|
|
}
|
|
|
|
struct Writer {
|
|
[]str out
|
|
}
|
|
|
|
fn write(Json j, str indent, str pad, ptr[Writer] w) {
|
|
nl := ""
|
|
inner := ""
|
|
sep := ","
|
|
colon := ":"
|
|
if indent.len > 0 {
|
|
nl = "\n"
|
|
inner = pad + indent
|
|
colon = ": "
|
|
}
|
|
match j.kind {
|
|
Null -> w.out.push("null")
|
|
Bool -> {
|
|
if j.flag {
|
|
w.out.push("true")
|
|
} else {
|
|
w.out.push("false")
|
|
}
|
|
}
|
|
Number -> w.out.push(number_text(j.num))
|
|
Text -> w.out.push(quote(j.text))
|
|
List -> {
|
|
if j.items.len == 0 {
|
|
w.out.push("[]")
|
|
return
|
|
}
|
|
w.out.push("[" + nl)
|
|
for i, item in j.items {
|
|
if i > 0 {
|
|
w.out.push(sep + nl)
|
|
}
|
|
w.out.push(inner)
|
|
write(item, indent, inner, w)
|
|
}
|
|
w.out.push(nl + pad + "]")
|
|
}
|
|
Object -> {
|
|
if j.items.len == 0 {
|
|
w.out.push("{}")
|
|
return
|
|
}
|
|
w.out.push("{" + nl)
|
|
for i, item in j.items {
|
|
if i > 0 {
|
|
w.out.push(sep + nl)
|
|
}
|
|
w.out.push(inner + quote(j.keys[i]) + colon)
|
|
write(item, indent, inner, w)
|
|
}
|
|
w.out.push(nl + pad + "}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// the value as text on one line
|
|
fn stringify(Json j) -> str {
|
|
w := Writer()
|
|
write(j, "", "", addr(w))
|
|
return w.out.join("")
|
|
}
|
|
|
|
// the value as text over several lines, indented by two spaces
|
|
fn pretty(Json j) -> str {
|
|
w := Writer()
|
|
write(j, " ", "", addr(w))
|
|
return w.out.join("")
|
|
}
|
|
|
|
// ---------- reading ----------
|
|
|
|
struct Reader {
|
|
str src
|
|
int i
|
|
}
|
|
|
|
// the line and column of a position, for a message
|
|
fn where(ptr[Reader] r) -> str {
|
|
line := 1
|
|
col := 1
|
|
for k in 0..r.i {
|
|
if k < r.src.len && r.src[k] == '\n' {
|
|
line += 1
|
|
col = 1
|
|
} else {
|
|
col += 1
|
|
}
|
|
}
|
|
return "line " + str(line) + ", column " + str(col)
|
|
}
|
|
|
|
fn peek(ptr[Reader] r) -> char {
|
|
if r.i < r.src.len {
|
|
return r.src[r.i]
|
|
}
|
|
return char(0)
|
|
}
|
|
|
|
fn skip_space(ptr[Reader] r) {
|
|
while r.i < r.src.len && (r.src[r.i] == ' ' || r.src[r.i] == '\n' || r.src[r.i] == '\t' || r.src[r.i] == '\r') {
|
|
r.i += 1
|
|
}
|
|
}
|
|
|
|
fn unexpected(ptr[Reader] r, str wanted) -> str {
|
|
if r.i >= r.src.len {
|
|
return "json: the text ends where " + wanted + " should be (" + where(r) + ")"
|
|
}
|
|
return "json: found '" + str(r.src[r.i]) + "' where " + wanted + " should be (" + where(r) + ")"
|
|
}
|
|
|
|
// does `word` come next?
|
|
fn comes(ptr[Reader] r, str word) -> bool {
|
|
return r.src.len - r.i >= word.len && r.src.sub(r.i, r.i + word.len) == word
|
|
}
|
|
|
|
fn expect_word(ptr[Reader] r, str word) -> ! {
|
|
if !comes(r, word) {
|
|
return error(unexpected(r, "'" + word + "'"))
|
|
}
|
|
r.i += word.len
|
|
}
|
|
|
|
fn hex_value(char c) -> int {
|
|
if c >= '0' && c <= '9' {
|
|
return int(c) - int('0')
|
|
}
|
|
if c >= 'a' && c <= 'f' {
|
|
return int(c) - int('a') + 10
|
|
}
|
|
if c >= 'A' && c <= 'F' {
|
|
return int(c) - int('A') + 10
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// a code point as UTF-8
|
|
fn utf8(int cp) -> str {
|
|
[]u8 b
|
|
if cp < 128 {
|
|
b.push(u8(cp))
|
|
} else if cp < 2048 {
|
|
b.push(u8(192 + cp / 64))
|
|
b.push(u8(128 + cp % 64))
|
|
} else if cp < 65536 {
|
|
b.push(u8(224 + cp / 4096))
|
|
b.push(u8(128 + (cp / 64) % 64))
|
|
b.push(u8(128 + cp % 64))
|
|
} else {
|
|
b.push(u8(240 + cp / 262144))
|
|
b.push(u8(128 + (cp / 4096) % 64))
|
|
b.push(u8(128 + (cp / 64) % 64))
|
|
b.push(u8(128 + cp % 64))
|
|
}
|
|
return str(b)
|
|
}
|
|
|
|
fn read_hex4(ptr[Reader] r) -> int! {
|
|
n := 0
|
|
for k in 0..4 {
|
|
if r.i >= r.src.len || hex_value(r.src[r.i]) < 0 {
|
|
return error(unexpected(r, "a hexadecimal digit"))
|
|
}
|
|
n = n * 16 + hex_value(r.src[r.i])
|
|
r.i += 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
fn read_string(ptr[Reader] r) -> str! {
|
|
r.i += 1 // the opening quote
|
|
[]str parts
|
|
start := r.i
|
|
while true {
|
|
if r.i >= r.src.len {
|
|
return error("json: a text is never closed (" + where(r) + ")")
|
|
}
|
|
c := r.src[r.i]
|
|
if c == '"' {
|
|
parts.push(r.src.sub(start, r.i))
|
|
r.i += 1
|
|
return parts.join("")
|
|
}
|
|
if int(c) < 32 {
|
|
return error("json: a line break or control character inside a text (" + where(r) + ")")
|
|
}
|
|
if c != '\\' {
|
|
r.i += 1
|
|
continue
|
|
}
|
|
parts.push(r.src.sub(start, r.i))
|
|
r.i += 1
|
|
e := peek(r)
|
|
r.i += 1
|
|
if e == 'n' {
|
|
parts.push("\n")
|
|
} else if e == 't' {
|
|
parts.push("\t")
|
|
} else if e == 'r' {
|
|
parts.push("\r")
|
|
} else if e == 'b' {
|
|
parts.push(utf8(8))
|
|
} else if e == 'f' {
|
|
parts.push(utf8(12))
|
|
} else if e == '"' || e == '\\' || e == '/' {
|
|
parts.push(str(e))
|
|
} else if e == 'u' {
|
|
cp := try read_hex4(r)
|
|
// a pair of 😀 stands for one character above U+FFFF
|
|
if cp >= 55296 && cp < 56320 && comes(r, "\\u") {
|
|
r.i += 2
|
|
low := try read_hex4(r)
|
|
cp = 65536 + (cp - 55296) * 1024 + (low - 56320)
|
|
}
|
|
parts.push(utf8(cp))
|
|
} else {
|
|
return error("json: unknown escape \\" + str(e) + " in a text (" + where(r) + ")")
|
|
}
|
|
start = r.i
|
|
}
|
|
}
|
|
|
|
fn read_number(ptr[Reader] r) -> Json! {
|
|
start := r.i
|
|
if peek(r) == '-' {
|
|
r.i += 1
|
|
}
|
|
while r.i < r.src.len && ((r.src[r.i] >= '0' && r.src[r.i] <= '9') || r.src[r.i] == '.' || r.src[r.i] == 'e' || r.src[r.i] == 'E' || r.src[r.i] == '+' || r.src[r.i] == '-') {
|
|
r.i += 1
|
|
}
|
|
n := float(r.src.sub(start, r.i)) or {
|
|
r.i = start
|
|
return error(unexpected(r, "a number"))
|
|
}
|
|
return new_number(n)
|
|
}
|
|
|
|
fn read_value(ptr[Reader] r) -> Json! {
|
|
skip_space(r)
|
|
c := peek(r)
|
|
if r.i >= r.src.len {
|
|
return error(unexpected(r, "a value"))
|
|
}
|
|
if c == '{' {
|
|
obj := new_object()
|
|
r.i += 1
|
|
skip_space(r)
|
|
if peek(r) == '}' {
|
|
r.i += 1
|
|
return obj
|
|
}
|
|
while true {
|
|
skip_space(r)
|
|
if peek(r) != '"' {
|
|
return error(unexpected(r, "a key in quotes"))
|
|
}
|
|
key := try read_string(r)
|
|
skip_space(r)
|
|
if peek(r) != ':' {
|
|
return error(unexpected(r, "':'"))
|
|
}
|
|
r.i += 1
|
|
v := try read_value(r)
|
|
obj.set(key, v)
|
|
skip_space(r)
|
|
if peek(r) == ',' {
|
|
r.i += 1
|
|
continue
|
|
}
|
|
if peek(r) == '}' {
|
|
r.i += 1
|
|
return obj
|
|
}
|
|
return error(unexpected(r, "',' or '}'"))
|
|
}
|
|
}
|
|
if c == '[' {
|
|
list := new_list()
|
|
r.i += 1
|
|
skip_space(r)
|
|
if peek(r) == ']' {
|
|
r.i += 1
|
|
return list
|
|
}
|
|
while true {
|
|
v := try read_value(r)
|
|
list.push(v)
|
|
skip_space(r)
|
|
if peek(r) == ',' {
|
|
r.i += 1
|
|
continue
|
|
}
|
|
if peek(r) == ']' {
|
|
r.i += 1
|
|
return list
|
|
}
|
|
return error(unexpected(r, "',' or ']'"))
|
|
}
|
|
}
|
|
if c == '"' {
|
|
s := try read_string(r)
|
|
return new_text(s)
|
|
}
|
|
if c == 't' {
|
|
try expect_word(r, "true")
|
|
return new_bool(true)
|
|
}
|
|
if c == 'f' {
|
|
try expect_word(r, "false")
|
|
return new_bool(false)
|
|
}
|
|
if c == 'n' {
|
|
try expect_word(r, "null")
|
|
return new_null()
|
|
}
|
|
if c == '-' || (c >= '0' && c <= '9') {
|
|
return try read_number(r)
|
|
}
|
|
return error(unexpected(r, "a value"))
|
|
}
|
|
|
|
// the JSON in `text`; an error with the line and column where it went wrong
|
|
fn parse(str text) -> Json! {
|
|
r := Reader(src: text)
|
|
v := try read_value(addr(r))
|
|
skip_space(addr(r))
|
|
if r.i < r.src.len {
|
|
return error(unexpected(addr(r), "the end of the text"))
|
|
}
|
|
return v
|
|
}
|