vyrn

std/jsonread

21 exports, from std/jsonread.vyrn.

The strict JSON reader: parseJson, and the cursor fromJson reads with.

import { parseJson } from "std/jsonread"
import { Json, JsonField, emit } from "std/json"

The reader imports the writer's tree, never the reverse, so a caller that only serializes links std/json alone. The name is parseJson because parse is a reserved builtin. Commas are required, trailing commas and duplicate keys are refused, every escape is decoded (a lone surrogate is an error), and object fields keep source order. Offsets are bytes; every error starts with line N, col M:.

The grammar lives in the JsonReader functions: parseJson builds a tree with them, and a fromJson decoder writes straight into its record. Each value reader expects the cursor on the value's first byte and leaves it on the byte after the value. The first refusal is the document's error; it moves the cursor to the end, so every later reader sees the end of input, refuses nothing more, and returns.

Dependencies

The import graph shows the whole library.

Imports
fn jsonKindAt(p: JsonReader) -> Int64

The kind of the value at the cursor, named by a byte that starts it: {, [, ", t for a boolean, n for null and 0 for a number. -1 where no value can start, which a read then refuses.

fn readJsonNumber(p: modify JsonReader) -> String

Reads a JSON number, the cursor on - or a digit, and returns its raw text. Grammar: -?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?. Refused, it answers "".

fn readJsonKey(p: modify JsonReader) -> String

Reads the key of an object member, the cursor on it. Refused, it answers "". The caller checks the key for a duplicate before readJsonColon, so a duplicate is refused just past the key.

fn readJsonKeyIf(p: modify JsonReader, name: String) -> Bool

Reads the key at the cursor and answers true if it is name spelled with no escape. Otherwise it moves nothing and answers false, and readJsonKey reads the key. It allocates nothing.

type JsonKeySet = { keys: Array<String>, slots: Array<Int64>, mask: Int64 }

The keys one object has shown: linear probing over FNV-1a-64, capacity a power of two (mask = cap - 1), grown at 70% load. A scan per member would make an N-key object O(N^2), which one wide untrusted body could exploit. Empty, it holds no slots, so a set no key reaches allocates nothing.

fn admitJsonKey(p: modify JsonReader, seen: modify JsonKeySet, key: String) -> Bool

Records key in seen and answers true, or refuses it as a duplicate and answers false.

fn parseJson(src: String) -> Result<Json, String>

Parses a whole JSON document, or returns a line N, col M: <reason> error. Numbers are validated and stored as raw text; nesting past maxDepth is an Err.

Tests

16 test blocks in std/jsonread.vyrn. CI runs all of them.

Test names
  • parses scalars, arrays, and objects
  • object field order is preserved in source order
  • decodes the full escape set
  • decodes a surrogate pair to UTF-8
  • rejects a lone high surrogate
  • rejects a lone low surrogate
  • rejects a trailing comma in an array
  • rejects a trailing comma in an object
  • rejects a missing comma between members
  • rejects a duplicate object key, naming it
  • rejects an invalid number
  • reports line and column across newlines
  • round-trip law: parse(emit(j)) is structurally j
  • the round-trip law holds for the awkward keys and values too
  • nesting past maxDepth is an Err, not a trap
  • nesting at maxDepth parses, and re-emits

Examples

2 programs in examples/ import this module. Each runs as WebAssembly and as a native binary.