F2.2: std/json reader — explicit-alloc parse with error surfacing
Add the JSON reader (parser) to library/modules/std/json.sx, the inverse of the F2.1 writer over the same value model: insertion-ordered objects, arrays, strings (full unescaping incl. \uXXXX + surrogate pairs), s64 integers, bool, null. Heap discipline (binding): exactly two allocation kinds, both through the EXPLICIT `alloc` parameter, never the implicit context allocator — composite backing stores (Array/Object.items via add/put) and decoded escaped-string buffers (bounded by the raw span). Un-escaped string values are zero-copy VIEWS into the input buffer (valid only while it lives); scalars carry no heap. Failure surfacing (hard contract): malformed input raises a meaningful JsonParseError variant (UnexpectedToken / UnexpectedEnd / BadEscape / BadNumber / TrailingGarbage) on the error channel, never a bogus value. Trailing non-whitespace is TrailingGarbage; fractions/exponents, out-of-s64 magnitudes, and leading zeros are BadNumber. Number accumulation runs in negative space so s64 MIN parses exactly. examples/0714-modules-json-reader.sx asserts the parsed structure (insertion order, every kind), proves the view-vs-decoded heap split by pointer containment, round-trips back through the writer byte-for-byte, decodes a surrogate-pair into 4 UTF-8 bytes, and checks every malformed variant. Filed issues/0078: a string `==` (or any sub-CFG operand) used in a short-circuit `and`/`or` emits invalid LLVM IR (stale PHI predecessor), hit while writing the example's assertions and worked around there by not combining comparisons with `and`/`or`. src/ untouched.
This commit is contained in:
137
examples/0714-modules-json-reader.sx
Normal file
137
examples/0714-modules-json-reader.sx
Normal file
@@ -0,0 +1,137 @@
|
||||
// JSON reader (parser) from `modules/std/json.sx` — the inverse of the
|
||||
// F2.1 writer.
|
||||
//
|
||||
// Parses a representative document (nested object + array + a
|
||||
// string-with-escapes + ints incl. negatives + bool + null) into the
|
||||
// shared value model, then proves:
|
||||
//
|
||||
// 1. STRUCTURE — the parsed tree has the expected keys (in INSERTION
|
||||
// order), values, and nesting.
|
||||
// 2. HEAP DISCIPLINE — an un-escaped string value is a zero-copy VIEW
|
||||
// into the input buffer (its bytes lie inside `src`), while an
|
||||
// escaped string is DECODED into a fresh `alloc`-ed buffer (its
|
||||
// bytes lie OUTSIDE `src`). Composite nodes + the decoded string are
|
||||
// the only allocations, all through the explicit Arena.
|
||||
// 3. ROUND-TRIP — feeding the parsed tree back to the writer reproduces
|
||||
// the canonical input byte-for-byte.
|
||||
// 4. UNICODE — `\uXXXX` (BMP + 2-byte) and a surrogate pair decode to
|
||||
// the right UTF-8 bytes.
|
||||
// 5. FAILURE SURFACING — every malformed input raises the right
|
||||
// `JsonParseError` variant on the error channel, never a bogus value.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/json.sx";
|
||||
|
||||
// Canonical document: no insignificant whitespace, escapes in the writer's
|
||||
// own form — so re-serializing the parse must reproduce it exactly.
|
||||
DOC :: "{\"name\":\"plain\",\"esc\":\"a\\nb\",\"xs\":[10,-20],\"yes\":true,\"nil\":null,\"sub\":{\"k\":\"v\"}}";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
// Half-open containment [lo, hi). Written with early returns (no `and`) so
|
||||
// the assertions below never combine comparisons with short-circuit
|
||||
// `and`/`or` — see issues/0078.
|
||||
in_range :: (x: s64, lo: s64, hi: s64) -> bool {
|
||||
if x < lo { return false; }
|
||||
if x >= hi { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
// True when `parse(src)` raised `want` — destructure captures the error
|
||||
// tag without `try`, so a malformed input never aborts the example.
|
||||
raises :: (src: string, want: JsonParseError, alloc: Allocator) -> bool {
|
||||
_, e := parse(src, alloc);
|
||||
e == want
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
gpa := GPA.init();
|
||||
arena := Arena.init(xx gpa, 8192);
|
||||
defer arena.deinit();
|
||||
|
||||
// ── 1. Structure ─────────────────────────────────────────────────
|
||||
src := DOC;
|
||||
root := try parse(src, xx arena);
|
||||
|
||||
is_object := if root == { case .object: true; else: false; };
|
||||
report("root-is-object", is_object);
|
||||
|
||||
o := root.object;
|
||||
report("member-count", o.len == 6);
|
||||
report("key-order-0", o.items[0].key == "name");
|
||||
report("string-plain", o.items[0].val.str == "plain");
|
||||
report("string-escaped", o.items[1].val.str == "a\nb"); // \n decoded to 0x0A
|
||||
|
||||
xs := o.items[2].val.array;
|
||||
report("array-len", xs.len == 2);
|
||||
report("array-pos", xs.items[0].int_ == 10);
|
||||
report("array-neg", xs.items[1].int_ == 0 - 20);
|
||||
|
||||
report("bool-value", o.items[3].val.bool_ == true);
|
||||
|
||||
is_null := if o.items[4].val == { case .null_: true; else: false; };
|
||||
report("null-value", is_null);
|
||||
|
||||
// Two separate reports (not `key=="k" and val=="v"`): a string `==`
|
||||
// as an operand of short-circuit `and`/`or` miscompiles — see
|
||||
// issues/0078. Every assertion here is therefore a single comparison.
|
||||
sub := o.items[5].val.object;
|
||||
report("nested-key", sub.items[0].key == "k");
|
||||
report("nested-val", sub.items[0].val.str == "v");
|
||||
|
||||
// ── 2. Heap discipline: view vs decoded ──────────────────────────
|
||||
base : s64 = xx src.ptr;
|
||||
stop := base + src.len;
|
||||
p_plain : s64 = xx o.items[0].val.str.ptr; // "plain": no escape -> VIEW into src
|
||||
p_esc : s64 = xx o.items[1].val.str.ptr; // "a\nb": escaped -> DECODED into arena
|
||||
report("plain-is-view", in_range(p_plain, base, stop));
|
||||
report("escaped-allocated", !in_range(p_esc, base, stop));
|
||||
|
||||
// ── 3. Round-trip back through the writer ────────────────────────
|
||||
buf : [256]u8 = ---;
|
||||
n := try write_to_buffer(root, string.{ ptr = @buf[0], len = 256 });
|
||||
rt := string.{ ptr = @buf[0], len = n };
|
||||
report("round-trip", rt == src);
|
||||
|
||||
// ── 4. Leading/trailing/inner whitespace is insignificant ────────
|
||||
// Each comparison is its own report (no `and`-combining — issues/0078).
|
||||
wsv := try parse(" [ 1 , 2 , 3 ] ", xx arena);
|
||||
wa := wsv.array;
|
||||
report("ws-count", wa.len == 3);
|
||||
report("ws-first", wa.items[0].int_ == 1);
|
||||
report("ws-last", wa.items[2].int_ == 3);
|
||||
|
||||
// Empty container literals (the manifest/db.json use these).
|
||||
ea := try parse("[]", xx arena);
|
||||
report("empty-array", ea.array.len == 0);
|
||||
eo := try parse("{}", xx arena);
|
||||
report("empty-object", eo.object.len == 0);
|
||||
|
||||
// ── 5. Unicode: \uXXXX (1- and 2-byte) + surrogate pair (4-byte) ──
|
||||
// JSON "Aé😀" -> 'A', 'é' (C3 A9), '😀' (F0 9F 98 80). One byte per report.
|
||||
univ := try parse("\"\\u0041\\u00e9\\uD83D\\uDE00\"", xx arena);
|
||||
u := univ.str;
|
||||
report("uni-len", u.len == 7);
|
||||
report("uni-A", u[0] == 0x41); // U+0041 -> 1 byte
|
||||
report("uni-e1", u[1] == 0xC3); // U+00E9 -> 2 bytes
|
||||
report("uni-e2", u[2] == 0xA9);
|
||||
report("uni-s0", u[3] == 0xF0); // U+1F600 (surrogate pair) -> 4 bytes
|
||||
report("uni-s1", u[4] == 0x9F);
|
||||
report("uni-s2", u[5] == 0x98);
|
||||
report("uni-s3", u[6] == 0x80);
|
||||
|
||||
// ── 6. Malformed inputs each surface the right error variant ─────
|
||||
report("err-truncated", raises("{\"a\":", error.UnexpectedEnd, xx arena));
|
||||
report("err-bad-escape", raises("\"a\\xb\"", error.BadEscape, xx arena));
|
||||
report("err-trailing-junk", raises("[1,2] x", error.TrailingGarbage, xx arena));
|
||||
report("err-bad-token", raises("xyz", error.UnexpectedToken, xx arena));
|
||||
report("err-fraction", raises("1.5", error.BadNumber, xx arena));
|
||||
report("err-leading-zero", raises("01", error.BadNumber, xx arena));
|
||||
report("err-overflow", raises("9223372036854775808", error.BadNumber, xx arena));
|
||||
report("err-unterminated", raises("\"abc", error.UnexpectedEnd, xx arena));
|
||||
|
||||
print("=== DONE ===\n");
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user