// =====================================================================
// json.sx — JSON value model + writer + reader (stable key order), pure sx.
//
// This module delivers the JSON VALUE MODEL, the WRITER, and the READER
// (parser). The model is built once and shared by both directions.
//
// NUMBERS ARE INTEGERS ONLY (i64) for this milestone — there is no
// fraction or exponent. A JSON value is one of: null, bool, integer,
// string, array, object. The reader REJECTS a fraction or exponent
// (`error.BadNumber`) rather than silently truncating it.
//
// STABLE KEY ORDER: an object is NOT a hash map. It is an ORDERED list
// of (key, value) pairs that preserves INSERTION ORDER. Keys are never
// sorted and never reordered to deduplicate — the order you `put` them
// in is the order the writer emits them. This is the "stable key order"
// guarantee the manifest / db.json rely on.
//
// HEAP DISCIPLINE (binding, see heap-discipline.md):
//   - Scalars (null / bool / int) carry no heap.
//   - String values are VIEWS (`string`) into caller-owned memory; the
//     node never copies the input bytes.
//   - Composite nodes (array / object) hold unbounded children, so they
//     genuinely need dynamic storage — but every allocation goes through
//     an EXPLICIT `allocator` parameter on the builder method
//     (`arr.add(v, alloc)` / `obj.put(key, val, alloc)`, mirroring
//     `List.append`). Allocation NEVER falls back to the implicit
//     context allocator silently.
//   - The WRITER adds ZERO output allocations. It emits into a
//     CALLER-PROVIDED sink: either a fixed `[]u8` buffer the caller owns
//     (overflow is reported, never silently truncated) or, streaming,
//     straight to an `fs.File` through a small caller-provided staging
//     buffer — so the db.json path holds no whole-document string in
//     memory (peak memory is O(staging), not O(document)). Integer
//     digits are formatted in a stack `[20]u8`. Sink/IO/overflow
//     failures surface on the error channel (`!JsonError`).
//
// Building a value (`#import "modules/std/json.sx";` brings these in):
//
//     obj : Object = .{};
//     obj.put("name", .str(name_view), alloc);   // name_view: a `string`
//     obj.put("size", .int_(123), alloc);
//     arr : Array = .{};
//     arr.add(.int_(1), alloc);
//     obj.put("xs", .array(arr), alloc);
//     root : Value = .object(obj);
//
// Writing into a caller buffer (returns bytes written; raises on overflow):
//
//     out : [4096]u8 = ---;
//     n := try write_to_buffer(root, string.{ ptr = @out[0], len = 4096 });
//
// Streaming straight to a file (no whole-document string):
//
//     f := open_file("db.json\0", .write)!;
//     stage : [4096]u8 = ---;
//     try write_to_file(root, @f, string.{ ptr = @stage[0], len = 4096 });
//     f.close();
// =====================================================================

#import "modules/std.sx";
// `Array`/`Object` methods take an explicit `alloc: Allocator`; bare-import
// visibility is non-transitive, so the module that names the type imports it.
#import "modules/std/mem.sx";
#import "modules/std/fs.sx";

// The writer's failure contract: a too-small caller buffer (Overflow) or
// a short/failed file write (Io). Surfaced on the error channel — never a
// silent truncation or default.
JsonError :: error { Overflow, Io }

// ── Value model ──────────────────────────────────────────────────────
//
// `Value` is a tagged union over the six JSON kinds. `null_`/`bool_`/
// `int_` are scalars; `str` is a VIEW into caller memory; `array` and
// `object` own growable child storage (see Array / Object).
//
// `Value` is defined before Array / Object so its `[*]Value` /
// `[*]Member` back-references resolve; Array / Object refer back to
// `Value` in turn (mutual recursion through pointers — each composite
// holds a pointer to its children, so the layout is finite).

Value :: enum {
    null_;
    bool_: bool;
    int_:  i64;
    str:   string;     // view into caller-owned bytes; not copied
    array: Array;
    object: Object;
}

// One ordered object entry. `key` is a view (not copied); `val` is owned
// by value inside the object's backing store.
Member :: struct {
    key: string;
    val: Value;
}

