json-as 1.3.7 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/assembly/deserialize/index/arbitrary.ts +2 -2
  4. package/assembly/deserialize/index/array.ts +29 -14
  5. package/assembly/deserialize/index/bool.ts +1 -1
  6. package/assembly/deserialize/index/date.ts +1 -1
  7. package/assembly/deserialize/index/float.ts +40 -1
  8. package/assembly/deserialize/index/integer.ts +3 -3
  9. package/assembly/deserialize/index/map.ts +1 -1
  10. package/assembly/deserialize/index/object.ts +1 -1
  11. package/assembly/deserialize/index/raw.ts +1 -1
  12. package/assembly/deserialize/index/set.ts +1 -1
  13. package/assembly/deserialize/index/staticarray.ts +4 -1
  14. package/assembly/deserialize/index/string.ts +28 -3
  15. package/assembly/deserialize/index/struct.ts +1 -1
  16. package/assembly/deserialize/index/typedarray.ts +25 -15
  17. package/assembly/deserialize/index/unsigned.ts +3 -3
  18. package/assembly/deserialize/index.ts +1 -0
  19. package/assembly/deserialize/naive/array/bool.ts +68 -0
  20. package/assembly/deserialize/naive/array/float.ts +63 -0
  21. package/assembly/deserialize/{simple → naive}/array/generic.ts +1 -2
  22. package/assembly/deserialize/naive/array/integer.ts +86 -0
  23. package/assembly/deserialize/{simple → naive}/array/map.ts +0 -1
  24. package/assembly/deserialize/{simple → naive}/array/object.ts +0 -1
  25. package/assembly/deserialize/naive/array/string.ts +69 -0
  26. package/assembly/deserialize/{simple → naive}/array/struct.ts +0 -1
  27. package/assembly/deserialize/{simple → naive}/array.ts +6 -11
  28. package/assembly/deserialize/naive/float.ts +135 -0
  29. package/assembly/deserialize/{simple → naive}/integer.ts +2 -2
  30. package/assembly/deserialize/{simple → naive}/map.ts +12 -6
  31. package/assembly/deserialize/{simple → naive}/object.ts +4 -7
  32. package/assembly/deserialize/{simple → naive}/set.ts +12 -27
  33. package/assembly/deserialize/{simple → naive}/staticarray/array.ts +1 -1
  34. package/assembly/deserialize/{simple → naive}/staticarray/bool.ts +1 -1
  35. package/assembly/deserialize/{simple → naive}/staticarray/float.ts +1 -1
  36. package/assembly/deserialize/{simple → naive}/staticarray/integer.ts +1 -1
  37. package/assembly/deserialize/{simple → naive}/staticarray/struct.ts +1 -2
  38. package/assembly/deserialize/{simple → naive}/staticarray.ts +4 -4
  39. package/assembly/deserialize/naive/string.ts +199 -0
  40. package/assembly/deserialize/{simple → naive}/typedarray.ts +4 -4
  41. package/assembly/deserialize/{simple → naive}/unsigned.ts +2 -2
  42. package/assembly/deserialize/simd/array/integer.ts +19 -19
  43. package/assembly/deserialize/simd/float.ts +303 -0
  44. package/assembly/deserialize/simd/string.ts +233 -108
  45. package/assembly/deserialize/swar/array/arbitrary.ts +6 -2
  46. package/assembly/deserialize/swar/array/array.ts +14 -7
  47. package/assembly/deserialize/swar/array/bool.ts +8 -3
  48. package/assembly/deserialize/swar/array/box.ts +6 -2
  49. package/assembly/deserialize/swar/array/float.ts +282 -6
  50. package/assembly/deserialize/swar/array/generic.ts +6 -2
  51. package/assembly/deserialize/swar/array/integer.ts +81 -74
  52. package/assembly/deserialize/swar/array/map.ts +6 -2
  53. package/assembly/deserialize/swar/array/object.ts +24 -32
  54. package/assembly/deserialize/swar/array/raw.ts +6 -2
  55. package/assembly/deserialize/swar/array/shared.ts +32 -8
  56. package/assembly/deserialize/swar/array/string.ts +127 -10
  57. package/assembly/deserialize/swar/array/struct.ts +45 -11
  58. package/assembly/deserialize/swar/array.ts +2 -56
  59. package/assembly/deserialize/swar/float.ts +304 -0
  60. package/assembly/deserialize/swar/string.ts +119 -104
  61. package/assembly/deserialize/swar/typedarray.ts +224 -0
  62. package/assembly/index.ts +203 -293
  63. package/assembly/serialize/index/array.ts +1 -1
  64. package/assembly/serialize/index/bool.ts +1 -1
  65. package/assembly/serialize/index/date.ts +1 -1
  66. package/assembly/serialize/index/float.ts +1 -1
  67. package/assembly/serialize/index/integer.ts +1 -1
  68. package/assembly/serialize/index/map.ts +1 -1
  69. package/assembly/serialize/index/raw.ts +1 -1
  70. package/assembly/serialize/index/set.ts +1 -1
  71. package/assembly/serialize/index/staticarray.ts +1 -1
  72. package/assembly/serialize/index/string.ts +1 -1
  73. package/assembly/serialize/index/struct.ts +1 -1
  74. package/assembly/serialize/index/typedarray.ts +2 -11
  75. package/assembly/serialize/index.ts +1 -0
  76. package/assembly/serialize/{simple → naive}/array.ts +87 -0
  77. package/assembly/serialize/{simple → naive}/string.ts +1 -1
  78. package/assembly/serialize/swar/string.ts +0 -139
  79. package/assembly/util/dragonbox.ts +10 -3
  80. package/assembly/util/itoa-fast.ts +29 -18
  81. package/assembly/util/scanValueEnd.ts +78 -0
  82. package/assembly/util/scientific.ts +132 -0
  83. package/lib/as-bs.ts +14 -1
  84. package/package.json +14 -13
  85. package/transform/lib/index.d.ts +4 -0
  86. package/transform/lib/index.d.ts.map +1 -1
  87. package/transform/lib/index.js +155 -238
  88. package/transform/lib/index.js.map +1 -1
  89. package/assembly/deserialize/simple/arbitrary.ts +0 -30
  90. package/assembly/deserialize/simple/array/bool.ts +0 -48
  91. package/assembly/deserialize/simple/array/float.ts +0 -55
  92. package/assembly/deserialize/simple/array/integer.ts +0 -33
  93. package/assembly/deserialize/simple/array/string.ts +0 -29
  94. package/assembly/deserialize/simple/float.ts +0 -206
  95. package/assembly/deserialize/simple/string.ts +0 -45
  96. package/assembly/serialize/simple/arbitrary.ts +0 -79
  97. package/assembly/serialize/simple/object.ts +0 -42
  98. /package/assembly/deserialize/{simple → naive}/array/arbitrary.ts +0 -0
  99. /package/assembly/deserialize/{simple → naive}/array/array.ts +0 -0
  100. /package/assembly/deserialize/{simple → naive}/array/box.ts +0 -0
  101. /package/assembly/deserialize/{simple → naive}/array/raw.ts +0 -0
  102. /package/assembly/deserialize/{simple → naive}/bool.ts +0 -0
  103. /package/assembly/deserialize/{simple → naive}/date.ts +0 -0
  104. /package/assembly/deserialize/{simple → naive}/raw.ts +0 -0
  105. /package/assembly/deserialize/{simple → naive}/staticarray/string.ts +0 -0
  106. /package/assembly/deserialize/{simple → naive}/struct.ts +0 -0
  107. /package/assembly/serialize/{simple → naive}/bool.ts +0 -0
  108. /package/assembly/serialize/{simple → naive}/date.ts +0 -0
  109. /package/assembly/serialize/{simple → naive}/float.ts +0 -0
  110. /package/assembly/serialize/{simple → naive}/integer.ts +0 -0
  111. /package/assembly/serialize/{simple → naive}/map.ts +0 -0
  112. /package/assembly/serialize/{simple → naive}/raw.ts +0 -0
  113. /package/assembly/serialize/{simple → naive}/set.ts +0 -0
  114. /package/assembly/serialize/{simple → naive}/staticarray.ts +0 -0
  115. /package/assembly/serialize/{simple → naive}/struct.ts +0 -0
  116. /package/assembly/serialize/{simple → naive}/typedarray.ts +0 -0
