json-as 1.5.0 → 1.6.0

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 (66) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/README.md +42 -0
  3. package/assembly/deserialize/error.ts +21 -0
  4. package/assembly/deserialize/index/arbitrary.ts +29 -8
  5. package/assembly/deserialize/index/array.ts +2 -1
  6. package/assembly/deserialize/index/bool.ts +1 -1
  7. package/assembly/deserialize/index/object.ts +3 -0
  8. package/assembly/deserialize/index/string.ts +37 -0
  9. package/assembly/deserialize/index.ts +0 -1
  10. package/assembly/deserialize/naive/array/arbitrary.ts +3 -2
  11. package/assembly/deserialize/naive/array/array.ts +1 -0
  12. package/assembly/deserialize/naive/array/bool.ts +21 -11
  13. package/assembly/deserialize/naive/array/box.ts +13 -33
  14. package/assembly/deserialize/naive/array/float.ts +23 -13
  15. package/assembly/deserialize/naive/array/integer.ts +33 -21
  16. package/assembly/deserialize/naive/array/object.ts +1 -0
  17. package/assembly/deserialize/naive/array/string.ts +29 -16
  18. package/assembly/deserialize/naive/bool.ts +17 -5
  19. package/assembly/deserialize/naive/float.ts +1 -2
  20. package/assembly/deserialize/naive/map.ts +339 -39
  21. package/assembly/deserialize/naive/object.ts +122 -65
  22. package/assembly/deserialize/naive/raw.ts +13 -1
  23. package/assembly/deserialize/naive/set.ts +0 -1
  24. package/assembly/deserialize/naive/staticarray.ts +3 -2
  25. package/assembly/deserialize/naive/string.ts +41 -52
  26. package/assembly/deserialize/parseMode.ts +216 -0
  27. package/assembly/deserialize/simd/array/integer.ts +6 -3
  28. package/assembly/deserialize/simd/float.ts +35 -29
  29. package/assembly/deserialize/simd/string.ts +79 -17
  30. package/assembly/deserialize/string-validation.ts +34 -0
  31. package/assembly/deserialize/swar/array/array.ts +63 -13
  32. package/assembly/deserialize/swar/array/bool.ts +14 -3
  33. package/assembly/deserialize/swar/array/float.ts +394 -13
  34. package/assembly/deserialize/swar/array/generic.ts +12 -2
  35. package/assembly/deserialize/swar/array/integer.ts +22 -62
  36. package/assembly/deserialize/swar/array/object.ts +11 -2
  37. package/assembly/deserialize/swar/array/shared.ts +1 -1
  38. package/assembly/deserialize/swar/array/string.ts +34 -5
  39. package/assembly/deserialize/swar/array/struct.ts +109 -15
  40. package/assembly/deserialize/swar/float.ts +19 -12
  41. package/assembly/deserialize/swar/string.ts +70 -111
  42. package/assembly/index.d.ts +6 -0
  43. package/assembly/index.ts +411 -33
  44. package/assembly/serialize/index/bool.ts +1 -1
  45. package/assembly/serialize/index/float.ts +1 -5
  46. package/assembly/serialize/index/integer.ts +1 -1
  47. package/assembly/serialize/index/object.ts +17 -0
  48. package/assembly/serialize/naive/array.ts +64 -0
  49. package/assembly/serialize/naive/float.ts +0 -5
  50. package/assembly/serialize/naive/map.ts +37 -0
  51. package/assembly/serialize/naive/set.ts +0 -1
  52. package/assembly/serialize/naive/staticarray.ts +0 -1
  53. package/assembly/serialize/naive/string.ts +0 -6
  54. package/assembly/serialize/naive/typedarray.ts +0 -1
  55. package/assembly/util/eisel-lemire.ts +179 -0
  56. package/assembly/util/prettyWhitespaceSimd.ts +69 -0
  57. package/assembly/util/scanValueEndSimd.ts +59 -36
  58. package/assembly/util/validateJson.ts +338 -0
  59. package/lib/as-bs.ts +28 -6
  60. package/package.json +13 -3
  61. package/transform/lib/index.d.ts +6 -0
  62. package/transform/lib/index.js +770 -111
  63. package/transform/lib/types.d.ts +1 -0
  64. package/transform/lib/types.js +3 -0
  65. package/assembly/deserialize/index/struct.ts +0 -1
  66. package/assembly/deserialize/naive/struct.ts +0 -21