// Ordered list of values. Same growable layout as `List`, but concrete
// (not generic) so it can be an enum payload, and its builder takes the
// allocator explicitly.
Array :: struct {
    items: [*]Value = null;
    len: i64 = 0;
    cap: i64 = 0;

    // Append `v`, preserving order. Grows the backing store through the
    // explicit `alloc` when full (doubling), freeing the old buffer.
    add :: (self: *Array, v: Value, alloc: Allocator) {
        if self.len >= self.cap {
            new_cap := if self.cap == 0 then 4 else self.cap * 2;
            new_items : [*]Value = xx alloc.alloc_bytes(new_cap * size_of(Value));
            if self.len > 0 {
                memcpy(new_items, self.items, self.len * size_of(Value));
                alloc.dealloc_bytes(self.items);
            }
            self.items = new_items;
            self.cap = new_cap;
        }
        self.items[self.len] = v;
        self.len += 1;
    }

    deinit :: (self: *Array, alloc: Allocator) {
        if self.items != null { alloc.dealloc_bytes(self.items); }
        self.items = null;
        self.len = 0;
        self.cap = 0;
    }
}

// Ordered list of (key, value) pairs — INSERTION ORDER preserved, never
// sorted, never deduplicated. Growable backing store through the
// explicit `alloc`.
Object :: struct {
    items: [*]Member = null;
    len: i64 = 0;
    cap: i64 = 0;

    // Append a (key, val) pair at the end. Does not check for or merge a
    // duplicate key — insertion order is the contract; a repeated key is
    // emitted twice, in the order added.
    put :: (self: *Object, key: string, v: Value, alloc: Allocator) {
        if self.len >= self.cap {
            new_cap := if self.cap == 0 then 4 else self.cap * 2;
            new_items : [*]Member = xx alloc.alloc_bytes(new_cap * size_of(Member));
            if self.len > 0 {
                memcpy(new_items, self.items, self.len * size_of(Member));
                alloc.dealloc_bytes(self.items);
            }
            self.items = new_items;
            self.cap = new_cap;
        }
        self.items[self.len] = Member.{ key = key, val = v };
        self.len += 1;
    }

    deinit :: (self: *Object, alloc: Allocator) {
        if self.items != null { alloc.dealloc_bytes(self.items); }
        self.items = null;
        self.len = 0;
        self.cap = 0;
    }
}

// ── Sink ─────────────────────────────────────────────────────────────
//
// A single concrete output sink with two modes, chosen by `file`:
//   - BUFFER mode (`file == null`): bytes land directly in the caller's
//     `dst`; when `dst` fills, `put`/`put_byte` raise `error.Overflow`.
//     `pos` is the running byte count.
//   - FILE mode (`file != null`): `dst` is a caller-provided STAGING
//     buffer; when it fills it is flushed to `file` and reused, so peak
//     memory is O(dst) regardless of document size. `flush()` writes any
//     remaining staged bytes. The staging buffer must be non-empty.
//
// The sink owns NO heap and holds NO whole-document string.

Sink :: struct {
    dst: []u8;            // caller-owned destination (buffer mode) or staging (file mode)
    pos: i64 = 0;         // bytes currently in `dst`
    file: *File = null;   // null => buffer mode

    put_byte :: (self: *Sink, b: u8) -> !JsonError {
        if self.pos >= self.dst.len {
            if self.file == null { raise error.Overflow; }
            try self.flush();
        }
        self.dst[self.pos] = b;
        self.pos += 1;
        return;
    }

    put :: (self: *Sink, bytes: string) -> !JsonError {
        i := 0;
        while i < bytes.len {
            try self.put_byte(bytes[i]);
            i += 1;
        }
        return;
    }

    // File mode: write staged bytes and reset. Buffer mode: no-op.
    flush :: (self: *Sink) -> !JsonError {
        if self.file == null { return; }
        if self.pos == 0 { return; }
        n := self.file.write(string.{ ptr = @self.dst[0], len = self.pos });
        if n != self.pos { raise error.Io; }
        self.pos = 0;
        return;
    }
}

// ── Writer ───────────────────────────────────────────────────────────

