Files
Asuro/doc/planning/json.md
T
t3hn3rd 63f6c5ecc1
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
feature: add RFC 8259 JSON parser/serializer and string utility extensions
Add core.fmt.json — a complete RFC 8259 JSON library with recursive-descent
parser, serializer, builders, dot/bracket path accessors, and full cleanup.
Supports all 6 JSON value types plus JSON_ERROR for parse error reporting.
Numbers stored as Double via x87 FPU. Unicode \uXXXX escapes handled
(ASCII range emitted, non-ASCII replaced with '?').

Supporting changes:
- core.strings: add int64ToString, doubleToStr (scientific notation for
  very large/small values, fixed-point for normal range, trailing zero
  trimming), plus unit tests for both
- asuro.pas: wire core.fmt.json.UnitTest into the boot test suite
- mkdocs.yml: add JSON nav entry under Formats
- doc/src/core/fmt/core.fmt.json.md: full API documentation
- doc/src/core/core.strings.md: document int64ToString and doubleToStr
- doc/planning/json.md: implementation plan and design spec
- doc/planning/webgpu.md: WebGPU-over-WASM design plan (software
  rasterizer backend for WASM apps importing webgpu.h)
2026-03-10 11:51:48 +00:00

7.1 KiB

JSON Library Plan — core.fmt.json.pas

General Rules

  • Always keep doc/src/core/fmt/core.fmt.json.md up-to-date with any changes to the implementation.
  • Add any generic utility code that isn't specific to the JSON unit to the appropriate place (e.g. doubleToStr in core.strings.pas, number-related helpers in core.util.pas) and update the corresponding documentation accordingly.

Spec Compliance

Full RFC 8259 compliance:

  • All 6 value types: null, true, false, number, string, object, array
  • Numbers: full JSON number grammar — integer, fractional, and exponent parts, stored as Double
  • Strings: full escape support including \uXXXX (codepoints U+0000-U+007F emitted as bytes; higher codepoints emit ? in v1 since the OS is ASCII-only)
  • Whitespace: skip $20, $09, $0A, $0D between tokens

Data Model — Unified with JSON_ERROR

TJSONType = (
    JSON_NULL,
    JSON_BOOL,
    JSON_NUMBER,
    JSON_STRING,
    JSON_ARRAY,
    JSON_OBJECT,
    JSON_ERROR
);

PJSONValue = ^TJSONValue;
TJSONValue = record
    Kind : TJSONType;
    case TJSONType of
        JSON_BOOL:   (boolVal   : boolean);
        JSON_NUMBER: (numberVal : Double);
        JSON_STRING: (stringVal : pchar);
        JSON_ARRAY:  (arrayVal  : PDList);
        JSON_OBJECT: (objectVal : PHashMap);
        JSON_ERROR:  (errPos    : uint32;
                      errMsg    : pchar);
end;

parse() always returns PJSONValue. The caller checks result^.Kind = JSON_ERROR for failure. The error carries the byte offset and a human-readable message.

Public API

{ --- Parsing --- }
function  parse(buf : pchar; len : uint32) : PJSONValue;

{ --- Path accessors --- }
function  getByPath(root : PJSONValue; path : pchar) : PJSONValue;
function  getStringByPath(root : PJSONValue; path : pchar; default : pchar) : pchar;
function  getNumberByPath(root : PJSONValue; path : pchar; default : Double) : Double;
function  getBoolByPath(root : PJSONValue; path : pchar; default : boolean) : boolean;
function  getObjectByPath(root : PJSONValue; path : pchar) : PJSONValue;
function  getArrayByPath(root : PJSONValue; path : pchar) : PJSONValue;

{ --- Direct accessors --- }
function  getType(val : PJSONValue) : TJSONType;
function  getBool(val : PJSONValue) : boolean;
function  getNumber(val : PJSONValue) : Double;
function  getString(val : PJSONValue) : pchar;
function  getArraySize(val : PJSONValue) : uint32;
function  getArrayItem(val : PJSONValue; idx : uint32) : PJSONValue;
function  getObjectItem(val : PJSONValue; key : pchar) : PJSONValue;

{ --- Builders --- }
function  newNull : PJSONValue;
function  newBool(b : boolean) : PJSONValue;
function  newNumber(n : Double) : PJSONValue;
function  newString(s : pchar) : PJSONValue;
function  newArray : PJSONValue;
function  newObject : PJSONValue;