@@ -3,11 +3,11 @@ import { JSON } from "../..";
3
3
  export {
4
4
  serializeArrayBufferUnsafe,
5
5
  serializeTypedArray,
6
- } from "../simple/typedarray";
6
+ } from "../naive/typedarray";
7
7
  import {
8
8
  serializeArrayBufferUnsafe,
9
9
  serializeTypedArray,
10
- } from "../simple/typedarray";
10
+ } from "../naive/typedarray";
11
11
 
12
12
 
13
13
  @inline export function serializeDynamic(type: u16, data: usize): void {
@@ -73,12 +73,3 @@ import {
73
73
  }
74
74
  }
75
75
  }
76
-
77
-
78
- @inline export function serializeArrayBuffer(data: ArrayBuffer): void {
79
- const dataStart = changetype<usize>(data);
80
- serializeArrayBufferUnsafe(
81
- dataStart,
82
- changetype<OBJECT>(dataStart - TOTAL_OVERHEAD).rtSize,
83
- );
84
- }
@@ -11,3 +11,4 @@ export * from "./index/set";
11
11
  export * from "./index/staticarray";
12
12
  export * from "./index/string";
13
13
  export * from "./index/struct";
14
+ export * from "./index/typedarray";
@@ -5,6 +5,10 @@ import { serializeBoolUnsafe } from "./bool";
5
5
  import { serializeFloat32Unsafe, serializeFloat64Unsafe } from "./float";
6
6
  import { serializeIntegerUnsafe } from "./integer";
7
7
  import { serializeString } from "../index/string";
8
+ import {
9
+ dragonbox_f32_buffered,
10
+ dragonbox_f64_buffered,
11
+ } from "../../util/dragonbox";
8
12
 