// Lowercase-hex ASCII byte for a 0..15 nibble. 48='0', 97='a'.
hex_digit :: (n: i64) -> u8 {
    if n < 10 then xx (n + 48) else xx (n - 10 + 97)
}

// `\u00XX` escape for a control byte (< 0x20). Two hex nibbles; the high
// byte of the code point is always 0x00 here.
write_u_escape :: (c: u8, sink: *Sink) -> !JsonError {
    try sink.put_byte(92);    // backslash
    try sink.put_byte(117);   // 'u'
    try sink.put_byte(48);    // '0'
    try sink.put_byte(48);    // '0'
    try sink.put_byte(hex_digit((cast(i64) c >> 4) & 0xF));
    try sink.put_byte(hex_digit(cast(i64) c & 0xF));
    return;
}

// Emit a JSON string: opening quote, escaped body, closing quote. Escapes
// quote, backslash, the named control shorthands (\b \t \n \f \r), and
// any other control byte (< 0x20) as `\u00XX`. Bytes >= 0x20 (including
// UTF-8 continuation bytes) pass through unchanged.
write_string :: (s: string, sink: *Sink) -> !JsonError {
    try sink.put_byte(34);    // opening quote
    i := 0;
    while i < s.len {
        c := s[i];
        if c == 34       { try sink.put_byte(92); try sink.put_byte(34); }    // \"
        else if c == 92  { try sink.put_byte(92); try sink.put_byte(92); }    // \\
        else if c == 8   { try sink.put_byte(92); try sink.put_byte(98); }    // \b
        else if c == 9   { try sink.put_byte(92); try sink.put_byte(116); }   // \t
        else if c == 10  { try sink.put_byte(92); try sink.put_byte(110); }   // \n
        else if c == 12  { try sink.put_byte(92); try sink.put_byte(102); }   // \f
        else if c == 13  { try sink.put_byte(92); try sink.put_byte(114); }   // \r
        else if c < 32   { try write_u_escape(c, sink); }
        else             { try sink.put_byte(c); }
        i += 1;
    }
    try sink.put_byte(34);    // closing quote
    return;
}

// Emit a signed integer in decimal, no allocation. Digits are formed in a
// stack buffer working in NEGATIVE space so i64 MIN
// (-9223372036854775808) — whose magnitude is not representable as a
// positive i64 — serializes correctly.
write_int :: (n: i64, sink: *Sink) -> !JsonError {
    if n == 0 { try sink.put_byte(48); return; }
    tmp : [20]u8 = ---;       // 19 digits + sign is the i64 worst case
    neg := n < 0;
    v := n;
    if !neg { v = 0 - n; }    // fold positives into negative space
    i := 20;
    while v < 0 {
        i -= 1;
        d := 0 - (v % 10);    // sx `%` keeps the dividend's sign; this is 0..9
        tmp[i] = xx (d + 48);
        v = v / 10;           // truncates toward zero
    }
    if neg { i -= 1; tmp[i] = 45; }   // '-'
    try sink.put(string.{ ptr = @tmp[i], len = 20 - i });
    return;
}

// Serialize one value into `sink`. Recurses for arrays / objects.
write_value :: (v: Value, sink: *Sink) -> !JsonError {
    if v == {
        case .null_:  try sink.put("null");
        case .bool_:  try sink.put(if v.bool_ then "true" else "false");
        case .int_:   try write_int(v.int_, sink);
        case .str:    try write_string(v.str, sink);
        case .array:  try write_array(v.array, sink);
        case .object: try write_object(v.object, sink);
    }
    return;
}

write_array :: (arr: Array, sink: *Sink) -> !JsonError {
    try sink.put_byte(91);    // '['
    i := 0;
    while i < arr.len {
        if i > 0 { try sink.put_byte(44); }   // ','
        try write_value(arr.items[i], sink);
        i += 1;
    }
    try sink.put_byte(93);    // ']'
    return;
}

// Emits members in stored order — the insertion order guarantee.
write_object :: (obj: Object, sink: *Sink) -> !JsonError {
    try sink.put_byte(123);   // '{'
    i := 0;
    while i < obj.len {
        if i > 0 { try sink.put_byte(44); }   // ','
        try write_string(obj.items[i].key, sink);
        try sink.put_byte(58);                 // ':'
        try write_value(obj.items[i].val, sink);
        i += 1;
    }
    try sink.put_byte(125);   // '}'
    return;
}

