Files
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

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)
```