import { bs } from "../../../lib/as-bs"; import { COMMA, BRACKET_RIGHT, BRACKET_LEFT } from "../../custom/chars"; import { JSON } from "../.."; import { serializeBoolUnsafe } from "./bool"; import { serializeFloat32Unsafe, serializeFloat64Unsafe } from "./float"; import { serializeIntegerUnsafe } from "./integer"; import { serializeString } from "../index/string"; import { dtoa_buffered, ftoa_buffered } from "xjb-as"; function maxIntegerBytes(): u32 { if (sizeof() == 1) return isSigned() ? 8 : 6; if (sizeof() == 2) return isSigned() ? 12 : 10; if (sizeof() == 4) return isSigned() ? 22 : 20; return isSigned() ? 42 : 40; } function reservePrimitiveArray(len: i32): void { if (len <= 0) return; if (isBoolean()) { bs.proposeSize(4 + len * 12); } else if (isInteger()) { bs.proposeSize(4 + len * (maxIntegerBytes() + 2)); } else if (isFloat()) { bs.proposeSize(4 + len * (sizeof() == 4 ? 34 : 66)); } else { bs.proposeSize(4 + (len - 1) * 2); } } function serializeArrayElement(value: T): void { if (isString()) { serializeString(value as string); return; } if (isBoolean()) { serializeBoolUnsafe(value); return; } if (isInteger()) { serializeIntegerUnsafe(value); return; } if (isFloat()) { if (sizeof() == 4) serializeFloat32Unsafe(value); else serializeFloat64Unsafe(value); return; } if (isManaged() || isReference()) { // Preserve runtime custom serializers for subclass instances stored in // parent-typed arrays before falling back to the static dispatcher. // @ts-ignore: transform-defined at runtime when present if (isDefined(value.__SERIALIZE_CUSTOM)) { // @ts-ignore: transform-defined at runtime when present value.__SERIALIZE_CUSTOM(); return; } } JSON.__serialize(value); } // --------------------------------------------------------------------------- // Specialized fast paths // --------------------------------------------------------------------------- // // `bool[]` and `u8[]` / `i8[]` serializers fold the element write and the // trailing comma into a single per-element store. The outer dispatcher // emits `[` once, the loop emits `VALUE,` once per element, and the closing // `]` overwrites the trailing comma. This eliminates the separate // comma-store + advance per element that the generic `serializeArray` does. // `"true,"` packed UTF-16: `t,r,u,e,,` lanes 0..4 = bytes 0..9. const TRUE_COMMA_LO: u64 = 0x0065_0075_0072_0074; const TRUE_COMMA_HI: u16 = 0x002c; // `"false,"` packed UTF-16: `f,a,l,s,e,,` lanes 0..5 = bytes 0..11. const FALSE_COMMA_LO: u64 = 0x0073_006c_0061_0066; const FALSE_COMMA_HI: u32 = 0x002c_0065; function serializeBoolArrayFast(src: bool[]): void { const len = src.length; // Worst case: every element is `"false,"` = 12 bytes; plus 4 for `[]`. bs.proposeSize(4 + len * 12); store(bs.offset, BRACKET_LEFT); bs.offset += 2; if (len == 0) { store(bs.offset, BRACKET_RIGHT); bs.offset += 2; return; } const dataStart = src.dataStart; for (let i: i32 = 0; i < len; i++) { if (load(dataStart + i)) { store(bs.offset, TRUE_COMMA_LO); store(bs.offset, TRUE_COMMA_HI, 8); bs.offset += 10; } else { store(bs.offset, FALSE_COMMA_LO); store(bs.offset, FALSE_COMMA_HI, 8); bs.offset += 12; } } // Overwrite the final trailing comma with `]`. store(bs.offset - 2, BRACKET_RIGHT); } // 256-entry table mapping `u8` value -> UTF-16 chars of `"DDD,"` packed in a // u64. Unused lanes hold garbage that the next element's store overwrites. const U8_SERIALIZE_LUT: usize = memory.data(2048); // 256 * sizeof // 256-entry table mapping `u8` value -> byte count of the packed encoding. const U8_SERIALIZE_LEN_LUT: usize = memory.data(256); let _u8LutInited: bool = false; function initU8Lut(): void { for (let i: i32 = 0; i < 256; i++) { let chars: u64; let bytes: u8; if (i < 10) { chars = u64(0x30 + i) | (u64(0x2c) << 16); bytes = 4; } else if (i < 100) { const d0 = i / 10; const d1 = i % 10; chars = u64(0x30 + d0) | (u64(0x30 + d1) << 16) | (u64(0x2c) << 32); bytes = 6; } else { const d0 = i / 100; const d1 = (i / 10) % 10; const d2 = i % 10; chars = u64(0x30 + d0) | (u64(0x30 + d1) << 16) | (u64(0x30 + d2) << 32) | (u64(0x2c) << 48); bytes = 8; } store(U8_SERIALIZE_LUT + ((i) << 3), chars); store(U8_SERIALIZE_LEN_LUT + i, bytes); } _u8LutInited = true; } function ensureU8Lut(): void { if (!_u8LutInited) initU8Lut(); } function serializeU8ArrayFast(src: u8[]): void { const len = src.length; // Worst case: every element is 3 digits + comma = 8 bytes; plus 4 for `[]`. bs.proposeSize(4 + len * 8); store(bs.offset, BRACKET_LEFT); bs.offset += 2; if (len == 0) { store(bs.offset, BRACKET_RIGHT); bs.offset += 2; return; } ensureU8Lut(); const dataStart = src.dataStart; for (let i: i32 = 0; i < len; i++) { const v = load(dataStart + i); const chars = load(U8_SERIALIZE_LUT + (v << 3)); const byteCount = load(U8_SERIALIZE_LEN_LUT + v); store(bs.offset, chars); bs.offset += byteCount; } store(bs.offset - 2, BRACKET_RIGHT); } // Specialized float-array serializer: xjb writer + trailing comma in a // uniform per-iteration body, then overwrite the final comma with `]`. The // generic dispatcher splits the loop into "N-1 elements with comma, then // last element without, then `]`" - the branch on each `i < end` check // stalls the loop's tight bs.offset advance pattern. This variant runs the // same number of stores per iteration (xjb output + COMMA), but the // uniform loop body inlines better and the trailing `]` is a single fixed // overwrite outside the loop. function serializeF64ArrayFast(src: f64[]): void { const len = src.length; // Worst case per element: a 21-char fixed integer (e.g. 1e20) or a ~24-char // exponent form, + comma = ~52 bytes; plus the writer's SIMD stores can // overshoot the logical end by up to 16 bytes. 80 covers both comfortably. bs.proposeSize(4 + len * 80); store(bs.offset, BRACKET_LEFT); bs.offset += 2; if (len == 0) { store(bs.offset, BRACKET_RIGHT); bs.offset += 2; return; } // Hoist `bs.offset` into a local so the loop body has a single // monotonically-advancing pointer instead of two reads + two writes back // to the global per iteration. const dataStart = src.dataStart; let offset = bs.offset; for (let i: i32 = 0; i < len; i++) { const v = load(dataStart + ((i) << 3)); offset += dtoa_buffered(offset, v) << 1; store(offset, COMMA); offset += 2; } // Overwrite the final trailing comma with `]`. store(offset - 2, BRACKET_RIGHT); bs.offset = offset; } function serializeF32ArrayFast(src: f32[]): void { const len = src.length; // Worst case for f32 is a 21-char fixed integer (e.g. 1e20 rounds to a // 21-digit value), + comma = ~44 bytes, plus up to 16 bytes of SIMD store // overshoot. 64 covers it. bs.proposeSize(4 + len * 64); store(bs.offset, BRACKET_LEFT); bs.offset += 2; if (len == 0) { store(bs.offset, BRACKET_RIGHT); bs.offset += 2; return; } const dataStart = src.dataStart; let offset = bs.offset; for (let i: i32 = 0; i < len; i++) { const v = load(dataStart + ((i) << 2)); offset += ftoa_buffered(offset, v) << 1; store(offset, COMMA); offset += 2; } store(offset - 2, BRACKET_RIGHT); bs.offset = offset; } function serializeI8ArrayFast(src: i8[]): void { const len = src.length; // Worst case: every element is `-DDD,` = 5 chars = 10 bytes; plus 4 for `[]`. bs.proposeSize(4 + len * 10); store(bs.offset, BRACKET_LEFT); bs.offset += 2; if (len == 0) { store(bs.offset, BRACKET_RIGHT); bs.offset += 2; return; } ensureU8Lut(); const dataStart = src.dataStart; for (let i: i32 = 0; i < len; i++) { let signed = load(dataStart + i); let absVal: u32; if (signed < 0) { store(bs.offset, 0x2d); // '-' bs.offset += 2; absVal = -(signed); } else { absVal = signed; } const chars = load(U8_SERIALIZE_LUT + ((absVal) << 3)); const byteCount = load(U8_SERIALIZE_LEN_LUT + absVal); store(bs.offset, chars); bs.offset += byteCount; } store(bs.offset - 2, BRACKET_RIGHT); } export function serializeArray(src: T): void { // Specialized fast paths fold the per-element comma into the element write, // saving one `store` + advance per iteration. AS folds the type checks // at compile time so the non-matching branches don't ship. if (isBoolean>()) { // @ts-expect-error: T is bool[] serializeBoolArrayFast(changetype(src)); return; } if ( isInteger>() && !isSigned>() && sizeof>() == 1 ) { // @ts-expect-error: T is u8[] serializeU8ArrayFast(changetype(src)); return; } if ( isInteger>() && isSigned>() && sizeof>() == 1 ) { // @ts-expect-error: T is i8[] serializeI8ArrayFast(changetype(src)); return; } if (isFloat>() && sizeof>() == 8) { // @ts-expect-error: T is f64[] serializeF64ArrayFast(changetype(src)); return; } if (isFloat>() && sizeof>() == 4) { // @ts-expect-error: T is f32[] serializeF32ArrayFast(changetype(src)); return; } const len = src.length; const end = len - 1; let i = 0; if (end == -1) { bs.proposeSize(4); store(bs.offset, 6094939); bs.offset += 4; return; } if ( isBoolean>() || isInteger>() || isFloat>() || isString>() ) { reservePrimitiveArray>(len); } else { bs.proposeSize(4 + (len - 1) * 2); } store(bs.offset, BRACKET_LEFT); bs.offset += 2; while (i < end) { const block = unchecked(src[i++]); serializeArrayElement>(block); store(bs.offset, COMMA); bs.offset += 2; } const lastBlock = unchecked(src[end]); serializeArrayElement>(lastBlock); store(bs.offset, BRACKET_RIGHT); bs.offset += 2; }