// ── Public entry points ──────────────────────────────────────────────

// Serialize `v` into the caller-owned buffer `dst`. Returns the number of
// bytes written. Raises `error.Overflow` if `dst` is too small (the
// partial contents of `dst` are then undefined — nothing is truncated
// silently). No allocation.
write_to_buffer :: (v: Value, dst: []u8) -> (i64, !JsonError) {
    sink := Sink.{ dst = dst };
    try write_value(v, @sink);
    return sink.pos;
}

// Serialize `v` straight to an open `file`, staging through the caller-
// owned `staging` buffer (which must be non-empty). No whole-document
// string is ever held — peak extra memory is O(staging). Raises
// `error.Io` on a short/failed write. No allocation.
write_to_file :: (v: Value, file: *File, staging: []u8) -> !JsonError {
    sink := Sink.{ dst = staging, file = file };
    try write_value(v, @sink);
    try sink.flush();
    return;
}

// ── Reader (parser) ───────────────────────────────────────────────────
//
// `parse(src, alloc)` turns a JSON document in `src` into the value model
// above. It is the inverse of the writer for the v0 scope: objects (in
// INSERTION ORDER), arrays, strings (with full unescaping incl. \uXXXX
// and surrogate pairs), i64 integers, bool, null.
//
// FAILURE SURFACING (hard contract): every malformed input raises on the
// error channel (`!JsonParseError`) — never a bogus or default value.
// Trailing non-whitespace after a complete value is `TrailingGarbage`.
// `pos` (the parser cursor) marks where the failure was detected.
//
// NOT SUPPORTED (rejected, not silently accepted): a fraction or exponent
// in a number (`1.5`, `1e9`) → `BadNumber`; a number outside i64 →
// `BadNumber`; a leading-zero integer (`01`) → `BadNumber`. An UNESCAPED
// raw control byte (U+0000..U+001F) inside a string → `BadControlChar`
// (RFC 8259 §7 requires those bytes to be escaped); the escaped forms
// (`\t`, `\n`, `\u0009`, …) stay valid and decode normally. Bytes >= 0x20,
// including 0x7F (DEL) and UTF-8 continuation bytes (>= 0x80), pass through.
//
// HEAP DISCIPLINE (binding, see heap-discipline.md). Exactly two kinds of
// allocation happen, both through the EXPLICIT `alloc` parameter, never
// the implicit context allocator:
//   1. Composite backing stores — `Array.items` / `Object.items` grow via
//      `arr.add(.., alloc)` / `obj.put(.., alloc)` (genuinely unbounded
//      children; mirrors `List`).
//   2. DECODED strings — a string containing escapes must be un-escaped
//      into fresh storage; that buffer is `alloc`-ed (bounded by the raw
//      span, since every escape shrinks). A string with NO escapes is a
//      zero-copy VIEW into `src`; scalars carry no heap.
//
// OWNERSHIP / LIFETIME: un-escaped string values are SLICES into `src` —
// they are valid only while `src` lives. Everything else (nodes, decoded
// strings) is owned by `alloc`; free it all by dropping that allocator
// (e.g. an Arena `deinit`). A typical caller parses under an Arena and
// keeps `src` alive for as long as the tree is used.
//
//     gpa   := GPA.init();
//     arena := Arena.init(xx gpa, 4096);
//     defer arena.deinit();
//     root  := parse(src, xx arena)!;   // composites + decoded strings in arena

// The reader's failure contract. Meaningful variants so a caller can tell
// a truncated document from a bad escape from trailing junk.
JsonParseError :: error { UnexpectedToken, UnexpectedEnd, BadEscape, BadNumber, TrailingGarbage, BadControlChar }

