///
import { bs } from "../lib/as-bs";
import { OBJECT, TOTAL_OVERHEAD } from "rt/common";
import {
serializeArray,
serializeMap,
serializeDate,
serializeArbitrary,
serializeSet,
serializeStaticArray,
serializeBool,
serializeInteger,
serializeFloat,
serializeFloat32,
serializeFloat64,
serializeStruct,
serializeObject,
serializeJsonArray,
serializeRaw,
serializeString,
serializeArrayBufferUnsafe,
serializeDynamic,
serializeTypedArray,
} from "./serialize";
import {
deserializeBoolean,
deserializeArray,
deserializeFloat,
deserializeMap,
deserializeDate,
deserializeInteger,
deserializeUnsigned,
deserializeSet,
deserializeStaticArray,
deserializeArbitrary,
deserializeObject,
deserializeJsonArray,
deserializeRaw,
deserializeString,
deserializeArrayBuffer,
deserializeTypedArray,
setParseSrc,
getParseSrc,
} from "./deserialize";
import {
BACK_SLASH,
BRACE_LEFT,
BRACE_RIGHT,
BRACKET_LEFT,
BRACKET_RIGHT,
COMMA,
NULL_WORD,
QUOTE,
NULL_WORD_U64,
TRUE_WORD_U64,
FALSE_WORD_U64,
} from "./custom/chars";
import { itoa_buffered } from "util/number";
import { dtoa_buffered, ftoa_buffered } from "xjb-as";
import { ptrToStr } from "./util/ptrToStr";
import { atoi, bytes, scanStringEnd } from "./util";
import { scanValueEnd_SIMD } from "./util/scanValueEndSimd";
import { scanValueEnd_SWAR } from "./util/scanValueEndSwar";
const VAL_QNAN: u64 = 0x7ffc000000000000; // boxed signature (quiet NaN)
const VAL_TAG_SHIFT: u8 = 45;
const VAL_PAYLOAD_MASK: u64 = 0x00001fffffffffff; // low 45 bits
const VAL_PTR_MASK: u64 = 0xffffffff; // wasm32 pointer
const VAL_BOX64: u64 = 0x8000000000000000; // sign bit: 64-bit int spilled to heap
const VAL_NULL: u64 = VAL_QNAN; // tag 0 (Null), payload 0
const VAL_I64_LIMIT: i64 = 17592186044416; // 2^44 - inline range is [-2^44, 2^44)
const VAL_U64_LIMIT: u64 = 35184372088832; // 2^45 - inline range is [0, 2^45)
// Lazy value-slot payload layout (45-bit box payload), see JSON.Value.lazyBits.
// Compact form (bit 44 = 0): a source-relative start offset (23 bits) + the
// value's length (21 bits), both in UTF-16 units. Offset-heavy on purpose:
// object/array fields are usually small while the document can be large, so
// offset overflow (a field late in a big doc) is the realistic trigger, not a
// single giant field - so the offset field gets the wider range. bit 44 flags
// the absolute (scan-on-demand) fallback for a source/value past those ranges.
// bits [0..22] offset (<=~16MB src) · [23..43] length (<=~4MB val) · 44 abs
const LZ_OFF_BITS: u64 = 23;
const LZ_OFF_MASK: u64 = 0x7fffff; // (1 << 23) - 1
const LZ_LEN_MASK: u64 = 0x1fffff; // (1 << 21) - 1
const LZ_ABS_FLAG: u64 = 0x100000000000; // 1 << 44
// A materialized String box only uses the low 32 payload bits for its pointer,
// so two spare bits [32..33] cache the value's serialize-escape class. This lets
// re-serializing a dynamic string skip the per-char escape scan - a clean string
// emits via a single memcpy. `valPtr` masks bit 32+ off, so the pointer (and GC)
// are unaffected.
// 0 = unclassified · 1 = clean (no escaping -> memcpy) · 2 = needs escaping
const VAL_STR_CLASS_SHIFT: u64 = 32;
const VAL_STR_CLASS_MASK: u64 = 0x0000000300000000; // bits [32..33]
const STR_CLASS_UNKNOWN: u32 = 0;
const STR_CLASS_CLEAN: u32 = 1;
const STR_CLASS_ESCAPE: u32 = 2;
function valBoxed(w: u64): bool {
return (w & VAL_QNAN) == VAL_QNAN;
}
function valTag(w: u64): u32 {
return ((w >> VAL_TAG_SHIFT) & 0x1f);
}
function valPayload(w: u64): u64 {
return w & VAL_PAYLOAD_MASK;
}
function valPtr(w: u64): usize {
return (w & VAL_PTR_MASK);
}
function valBox(tag: u32, payload: u64): u64 {
return (
VAL_QNAN | ((tag) << VAL_TAG_SHIFT) | (payload & VAL_PAYLOAD_MASK)
);
}
function valLazy(w: u64): bool {
return valBoxed(w) && valTag(w) == JSON.Types.Lazy;
}
function valIntTag(): u32 {
if (sizeof() == 1) return isSigned() ? JSON.Types.I8 : JSON.Types.U8;
if (sizeof() == 2) return isSigned() ? JSON.Types.I16 : JSON.Types.U16;
if (sizeof() == 4) return isSigned() ? JSON.Types.I32 : JSON.Types.U32;
return isSigned() ? JSON.Types.I64 : JSON.Types.U64;
}
function hashUtf16(ptr: usize, len: i32): u32 {
let h: u32 = 2166136261;
for (let i = 0; i < len; i++) {
h ^= load(ptr + ((i) << 1));
h *= 16777619;
}
h ^= len;
h *= 16777619;
return h;
}
// v128 path: compare 8 code units (16 bytes) per step, then a u64 step (4) and
// a scalar tail. Each load is bounded by `len`, so it never reads past either
// key. Only reachable (and thus only compiled) when the SIMD feature is on, so
// the intrinsics don't break the naive/swar builds.
function utf16Equals_SIMD(ptrA: usize, ptrB: usize, len: i32): bool {
let i = 0;
for (; i + 8 <= len; i += 8) {
const off = (i) << 1;
if (v128.any_true(v128.xor(v128.load(ptrA + off), v128.load(ptrB + off))))
return false;
}
for (; i + 4 <= len; i += 4) {
const off = (i) << 1;
if (load(ptrA + off) != load(ptrB + off)) return false;
}
for (; i < len; i++) {
const off = (i) << 1;
if (load(ptrA + off) != load(ptrB + off)) return false;
}
return true;
}
function utf16Equals(ptrA: usize, ptrB: usize, len: i32): bool {
if (ASC_FEATURE_SIMD) return utf16Equals_SIMD(ptrA, ptrB, len);
// Scalar: 4 code units (one u64) per step, scalar tail. Bounded by `len`.
let i = 0;
for (; i + 4 <= len; i += 4) {
const off = (i) << 1;
if (load(ptrA + off) != load(ptrB + off)) return false;
}
for (; i < len; i++) {
const off = (i) << 1;
if (load(ptrA + off) != load(ptrB + off)) return false;
}
return true;
}
// Shared zero-length sentinel for JSON.Obj's key buffer, so an empty object
// allocates no key storage until its first key is inserted. Never mutated.
// @ts-expect-error: Decorator valid here
@lazy const EMPTY_KEYS: StaticArray = new StaticArray(0);
// Shared zero-length sentinel for JSON.Obj's key-position/index buffers.
// Never mutated.
// @ts-expect-error: Decorator valid here
@lazy const EMPTY_I32S: StaticArray = new StaticArray(0);
// Shared zero-length sentinel for JSON.Obj's value-slot buffer, so an empty
// object allocates no slot storage until its first value. Never mutated.
// @ts-expect-error: Decorator valid here
@lazy const EMPTY_VALS: StaticArray = new StaticArray(0);
// A JSON.Obj with at most this many keys resolves lookups by linear scan and
// never allocates/hashes a key index. Most JSON objects are small and dynamic
// access touches only a few keys, so the O(n) index build a hash table needs is
// pure overhead below this size. Above it, the hash index amortizes.
const OBJ_LINEAR_MAX: i32 = 6;
// Deferred-value record for a lazy JSON.Value (see JSON.Types.Lazy). `lz` packs
// the unparsed source slice as (sliceStart << 32) | sliceEnd - the same encoding
// the transform uses for @lazy struct fields. `src` is the GC anchor that keeps
// the source string (and therefore the slice pointers, which index into its
// UTF-16 buffer) alive until the value is materialized. Managed so `src` is
// traced automatically; a JSON.Value's __visit traces the LazyRef itself.
// @ts-expect-error: decorators allowed here
@final
class LazyRef {
lz: u64 = 0;
src: string = "";
}
export namespace JSON {
/**
* On-demand field marker. `JSON.Lazy` is structurally just `T` (a no-op
* type alias), so a field declared `JSON.Lazy` is typed and accessed
* exactly like `T`. The transform detects the annotation and defers that
* field: its raw JSON slice is stored at parse time and parsed into `T` on
* first access (a generated get accessor).
*/
export type Lazy = T;
/**
* Whether a lazy slot's value is JSON null - for `@omitnull` on lazy fields,
* without forcing materialization. The slot encodes the state: `u64.MAX_VALUE`
* = materialized (null iff the value pointer is 0), `0` = absent (null), any
* other value = a not-yet-parsed slice range (null iff it is literally `null`).
* @param valPtr pointer of the materialized value (0 when null)
* @param lz the packed slot
*/
export function __lazyIsNull(valPtr: usize, lz: u64): bool {
if (lz == u64.MAX_VALUE) return valPtr == 0;
if (lz == 0) return true;
const hi = (lz >>> 32);
// raw slice of length 4 (8 bytes) equal to the UTF-16 word "null"
return (lz) - hi == 8 && load(hi) == 0x006c006c0075006e;
}
/**
* Memory management utilities for the JSON serialization buffer.
*/
export namespace Memory {
/**
* Shrinks the internal serialization buffer to free memory.
* Call this after processing large JSON documents to release unused memory.
*
* @example
* ```typescript
* const largeJson = JSON.stringify(hugeObject);
* // ... process the JSON ...
* JSON.Memory.shrink(); // Free the buffer memory
* ```
*/
export function shrink(): void {
bs.shrink();
}
}
/**
* Serializes valid JSON data
* ```js
* JSON.stringify(data)
* ```
* @param data T
* @returns string
*/
export function stringify(data: T, out: string | null = null): string {
if (isBoolean()) {
if (out) {
if (data == true) {
out = changetype(__renew(changetype(out), 8));
store(changetype(out), TRUE_WORD_U64);
} else {
out = changetype(__renew(changetype(out), 10));
store(changetype(out), FALSE_WORD_U64);
store(changetype(out), 101, 8);
}
return out;
}
return data ? "true" : "false";
} else if (
isInteger() &&
!isSigned() &&
nameof() == "usize" &&
data == 0
) {
if (out) {
out = changetype(__renew(changetype(out), 8));
store(changetype(out), NULL_WORD_U64);
return out;
}
return NULL_WORD;
} else if (isInteger(data)) {
if (out) {
out = changetype(
__renew(changetype(out), sizeof() << 3),
);
const bytes = itoa_buffered(changetype(out), data) << 1;
return (out = changetype(
__renew(changetype(out), bytes),
));
}
return data.toString();
} else if (isFloat(data)) {
out = out
? changetype(__renew(changetype(out), 128))
: changetype(__new(128, idof()));
const startPtr = changetype(out);
const bytes =
(sizeof() == 4
? ftoa_buffered(startPtr, data)
: dtoa_buffered(startPtr, data)) << 1;
return changetype(__renew(startPtr, bytes));
} else if (isNullable() && changetype(data) == 0) {
if (out) {
out = changetype(__renew(changetype(out), 8));
store(changetype(out), NULL_WORD_U64);
return out;
}
return NULL_WORD;
} else if (isString>()) {
serializeString(data as string);
return out ? bs.outTo(changetype(out)) : bs.out();
// @ts-expect-error: Defined by transform
} else if (isDefined(data.__SERIALIZE_CUSTOM)) {
// @ts-expect-error: Defined by transform
data.__SERIALIZE_CUSTOM();
return out ? bs.outTo(changetype(out)) : bs.out();
// @ts-expect-error: Defined by transform
} else if (isDefined(data.__SERIALIZE)) {
// @ts-expect-error: Defined by transform
data.__SERIALIZE(changetype(data));
return out ? bs.outTo(changetype(out)) : bs.out();
} else if (data instanceof Date) {
out = out
? changetype(__renew(changetype(out), 52))
: changetype(__new(52, idof()));
store(changetype(out), QUOTE);
memory.copy(
changetype(out) + 2,
changetype(data.toISOString()),
48,
);
store(changetype(out), QUOTE, 50);
return changetype(out);
} else {
serializeReference(data);
return out ? bs.outTo(changetype(out)) : bs.out();
}
}
/**
* Parses valid JSON strings into their original format
* ```js
* JSON.parse(data)
* ```
* Pass an existing object as `out` to deserialize into it, reusing its
* allocations (symmetric with `stringify(data, out)`). On the fast path the
* per-field reuse logic (nested structs reused as `dst`, strings `__renew`d in
* place when sizes match, arrays keeping capacity) makes a steady-state
* re-parse of the same shape allocate ~nothing after the first call.
* @param data string
* @param out optional existing object to reuse (structs/composites only)
* @returns T
*/
// A type-correct "zero" for any T: null pointer for references, 0/false for
// value types. `changetype(0)` alone fails for bool/f64 (size mismatch),
// so branch on isReference at compile time.
function __zero(): T {
// @ts-ignore: compile-time intrinsic
if (isReference() || isManaged()) return changetype(0);
return 0;
}
export function parse(data: string, out: T = __zero()): T {
// Anchor the source for any lazy JSON.Obj/JSON.Value built while parsing, so
// their stored slice pointers (into `data`'s buffer) stay valid and resolve
// against the right string. Save/restore makes nested parses (e.g. a custom
// deserializer calling JSON.parse, or JSON.Obj.from) re-entrant-safe.
const prevSrc = getParseSrc();
setParseSrc(data);
const result = parseInternal(data, out);
setParseSrc(prevSrc);
return result;
}
function parseInternal(data: string, out: T = __zero()): T {
let dataPtr = changetype(data);
const dataEnd = dataPtr + bytes(data);
// Entry point skips leading whitespace: every deserialize handler may then
// assume srcStart points at the first non-whitespace char. Handlers must
// NOT re-skip leading whitespace themselves. (Trailing whitespace is left
// intact - scalars stop at the value end, composites self-trim, and
// JSON.Raw intentionally preserves trailing bytes.)
while (dataPtr < dataEnd && JSON.Util.isSpace(load(dataPtr)))
dataPtr += 2;
const dataSize = dataEnd - dataPtr;
if (isBoolean()) {
return deserializeBoolean(dataPtr, dataPtr + dataSize) as T;
} else if (isInteger()) {
return isSigned()
? deserializeInteger(dataPtr, dataPtr + dataSize)
: deserializeUnsigned(dataPtr, dataPtr + dataSize);
} else if (isFloat()) {
return deserializeFloat(dataPtr, dataPtr + dataSize);
} else if (
isNullable() &&
dataSize == 8 &&
load(dataPtr) == NULL_WORD_U64
) {
return null;
} else if (isString()) {
return deserializeString(dataPtr, dataPtr + dataSize) as T;
} else {
let type: nonnull = changetype>(0);
// @ts-expect-error: Defined by transform
if (isDefined(type.__DESERIALIZE_CUSTOM)) {
const obj = changetype>(0);
// @ts-expect-error
return obj.__DESERIALIZE_CUSTOM(data);
// @ts-expect-error: Defined by transform
} else if (
isDefined(type.__DESERIALIZE_SLOW) ||
isDefined(type.__DESERIALIZE_FAST)
) {
// Reuse the caller-supplied `out` graph when given; otherwise allocate.
const reuse = changetype(out) != 0;
const obj = reuse
? changetype>(changetype(out))
: changetype>(
__new(offsetof>(), idof>()),
);
// A freshly allocated object holds uninitialized fields (__new does not
// zero). The fast path writes fields in place and may leave some
// unwritten (@optional / skip-unknown), so it must run against defaults,
// not garbage. A reused graph is already initialized - skip it.
// @ts-expect-error: Defined by transform
if (!reuse && isDefined(type.__INITIALIZE)) obj.__INITIALIZE();
// @ts-expect-error: Defined by transform
if (isDefined(type.__DESERIALIZE_FAST)) {
// @ts-expect-error: Defined by transform
const fastEnd = obj.__DESERIALIZE_FAST(
dataPtr,
dataPtr + dataSize,
obj,
);
// A non-zero return means the fast path matched; accept it when only
// trailing whitespace remains (pretty-printed input ends with a
// newline, so the cursor stops just past `}` rather than at srcEnd).
if (
fastEnd != 0 &&
JSON.Util.skipWhitespace(fastEnd, dataPtr + dataSize) ==
dataPtr + dataSize
) {
// @ts-expect-error: Defined by transform for @lazy-field structs -
// pins the source so stored slice ranges stay valid.
if (isDefined(obj.__SET_SRC)) obj.__SET_SRC(data);
return obj;
}
}
if (isDefined(type.__INITIALIZE)) obj.__INITIALIZE();
// @ts-expect-error: Defined by transform
if (isDefined(type.__DESERIALIZE_SLOW)) {
// @ts-expect-error: Defined by transform
obj.__DESERIALIZE_SLOW(dataPtr, dataPtr + dataSize, obj);
// @ts-expect-error: Defined by transform for @lazy-field structs.
if (isDefined(obj.__SET_SRC)) obj.__SET_SRC(data);
return obj;
}
throw new Error(`No deserialize method defined for type ${type}`);
}
if (type instanceof StaticArray) {
// @ts-expect-error
return deserializeStaticArray>(
dataPtr,
dataPtr + dataSize,
0,
);
} else if (type instanceof Array) {
// Reuse the caller-supplied array when given (no allocation); the
// element loop overwrites slots and trims length. Otherwise allocate.
// @ts-expect-error
return deserializeArray>(
dataPtr,
dataPtr + dataSize,
changetype(out) != 0
? changetype(out)
: changetype(instantiate()),
);
} else if (
type instanceof Int8Array ||
type instanceof Uint8Array ||
type instanceof Uint8ClampedArray ||
type instanceof Int16Array ||
type instanceof Uint16Array ||
type instanceof Int32Array ||
type instanceof Uint32Array ||
type instanceof Int64Array ||
type instanceof Uint64Array ||
type instanceof Float32Array ||
type instanceof Float64Array
) {
return deserializeTypedArray>(
dataPtr,
dataPtr + dataSize,
0,
) as T;
} else if (type instanceof ArrayBuffer) {
return deserializeArrayBuffer(dataPtr, dataPtr + dataSize, 0) as T;
} else if (type instanceof Set) {
// @ts-expect-error
return deserializeSet>(dataPtr, dataPtr + dataSize, 0);
} else if (type instanceof Map) {
// Reuse the caller-supplied map when given (keys overwrite in place).
// @ts-expect-error
return deserializeMap>(
dataPtr,
dataPtr + dataSize,
changetype(out),
);
} else if (type instanceof Date) {
// @ts-expect-error
return deserializeDate(dataPtr, dataPtr + dataSize);
} else if (type instanceof JSON.Raw) {
// @ts-expect-error: type
return deserializeRaw(dataPtr, dataPtr + dataSize);
} else if (type instanceof JSON.Value) {
// Reuse the caller-supplied JSON.Value handle when given (`out`); the
// deserializer writes the parsed bits into it. Otherwise allocate.
// @ts-expect-error
return deserializeArbitrary(
dataPtr,
dataPtr + dataSize,
changetype(out),
);
} else if (type instanceof JSON.Obj) {
// Reuse the caller-supplied JSON.Obj (cleared, buffers kept). Otherwise allocate.
// @ts-expect-error
return deserializeObject(
dataPtr,
dataPtr + dataSize,
changetype(out),
);
} else if (type instanceof JSON.Arr) {
// Reuse the caller-supplied JSON.Arr (cleared, buffers kept). Otherwise allocate.
// @ts-expect-error
return deserializeJsonArray(
dataPtr,
dataPtr + dataSize,
changetype(out),
);
} else if (type instanceof JSON.Box) {
// @ts-expect-error
return new JSON.Box(parseBox(data, changetype>(0).value));
} else {
throw new Error(
`Could not deserialize JSON to type '${nameof()}'. ` +
`If this is a custom class, ensure it has the @json decorator: @json class ${nameof()} { ... }. ` +
`Input: "${data.length > 50 ? data.slice(0, 50) + "..." : data}"`,
);
}
}
}
/**
* Type alias for JSON type identifiers.
*/
export type Types = u16;
/**
* Enum-like namespace representing the different types supported by JSON.Value.
*
* Used internally to track the runtime type of values stored in JSON.Value instances.
* Types 0-19 are reserved for built-in types; custom @json classes use idof() + Struct.
*/
export namespace Types {
/** Represents a null value */
export const Null: u16 = 0;
export const Raw: u16 = 1;
export const U8: u16 = 2;
export const U16: u16 = 3;
export const U32: u16 = 4;
export const U64: u16 = 5;
export const I8: u16 = 6;
export const I16: u16 = 7;
export const I32: u16 = 8;
export const I64: u16 = 9;
export const F32: u16 = 10;
export const F64: u16 = 11;
export const Bool: u16 = 12;
// Managed
export const String: u16 = 13;
export const Object: u16 = 14;
export const Array: u16 = 15;
export const Map: u16 = 16;
export const Struct: u16 = 17;
export const TypedArray: u16 = 18;
export const ArrayBuffer: u16 = 19;
/**
* Internal: a not-yet-materialized value holding a raw source slice
* (see LazyRef). Never returned by `JSON.Value.type` - accessing the value
* materializes it first, so callers only ever observe the concrete type.
*/
export const Lazy: u16 = 20;
}
/**
* Wrapper for pre-formatted JSON strings that should be inserted as-is.
*
* Use this when you have a string that is already valid JSON and you don't
* want it to be re-serialized (which would escape quotes and add extra quotes).
*
* @example
* ```typescript
* const map = new Map();
* map.set("pos", new JSON.Raw('{"x":1.0,"y":2.0}'));
* JSON.stringify(map); // {"pos":{"x":1.0,"y":2.0}}
* ```
*/
export class Raw {
/** The raw JSON string data */
public data: string;
/**
* Creates a new Raw JSON wrapper.
* @param data - A valid JSON string to be inserted as-is
*/
constructor(data: string) {
this.data = data;
}
/**
* Updates the raw JSON data.
* @param data - New JSON string
*/
set(data: string): void {
this.data = data;
}
/**
* Returns the raw JSON string.
* @returns The raw JSON data
*/
toString(): string {
return this.data;
}
/**
* Creates a new Raw instance from a string.
* @param data - A valid JSON string
* @returns A new Raw instance
*/
static from(data: string): JSON.Raw {
return new JSON.Raw(data);
}
}
/**
* Dynamic value container that can hold any JSON-compatible type at runtime.
*
* Use JSON.Value when dealing with JSON data whose structure is unknown at compile time,
* or when you need to store values of different types in a single container.
*
* @example
* ```typescript
* // Parse unknown JSON structure
* const arr = JSON.parse('["string", 42, true]');
* console.log(arr[0].get()); // "string"
* console.log(arr[1].get().toString()); // 42
*
* // Create dynamic values
* const val = JSON.Value.from(42);
* val.set("now a string");
* ```
*/
// @ts-expect-error: decorators allowed here
@final export class Value {
/** Map of struct type IDs to their serialization function indices */
@lazy static METHODS: Map = new Map();
/** NaN-boxed word holding both the type tag and the value (8 bytes). */
private bits: u64;
private constructor() {
unreachable();
}
/**
* The runtime type identifier (see JSON.Types), decoded from the boxed word.
* Struct values report `idof() + JSON.Types.Struct`, recovered from the
* stored object's runtime header.
*/
get type(): u16 {
this.materialize();
const w = this.bits;
if (!valBoxed(w)) return JSON.Types.F64;
const tag = valTag(w);
if (tag == JSON.Types.Struct) {
const rtId = changetype