9
13
 
10
14
  @inline
@@ -50,6 +54,16 @@ function serializeArrayElement<T>(value: T): void {
50
54
  else serializeFloat64Unsafe(<f64>value);
51
55
  return;
52
56
  }
57
+ if (isManaged<T>() || isReference<T>()) {
58
+ // Preserve runtime custom serializers for subclass instances stored in
59
+ // parent-typed arrays before falling back to the static dispatcher.
60
+ // @ts-ignore: transform-defined at runtime when present
61
+ if (isDefined(value.__SERIALIZE_CUSTOM)) {
62
+ // @ts-ignore: transform-defined at runtime when present
63
+ value.__SERIALIZE_CUSTOM();
64
+ return;
65
+ }
66
+ }
53
67
  JSON.__serialize<T>(value);
54
68
  }
55
69
 
@@ -167,6 +181,69 @@ function serializeU8ArrayFast(src: u8[]): void {
167
181
  store<u16>(bs.offset - 2, BRACKET_RIGHT);
168
182
  }
169
183
 
184
+ // Specialized float-array serializer: dragonbox + trailing comma in a
185
+ // uniform per-iteration body, then overwrite the final comma with `]`. The
186
+ // generic dispatcher splits the loop into "N-1 elements with comma, then
187
+ // last element without, then `]`" — the branch on each `i < end` check
188
+ // stalls the loop's tight bs.offset advance pattern. This variant runs the
189
+ // same number of stores per iteration (dragonbox output + COMMA), but the
190
+ // uniform loop body inlines better and the trailing `]` is a single fixed
191
+ // overwrite outside the loop.
192
+ function serializeF64ArrayFast(src: f64[]): void {
193
+ const len = src.length;
194
+ // Worst case per element: ~24 chars for f64 + comma = 50 bytes.
195
+ // Slight over-reserve (66) matches the existing `reservePrimitiveArray`
196
+ // budget and keeps a safety margin for any NaN/Inf spelling.
197
+ bs.proposeSize(4 + <u32>len * 66);
198
+ store<u16>(bs.offset, BRACKET_LEFT);
199
+ bs.offset += 2;
200
+ if (len == 0) {
201
+ store<u16>(bs.offset, BRACKET_RIGHT);
202
+ bs.offset += 2;
203
+ return;
204
+ }
205
+
206
+ // Hoist `bs.offset` into a local so the loop body has a single
207
+ // monotonically-advancing pointer instead of two reads + two writes back
208
+ // to the global per iteration.
209
+ const dataStart = src.dataStart;
210
+ let offset = bs.offset;
211
+ for (let i: i32 = 0; i < len; i++) {
212
+ const v = load<f64>(dataStart + ((<usize>i) << 3));
213
+ const size = dragonbox_f64_buffered(offset, v) << 1;
214
+ store<u16>(offset + size, COMMA);
215
+ offset += size + 2;
216
+ }
217
+ // Overwrite the final trailing comma with `]`.
218
+ store<u16>(offset - 2, BRACKET_RIGHT);
219
+ bs.offset = offset;
220
+ }
221
+
222
+ function serializeF32ArrayFast(src: f32[]): void {
223
+ const len = src.length;
224
+ // Worst case for f32 is ~16 chars + comma = ~34 bytes; mirror the budget
225
+ // used by `reservePrimitiveArray`.
226
+ bs.proposeSize(4 + <u32>len * 34);
227
+ store<u16>(bs.offset, BRACKET_LEFT);
228
+ bs.offset += 2;
229
+ if (len == 0) {
230
+ store<u16>(bs.offset, BRACKET_RIGHT);
231
+ bs.offset += 2;
232
+ return;
233
+ }
234
+
235
+ const dataStart = src.dataStart;
236
+ let offset = bs.offset;
237
+ for (let i: i32 = 0; i < len; i++) {
238
+ const v = load<f32>(dataStart + ((<usize>i) << 2));
239
+ const size = dragonbox_f32_buffered(offset, v) << 1;
240
+ store<u16>(offset + size, COMMA);
241
+ offset += size + 2;
242
+ }
243
+ store<u16>(offset - 2, BRACKET_RIGHT);
244
+ bs.offset = offset;
245
+ }
246
+
170
247
  function serializeI8ArrayFast(src: i8[]): void {
171
248
  const len = src.length;
172
249
  // Worst case: every element is `-DDD,` = 5 chars = 10 bytes; plus 4 for `[]`.
@@ -226,6 +303,16 @@ export function serializeArray<T extends any[]>(src: T): void {
226
303
  serializeI8ArrayFast(changetype<i8[]>(src));
227
304
  return;
228
305
  }
306
+ if (isFloat<valueof<T>>() && sizeof<valueof<T>>() == 8) {
307
+ // @ts-expect-error: T is f64[]
308
+ serializeF64ArrayFast(changetype<f64[]>(src));
309
+ return;
310
+ }
311
+ if (isFloat<valueof<T>>() && sizeof<valueof<T>>() == 4) {
312
+ // @ts-expect-error: T is f32[]
313
+ serializeF32ArrayFast(changetype<f32[]>(src));
314
+ return;
315
+ }
229
316
 
230
317
  const len = src.length;
231
318
  const end = len - 1;
@@ -16,7 +16,7 @@ import { serializeStruct } from "./struct";
16
16
  * @returns void
17
17
  */
18
18
  // @ts-ignore: inline
19
- @inline export function serializeString(src: string): void {
19
+ @inline export function serializeString_NAIVE(src: string): void {
20
20
  const srcSize = bytes(src);
21
21
  bs.proposeSize(srcSize + 4);
22
22
  let srcPtr = changetype<usize>(src);
@@ -174,132 +174,6 @@ export function serializeString_SWAR(src: string): void {
174
174
  bs.offset += 2;
175
175
  }
176
176
 
177
- export function serializeString_SWAR_ExperimentalTableEscapes(
178
- src: string,
179
- ): void {
180
- let srcStart = changetype<usize>(src);
181
- const srcSize = changetype<OBJECT>(srcStart - TOTAL_OVERHEAD).rtSize;
182
- const srcEnd = srcStart + srcSize;
183
- const srcEnd8 = srcEnd - 8;
184
-
185
- bs.proposeSize(srcSize + 4);
186
- store<u16>(bs.offset, 34); // "
187
- bs.offset += 2;
188
-
189
- while (srcStart < srcEnd8) {
190
- const block = load<u64>(srcStart);
191
- let mask = detect_escapable_u64_swar_safe(block);
192
- store<u64>(bs.offset, block);
193
-
194
- if (mask === 0) {
195
- srcStart += 8;
196
- bs.offset += 8;
197
- continue;
198
- }
199
-
200
- do {
201
- const laneIdx = usize(ctz(mask) >> 3);
202
- const srcIdx = srcStart + laneIdx;
203
- // Even (0 2 4 6) -> Confirmed ASCII Escape
204
- // Odd (1 3 5 7) -> Possibly a Unicode code unit or surrogate
205
- if ((laneIdx & 1) === 0) {
206
- mask &= mask - 1;
207
- const code = load<u16>(srcIdx);
208
- const escaped = load<u32>(SERIALIZE_ESCAPE_TABLE + (code << 2));
209
-
210
- if ((escaped & 0xffff) != BACK_SLASH) {
211
- bs.growSize(10);
212
- const dstIdx = bs.offset + laneIdx;
213
- store<u64>(dstIdx, U00_MARKER);
214
- store<u32>(dstIdx, escaped, 8);
215
- store<u64>(dstIdx, load<u64>(srcIdx, 2), 12);
216
- bs.offset += 10;
217
- } else {
218
- bs.growSize(2);
219
- const dstIdx = bs.offset + laneIdx;
220
- store<u32>(dstIdx, escaped);
221
- store<u64>(dstIdx, load<u64>(srcIdx, 2), 4);
222
- bs.offset += 2;
223
- }
224
- continue;
225
- }
226
- mask &= mask - 1;
227
-
228
- const code = load<u16>(srcIdx - 1);
229
- if (code < 0xd800 || code > 0xdfff) continue;
230
-
231
- if (code <= 0xdbff && srcIdx + 2 < srcEnd) {
232
- const next = load<u16>(srcIdx, 1);
233
- if (next >= 0xdc00 && next <= 0xdfff) {
234
- // paired surrogate
235
- // mask &= ~(0xFF << ((laneIdx+2) << 3));
236
- mask &= mask - 1;
237
- continue;
238
- }
239
- }
240
-
241
- bs.growSize(10);
242
-
243
- // unpaired high/low surrogate
244
- const dstIdx = bs.offset + laneIdx - 1;
245
- store<u32>(dstIdx, U_MARKER); // \u
246
- store<u64>(dstIdx, u16_to_hex4_swar(code), 4);
247
- store<u64>(dstIdx, load<u64>(srcIdx, 1), 12);
248
- bs.offset += 10;
249
- } while (mask !== 0);
250
-
251
- srcStart += 8;
252
- bs.offset += 8;
253
- }
254
-
255
- while (srcStart <= srcEnd - 2) {
256
- const code = load<u16>(srcStart);
257
-
258
- if (code == BACK_SLASH || code == QUOTE || code < 32) {
259
- const escaped = load<u32>(SERIALIZE_ESCAPE_TABLE + (code << 2));
260
- if ((escaped & 0xffff) != BACK_SLASH) {
261
- bs.growSize(10);
262
- store<u64>(bs.offset, U00_MARKER);
263
- store<u32>(bs.offset, escaped, 8);
264
- bs.offset += 12;
265
- } else {
266
- bs.growSize(2);
267
- store<u32>(bs.offset, escaped);
268
- bs.offset += 4;
269
- }
270
- srcStart += 2;
271
- continue;
272
- }
273
-
274
- if (code < 0xd800 || code > 0xdfff) {
275
- store<u16>(bs.offset, code);
276
- bs.offset += 2;
277
- srcStart += 2;
278
- continue;
279
- }
280
-
281
- if (code <= 0xdbff && srcStart + 2 <= srcEnd - 2) {
282
- const next = load<u16>(srcStart, 2);
283
- if (next >= 0xdc00 && next <= 0xdfff) {
284
- // valid surrogate pair
285
- store<u16>(bs.offset, code);
286
- store<u16>(bs.offset + 2, next);
287
- bs.offset += 4;
288
- srcStart += 4;
289
- continue;
290
- }
291
- }
292
-
293
- // unpaired high/low surrogate
294
- write_u_escape(code);
295
- srcStart += 2;
296
- continue;
297
- }
298
-
299
- store<u16>(bs.offset, 34); // "
300
- bs.offset += 2;
301
- }
302
-
303
177
  // @ts-expect-error: @inline is a valid decorator
304
178
  @inline function write_u_escape(code: u16): void {
305
179
  bs.growSize(10);
@@ -326,16 +200,3 @@ export function serializeString_SWAR_ExperimentalTableEscapes(
326
200
  0x8000_8000_8000_8000;
327
201
  return (ascii_mask & (~hi_mask >> 8)) | hi_mask;
328
202
  }
329
-
330
- // @ts-expect-error: @inline is a valid decorator
331
- @inline export function detect_escapable_u64_swar_unsafe(block: u64): u64 {
332
- const lo = block & 0x00ff_00ff_00ff_00ff;
333
- const loSafe = lo | 0x0100_0100_0100_0100;
334
- const ascii_mask =
335
- ((loSafe - 0x0020_0020_0020_0020) | // lt 0x20
336
- ((loSafe ^ 0x0022_0022_0022_0022) - 0x0001_0001_0001_0001) | // eq 0x22
337
- ((loSafe ^ 0x005c_005c_005c_005c) - 0x0001_0001_0001_0001)) & // eq 0x5C
338
- (0x0080_0080_0080_0080 & ~lo); // replace each lane with 0x80
339
- const hi = block & 0xff00_ff00_ff00_ff00; // add possible non-ascii code units
340
- return ascii_mask | hi;
341
- }
@@ -1,5 +1,6 @@
1
1
  import { DRAGONBOX_F32_CACHE, DRAGONBOX_F64_CACHE } from "./dragonbox-cache";
2
- import { decimalCount32, itoa_buffered, utoa32_dec_core } from "util/number";
2
+ import { decimalCount32, utoa32_dec_core } from "util/number";
3
+ import { ensureItoaPairs, itoaU32, itoaU64 } from "./itoa-fast";
3
4
 
4
5
  const CHAR_MINUS: u16 = 45;
5
6
  const CHAR_DOT: u16 = 46;
@@ -572,7 +573,11 @@ function dragonboxCoreF32(buffer: usize, value: f32): u32 {
572
573
  reinterpret<u32>(value) & 0x7fffff,
573
574
  (reinterpret<u32>(value) >>> 23) & 0xff,
574
575
  );
575
- let len = itoa_buffered<u32>(buffer + ((<usize>sign) << 1), digits);
576
+ // Use the jeaiii-style itoa (forward write + 2-digit-pair LUT) instead
577
+ // of AS stdlib's `itoa_buffered`, which runs a separate width classifier
578
+ // + backward div-by-10000 loop. Saves a function call and ~half the
579
+ // per-digit work for the typical 7-17 digit dragonbox output.
580
+ let len = itoaU32(buffer + ((<usize>sign) << 1), digits);
576
581
  return <u32>(prettify(buffer + ((<usize>sign) << 1), len, _dbK) + sign);
577
582
  }
578
583
 
@@ -588,7 +593,7 @@ function dragonboxCoreF64(buffer: usize, value: f64): u32 {
588
593
  bits & 0x000fffffffffffff,
589
594
  <i32>((bits >>> 52) & 0x7ff),
590
595
  );
591
- let len = itoa_buffered<u64>(buffer + ((<usize>sign) << 1), digits);
596
+ let len = itoaU64(buffer + ((<usize>sign) << 1), digits);
592
597
  return <u32>(prettify(buffer + ((<usize>sign) << 1), len, _dbK) + sign);
593
598
  }
594
599
 
@@ -615,6 +620,7 @@ export function dragonbox_f32_buffered(buffer: usize, value: f32): u32 {
615
620
  store<u64>(buffer + 8, 0x7900740069006e);
616
621
  return 8 + (sign ? 1 : 0);
617
622
  }
623
+ ensureItoaPairs();
618
624
  return dragonboxCoreF32(buffer, value);
619
625
  }
620
626
 
@@ -641,6 +647,7 @@ export function dragonbox_f64_buffered(buffer: usize, value: f64): u32 {
641
647
  store<u64>(buffer + 8, 0x7900740069006e);
642
648
  return 8 + (sign ? 1 : 0);
643
649
  }
650
+ ensureItoaPairs();
644
651
  return dragonboxCoreF64(buffer, value);
645
652
  }