// Lowercase/uppercase hex nibble value (0..15) of an ASCII byte; a non-hex
// byte in a `\uXXXX` escape is a `BadEscape`.
hex_value :: (c: u8) -> (i64, !JsonParseError) {
    if c >= 48 and c <= 57  { return (cast(i64) c) - 48; }        // '0'..'9'
    if c >= 97 and c <= 102 { return (cast(i64) c) - 97 + 10; }   // 'a'..'f'
    if c >= 65 and c <= 70  { return (cast(i64) c) - 65 + 10; }   // 'A'..'F'
    raise error.BadEscape;
}

// Encode code point `cp` (already validated 0..0x10FFFF, non-surrogate) as
// UTF-8 into `out`, returning the byte count (1..4). No bounds check: the
// decode buffer is sized to the raw escaped span, which always dominates.
encode_utf8 :: (cp: i64, out: [*]u8) -> i64 {
    if cp < 0x80 {
        out[0] = xx cp;
        return 1;
    }
    if cp < 0x800 {
        out[0] = xx (0xC0 | (cp >> 6));
        out[1] = xx (0x80 | (cp & 0x3F));
        return 2;
    }
    if cp < 0x10000 {
        out[0] = xx (0xE0 | (cp >> 12));
        out[1] = xx (0x80 | ((cp >> 6) & 0x3F));
        out[2] = xx (0x80 | (cp & 0x3F));
        return 3;
    }
    out[0] = xx (0xF0 | (cp >> 18));
    out[1] = xx (0x80 | ((cp >> 12) & 0x3F));
    out[2] = xx (0x80 | ((cp >> 6) & 0x3F));
    out[3] = xx (0x80 | (cp & 0x3F));
    return 4;
}