{ --- Mutation --- }
procedure arrayAppend(arr : PJSONValue; item : PJSONValue);
procedure objectSet(obj : PJSONValue; key : pchar; item : PJSONValue);

{ --- Serialization --- }
function  stringify(val : PJSONValue) : pchar;

{ --- Cleanup --- }
procedure freeValue(val : PJSONValue);

{ --- Tests --- }
procedure UnitTest;

Parser Internals

Recursive-descent with a parser state record:

TJSONParser = record
    buf    : pchar;
    len    : uint32;
    pos    : uint32;
    hasErr : boolean;
    errPos : uint32;
    errMsg : pchar;
end;
Internal function Role
skipWS Skip $20, $09, $0A, $0D
peek Return current char without advancing
advance Return current char and increment pos
expect Consume expected char or call setError
setError Record pos + message, set hasErr flag
parseValue Dispatch on first non-WS char
parseString "..." with full \ escape + \uXXXX handling
parseNumber Full JSON number grammar -> Double
parseArray [ value (, value)* ]
parseObject { string : value (, ...) }
parseLiteral true / false / null

Every sub-parser checks hasErr on entry and bails immediately if set — errors propagate cleanly to the top without exceptions.

Number Parsing (RFC 8259 compliant)

number = [ "-" ] int [ frac ] [ exp ]
int    = "0" / ( digit1-9 *DIGIT )
frac   = "." 1*DIGIT
exp    = ( "e" / "E" ) [ "+" / "-" ] 1*DIGIT

All arithmetic in Double via x87 FPU. Leading zeros rejected per spec (01 is invalid).

Number Serialization (internal doubleToStr)

doubleToStr is generic string utility code and belongs in core.strings.pas (not the JSON unit).

  • If no fractional part -> emit as integer (via int64 -> digit extraction)
  • Otherwise -> emit integer part, ., then fractional digits (up to 15 significant digits), trim trailing zeros
  • Very large/small numbers -> scientific notation (1.23e+10)
  • Negative -> leading -
  • Zero -> 0

String Escapes (full spec)

Parse:

Sequence Byte
\" $22
\\ $5C
\/ $2F
\b $08
\f $0C
\n $0A
\r $0D
\t $09
\uXXXX Emit byte if U+0000-U+007F, ? otherwise

Stringify: reverse mapping — emit \n, \t, etc. for control chars; \\ and \" for those literals.

Path Accessor Internals

Path syntax: "key.nested[0].deep" — dot-separated keys with [N] for array indices.

getByPath walks left to right:

  1. Accumulate chars until ., [, or end-of-string -> object key lookup via hashmap.get
  2. On [ -> parse digits until ] -> array index lookup via DL_Get
  3. Continue until path exhausted or lookup returns nil

freeValue — Recursive Cleanup

Kind Action
JSON_STRING kfree(stringVal)
JSON_ARRAY Iterate DList, freeValue each PJSONValue, then DL_Free
JSON_OBJECT hashmap.forEach -> freeValue each value, then free the map
JSON_ERROR kfree(errMsg)
All kfree the TJSONValue record itself

Test Suite (UnitTest)

Follows the same pattern as core.strings.pas — nested Assert procedure, pass/fail counters, PrintSummary at end, log tag 'JSON'.

Test categories:

Category What's tested
Literals null, true, false
Numbers Integer, negative, fractional, exponent, negative exponent, leading-zero rejection
Strings Simple, escapes (\n, \t, \\, \"), \uXXXX, empty string
Arrays Empty [], nested, mixed types
Objects Empty {}, nested, duplicate keys
Path accessors Dot navigation, array indexing, combined, missing paths return default
Error handling Unterminated string, trailing comma, missing colon, unexpected EOF, leading zeros
Builders Construct values programmatically, verify with accessors
Stringify Round-trip: parse -> stringify -> parse equivalence for each type
freeValue Ensure no crashes on deeply nested structures (smoke test)

Dependencies

uses
    memory.heap,
    core.strings,
    core.ds.lists,
    core.ds.hashmap,
    core.util;

Files

src/core/fmt/core.fmt.json.pas     <- implementation + UnitTest
doc/src/core/fmt/core.fmt.json.md  <- documentation (keep up-to-date)