@@ -1 +1 @@
1
- export { serializeBool } from "../naive/bool";
1
+ export { serializeBool, serializeBoolUnsafe } from "../naive/bool";
@@ -1,5 +1 @@
1
- export {
2
- serializeFloat,
3
- serializeFloat32,
4
- serializeFloat64,
5
- } from "../naive/float";
1
+ export { serializeFloat32, serializeFloat64 } from "../naive/float";
@@ -1 +1 @@
1
- export { serializeInteger } from "../naive/integer";
1
+ export { serializeInteger, serializeIntegerUnsafe } from "../naive/integer";
@@ -5,6 +5,23 @@ import { serializeArbitrary } from "./arbitrary";
5
5
  import { serializeStringRange } from "../naive/string";
6
6
 
7
7
  export function serializeObject(src: JSON.Obj): void {
8
+ const rawStart = src._rawStart;
9
+ if (rawStart != 0) {
10
+ const rawSize = src._rawEnd - rawStart;
11
+ // At top level, `bs.out` can copy the anchored source slice straight into
12
+ // the returned string. Nested objects already have parent bytes in the
13
+ // staging buffer and therefore keep the regular append path below.
14
+ if (bs.offset == bs.buffer && bs.cacheOutput == 0) {
15
+ bs.cacheOutput = rawStart;
16
+ bs.cacheOutputLen = rawSize;
17
+ return;
18
+ }
19
+ bs.proposeSize(rawSize);
20
+ memory.copy(bs.offset, rawStart, rawSize);
21
+ bs.offset += rawSize;
22
+ return;
23
+ }
24
+
8
25
  const srcSize = src.size;
9
26
 
10
27
  if (srcSize == 0) {
@@ -5,6 +5,7 @@ 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 { ensureItoaPairs, itoaFast } from "../../util/itoa-fast";
8
9
  import { dtoa_buffered, ftoa_buffered } from "xjb-as";
9
10
 
10
11
  function maxIntegerBytes<T extends number>(): u32 {
@@ -265,6 +266,59 @@ function serializeI8ArrayFast(src: i8[]): void {
265
266
  store<u16>(bs.offset - 2, BRACKET_RIGHT);
266
267
  }
267
268
 
269
+ // Integer arrays wider than one byte use the same uniform trailing-comma
270
+ // layout as the specialized float paths. Keeping the output cursor local
271
+ // avoids two global `bs.offset` accesses per value, while reading dataStart
272
+ // directly removes the Array.__uget call left in the generic array loop.
273
+ function serializeIntegerArrayFast<T extends number[]>(src: T): void {
274
+ const len = src.length;
275
+ bs.proposeSize(4 + <u32>len * (maxIntegerBytes<valueof<T>>() + 2));
276
+ store<u16>(bs.offset, BRACKET_LEFT);
277
+ bs.offset += 2;
278
+ if (len == 0) {
279
+ store<u16>(bs.offset, BRACKET_RIGHT);
280
+ bs.offset += 2;
281
+ return;
282
+ }
283
+
284
+ ensureItoaPairs();
285
+ const dataStart = src.dataStart;
286
+ const stride = sizeof<valueof<T>>();
287
+ let offset = bs.offset;
288
+ for (let i: i32 = 0; i < len; i++) {
289
+ const value = load<valueof<T>>(dataStart + <usize>i * stride);
290
+ offset += (<usize>itoaFast<valueof<T>>(offset, value)) << 1;
291
+ store<u16>(offset, COMMA);
292
+ offset += 2;
293
+ }
294
+ store<u16>(offset - 2, BRACKET_RIGHT);
295
+ bs.offset = offset;
296
+ }
297
+
298
+ function serializeStringArrayFast(src: string[]): void {
299
+ const len = src.length;
300
+ // String serialization reserves its own payload; reserve delimiters here.
301
+ bs.proposeSize(4 + (len > 0 ? <u32>(len - 1) * 2 : 0));
302
+ store<u16>(bs.offset, BRACKET_LEFT);
303
+ bs.offset += 2;
304
+ if (len == 0) {
305
+ store<u16>(bs.offset, BRACKET_RIGHT);
306
+ bs.offset += 2;
307
+ return;
308
+ }
309
+
310
+ const dataStart = src.dataStart;
311
+ for (let i: i32 = 0; i < len; i++) {
312
+ const value = changetype<string>(
313
+ load<usize>(dataStart + ((<usize>i) << 2)),
314
+ );
315
+ serializeString(value);
316
+ store<u16>(bs.offset, COMMA);
317
+ bs.offset += 2;
318
+ }
319
+ store<u16>(bs.offset - 2, BRACKET_RIGHT);
320
+ }
321
+
268
322
  export function serializeArray<T extends any[]>(src: T): void {
269
323
  // Specialized fast paths fold the per-element comma into the element write,
270
324
  // saving one `store<u16>` + advance per iteration. AS folds the type checks
@@ -292,6 +346,16 @@ export function serializeArray<T extends any[]>(src: T): void {
292
346
  serializeI8ArrayFast(changetype<i8[]>(src));
293
347
  return;
294
348
  }
349
+ if (isInteger<valueof<T>>()) {
350
+ // u8[] and i8[] returned through their lookup-table paths above.
351
+ serializeIntegerArrayFast<T>(src);
352
+ return;
353
+ }
354
+ if (isString<valueof<T>>()) {
355
+ // @ts-expect-error: T is string[]
356
+ serializeStringArrayFast(changetype<string[]>(src));
357
+ return;
358
+ }
295
359
  if (isFloat<valueof<T>>() && sizeof<valueof<T>>() == 8) {
296
360
  // @ts-expect-error: T is f64[]
297
361
  serializeF64ArrayFast(changetype<f64[]>(src));
@@ -27,8 +27,3 @@ export function serializeFloat64(data: f64): void {
27
27
  bs.stackSize += bytes;
28
28
  bs.offset += bytes;
29
29
  }
30
-
31
- export function serializeFloat<T extends number>(data: T): void {
32
- if (sizeof<T>() == 4) serializeFloat32(<f32>data);
33
- else serializeFloat64(<f64>data);
34
- }
@@ -1,6 +1,34 @@
1
1
  import { bs } from "../../../lib/as-bs";
2
2
  import { JSON } from "../..";
3
3
  import { BRACE_LEFT, BRACE_RIGHT, COLON, COMMA } from "../../custom/chars";
4
+ import { serializeString } from "../index/string";
5
+ import { serializeRaw } from "./raw";
6
+
7
+ function serializeRawMapFast<T extends Map<any, any>>(
8
+ keys: Array<indexof<T>>,
9
+ values: Array<valueof<T>>,
10
+ ): void {
11
+ const len = keys.length;
12
+ bs.proposeSize(4 + <u32>len * 4);
13
+ store<u16>(bs.offset, BRACE_LEFT);
14
+ bs.offset += 2;
15
+
16
+ const keyData = keys.dataStart;
17
+ const valueData = values.dataStart;
18
+ for (let i = 0; i < len; i++) {
19
+ const key = changetype<string>(load<usize>(keyData + ((<usize>i) << 2)));
20
+ const value = changetype<JSON.Raw>(
21
+ load<usize>(valueData + ((<usize>i) << 2)),
22
+ );
23
+ serializeString(key);
24
+ store<u16>(bs.offset, COLON);
25
+ bs.offset += 2;
26
+ serializeRaw(value);
27
+ store<u16>(bs.offset, COMMA);
28
+ bs.offset += 2;
29
+ }
30
+ store<u16>(bs.offset - 2, BRACE_RIGHT);
31
+ }
4
32
 
5
33
  export function serializeMap<T extends Map<any, any>>(src: T): void {
6
34
  const srcSize = src.size;
@@ -17,6 +45,15 @@ export function serializeMap<T extends Map<any, any>>(src: T): void {
17
45
  let values = src.values();
18
46
  const keyIsString = isString<indexof<T>>();
19
47
 
48
+ if (keyIsString && isReference<valueof<T>>()) {
49
+ const valueType = changetype<nonnull<valueof<T>>>(0);
50
+ // @ts-ignore: instanceof on the reference value type
51
+ if (valueType instanceof JSON.Raw) {
52
+ serializeRawMapFast<T>(keys, values);
53
+ return;
54
+ }
55
+ }
56
+
20
57
  bs.proposeSize(4 + <u32>(srcSize - 1) * 2 + <u32>srcSize * 2);
21
58
 
22
59
  store<u16>(bs.offset, BRACE_LEFT);
@@ -14,7 +14,6 @@ function maxIntegerBytes<T extends number>(): u32 {
14
14
  }
15
15
 
16
16
  function reservePrimitiveSet<T>(len: i32): void {
17
- if (len <= 0) return;
18
17
  if (isBoolean<T>()) {
19
18
  bs.proposeSize(4 + <u32>len * 12);
20
19
  } else if (isInteger<T>()) {
@@ -13,7 +13,6 @@ function maxIntegerBytes<T extends number>(): u32 {
13
13
  return isSigned<T>() ? 42 : 40;
14
14
  }
15
15
  function reservePrimitiveStaticArray<T>(len: i32): void {
16
- if (len <= 0) return;
17
16
  if (isBoolean<T>()) {
18
17
  bs.proposeSize(4 + <u32>len * 12);
19
18
  } else if (isInteger<T>()) {
@@ -85,9 +85,3 @@ export function serializeStringRange(srcPtr: usize, srcSize: usize): void {
85
85
  store<u16>(bs.offset, QUOTE);
86
86
  bs.offset += 2;
87
87
  }
88
- function write_u_escape(code: u16): void {
89
- bs.growSize(10);
90
- store<u32>(bs.offset, U_MARKER); // "\u"
91
- store<u64>(bs.offset, u16_to_hex4_swar(code), 4);
92
- bs.offset += 12;
93
- }
@@ -11,7 +11,6 @@ function maxIntegerBytes<T extends number>(): u32 {
11
11
  }
12
12
 
13
13
  function reserveTypedArray<T extends ArrayLike<number>>(len: i32): void {
14
- if (len <= 0) return;
15
14
  if (isFloat<valueof<T>>()) {
16
15
  bs.proposeSize(4 + <u32>len * (sizeof<valueof<T>>() == 4 ? 34 : 66));
17
16
  } else {
@@ -0,0 +1,179 @@
1
+ // Fast, correctly-rounded conversion of `mantissa * 10^power` for the common
2
+ // decimal-exponent range [-22, 22]. This is the Eisel-Lemire conversion used by
3
+ // simdjson's number parser, specialized to the range where json-as previously
4
+ // fell into `scientific()` after the exact-Clinger path rejected mantissas above
5
+ // 2^53. Keeping only 45 cached powers costs 720 bytes instead of simdjson's
6
+ // general 10 KiB table; callers retain `scientific()` as the wide-range fallback.
7
+
8
+ // Consecutive {primary, secondary} truncated 128-bit powers of five for decimal
9
+ // exponents -22 through 22. Sourced from simdjson's power_of_five_128 table.
10
+ // @ts-expect-error: @lazy is a valid decorator
11
+ @lazy const POWERS_OF_FIVE_128: usize = memory.data<u64>([
12
+ 0xf1c90080baf72cb1, 0x5324c68b12dd6800, 0x971da05074da7bee,
13
+ 0xd3f6fc16ebca8000, 0xbce5086492111aea, 0x88f4bb1ca6bd0000,
14
+ 0xec1e4a7db69561a5, 0x2b31e9e3d0700000, 0x9392ee8e921d5d07,
15
+ 0x3aff322e62600000, 0xb877aa3236a4b449, 0x09befeb9fad487c3,
16
+ 0xe69594bec44de15b, 0x4c2ebe687989a9b4, 0x901d7cf73ab0acd9,
17
+ 0x0f9d37014bf60a11, 0xb424dc35095cd80f, 0x538484c19ef38c95,
18
+ 0xe12e13424bb40e13, 0x2865a5f206b06fba, 0x8cbccc096f5088cb,
19
+ 0xf93f87b7442e45d4, 0xafebff0bcb24aafe, 0xf78f69a51539d749,
20
+ 0xdbe6fecebdedd5be, 0xb573440e5a884d1c, 0x89705f4136b4a597,
21
+ 0x31680a88f8953031, 0xabcc77118461cefc, 0xfdc20d2b36ba7c3e,
22
+ 0xd6bf94d5e57a42bc, 0x3d32907604691b4d, 0x8637bd05af6c69b5,
23
+ 0xa63f9a49c2c1b110, 0xa7c5ac471b478423, 0x0fcf80dc33721d54,
24
+ 0xd1b71758e219652b, 0xd3c36113404ea4a9, 0x83126e978d4fdf3b,
25
+ 0x645a1cac083126ea, 0xa3d70a3d70a3d70a, 0x3d70a3d70a3d70a4,
26
+ 0xcccccccccccccccc, 0xcccccccccccccccd, 0x8000000000000000,
27
+ 0x0000000000000000, 0xa000000000000000, 0x0000000000000000,
28
+ 0xc800000000000000, 0x0000000000000000, 0xfa00000000000000,
29
+ 0x0000000000000000, 0x9c40000000000000, 0x0000000000000000,
30
+ 0xc350000000000000, 0x0000000000000000, 0xf424000000000000,
31
+ 0x0000000000000000, 0x9896800000000000, 0x0000000000000000,
32
+ 0xbebc200000000000, 0x0000000000000000, 0xee6b280000000000,
33
+ 0x0000000000000000, 0x9502f90000000000, 0x0000000000000000,
34
+ 0xba43b74000000000, 0x0000000000000000, 0xe8d4a51000000000,
35
+ 0x0000000000000000, 0x9184e72a00000000, 0x0000000000000000,
36
+ 0xb5e620f480000000, 0x0000000000000000, 0xe35fa931a0000000,
37
+ 0x0000000000000000, 0x8e1bc9bf04000000, 0x0000000000000000,
38
+ 0xb1a2bc2ec5000000, 0x0000000000000000, 0xde0b6b3a76400000,
39
+ 0x0000000000000000, 0x8ac7230489e80000, 0x0000000000000000,
40
+ 0xad78ebc5ac620000, 0x0000000000000000, 0xd8d726b7177a8000,
41
+ 0x0000000000000000, 0x878678326eac9000, 0x0000000000000000,
42
+ ]);
43
+
44
+ // High half of a full 64x64 -> 128 multiplication, using four 32-bit partial
45
+ // products. WebAssembly MVP has no i128 or mul-high instruction.
46
+ function mul64High(a: u64, b: u64): u64 {
47
+ const aLo = <u64>(<u32>a);
48
+ const aHi = a >> 32;
49
+ const bLo = <u64>(<u32>b);
50
+ const bHi = b >> 32;
51
+ const lowLow = aLo * bLo;
52
+ const highLow = aHi * bLo;
53
+ const middle = highLow + aLo * bHi;
54
+ const middleCarry = <u64>(middle < highLow);
55
+ const low = lowLow + (middle << 32);
56
+ return aHi * bHi + (middle >> 32) + (middleCarry << 32) + <u64>(low < lowLow);
57
+ }
58
+
59
+ /**
60
+ * Fixed-power Eisel-Lemire conversion for GeoJSON-style coordinates with 14
61
+ * fractional digits. This is the `power == -14` specialization of
62
+ * {@link eiselLemire22}: the cached-power address and binary-exponent base are
63
+ * constants, and the exact-halfway correction is unreachable for this power.
64
+ */
65
+ export function eiselLemireMinus14(significand: u64): f64 {
66
+ let i = significand;
67
+ let leading = <i32>clz(i);
68
+ i <<= leading;
69
+
70
+ let lower = i * 0xb424dc35095cd80f;
71
+ let upper = mul64High(i, 0xb424dc35095cd80f);
72
+
73
+ if ((upper & 0x1ff) == 0x1ff) {
74
+ const secondUpper = mul64High(i, 0x538484c19ef38c95);
75
+ const oldLower = lower;
76
+ lower += secondUpper;
77
+ if (lower < oldLower) upper++;
78
+ }
79
+
80
+ const upperBit = upper >> 63;
81
+ let mantissa = upper >> (<i32>(upperBit + 9));
82
+ leading += <i32>(1 ^ upperBit);
83
+ let realExponent = 1040 - leading;
84
+
85
+ mantissa += mantissa & 1;
86
+ mantissa >>= 1;
87
+
88
+ if (mantissa >= (<u64>1) << 53) {
89
+ mantissa = (<u64>1) << 52;
90
+ realExponent++;
91
+ }
92
+
93
+ const bits = (mantissa & ~((<u64>1) << 52)) | ((<u64>realExponent) << 52);
94
+ return reinterpret<f64>(bits);
95
+ }
96
+
97
+ /** Fixed -14 conversion for significands in [2^53, 2^54). */
98
+ export function eiselLemireMinus14_54Bit(significand: u64): f64 {
99
+ const i = significand << 10;
100
+
101
+ let lower = i * 0xb424dc35095cd80f;
102
+ let upper = mul64High(i, 0xb424dc35095cd80f);
103
+
104
+ if ((upper & 0x1ff) == 0x1ff) {
105
+ const secondUpper = mul64High(i, 0x538484c19ef38c95);
106
+ const oldLower = lower;
107
+ lower += secondUpper;
108
+ if (lower < oldLower) upper++;
109
+ }
110
+
111
+ const upperBit = upper >> 63;
112
+ let mantissa = upper >> (<i32>(upperBit + 9));
113
+ let realExponent = 1030 - <i32>(1 ^ upperBit);
114
+
115
+ mantissa += mantissa & 1;
116
+ mantissa >>= 1;
117
+
118
+ if (mantissa >= (<u64>1) << 53) {
119
+ mantissa = (<u64>1) << 52;
120
+ realExponent++;
121
+ }
122
+
123
+ const bits = (mantissa & ~((<u64>1) << 52)) | ((<u64>realExponent) << 52);
124
+ return reinterpret<f64>(bits);
125
+ }
126
+
127
+ /**
128
+ * Convert a non-zero u64 significand at a decimal exponent in [-22, 22].
129
+ * The result is correctly rounded to IEEE-754 binary64.
130
+ */
131
+ export function eiselLemire22(significand: u64, power: i32): f64 {
132
+ let i = significand;
133
+ let leading = <i32>clz(i);
134
+ i <<= leading;
135
+
136
+ const tableIndex = (<usize>(power + 22)) << 4;
137
+ const factor = load<u64>(POWERS_OF_FIVE_128 + tableIndex);
138
+ let lower = i * factor;
139
+ let upper = mul64High(i, factor);
140
+
141
+ // A primary product ending in nine uncertain bits needs the second table
142
+ // limb. This is rare, but removes Eisel-Lemire's historical fallback.
143
+ if ((upper & 0x1ff) == 0x1ff) {
144
+ const secondUpper = mul64High(
145
+ i,
146
+ load<u64>(POWERS_OF_FIVE_128 + tableIndex, 8),
147
+ );
148
+ const oldLower = lower;
149
+ lower += secondUpper;
150
+ if (lower < oldLower) upper++;
151
+ }
152
+
153
+ const upperBit = upper >> 63;
154
+ let mantissa = upper >> (<i32>(upperBit + 9));
155
+ leading += <i32>(1 ^ upperBit);
156
+ let realExponent = <i32>(((<i64>217706 * power) >> 16) + 1087) - leading;
157
+ const halfwayShift = <i32>(upperBit + 9);
158
+
159
+ // Round exact halfway cases to even.
160
+ if (
161
+ lower <= 1 &&
162
+ power >= -4 &&
163
+ power <= 22 &&
164
+ (mantissa & 3) == 1 &&
165
+ mantissa << halfwayShift == upper
166
+ ) {
167
+ mantissa &= ~(<u64>1);
168
+ }
169
+ mantissa += mantissa & 1;
170
+ mantissa >>= 1;
171
+
172
+ if (mantissa >= (<u64>1) << 53) {
173
+ mantissa = (<u64>1) << 52;
174
+ realExponent++;
175
+ }
176
+
177
+ const bits = (mantissa & ~((<u64>1) << 52)) | ((<u64>realExponent) << 52);
178
+ return reinterpret<f64>(bits);
179
+ }
@@ -0,0 +1,69 @@
1
+ // Whitespace-run scanner selected once for large pretty JSON documents.
2
+
3
+ // @ts-expect-error: @lazy is a valid decorator
4
+ @lazy const SPLAT_SPACE = i16x8.splat(0x20);
5
+ // @ts-expect-error: @lazy is a valid decorator
6
+ @lazy const SPLAT_WS_LO = i16x8.splat(9);
7
+ // @ts-expect-error: @lazy is a valid decorator
8
+ @lazy const SPLAT_WS_SPAN = i16x8.splat(4);
9
+ // @ts-expect-error: @lazy is a valid decorator
10
+ @lazy const SPLAT_LF = i16x8.splat(10);
11
+
12
+
13
+ @inline
14
+ function whitespaceMask(block: v128): i32 {
15
+ return i16x8.bitmask(
16
+ v128.or(
17
+ i16x8.eq(block, SPLAT_SPACE),
18
+ i16x8.le_u(i16x8.sub(block, SPLAT_WS_LO), SPLAT_WS_SPAN),
19
+ ),
20
+ );
21
+ }
22
+
23
+ /** Detect an eight-space indentation run near the document head. */
24
+ export function hasLongPrettyIndent_SIMD(srcStart: usize, srcEnd: usize): bool {
25
+ const sampleEnd = min(srcEnd, srcStart + 4096);
26
+ const vectorEnd = sampleEnd >= 16 ? sampleEnd - 16 : 0;
27
+ while (srcStart <= vectorEnd) {
28
+ let mask = i16x8.bitmask(i16x8.eq(load<v128>(srcStart), SPLAT_LF));
29
+ while (mask != 0) {
30
+ const newline = srcStart + ((<usize>ctz(mask)) << 1);
31
+ if (
32
+ newline + 18 <= srcEnd &&
33
+ !v128.any_true(v128.xor(load<v128>(newline, 2), i16x8.splat(0x20)))
34
+ )
35
+ return true;
36
+ mask &= mask - 1;
37
+ }
38
+ srcStart += 16;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ /** `srcStart` is known to point at JSON whitespace. */
44
+ export function skipPrettyWhitespace_SIMD(
45
+ srcStart: usize,
46
+ srcEnd: usize,
47
+ ): usize {
48
+ srcStart += 2;
49
+ if (srcStart >= srcEnd) return srcStart;
50
+ let code = load<u16>(srcStart);
51
+ if (code != 0x20 && code - 9 > 4) return srcStart;
52
+
53
+ const vectorEnd = srcEnd >= 16 ? srcEnd - 16 : 0;
54
+ while (srcStart <= vectorEnd) {
55
+ const mask = whitespaceMask(load<v128>(srcStart));
56
+ if (mask == 0xff) {
57
+ srcStart += 16;
58
+ continue;
59
+ }
60
+ srcStart += (<usize>ctz(~mask & 0xff)) << 1;
61
+ return srcStart;
62
+ }
63
+ do {
64
+ srcStart += 2;
65
+ if (srcStart >= srcEnd) return srcStart;
66
+ code = load<u16>(srcStart);
67
+ } while (code == 0x20 || code - 9 <= 4);
68
+ return srcStart;
69
+ }
@@ -4,7 +4,6 @@ import {
4
4
  BRACE_RIGHT,
5
5
  BRACKET_LEFT,
6
6
  BRACKET_RIGHT,
7
- COLON,
8
7
  COMMA,
9
8
  QUOTE,
10
9
  } from "../custom/chars";
@@ -104,49 +103,73 @@ function scanQuotedValueEnd_SIMD(srcStart: usize, srcEnd: usize): usize {
104
103
  }
105
104
 
106
105
  function scanCompositeValueEnd_SIMD(srcStart: usize, srcEnd: usize): usize {
107
- // Process structural tokens scalar-side (cheap, and token-dense regions stay
108
- // in a tight loop), but bulk-skip the bytes between them: nested string VALUES
109
- // via the vectorized quoted scan (URLs, base64, prose), and runs of digits /
110
- // punctuation / whitespace (numeric arrays like coordinate lists) via a
111
- // vectorized hunt for the next `"`/`{`/`}`/`[`/`]`.
106
+ // Walk every structural event in a loaded block before advancing. This is
107
+ // substantially cheaper for object-heavy values than restarting a SIMD hunt
108
+ // after every short key: one v128 load now covers all quotes and brackets in
109
+ // its eight UTF-16 lanes. Brackets inside strings are ignored, and a closing
110
+ // quote is active only when preceded by an even-length backslash run.
112
111
  let depth: i32 = 1;
113
112
  let ptr = srcStart + 2;
114
113
  const srcEnd16 = srcEnd >= 16 ? srcEnd - 16 : 0;
114
+ let inString = false;
115
+
116
+ while (ptr <= srcEnd16) {
117
+ let mask = structuralOrQuoteMask(load<v128>(ptr));
118
+ while (mask != 0) {
119
+ const lane = usize(ctz(mask) << 1);
120
+ mask &= mask - 1;
121
+ const eventPtr = ptr + lane;
122
+ const code = load<u16>(eventPtr);
123
+
124
+ if (code == QUOTE) {
125
+ if (!inString) {
126
+ inString = true;
127
+ } else {
128
+ let slash = eventPtr - 2;
129
+ let escaped = false;
130
+ while (slash > srcStart && load<u16>(slash) == BACK_SLASH) {
131
+ escaped = !escaped;
132
+ slash -= 2;
133
+ }
134
+ if (!escaped) inString = false;
135
+ }
136
+ continue;
137
+ }
138
+ if (inString) continue;
139
+
140
+ const folded = code & 0xffdf;
141
+ if (folded == BRACKET_LEFT) {
142
+ depth++;
143
+ } else if (folded == BRACKET_RIGHT && --depth == 0) {
144
+ return eventPtr + 2;
145
+ }
146
+ }
147
+ ptr += 16;
148
+ }
149
+
115
150
  while (ptr < srcEnd) {
116
151
  const code = load<u16>(ptr);
117
152
  if (code == QUOTE) {
118
- ptr = scanQuotedValueEnd_SIMD(ptr, srcEnd);
119
- if (!ptr) return 0;
120
- continue;
121
- }
122
- const folded = code & 0xffdf;
123
- if (folded == BRACKET_LEFT) {
124
- // `[` or `{`
125
- depth++;
126
- ptr += 2;
127
- continue;
128
- }
129
- if (folded == BRACKET_RIGHT) {
130
- // `]` or `}`
131
- if (--depth == 0) return ptr + 2;
132
- ptr += 2;
133
- continue;
134
- }
135
- ptr += 2;
136
- // `,` and `:` sit one byte from the next token, so vectorizing them only
137
- // adds SIMD setup on string-dense objects - stay scalar. Other fillers
138
- // (number digits, whitespace, true/false/null) can run long; vectorize past
139
- // them to the next `"`/`{`/`}`/`[`/`]`.
140
- if (code == COMMA || code == COLON) continue;
141
- while (ptr <= srcEnd16) {
142
- const mask = structuralOrQuoteMask(load<v128>(ptr));
143
- if (mask == 0) {
144
- ptr += 16;
145
- continue;
153
+ if (!inString) {
154
+ inString = true;
155
+ } else {
156
+ let slash = ptr - 2;
157
+ let escaped = false;
158
+ while (slash > srcStart && load<u16>(slash) == BACK_SLASH) {
159
+ escaped = !escaped;
160
+ slash -= 2;
161
+ }
162
+ if (!escaped) inString = false;
163
+ }
164
+ } else if (!inString) {
165
+ const folded = code & 0xffdf;
166
+ if (folded == BRACKET_LEFT) {
167
+ depth++;
168
+ } else if (folded == BRACKET_RIGHT && --depth == 0) {
169
+ return ptr + 2;
146
170
  }
147
- ptr += usize(ctz(mask) << 1);
148
- break;
149
171
  }
172
+ ptr += 2;
150
173
  }
151
174
  return 0;
152
175
  }