646
653
 
@@ -1,20 +1,31 @@
1
- // Fast integer -> UTF-16 stringification (jeaiii-style).
1
+ // Fast integer -> UTF-16 stringification.
2
2
  //
3
- // Background: AS std's `itoa_buffered` runs `decimalCount32` (a width
4
- // classifier) then `utoa32_dec_lut` (a backward-writing div-by-10000
5
- // loop). On wasm/V8 the function-call overhead between those steps shows
6
- // up clearly for short widths (2-4 digits) where the per-element work is
7
- // otherwise tiny.
3
+ // We tried the "real" jeaiii algorithm (fixed-point magic-multiplication
4
+ // per bucket; `f0 -> f2 -> f4 -> f6` chained fractional-part extractions)
5
+ // and it ran ~5-7% slower on V8/wasm than the div-by-constant variant
6
+ // below. Two reasons:
8
7
  //
9
- // This module ports the jeaiii (James Edward Anhalt III) algorithm:
10
- // - One `@inline` function per signed/unsigned u32/u64.
11
- // - Width-ladder of `if (v < 10^k)` checks emits digits forward in a
12
- // single pass; the same comparisons that would have driven a separate
13
- // `decimalCount` are reused as the dispatch.
14
- // - Each digit-pair is written as a single `store<u32>` from a 400-byte
15
- // LUT (`DIGIT_PAIRS_UTF16`) keyed on `value % 100`.
16
- // - All `/` and `%` are by compile-time constants (10 / 100 / 10000 /
17
- // 10^8) so the wasm tier lowers them to multiply-shift.
8
+ // 1. V8/wasm lowers `v / 100` (and other `/ <const>`s) to a single
9
+ // multiply-shift, so jeaiii's main selling point — avoiding division
10
+ // hardware gives no win on this target. The op counts come out
11
+ // roughly equal.
12
+ //
13
+ // 2. The div-by-const variant computes each digit pair independently
14
+ // from `v` (`h = v / 100`, `l = v - h*100`, etc), so V8 schedules
15
+ // the LUT loads + stores for all pairs in parallel. The jeaiii
16
+ // chain forces them serial.
17
+ //
18
+ // What we keep from jeaiii here:
19
+ //
20
+ // - Width-ladder dispatch (`if v < 100 / 10_000 / 1_000_000 / ...`) so
21
+ // the same comparisons that would drive a separate `decimalCount`
22
+ // pass become the bucket pick.
23
+ //
24
+ // - A 100-entry digit-pair LUT keyed on `value % 100`. One `store<u32>`
25
+ // emits a UTF-16 pair.
26
+ //
27
+ // - Forward write in one pass — no `decimalCount32` precomputation, no
28
+ // backward write.
18
29
  //
