examples/json/
Un lettore di JSON che ricostruisce la struttura del documento e lo riscrive in un formato uniforme.
- 674 righe
- 3 moduli
- riga di comando
- legge un file
Per scegliere come proseguire, il programma legge un byte in anticipo. I dati possono contenere altri dati annidati, sia in array, cioè sequenze, sia in mappe, cioè associazioni tra chiavi e valori. Il programma converte i numeri cifra per cifra, perché Heroes non include una conversione diretta da stringa a numero.
Eseguilo
values: 12
depth: 3
{"compiled":true,"limits":{"file_lines":300.0,"spec_tokens":4096.0},"motto":"every plausible mistake is a compile \"error\"","successor":null,"targets":["c11","qbe"],"title":"Heroes","version":0.1}
field title: "Heroes"
exit 0 · 4 righe
Il programma
# A JSON reader: a document in, its shape and its canonical form out.
#
# heroes run examples/json/main.hero -- examples/json/sample.json title
#
# The program's three edges are all here (§1.11): it takes `args()`, it reads a
# file, and it ends with a status a shell can branch on — 0 for a document it
# read, 1 for one it could not, 2 for being asked wrong. That last distinction is
# the compiler's own contract (CLAUDE.md §10) applied to a program: *the input is
# bad* and *you called me wrong* are different answers.
#
# main.hero the program: arguments, the file, the report
# parse.hero the reader — recursive descent, one byte of lookahead
# value.hero the document, its canonical form, and how to ask it questions
#
# What it exercises: a variant recursive through **both** `[T]` and `{K: V}`, an
# `@` record travelling through a parser, `T?` with `?` propagation across two
# modules, `f64` arithmetic built up digit by digit, and a `{str: Json}` walked
# in sorted key order because a map remembers no other one.
use parse
use value
function main()
given = args()
if len(given) == 0
print("usage: json <file> [key]")
exit(2)
match read_file(given[0])
.err e => stop("cannot read " + given[0] + ": " + e.code)
.ok text => report(text, given)
# Every unhappy ending in one place, so the exit code is decided once.
function stop(message: str)
print(message)
exit(1)
function report(text: str, given: [str])
match parse.read(text)
.err e => stop(e.code + ": " + e.msg)
.ok doc => describe(doc, given)
function describe(doc: value.Json, given: [str])
print("values: ", value.size(doc))
print("depth: ", value.depth(doc))
print(value.render(doc))
if len(given) > 1
match value.field(doc, given[1])
.err e => print("field ", given[1], ": ", e.code)
.ok got => print("field ", given[1], ": ", value.render(got))
test "the report reads its own sample"
doc = parse.read(read_file("examples/json/sample.json").must()).must()
assert value.render(value.field(doc, "title").must()) == "\"Heroes\""
assert value.field(doc, "nothing_here").is_err()
assert value.field(.number(v: 1.0), "title").is_err()
test "a document survives a round trip through its own canonical form"
once = read_file("examples/json/sample.json").must()
first = value.render(parse.read(once).must())
twice = value.render(parse.read(first).must())
assert first == twice
parse.hero · 419 righe
# The reader: text in, a `Json` out — or an error saying where it stopped.
#
# A recursive-descent parser with no separate tokeniser, which is what JSON's
# grammar invites: every value is decided by its first byte. The shape is the
# calculator's — a record carrying the text and a position, travelling through
# the parser as an `@` parameter, copy in and copy out.
#
# Every failure is a `fail(code, msg)` with a stable snake_case code and the
# position in the message, because §4.17's rule holds for programs too: an error
# should carry what is needed to fix the input without opening another file.
use value
## Error codes
#
# One `constant` per code, so a typo is `error[unknown_name]` here rather
# than a comparison that is quietly false forever (panel 034 R4).
constant ERR_BAD_ESCAPE: str
"bad_escape"
constant ERR_BAD_NUMBER: str
"bad_number"
constant ERR_EXPECTED_COLON: str
"expected_colon"
constant ERR_EXPECTED_KEY: str
"expected_key"
constant ERR_EXPECTED_SEPARATOR: str
"expected_separator"
constant ERR_EXPONENT_OUT_OF_RANGE: str
"exponent_out_of_range"
constant ERR_TRAILING_INPUT: str
"trailing_input"
constant ERR_UNCLOSED_ARRAY: str
"unclosed_array"
constant ERR_UNCLOSED_OBJECT: str
"unclosed_object"
constant ERR_UNEXPECTED_CHARACTER: str
"unexpected_character"
constant ERR_UNEXPECTED_END: str
"unexpected_end"
constant ERR_UNSUPPORTED_ESCAPE: str
"unsupported_escape"
constant ERR_UNTERMINATED_STRING: str
"unterminated_string"
record Reader
text: str
pos: i64
function at_end(r: Reader) -> bool
return r.pos >= r.text.len()
function here(r: Reader) -> u8
return r.text[r.pos]
function advance(@r: Reader)
r.pos @ r.pos + 1
function is_digit(c: u8) -> bool
return c >= '0' && c <= '9'
function digit_of(c: u8) -> i64
return to_i64(c - '0').must()
function skip_space(@r: Reader)
while !at_end(r) && is_space(here(r))
advance(@r)
function is_space(c: u8) -> bool
return c == ' ' || c == '\t' || c == '\n' || c == 13
# Whether the next bytes spell `spelling` — asked without moving, and without
# ever slicing past the end, because an out-of-bounds slice **aborts** (§4.14)
# rather than failing.
function has_word(r: Reader, spelling: str) -> bool
stop = r.pos + spelling.len()
if stop > r.text.len()
return false
return r.text.slice(from: r.pos, to: stop) == spelling
function skip(@r: Reader, n: i64)
r.pos @ r.pos + n
## Values
# One value, whatever it turns out to be. JSON is decided by the first byte, so
# this is a dispatch rather than a search.
function read_value(@r: Reader) -> value.Json?
skip_space(@r)
if at_end(r)
return fail(code: ERR_UNEXPECTED_END, msg: "the document ends where a value was expected")
c = here(r)
if c == '{'
return read_object(@r)
else if c == '['
return read_array(@r)
else if c == '"'
return ok(.text(s: read_string(@r)?))
else if c == '-' || is_digit(c)
return ok(.number(v: read_number(@r)?))
else if has_word(r, "true")
skip(@r, 4)
return ok(.boolean(b: true))
else if has_word(r, "false")
skip(@r, 5)
return ok(.boolean(b: false))
else if has_word(r, "null")
skip(@r, 4)
return ok(.nothing)
return fail(code: ERR_UNEXPECTED_CHARACTER, msg: "no value starts at " + r.pos.to_str())
# A string literal, from its opening quote to its closing one.
#
# Pieces are collected and joined rather than concatenated in a loop: `+` on
# `str` copies both sides, so the obvious spelling is quadratic in the length of
# the string, and `join` is linear.
function read_string(@r: Reader) -> str?
advance(@r)
parts: [str] @= []
while !at_end(r) && here(r) != '"'
if here(r) == '\\'
advance(@r)
if at_end(r)
return fail(code: ERR_UNTERMINATED_STRING, msg: "the document ends inside an escape")
parts @ parts.push(escape_at(r)?)
advance(@r)
else
parts @ parts.push(r.text.slice(from: r.pos, to: r.pos + 1))
advance(@r)
if at_end(r)
return fail(code: ERR_UNTERMINATED_STRING, msg: "the document ends inside a string")
advance(@r)
return ok(join(parts, ""))
# What one escape stands for.
#
# Five of JSON's eight are read, and the missing three are a fact about the
# language rather than a shortcut taken here: a Heroes string literal has
# **exactly five escapes** — `\n` `\t` `\\` `\"` and `\'` in a character — so
# `\r`, `\b` and `\f` cannot be *written* as the values they stand for, and
# neither can `\u`'s code point without UTF-8 encoding. Refusing them is the
# only honest arm: a reader that quietly substituted a space would produce a
# document that is wrong in a way nothing downstream could see.
function escape_at(r: Reader) -> str?
c = here(r)
if c == '"'
return ok("\"")
else if c == '\\'
return ok("\\")
else if c == '/'
return ok("/")
else if c == 'n'
return ok("\n")
else if c == 't'
return ok("\t")
else if c == 'r' || c == 'b' || c == 'f' || c == 'u'
return fail(
code: ERR_UNSUPPORTED_ESCAPE,
msg: "this reader does not decode `\\" + r.text.slice(from: r.pos, to: r.pos + 1) + "`, at " + r.pos.to_str()
)
return fail(
code: ERR_BAD_ESCAPE,
msg: "no escape `\\" + r.text.slice(from: r.pos, to: r.pos + 1) + "` at " + r.pos.to_str()
)
# A number. JSON's numbers are doubles, so this builds an `f64` — digit by
# digit, because the language deliberately offers no `to_f64` on a `str`:
# parsing can fail, and a conversion that can fail is not a conversion.
function read_number(@r: Reader) -> f64?
negative: bool @= false
if here(r) == '-'
negative @ true
advance(@r)
whole: f64 @= 0.0
digits: i64 @= 0
while !at_end(r) && is_digit(here(r))
whole @ whole * 10.0 + to_f64(digit_of(here(r)))
digits @ digits + 1
advance(@r)
if digits == 0
return fail(code: ERR_BAD_NUMBER, msg: "a number needs a digit at " + r.pos.to_str())
if !at_end(r) && here(r) == '.'
advance(@r)
scale: f64 @= 1.0
after: i64 @= 0
while !at_end(r) && is_digit(here(r))
scale @ scale / 10.0
whole @ whole + to_f64(digit_of(here(r))) * scale
after @ after + 1
advance(@r)
if after == 0
return fail(code: ERR_BAD_NUMBER, msg: "a `.` needs a digit after it, at " + r.pos.to_str())
if !at_end(r) && (here(r) == 'e' || here(r) == 'E')
advance(@r)
whole @ whole * power_of_ten(read_exponent(@r)?)
if negative
whole @ -whole
return ok(whole)
# The exponent's own sign and digits, as an `i64`. Capped, and the cap is a
# fact about `f64` rather than about this program: past ~308 the value is
# infinite either way, and an uncapped loop would spin on `1e999999999`.
function read_exponent(@r: Reader) -> i64?
negative: bool @= false
if !at_end(r) && (here(r) == '+' || here(r) == '-')
negative @ here(r) == '-'
advance(@r)
exponent: i64 @= 0
digits: i64 @= 0
while !at_end(r) && is_digit(here(r))
exponent @ exponent * 10 + digit_of(here(r))
digits @ digits + 1
if exponent > 400
return fail(code: ERR_EXPONENT_OUT_OF_RANGE, msg: "an exponent beyond an f64's range at " + r.pos.to_str())
advance(@r)
if digits == 0
return fail(code: ERR_BAD_NUMBER, msg: "an exponent needs a digit at " + r.pos.to_str())
if negative
return ok(-exponent)
return ok(exponent)
function power_of_ten(n: i64) -> f64
out: f64 @= 1.0
if n < 0
for _ in range(from: n, to: 0)
out @ out / 10.0
return out
for _ in range(from: 0, to: n)
out @ out * 10.0
return out
## The two containers
# `[` … `]`, with the empty array a case of its own rather than a special exit
# out of the loop.
function read_array(@r: Reader) -> value.Json?
advance(@r)
items: [value.Json] @= []
skip_space(@r)
if at_end(r)
return fail(code: ERR_UNCLOSED_ARRAY, msg: "the document ends inside an array")
if here(r) == ']'
advance(@r)
return ok(.array(items: items))
more: bool @= true
while more
items @ items.push(read_value(@r)?)
skip_space(@r)
if at_end(r)
return fail(code: ERR_UNCLOSED_ARRAY, msg: "the document ends inside an array")
if here(r) == ','
advance(@r)
else if here(r) == ']'
advance(@r)
more @ false
else
return fail(code: ERR_EXPECTED_SEPARATOR, msg: "expected `,` or `]` at " + r.pos.to_str())
return ok(.array(items: items))
# `{` … `}`. A repeated key **replaces**, which is `m[k] @ v`'s own rule and
# also what most JSON readers do.
function read_object(@r: Reader) -> value.Json?
advance(@r)
fields: {str: value.Json} @= {}
skip_space(@r)
if at_end(r)
return fail(code: ERR_UNCLOSED_OBJECT, msg: "the document ends inside an object")
if here(r) == '}'
advance(@r)
return ok(.object(fields: fields))
more: bool @= true
while more
skip_space(@r)
if at_end(r) || here(r) != '"'
return fail(code: ERR_EXPECTED_KEY, msg: "an object's key is a string, at " + r.pos.to_str())
key = read_string(@r)?
skip_space(@r)
if at_end(r) || here(r) != ':'
return fail(code: ERR_EXPECTED_COLON, msg: "expected `:` after a key, at " + r.pos.to_str())
advance(@r)
fields[key] @ read_value(@r)?
skip_space(@r)
if at_end(r)
return fail(code: ERR_UNCLOSED_OBJECT, msg: "the document ends inside an object")
if here(r) == ','
advance(@r)
else if here(r) == '}'
advance(@r)
more @ false
else
return fail(code: ERR_EXPECTED_SEPARATOR, msg: "expected `,` or `}` at " + r.pos.to_str())
return ok(.object(fields: fields))
## The entry point
# One document, and nothing after it. The trailing check is the half a happy
# path never reaches: `1 2` is two valid values and not one valid document.
function read(text: str) -> value.Json?
r: Reader @= Reader(text: text, pos: 0)
j = read_value(@r)?
skip_space(@r)
if !at_end(r)
return fail(code: ERR_TRAILING_INPUT, msg: "the document ends at " + r.pos.to_str() + " and the text does not")
return ok(j)
# The code a document fails with — or `no_error` for one that does not, which is
# a code no arm above can produce, so a test asserting a failure cannot pass by
# the document turning out to be fine.
function code_of(text: str) -> str
return match read(text)
.ok _ => "no_error"
.err e => e.code
# What a document renders as, in one call: every test below reads through the
# canonical form rather than through the tree, so the two modules are checked
# against each other rather than each against itself.
function shape_of(text: str) -> str
return value.render(read(text).must())
test "reads the six kinds"
assert shape_of("null") == "null"
assert shape_of("true") == "true"
assert shape_of("12") == "12.0"
assert shape_of("\"hi\"") == "\"hi\""
assert shape_of("[1,2]") == "[1.0,2.0]"
assert shape_of("{\"a\":1}") == "{\"a\":1.0}"
test "whitespace is not part of the grammar"
assert shape_of(" [ 1 , 2 ] ") == "[1.0,2.0]"
assert shape_of("[]") == "[]"
assert shape_of("{}") == "{}"
test "numbers: sign, fraction, exponent"
assert shape_of("-3") == "-3.0"
assert shape_of("1.5") == "1.5"
assert shape_of("2e3") == "2000.0"
assert shape_of("2e-3") == "0.002"
assert shape_of("-0.25") == "-0.25"
test "escapes come back as the characters they stand for"
assert shape_of("\"a\\nb\"") == "\"a\\nb\""
assert shape_of("\"a\\\"b\"") == "\"a\\\"b\""
assert shape_of("\"a\\\\b\"") == "\"a\\\\b\""
assert shape_of("\"a\\/b\"") == "\"a/b\""
test "a nested document keeps its shape"
doc = read("{\"a\":[1,{\"b\":null}],\"c\":true}").must()
assert value.render(doc) == "{\"a\":[1.0,{\"b\":null}],\"c\":true}"
assert value.depth(doc) == 4
assert value.size(doc) == 6
test "a repeated key replaces, and that is `m[k] @ v`'s own rule"
assert shape_of("{\"a\":1,\"a\":2}") == "{\"a\":2.0}"
test "every failure says what and where"
assert code_of("") == ERR_UNEXPECTED_END
assert code_of("[1,") == ERR_UNEXPECTED_END
assert code_of("[1 2]") == ERR_EXPECTED_SEPARATOR
assert code_of("{a:1}") == ERR_EXPECTED_KEY
assert code_of("{\"a\" 1}") == ERR_EXPECTED_COLON
assert code_of("\"oops") == ERR_UNTERMINATED_STRING
assert code_of("\"\\u0041\"") == ERR_UNSUPPORTED_ESCAPE
assert code_of("\"\\q\"") == ERR_BAD_ESCAPE
assert code_of("1 2") == ERR_TRAILING_INPUT
assert code_of("-") == ERR_BAD_NUMBER
assert code_of("1e") == ERR_BAD_NUMBER
assert code_of("1e999") == ERR_EXPONENT_OUT_OF_RANGE
assert code_of("$") == ERR_UNEXPECTED_CHARACTER
value.hero · 191 righe
# The JSON value, and how it prints.
#
# A JSON document is a tree of six kinds, and this is the module that says so.
# It names no other module: like the calculator's lexer, a module with no `use`
# line is the base case of the graph.
#
# One name here is decided by the language rather than by JSON. The `null` case
# is called `nothing`, because `null` is a **refused word** — `heroes check` on
# it says *there is no null in this language; absence is a fallible type*. That
# refusal is about the language having no null **value**, and a JSON document is
# free to contain the word; what it cannot do is spell a Heroes name with it.
## Error codes
#
# One `constant` per code, so a typo is `error[unknown_name]` here rather
# than a comparison that is quietly false forever (panel 034 R4).
constant ERR_NOT_AN_OBJECT: str
"not_an_object"
variant Json
nothing
boolean
b: bool
number
v: f64
text
s: str
array
items: [Json]
object
fields: {str: Json}
## Rendering
# The document as text again, in a canonical form: no spaces, and an object's
# keys in sorted order.
#
# Sorted rather than as-written, and that is a property of the language rather
# than a choice made here — a `{K: V}` does not remember insertion order, and
# `==` on two maps ignores it. `for k in sort(keys(m))` is the spec's own idiom
# for walking one in an order a reader can predict.
#
# It does not round-trip byte for byte, and the reason is worth stating: every
# JSON number is an `f64`, so `1` comes back as `1.0`. That is JSON's own model
# (its numbers are doubles), not a shortcut.
function render(j: Json) -> str
return match j
.nothing => "null"
.boolean b => b.b.to_str()
.number n => n.v.to_str()
.text t => quote(t.s)
.array a => "[" + join(rendered(a.items), ",") + "]"
.object o => "{" + join(pairs(o.fields), ",") + "}"
# Every element, rendered. Separate from `render` because a `match` arm is one
# expression and a loop is not one.
function rendered(items: [Json]) -> [str]
out: [str] @= []
for item in items
out @ out.push(render(item))
return out
function pairs(fields: {str: Json}) -> [str]
out: [str] @= []
for k in sort(keys(fields))
out @ out.push(quote(k) + ":" + render(fields[k].must()))
return out
# A string, with the five escapes JSON needs on the way out.
#
# Built as an array and joined rather than with `+` in a loop, which the spec
# prices: `+` on `str` copies both sides, so accumulating in a loop is
# quadratic, and `join` is linear.
function quote(s: str) -> str
out: [str] @= ["\""]
for c in s.chars()
if c == "\""
out @ out.push("\\\"")
else if c == "\\"
out @ out.push("\\\\")
else if c == "\n"
out @ out.push("\\n")
else if c == "\t"
out @ out.push("\\t")
else
out @ out.push(c)
out @ out.push("\"")
return join(out, "")
## Asking a document questions
# How many values the tree holds, counting itself.
function size(j: Json) -> i64
return match j
.nothing => 1
.boolean _ => 1
.number _ => 1
.text _ => 1
.array a => 1 + size_of_all(a.items)
.object o => 1 + size_of_fields(o.fields)
function size_of_all(items: [Json]) -> i64
total: i64 @= 0
for item in items
total @ total + size(item)
return total
function size_of_fields(fields: {str: Json}) -> i64
total: i64 @= 0
for k in keys(fields)
total @ total + size(fields[k].must())
return total
# How deep the tree goes: a leaf is 1.
function depth(j: Json) -> i64
return match j
.nothing => 1
.boolean _ => 1
.number _ => 1
.text _ => 1
.array a => 1 + deepest(a.items)
.object o => 1 + deepest(fields_of(o.fields))
function deepest(items: [Json]) -> i64
best: i64 @= 0
for item in items
d = depth(item)
if d > best
best @ d
return best
function fields_of(fields: {str: Json}) -> [Json]
out: [Json] @= []
for k in sort(keys(fields))
out @ out.push(fields[k].must())
return out
# The value at `key`, for an object — and an error for everything else, because
# "not an object" and "no such key" are different mistakes and a caller usually
# wants to tell them apart.
#
# The object arm hands back `o.fields[key]` unchanged, which is already a `Json?`
# with the code `missing_key`. Writing it any other way costs correctness: a
# document is allowed to contain `null`, so `.default(.nothing)` followed by a
# test for `.nothing` would report a present null as an absent field.
function field(j: Json, key: str) -> Json?
return match j
.object o => o.fields[key]
.nothing | .boolean _ | .number _ | .text _ | .array _ =>
fail(code: ERR_NOT_AN_OBJECT, msg: "asked for `" + key + "` of a value that is not an object")
test "rendering is canonical"
assert render(.nothing) == "null"
assert render(.boolean(b: true)) == "true"
assert render(.number(v: 1.5)) == "1.5"
assert render(.text(s: "hi")) == "\"hi\""
assert render(.array(items: [.number(v: 1.0), .nothing])) == "[1.0,null]"
test "a string is escaped on the way out"
assert quote("a\"b") == "\"a\\\"b\""
assert quote("a\\b") == "\"a\\\\b\""
assert quote("a\nb") == "\"a\\nb\""
test "an object prints its keys in sorted order"
m: {str: Json} @= {}
m["z"] @ .number(v: 1.0)
m["a"] @ .boolean(b: false)
assert render(.object(fields: m)) == "{\"a\":false,\"z\":1.0}"
test "size and depth count the tree"
leaf: Json = .number(v: 1.0)
nested: Json = .array(items: [leaf, .array(items: [leaf])])
assert size(nested) == 4
assert depth(nested) == 3
assert depth(leaf) == 1
Input
{
"title": "Heroes",
"version": 0.1,
"compiled": true,
"successor": null,
"targets": ["c11", "qbe"],
"limits": { "spec_tokens": 4096, "file_lines": 300 },
"motto": "every plausible mistake is a compile \"error\""
}
impronta sha256 13324c26d08c · 6 file · index.json
Successivo: markdown. Tutti gli esempi. ← Precedente: interpreter
Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.