// The cursor over the input. `src` is borrowed (never written); `pos` is
// the running offset and doubles as the failure position; `alloc` is the
// EXPLICIT allocator for composites + decoded strings.
Parser :: struct {
    src: string;
    pos: i64 = 0;
    alloc: Allocator;

    // Advance past JSON whitespace (space / tab / LF / CR).
    skip_ws :: (self: *Parser) {
        while self.pos < self.src.len {
            c := self.src[self.pos];
            if c == 32 or c == 9 or c == 10 or c == 13 { self.pos += 1; }
            else { break; }
        }
    }

    // Consume an exact literal (`true` / `false` / `null`) or fail.
    expect_lit :: (self: *Parser, lit: string) -> !JsonParseError {
        if self.pos + lit.len > self.src.len { raise error.UnexpectedEnd; }
        i := 0;
        while i < lit.len {
            if self.src[self.pos + i] != lit[i] { raise error.UnexpectedToken; }
            i += 1;
        }
        self.pos += lit.len;
        return;
    }

    // Read 4 hex digits at `i` (which must lie within [.., end)); returns
    // the 16-bit value. Fewer than 4 digits before `end` is a BadEscape.
    read_hex4 :: (self: *Parser, i: i64, end: i64) -> (i64, !JsonParseError) {
        if i + 4 > end { raise error.BadEscape; }
        v := 0;
        k := 0;
        while k < 4 {
            v = v * 16 + (try hex_value(self.src[i + k]));
            k += 1;
        }
        return v;
    }

    // Decode the escaped string body in [start, end) into `out`, returning
    // the decoded byte length. Pass 1 (in parse_string) guarantees there is
    // no dangling backslash, so the byte after every `\` is in range.
    decode_into :: (self: *Parser, start: i64, end: i64, out: [*]u8) -> (i64, !JsonParseError) {
        di := 0;
        i := start;
        while i < end {
            c := self.src[i];
            if c == 92 {                         // backslash
                i += 1;
                e := self.src[i];
                if      e == 34  { out[di] = 34; di += 1; i += 1; }   // \"
                else if e == 92  { out[di] = 92; di += 1; i += 1; }   // \\
                else if e == 47  { out[di] = 47; di += 1; i += 1; }   // \/
                else if e == 98  { out[di] = 8;  di += 1; i += 1; }   // \b
                else if e == 102 { out[di] = 12; di += 1; i += 1; }   // \f
                else if e == 110 { out[di] = 10; di += 1; i += 1; }   // \n
                else if e == 114 { out[di] = 13; di += 1; i += 1; }   // \r
                else if e == 116 { out[di] = 9;  di += 1; i += 1; }   // \t
                else if e == 117 {                                    // \uXXXX
                    hpos := i + 1;
                    u := try self.read_hex4(hpos, end);
                    if u >= 0xD800 and u <= 0xDBFF {
                        // high surrogate: require a following \uYYYY low surrogate
                        lpos := hpos + 4;
                        if lpos + 2 > end { raise error.BadEscape; }
                        if self.src[lpos] != 92 or self.src[lpos + 1] != 117 { raise error.BadEscape; }
                        lo := try self.read_hex4(lpos + 2, end);
                        if lo < 0xDC00 or lo > 0xDFFF { raise error.BadEscape; }
                        cp := 0x10000 + ((u - 0xD800) << 10) + (lo - 0xDC00);
                        di += encode_utf8(cp, @out[di]);
                        i = lpos + 6;
                    } else {
                        if u >= 0xDC00 and u <= 0xDFFF { raise error.BadEscape; }   // lone low surrogate
                        di += encode_utf8(u, @out[di]);
                        i = hpos + 4;
                    }
                }
                else { raise error.BadEscape; }
            } else {
                out[di] = c; di += 1; i += 1;
            }
        }
        return di;
    }

    // Parse a string starting at the opening quote (current `pos`). Returns
    // a zero-copy VIEW into `src` when the body has no escapes; otherwise
    // decodes into an `alloc`-ed buffer (bounded by the raw span). `pos`
    // ends just past the closing quote.
    parse_string :: (self: *Parser) -> (string, !JsonParseError) {
        self.pos += 1;                 // consume opening quote
        start := self.pos;
        has_escape := false;
        i := start;
        while i < self.src.len {
            c := self.src[i];
            if c == 34 { break; }      // closing quote
            if c == 92 {               // backslash escapes the next byte
                has_escape = true;
                i += 1;
                if i >= self.src.len { raise error.UnexpectedEnd; }
            } else if c < 32 {
                // RFC 8259 §7: a raw control byte (U+0000..U+001F) must be
                // escaped inside a string; an unescaped one is invalid JSON.
                self.pos = i;
                raise error.BadControlChar;
            }
            i += 1;
        }
        if i >= self.src.len { raise error.UnexpectedEnd; }   // unterminated
        end := i;
        if !has_escape {
            self.pos = end + 1;
            return string.{ ptr = @self.src[start], len = end - start };
        }
        raw_len := end - start;        // decoded length <= raw_len (escapes shrink)
        out : [*]u8 = xx self.alloc.alloc_bytes(raw_len);
        dlen := try self.decode_into(start, end, out);
        self.pos = end + 1;
        return string.{ ptr = out, len = dlen };
    }

    // Parse an i64 integer (optional '-', then digits). Rejects leading
    // zeros, a fraction/exponent tail, and any value outside i64 — all
    // `BadNumber`. Accumulates in NEGATIVE space so i64 MIN parses exactly.
    parse_number :: (self: *Parser) -> (i64, !JsonParseError) {
        // i64 bounds, built positionally because |MIN| is not a
        // representable positive i64 literal. `min_div10` is `MIN / 10`
        // truncated toward zero (remainder -8) — the digit loop's overflow
        // threshold. Accumulation runs in NEGATIVE space so MIN is exact.
        i64_min := 0 - 9223372036854775807 - 1;
        min_div10 := 0 - 922337203685477580;
        neg := false;
        if self.src[self.pos] == 45 { neg = true; self.pos += 1; }   // '-'
        if self.pos >= self.src.len { raise error.BadNumber; }       // '-' with no digit
        dstart := self.pos;
        c0 := self.src[self.pos];
        if c0 < 48 or c0 > 57 { raise error.BadNumber; }
        val : i64 = 0;
        digits := 0;
        while self.pos < self.src.len {
            c := self.src[self.pos];
            if c < 48 or c > 57 { break; }
            d := (cast(i64) c) - 48;
            if val < min_div10 { raise error.BadNumber; }
            if val == min_div10 and d > 8 { raise error.BadNumber; }
            val = val * 10 - d;
            digits += 1;
            self.pos += 1;
        }
        if self.src[dstart] == 48 and digits > 1 { raise error.BadNumber; }   // no leading zeros
        if self.pos < self.src.len {
            nc := self.src[self.pos];
            if nc == 46 or nc == 101 or nc == 69 { raise error.BadNumber; }   // '.' / 'e' / 'E' — ints only
        }
        if !neg {
            if val == i64_min { raise error.BadNumber; }   // |MIN| not representable as +i64
            val = 0 - val;
        }
        return val;
    }

    // Parse an array starting at '['. Builds an `Array` through `alloc`.
    parse_array :: (self: *Parser) -> (Value, !JsonParseError) {
        self.pos += 1;                 // consume '['
        arr : Array = .{};
        self.skip_ws();
        if self.pos < self.src.len and self.src[self.pos] == 93 {   // empty ']'
            self.pos += 1;
            return Value.array(arr);
        }
        loop := true;
        while loop {
            v := try self.parse_value();
            arr.add(v, self.alloc);
            self.skip_ws();
            if self.pos >= self.src.len { raise error.UnexpectedEnd; }
            c := self.src[self.pos];
            if c == 44 { self.pos += 1; }                          // ',' more
            else if c == 93 { self.pos += 1; loop = false; }       // ']' done
            else { raise error.UnexpectedToken; }
        }
        return Value.array(arr);
    }

    // Parse an object starting at '{'. Keys must be strings; insertion
    // order is preserved (duplicate keys are kept, never merged).
    parse_object :: (self: *Parser) -> (Value, !JsonParseError) {
        self.pos += 1;                 // consume '{'
        obj : Object = .{};
        self.skip_ws();
        if self.pos < self.src.len and self.src[self.pos] == 125 {  // empty '}'
            self.pos += 1;
            return Value.object(obj);
        }
        loop := true;
        while loop {
            self.skip_ws();
            if self.pos >= self.src.len { raise error.UnexpectedEnd; }
            if self.src[self.pos] != 34 { raise error.UnexpectedToken; }   // key must be a string
            key := try self.parse_string();
            self.skip_ws();
            if self.pos >= self.src.len { raise error.UnexpectedEnd; }
            if self.src[self.pos] != 58 { raise error.UnexpectedToken; }   // ':'
            self.pos += 1;
            v := try self.parse_value();
            obj.put(key, v, self.alloc);
            self.skip_ws();
            if self.pos >= self.src.len { raise error.UnexpectedEnd; }
            c := self.src[self.pos];
            if c == 44 { self.pos += 1; }                          // ',' more
            else if c == 125 { self.pos += 1; loop = false; }      // '}' done
            else { raise error.UnexpectedToken; }
        }
        return Value.object(obj);
    }

    // Parse any single value (after skipping leading whitespace).
    parse_value :: (self: *Parser) -> (Value, !JsonParseError) {
        self.skip_ws();
        if self.pos >= self.src.len { raise error.UnexpectedEnd; }
        c := self.src[self.pos];
        if c == 123 { return try self.parse_object(); }            // '{'
        if c == 91  { return try self.parse_array(); }             // '['
        if c == 34  { s := try self.parse_string(); return Value.str(s); }   // '"'
        if c == 116 { try self.expect_lit("true");  return Value.bool_(true); }    // 't'
        if c == 102 { try self.expect_lit("false"); return Value.bool_(false); }   // 'f'
        if c == 110 { try self.expect_lit("null");  nv : Value = .null_; return nv; }   // 'n'
        if c == 45 or (c >= 48 and c <= 57) { n := try self.parse_number(); return Value.int_(n); }   // '-' / digit
        raise error.UnexpectedToken;
    }
}

// Parse a complete JSON document from `src` into the value model, using
// `alloc` for composite nodes and decoded (escaped) strings. Un-escaped
// string values are VIEWS into `src` and are valid only while `src` lives.
// Trailing non-whitespace after the value raises `error.TrailingGarbage`.
parse :: (src: string, alloc: Allocator) -> (Value, !JsonParseError) {
    p := Parser.{ src = src, alloc = alloc };
    v := try p.parse_value();
    p.skip_ws();
    if p.pos != p.src.len { raise error.TrailingGarbage; }
    return v;
}