19
30
  // Reference H2H bench: `__benches__/custom/itoa-h2h.bench.ts`.
20
31
 
@@ -43,7 +54,7 @@ function initPairs(): void {
43
54
  }
44
55
 
45
56
  /**
46
- * jeaiii-style u32 -> UTF-16 stringification, forward write.
57
+ * u32 -> UTF-16 stringification, forward write.
47
58
  * Returns the number of UTF-16 chars written (caller multiplies by 2 for
48
59
  * a byte offset). Caller must ensure the buffer has at least 20 bytes
49
60
  * available (max 10 chars).
@@ -169,7 +180,7 @@ function initPairs(): void {
169
180
  }
170
181
 
171
182
  /**
172
- * jeaiii-style u64 -> UTF-16 stringification.
183
+ * u64 -> UTF-16 stringification.
173
184
  * Small values delegate to `itoaU32`. For 11+ digit values, peel 8 digits
174
185
  * from the bottom (always fits in u32), emit the remaining top via the
175
186
  * u32 path, then emit the 8 trailing digits with leading-zero padding.
@@ -224,7 +235,7 @@ function initPairs(): void {
224
235
  store<u16>(buf, 0x2d); // '-'
225
236
  return 1 + itoaU64(buf + 2, <u64>-v);
226
237
  }
227
- return itoaU64(buf, <u64>v);
238
+ return itoaU64(buf, <u64>value);
228
239
  }
229
240
  return itoaU64(buf, <u64>value);
230
241
  }
@@ -0,0 +1,78 @@
1
+ import {
2
+ BRACE_LEFT,
3
+ BRACE_RIGHT,
4
+ BRACKET_LEFT,
5
+ BRACKET_RIGHT,
6
+ COMMA,
7
+ QUOTE,
8
+ } from "../custom/chars";
9
+ import { isSpace } from "./isSpace";
10
+ import { scanStringEnd } from "./stringScan";
11
+
12
+ /**
13
+ * Pure-scalar value-end scanner used by the NAIVE container deserializers.
14
+ *
15
+ * Returns the position immediately after the value that begins at `srcStart`:
16
+ *
17
+ * - For a quoted string: position past the closing `"` (uses scalar
18
+ * {@link scanStringEnd}).
19
+ * - For an object/array: position past the matching `}`/`]`, tracking depth
20
+ * and skipping nested quoted strings.
21
+ * - For anything else: position of the first `,`, `]`, or `}` (the value's
22
+ * structural terminator).
23
+ *
24
+ * Returns `0` when the input is empty or no terminator is found.
25
+ *
26
+ * Mirrors the semantics of `deserialize/swar/array/shared.ts:scanValueEnd`
27
+ * but stays scalar so `naive/` callers don't pull SWAR into the correctness
28
+ * baseline.
29
+ */
30
+ // @ts-ignore: inline
31
+ @inline export function scanValueEnd(srcStart: usize, srcEnd: usize): usize {
32
+ if (srcStart >= srcEnd) return 0;
33
+ const first = load<u16>(srcStart);
34
+
35
+ if (first == QUOTE) {
36
+ const end = scanStringEnd(srcStart, srcEnd);
37
+ return end >= srcEnd ? 0 : end + 2;
38
+ }
39
+
40
+ if (first == BRACE_LEFT || first == BRACKET_LEFT) {
41
+ let depth: i32 = 1;
42
+ let ptr = srcStart + 2;
43
+ while (ptr < srcEnd) {
44
+ const code = load<u16>(ptr);
45
+ if (code == QUOTE) {
46
+ const end = scanStringEnd(ptr, srcEnd);
47
+ if (end >= srcEnd) return 0;
48
+ ptr = end + 2;
49
+ continue;
50
+ }
51
+ if (code == BRACE_LEFT || code == BRACKET_LEFT) {
52
+ depth++;
53
+ } else if (code == BRACE_RIGHT || code == BRACKET_RIGHT) {
54
+ if (--depth == 0) return ptr + 2;
55
+ }
56
+ ptr += 2;
57
+ }
58
+ return 0;
59
+ }
60
+
61
+ while (srcStart < srcEnd) {
62
+ const code = load<u16>(srcStart);
63
+ // Stop at the structural terminator OR trailing whitespace, so the returned
64
+ // range is the exact value (scalar parsers assume [srcStart,srcEnd) is the
65
+ // value with no trailing whitespace). Callers skip whitespace to reach the
66
+ // following `,`/`]`/`}`.
67
+ if (
68
+ code == COMMA ||
69
+ code == BRACKET_RIGHT ||
70
+ code == BRACE_RIGHT ||
71
+ isSpace(code)
72
+ )
73
+ return srcStart;
74
+ srcStart += 2;
75
+ }
76
+
77
+ return 0;
78
+ }
@@ -0,0 +1,132 @@
1
+ // Direct decimal-to-f64 conversion from a `(u64 mantissa, i32 decimal exp)`
2
+ // pair. Bit-identical to `f64.parse` / `f32.parse` for any input the SWAR
3
+ // float deserializer can produce.
4
+ //
5
+ // Ported from AssemblyScript std's `util/string.ts` (which itself is adapted
6
+ // from the "metallic" library). The reason we duplicate it: AS std exposes
7
+ // `strtod(str)` but the underlying `scientific(mantissa, exp)` helper is
8
+ // module-private. Going through `strtod` requires a string allocation and a
9
+ // re-parse of digits we've already accumulated in the SWAR loop. Calling
10
+ // `scientific` directly skips both costs.
11
+ //
12
+ // scientific() is correctly rounded for all u64 mantissas and decimal
13
+ // exponents that fit in IEEE-754 f64's range — including the [2^53, 2^64)
14
+ // mantissa range that breaks Lemire's single-fmul fast path.
15
+
16
+ const POWERS10: usize = memory.data<f64>([
17
+ 1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14,
18
+ 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
19
+ ]);
20
+
21
+ // 5^i for i in [0, 13]. ipow32(5, e) for the exponent ranges scaledown
22
+ // and scaleup actually call it with.
23
+ const POWERS5: usize = memory.data<i32>([
24
+ 1, 5, 25, 125, 625, 3125, 15625, 78125, 390625, 1953125, 9765625, 48828125,
25
+ 244140625, 1220703125,
26
+ ]);
27
+
28
+ // @ts-ignore: inline
29
+ @inline function pow10(n: i32): f64 {
30
+ return load<f64>(POWERS10 + ((<usize>n) << alignof<f64>()));
31
+ }
32
+
33
+ // @ts-ignore: inline
34
+ @inline function pow5_32(n: i32): i32 {
35
+ return load<i32>(POWERS5 + ((<usize>n) << alignof<i32>()));
36
+ }
37
+
38
+ // __fixmulShift is mutated by `fixmul` and read by `scaleup`. AS std uses a
39
+ // module-level @lazy variable for the same reason; matching that.
40
+ // @ts-ignore: lazy decorator
41
+ @lazy let __fixmulShift: u64 = 0;
42
+
43
+ // @ts-ignore: inline
44
+ @inline function fixmul(a: u64, b: u32): u64 {
45
+ const low = (a & 0xffffffff) * b;
46
+ const high = (a >> 32) * b + (low >> 32);
47
+ const overflow = <u32>(high >> 32);
48
+ const space = clz(overflow);
49
+ const revspace: u64 = 32 - space;
50
+ __fixmulShift += revspace;
51
+ return (
52
+ ((high << space) | ((low & 0xffffffff) >> revspace)) +
53
+ (((low << space) >> 31) & 1)
54
+ );
55
+ }
56
+
57
+ // @ts-ignore: inline
58
+ @inline function scaledown(significand: u64, exp: i32): f64 {
59
+ const denom: u64 = 6103515625; // 1e14 * 0x1p-14
60
+ const scale = reinterpret<f64>(0x3f06849b86a12b9b); // 1e-14 * 0x1p32
61
+
62
+ let shift = clz(significand);
63
+ significand <<= shift;
64
+ shift = exp - shift;
65
+
66
+ for (; exp <= -14; exp += 14) {
67
+ const q = significand / denom;
68
+ const r = significand % denom;
69
+ const s = clz(q);
70
+ significand = (q << s) + <u64>nearest(scale * <f64>(r << (s - 18)));
71
+ shift -= s;
72
+ }
73
+ const b = <u64>pow5_32(-exp);
74
+ const q = significand / b;
75
+ const r = significand % b;
76
+ const s = clz(q);
77
+ significand =
78
+ (q << s) +
79
+ <u64>(reinterpret<f64>(reinterpret<u64>(<f64>r) + (s << 52)) / <f64>b);
80
+ shift -= s;
81
+
82
+ return NativeMath.scalbn(<f64>significand, <i32>shift);
83
+ }
84
+
85
+ // @ts-ignore: inline
86
+ @inline function scaleup(significand: u64, exp: i32): f64 {
87
+ const coeff: u32 = 1220703125; // 1e13 * 0x1p-13;
88
+ let shift = ctz(significand);
89
+ significand >>= shift;
90
+ shift += exp;
91
+
92
+ __fixmulShift = shift;
93
+ for (; exp >= 13; exp -= 13) {
94
+ significand = fixmul(significand, coeff);
95
+ }
96
+ significand = fixmul(significand, <u32>pow5_32(exp));
97
+ shift = __fixmulShift;
98
+ return NativeMath.scalbn(<f64>significand, <i32>shift);
99
+ }
100
+
101
+ /**
102
+ * Construct an f64 from a u64 mantissa and decimal exponent. Result is
103
+ * correctly rounded — bit-identical to `f64.parse` for any input the SWAR
104
+ * float deserializer can pre-parse into this form.
105
+ *
106
+ * Caller guarantees the digit run that produced `significand` was already
107
+ * scanned and validated; this function only handles the value computation,
108
+ * not the lexing.
109
+ *
110
+ * @param significand u64 mantissa (any value from 0 to U64.MAX_VALUE)
111
+ * @param exp Decimal exponent (e.g. for "12.34" pass 1234 and -2)
112
+ * @returns The correctly rounded f64, or 0 / Infinity at the extremes.
113
+ */
114
+ // @ts-ignore: inline
115
+ @inline export function scientific(significand: u64, exp: i32): f64 {
116
+ if (!significand || exp < -342) return 0;
117
+ if (exp > 308) return Infinity;
118
+ let significandf = <f64>significand;
119
+ if (!exp) return significandf;
120
+ if (exp > 22 && exp <= 22 + 15) {
121
+ significandf *= pow10(exp - 22);
122
+ exp = 22;
123
+ }
124
+ if (significand <= 9007199254740991 && abs(exp) <= 22) {
125
+ if (exp > 0) return significandf * pow10(exp);
126
+ return significandf / pow10(-exp);
127
+ } else if (exp < 0) {
128
+ return scaledown(significand, exp);
129
+ } else {
130
+ return scaleup(significand, exp);
131
+ }
132
+ }
package/lib/as-bs.ts CHANGED
@@ -21,7 +21,7 @@ export namespace bs {
21
21
  export let offset: usize = buffer;
22
22
 
23
23
  /** Byte length of the buffer. */
24
- let bufferSize: usize = MIN_BUFFER_SIZE;
24
+ export let bufferSize: usize = MIN_BUFFER_SIZE;
25
25
 
26
26
  /** Proposed size of output */
27
27
  export let stackSize: usize = 0;
@@ -104,6 +104,19 @@ export namespace bs {
104
104
  pauseStackSizes.length = index;
105
105
  }
106
106
 
107
+ /**
108
+ * Resets the buffer to a clean, empty state. Call this after a throw aborts a
109
+ * serialize/deserialize op mid-flight: a partial run can leave `offset`
110
+ * advanced and the pause stacks non-empty, which would corrupt the next op.
111
+ */
112
+ // @ts-expect-error: @inline is a valid decorator
113
+ @inline export function reset(): void {
114
+ offset = buffer;
115
+ stackSize = 0;
116
+ pauseOffsets.length = 0;
117
+ pauseStackSizes.length = 0;
118
+ }
119
+
107
120
  /**
108
121
  * Proposes that the buffer size is should be greater than or equal to the proposed size.
109
122
  * If necessary, reallocates the buffer to the exact new size.