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)
215 lines
7.1 KiB
Markdown
215 lines
7.1 KiB
Markdown
# 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`
|
|
|
|
```pascal
|
|
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
|
|
|
|
```pascal
|
|
{ --- 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:
|
|
|
|
```pascal
|
|
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
|
|
|
|
```pascal
|
|
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)
|
|
```
|