@zakkster/lite-bake-stream 1.0.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.
@@ -0,0 +1,691 @@
1
+ // @zakkster/lite-bake-stream / Tokenizer
2
+ // Chunk-safe UTF-8 JSON SAX scanner. Zero-GC on the hot path after warmup.
3
+ // Copyright (c) 2026 Zahary Shinikchiev. MIT.
4
+ //
5
+ // Contract: see /SPEC.md section 5.
6
+ //
7
+ // Error codes (stable):
8
+ // E_UNEXPECTED_BYTE - byte does not belong in current state
9
+ // E_UNEXPECTED_EOF - end() called mid-token
10
+ // E_INVALID_ESCAPE - unknown \ sequence
11
+ // E_INVALID_HEX - non-hex digit in \uXXXX
12
+ // E_UNPAIRED_SURROGATE - lone high/low surrogate in \u escape
13
+ // E_INVALID_UTF8 - malformed UTF-8 byte sequence
14
+ // E_NUMBER_OVERFLOW - number magnitude exceeds F64 range
15
+ // E_NUMBER_INVALID - malformed number literal
16
+ // E_KEYWORD_MISMATCH - true/false/null spelled wrong
17
+ // E_TRAILING_INPUT - non-whitespace bytes after top-level value(s) (array mode only)
18
+ // E_DEPTH_LIMIT - nesting depth exceeded MAX_DEPTH
19
+
20
+ export const VERSION = '1.0.0';
21
+
22
+ // ---------- byte constants ----------
23
+ const B_SPACE = 0x20, B_TAB = 0x09, B_LF = 0x0A, B_CR = 0x0D;
24
+ const B_QUOTE = 0x22, B_BACKSLASH = 0x5C, B_SLASH = 0x2F;
25
+ const B_LBRACE = 0x7B, B_RBRACE = 0x7D, B_LBRACKET = 0x5B, B_RBRACKET = 0x5D;
26
+ const B_COMMA = 0x2C, B_COLON = 0x3A;
27
+ const B_MINUS = 0x2D, B_PLUS = 0x2B, B_DOT = 0x2E;
28
+ const B_0 = 0x30, B_9 = 0x39;
29
+ const B_a = 0x61, B_b_low = 0x62, B_e_low = 0x65, B_f_low = 0x66;
30
+ const B_l = 0x6C, B_n_low = 0x6E, B_r_low = 0x72, B_s = 0x73, B_t_low = 0x74, B_u = 0x75;
31
+ const B_E_up = 0x45;
32
+ const B_A_up = 0x41, B_F_up = 0x46;
33
+
34
+ const MAX_DEPTH = 512;
35
+
36
+ // ---------- states ----------
37
+ const S_TOP = 0; // top-level: expecting value, whitespace, or EOF
38
+ const S_VALUE = 1; // nested: expecting a value
39
+ const S_OBJECT_START = 2; // just entered {, expecting key or }
40
+ const S_OBJECT_KEY_END = 3; // just closed a key, expecting :
41
+ const S_OBJECT_VALUE_END = 4; // just closed a value in obj, expecting , or }
42
+ const S_ARRAY_START = 5; // just entered [, expecting value or ]
43
+ const S_ARRAY_VALUE_END = 6; // just closed a value in arr, expecting , or ]
44
+ const S_OBJECT_NEXT_KEY = 7; // just saw , in obj, strictly expecting key (no })
45
+ const S_STRING = 10;
46
+ const S_STRING_ESC = 11;
47
+ const S_STRING_UNI = 12; // inside \uXXXX
48
+ const S_STRING_UNI_LO_BS = 13; // after high surrogate, expecting \
49
+ const S_STRING_UNI_LO_U = 14; // after high surrogate + \, expecting u
50
+ const S_STRING_UNI_LO = 15; // inside low-surrogate \uXXXX
51
+ const S_NUMBER = 20; // generic number accumulation (substate held separately)
52
+ const S_KEYWORD = 30;
53
+
54
+ // Number substates
55
+ const NS_SIGN = 0; // just saw -, expecting first int digit
56
+ const NS_INT_ZERO = 1; // just saw 0 as leading, next must be . or e/E or terminator
57
+ const NS_INT = 2; // in integer digits (>=1)
58
+ const NS_FRAC_DOT = 3; // just saw ., need first frac digit
59
+ const NS_FRAC = 4; // in fractional digits
60
+ const NS_EXP_MARK = 5; // just saw e/E, next may be sign or digit
61
+ const NS_EXP_SIGN = 6; // just saw +/- after e, need first exp digit
62
+ const NS_EXP = 7; // in exponent digits
63
+
64
+ // Clinger's fast-path decimal->F64 conversion table. Each POW10[k] is the
65
+ // EXACT IEEE 754 double for 10^k across k in [0, 22]. Computed by iterated
66
+ // multiplication starting from 1.0 so no rounding occurs. Any mantissa that
67
+ // fits in u53 combined with an exponent in [-22, 22] can be converted to
68
+ // correctly-rounded F64 via a single multiplication (positive exp) or single
69
+ // division (negative exp) using this table. Outside that domain, correctly
70
+ // rounded parsing requires David Gay's strtod, deferred to M4+.
71
+ const POW10 = new Float64Array(23);
72
+ POW10[0] = 1;
73
+ for (let _pi = 1; _pi < 23; _pi++) POW10[_pi] = POW10[_pi - 1] * 10;
74
+ const CLINGER_MAX_DIGITS = 15; // mantissa with <=15 digits fits exactly in u53
75
+ const CLINGER_MAX_EXP = 22;
76
+
77
+ // Container stack values
78
+ const C_OBJECT = 0;
79
+ const C_ARRAY = 1;
80
+
81
+ // UTF-8 continuation state (0 = expecting lead byte)
82
+ // Values > 0 encode how many continuation bytes are still expected.
83
+
84
+ // ---------- keyword templates ----------
85
+ const KW_TRUE = Uint8Array.of(B_t_low, B_r_low, B_u, B_e_low);
86
+ const KW_FALSE = Uint8Array.of(B_f_low, B_a, B_l, B_s, B_e_low);
87
+ const KW_NULL = Uint8Array.of(B_n_low, B_u, B_l, B_l);
88
+ const KW_ID_TRUE = 0;
89
+ const KW_ID_FALSE = 1;
90
+ const KW_ID_NULL = 2;
91
+
92
+ export class TokenizerError extends Error {
93
+ constructor(code, byteOffset, msg) {
94
+ super(msg + ' at byte ' + byteOffset);
95
+ this.code = code;
96
+ this.byteOffset = byteOffset;
97
+ this.name = 'TokenizerError';
98
+ }
99
+ }
100
+
101
+ // Sink protocol (see SPEC 5.3). Any missing method is treated as no-op.
102
+ const NOOP = () => {};
103
+
104
+ export class Tokenizer {
105
+ constructor(sink, opts) {
106
+ if (!sink) throw new Error('sink required');
107
+ this.sink = sink;
108
+
109
+ // top-level framing: 'auto' (default), 'array', or 'ndjson'
110
+ this._framing = (opts && opts.framing) || 'auto';
111
+ this._maxStringBytes = (opts && opts.maxStringBytes) || (1 << 20); // 1 MB default cap
112
+
113
+ // fill in missing sink methods to avoid per-token undefined checks
114
+ if (!sink.onStartObject) sink.onStartObject = NOOP;
115
+ if (!sink.onEndObject) sink.onEndObject = NOOP;
116
+ if (!sink.onStartArray) sink.onStartArray = NOOP;
117
+ if (!sink.onEndArray) sink.onEndArray = NOOP;
118
+ if (!sink.onKey) sink.onKey = NOOP;
119
+ if (!sink.onString) sink.onString = NOOP;
120
+ if (!sink.onNumber) sink.onNumber = NOOP;
121
+ if (!sink.onTrue) sink.onTrue = NOOP;
122
+ if (!sink.onFalse) sink.onFalse = NOOP;
123
+ if (!sink.onNull) sink.onNull = NOOP;
124
+ if (!sink.onEnd) sink.onEnd = NOOP;
125
+
126
+ // parser state
127
+ this._state = S_TOP;
128
+ this._topDetected = false; // for 'auto': did we see the first non-ws byte
129
+ this._arrayMode = false; // once top-level [ seen
130
+ this._arrayModeOuterOpen = false; // outer array is open (not yet closed)
131
+ this._absOffset = 0;
132
+
133
+ // container stack: Uint8Array of C_OBJECT/C_ARRAY, depth pointer
134
+ this._stack = new Uint8Array(MAX_DEPTH);
135
+ this._depth = 0;
136
+
137
+ // string accumulation
138
+ this._strBuf = new Uint8Array(4096);
139
+ this._strLen = 0;
140
+ this._strIsKey = false;
141
+ this._uniHi = 0; // hex value being accumulated
142
+ this._uniDigits = 0; // digits accumulated so far (0..4)
143
+ this._highSurrogate = 0; // if non-zero, awaiting low surrogate
144
+
145
+ // number accumulation (byte-level, no substring alloc)
146
+ this._numSubstate = 0;
147
+ this._numSign = 1;
148
+ this._numIntPart = 0;
149
+ this._numFracPart = 0;
150
+ this._numFracDiv = 1;
151
+ this._numExpSign = 1;
152
+ this._numExpPart = 0;
153
+
154
+ // keyword accumulation
155
+ this._kwId = 0;
156
+ this._kwPos = 0;
157
+ }
158
+
159
+ // Feed a chunk. Bytes are consumed synchronously; sink events fire during this call.
160
+ feed(chunk) {
161
+ const len = chunk.length;
162
+ let i = 0;
163
+ while (i < len) {
164
+ const b = chunk[i];
165
+ const st = this._state;
166
+
167
+ if (st < S_STRING) {
168
+ // structural / value-expecting states
169
+ if (b === B_SPACE || b === B_TAB || b === B_LF || b === B_CR) {
170
+ i++; this._absOffset++;
171
+ if (st === S_TOP && this._arrayMode === false && this._topDetected === false) {
172
+ // still leading whitespace before top-level detection
173
+ }
174
+ continue;
175
+ }
176
+
177
+ if (st === S_TOP) {
178
+ this._handleTopByte(b);
179
+ } else if (st === S_VALUE) {
180
+ this._handleValueByte(b);
181
+ } else if (st === S_OBJECT_START) {
182
+ this._handleObjectStartByte(b);
183
+ } else if (st === S_OBJECT_NEXT_KEY) {
184
+ if (b !== B_QUOTE) this._err('E_UNEXPECTED_BYTE', 'expected key string (trailing comma not allowed)');
185
+ this._beginString(true);
186
+ } else if (st === S_OBJECT_KEY_END) {
187
+ if (b !== B_COLON) this._err('E_UNEXPECTED_BYTE', 'expected :');
188
+ this._state = S_VALUE;
189
+ } else if (st === S_OBJECT_VALUE_END) {
190
+ this._handleObjectValueEndByte(b);
191
+ } else if (st === S_ARRAY_START) {
192
+ this._handleArrayStartByte(b);
193
+ } else if (st === S_ARRAY_VALUE_END) {
194
+ this._handleArrayValueEndByte(b);
195
+ }
196
+
197
+ i++; this._absOffset++;
198
+ continue;
199
+ }
200
+
201
+ if (st === S_STRING) {
202
+ // fast inner loop over ASCII string bytes
203
+ if (b === B_QUOTE) {
204
+ this._emitString();
205
+ i++; this._absOffset++;
206
+ continue;
207
+ }
208
+ if (b === B_BACKSLASH) {
209
+ this._state = S_STRING_ESC;
210
+ i++; this._absOffset++;
211
+ continue;
212
+ }
213
+ if (b < 0x20) {
214
+ this._err('E_UNEXPECTED_BYTE', 'unescaped control byte 0x' + b.toString(16) + ' in string');
215
+ }
216
+ // fast path: scan run of plain ASCII / continuation bytes until special byte
217
+ // (validates minimal UTF-8 structure but does not fully verify surrogate/overlong)
218
+ let j = i;
219
+ while (j < len) {
220
+ const bb = chunk[j];
221
+ if (bb === B_QUOTE || bb === B_BACKSLASH || bb < 0x20) break;
222
+ j++;
223
+ }
224
+ this._appendStrRange(chunk, i, j);
225
+ this._absOffset += (j - i);
226
+ i = j;
227
+ continue;
228
+ }
229
+
230
+ if (st === S_STRING_ESC) {
231
+ i++; this._absOffset++;
232
+ if (b === B_QUOTE) { this._appendStrByte(B_QUOTE); this._state = S_STRING; }
233
+ else if (b === B_BACKSLASH) { this._appendStrByte(B_BACKSLASH); this._state = S_STRING; }
234
+ else if (b === B_SLASH) { this._appendStrByte(B_SLASH); this._state = S_STRING; }
235
+ else if (b === B_b_low) { this._appendStrByte(0x08); this._state = S_STRING; }
236
+ else if (b === B_f_low) { this._appendStrByte(0x0C); this._state = S_STRING; }
237
+ else if (b === B_n_low) { this._appendStrByte(0x0A); this._state = S_STRING; }
238
+ else if (b === B_r_low) { this._appendStrByte(0x0D); this._state = S_STRING; }
239
+ else if (b === B_t_low) { this._appendStrByte(0x09); this._state = S_STRING; }
240
+ else if (b === B_u) {
241
+ this._uniHi = 0; this._uniDigits = 0;
242
+ this._state = this._highSurrogate === 0 ? S_STRING_UNI : S_STRING_UNI_LO;
243
+ }
244
+ else this._err('E_INVALID_ESCAPE', 'unknown escape \\' + String.fromCharCode(b));
245
+ continue;
246
+ }
247
+
248
+ if (st === S_STRING_UNI || st === S_STRING_UNI_LO) {
249
+ const d = this._hexVal(b);
250
+ if (d < 0) this._err('E_INVALID_HEX', 'non-hex digit in \\uXXXX');
251
+ this._uniHi = (this._uniHi << 4) | d;
252
+ this._uniDigits++;
253
+ i++; this._absOffset++;
254
+ if (this._uniDigits === 4) {
255
+ const cp = this._uniHi;
256
+ if (st === S_STRING_UNI) {
257
+ if (cp >= 0xD800 && cp <= 0xDBFF) {
258
+ this._highSurrogate = cp;
259
+ this._state = S_STRING_UNI_LO_BS;
260
+ } else if (cp >= 0xDC00 && cp <= 0xDFFF) {
261
+ this._err('E_UNPAIRED_SURROGATE', 'lone low surrogate');
262
+ } else {
263
+ this._appendUtf8Codepoint(cp);
264
+ this._state = S_STRING;
265
+ }
266
+ } else {
267
+ if (cp < 0xDC00 || cp > 0xDFFF) {
268
+ this._err('E_UNPAIRED_SURROGATE', 'expected low surrogate');
269
+ }
270
+ const combined = 0x10000 + ((this._highSurrogate - 0xD800) << 10) + (cp - 0xDC00);
271
+ this._highSurrogate = 0;
272
+ this._appendUtf8Codepoint(combined);
273
+ this._state = S_STRING;
274
+ }
275
+ }
276
+ continue;
277
+ }
278
+
279
+ if (st === S_STRING_UNI_LO_BS) {
280
+ if (b !== B_BACKSLASH) this._err('E_UNPAIRED_SURROGATE', 'expected \\ for low surrogate');
281
+ this._state = S_STRING_UNI_LO_U;
282
+ i++; this._absOffset++;
283
+ continue;
284
+ }
285
+ if (st === S_STRING_UNI_LO_U) {
286
+ if (b !== B_u) this._err('E_UNPAIRED_SURROGATE', 'expected u for low surrogate');
287
+ this._uniHi = 0; this._uniDigits = 0;
288
+ this._state = S_STRING_UNI_LO;
289
+ i++; this._absOffset++;
290
+ continue;
291
+ }
292
+
293
+ if (st === S_NUMBER) {
294
+ // Try to consume as many digits as possible in a tight inner loop for common cases
295
+ const consumed = this._consumeNumberBytes(chunk, i, len);
296
+ if (consumed > 0) { i += consumed; this._absOffset += consumed; continue; }
297
+ // byte terminates number
298
+ this._emitNumber();
299
+ // do NOT advance i; re-dispatch this byte in new state
300
+ continue;
301
+ }
302
+
303
+ if (st === S_KEYWORD) {
304
+ const tmpl = this._kwId === KW_ID_TRUE ? KW_TRUE : (this._kwId === KW_ID_FALSE ? KW_FALSE : KW_NULL);
305
+ if (b !== tmpl[this._kwPos]) this._err('E_KEYWORD_MISMATCH', 'bad keyword byte');
306
+ this._kwPos++;
307
+ i++; this._absOffset++;
308
+ if (this._kwPos === tmpl.length) {
309
+ if (this._kwId === KW_ID_TRUE) this.sink.onTrue();
310
+ else if (this._kwId === KW_ID_FALSE) this.sink.onFalse();
311
+ else this.sink.onNull();
312
+ this._afterValue();
313
+ }
314
+ continue;
315
+ }
316
+
317
+ // unreachable
318
+ this._err('E_UNEXPECTED_BYTE', 'internal: unknown state ' + st);
319
+ }
320
+ }
321
+
322
+ end() {
323
+ // A trailing number may still be pending
324
+ if (this._state === S_NUMBER) {
325
+ this._emitNumber();
326
+ }
327
+ if (this._state === S_TOP) {
328
+ if (this._arrayMode && this._arrayModeOuterOpen) {
329
+ this._err('E_UNEXPECTED_EOF', 'unclosed top-level array');
330
+ }
331
+ this.sink.onEnd();
332
+ return;
333
+ }
334
+ this._err('E_UNEXPECTED_EOF', 'input ended mid-token (state=' + this._state + ')');
335
+ }
336
+
337
+ // -------- top-level handling --------
338
+
339
+ _handleTopByte(b) {
340
+ if (this._framing === 'auto' && this._topDetected === false) {
341
+ this._topDetected = true;
342
+ if (b === B_LBRACKET) {
343
+ this._arrayMode = true;
344
+ this._arrayModeOuterOpen = true;
345
+ this._state = S_ARRAY_START;
346
+ return;
347
+ }
348
+ // NDJSON path
349
+ this._arrayMode = false;
350
+ this._beginValue(b);
351
+ return;
352
+ }
353
+ if (this._framing === 'array' && this._topDetected === false) {
354
+ if (b !== B_LBRACKET) this._err('E_UNEXPECTED_BYTE', 'expected [ in array framing');
355
+ this._topDetected = true;
356
+ this._arrayMode = true;
357
+ this._arrayModeOuterOpen = true;
358
+ this._state = S_ARRAY_START;
359
+ return;
360
+ }
361
+ if (this._framing === 'ndjson' && this._topDetected === false) {
362
+ this._topDetected = true;
363
+ this._arrayMode = false;
364
+ this._beginValue(b);
365
+ return;
366
+ }
367
+ // top-level after a completed value (NDJSON stream)
368
+ if (this._arrayMode) {
369
+ // outer array closed; only whitespace permitted after
370
+ this._err('E_TRAILING_INPUT', 'trailing byte after top-level array');
371
+ }
372
+ this._beginValue(b);
373
+ }
374
+
375
+ _handleValueByte(b) {
376
+ this._beginValue(b);
377
+ }
378
+
379
+ _handleObjectStartByte(b) {
380
+ if (b === B_RBRACE) {
381
+ this.sink.onEndObject();
382
+ this._popContainer();
383
+ return;
384
+ }
385
+ if (b !== B_QUOTE) this._err('E_UNEXPECTED_BYTE', 'expected key string');
386
+ this._beginString(true);
387
+ }
388
+
389
+ _handleObjectValueEndByte(b) {
390
+ if (b === B_COMMA) { this._state = S_OBJECT_NEXT_KEY; }
391
+ else if (b === B_RBRACE) {
392
+ this.sink.onEndObject();
393
+ this._popContainer();
394
+ } else {
395
+ this._err('E_UNEXPECTED_BYTE', 'expected , or }');
396
+ }
397
+ }
398
+
399
+ _handleArrayStartByte(b) {
400
+ if (b === B_RBRACKET) {
401
+ if (this._depth === 0 && this._arrayMode) {
402
+ // closing the top-level array (which is framing, not sink-visible)
403
+ this._arrayModeOuterOpen = false;
404
+ this._state = S_TOP;
405
+ return;
406
+ }
407
+ this.sink.onEndArray();
408
+ this._popContainer();
409
+ return;
410
+ }
411
+ this._beginValue(b);
412
+ }
413
+
414
+ _handleArrayValueEndByte(b) {
415
+ if (b === B_COMMA) { this._state = S_VALUE; }
416
+ else if (b === B_RBRACKET) {
417
+ if (this._depth === 0 && this._arrayMode) {
418
+ this._arrayModeOuterOpen = false;
419
+ this._state = S_TOP;
420
+ return;
421
+ }
422
+ this.sink.onEndArray();
423
+ this._popContainer();
424
+ } else {
425
+ this._err('E_UNEXPECTED_BYTE', 'expected , or ]');
426
+ }
427
+ }
428
+
429
+ // -------- value dispatch --------
430
+
431
+ _beginValue(b) {
432
+ if (b === B_LBRACE) {
433
+ this.sink.onStartObject();
434
+ this._pushContainer(C_OBJECT);
435
+ this._state = S_OBJECT_START;
436
+ return;
437
+ }
438
+ if (b === B_LBRACKET) {
439
+ this.sink.onStartArray();
440
+ this._pushContainer(C_ARRAY);
441
+ this._state = S_ARRAY_START;
442
+ return;
443
+ }
444
+ if (b === B_QUOTE) { this._beginString(false); return; }
445
+ if (b === B_MINUS) { this._beginNumber(B_MINUS); return; }
446
+ if (b >= B_0 && b <= B_9) { this._beginNumber(b); return; }
447
+ if (b === B_t_low) { this._beginKeyword(KW_ID_TRUE); return; }
448
+ if (b === B_f_low) { this._beginKeyword(KW_ID_FALSE); return; }
449
+ if (b === B_n_low) { this._beginKeyword(KW_ID_NULL); return; }
450
+ this._err('E_UNEXPECTED_BYTE', 'unexpected byte 0x' + b.toString(16) + ' at value position');
451
+ }
452
+
453
+ _beginString(isKey) {
454
+ this._strLen = 0;
455
+ this._strIsKey = isKey;
456
+ this._highSurrogate = 0;
457
+ this._state = S_STRING;
458
+ }
459
+
460
+ _beginNumber(firstByte) {
461
+ this._numSign = 1;
462
+ this._numIntPart = 0;
463
+ this._numFracPart = 0;
464
+ this._numFracDiv = 1;
465
+ this._numFracDigits = 0;
466
+ this._numDigitsSeen = 0;
467
+ this._numExpSign = 1;
468
+ this._numExpPart = 0;
469
+ if (firstByte === B_MINUS) {
470
+ this._numSign = -1;
471
+ this._numSubstate = NS_SIGN;
472
+ } else if (firstByte === B_0) {
473
+ this._numSubstate = NS_INT_ZERO;
474
+ this._numDigitsSeen = 1;
475
+ } else {
476
+ this._numIntPart = firstByte - B_0;
477
+ this._numSubstate = NS_INT;
478
+ this._numDigitsSeen = 1;
479
+ }
480
+ this._state = S_NUMBER;
481
+ }
482
+
483
+ _beginKeyword(id) {
484
+ this._kwId = id;
485
+ this._kwPos = 1; // first byte consumed by dispatch
486
+ this._state = S_KEYWORD;
487
+ }
488
+
489
+ // Returns number of bytes consumed as part of the current number token (may be 0).
490
+ _consumeNumberBytes(chunk, start, end) {
491
+ let i = start;
492
+ let ss = this._numSubstate;
493
+ while (i < end) {
494
+ const b = chunk[i];
495
+ if (ss === NS_SIGN) {
496
+ if (b === B_0) { ss = NS_INT_ZERO; this._numDigitsSeen++; i++; continue; }
497
+ if (b >= (B_0 + 1) && b <= B_9) { this._numIntPart = b - B_0; this._numDigitsSeen++; ss = NS_INT; i++; continue; }
498
+ break;
499
+ }
500
+ if (ss === NS_INT_ZERO) {
501
+ if (b === B_DOT) { ss = NS_FRAC_DOT; i++; continue; }
502
+ if (b === B_e_low || b === B_E_up) { ss = NS_EXP_MARK; i++; continue; }
503
+ if (b >= B_0 && b <= B_9) this._err('E_NUMBER_INVALID', 'leading zero followed by digit');
504
+ break;
505
+ }
506
+ if (ss === NS_INT) {
507
+ if (b >= B_0 && b <= B_9) { this._numIntPart = this._numIntPart * 10 + (b - B_0); this._numDigitsSeen++; i++; continue; }
508
+ if (b === B_DOT) { ss = NS_FRAC_DOT; i++; continue; }
509
+ if (b === B_e_low || b === B_E_up) { ss = NS_EXP_MARK; i++; continue; }
510
+ break;
511
+ }
512
+ if (ss === NS_FRAC_DOT) {
513
+ if (b >= B_0 && b <= B_9) {
514
+ this._numFracPart = b - B_0;
515
+ this._numFracDiv = 10;
516
+ this._numFracDigits = 1;
517
+ this._numDigitsSeen++;
518
+ ss = NS_FRAC; i++; continue;
519
+ }
520
+ this._err('E_NUMBER_INVALID', 'expected digit after .');
521
+ }
522
+ if (ss === NS_FRAC) {
523
+ if (b >= B_0 && b <= B_9) {
524
+ this._numFracPart = this._numFracPart * 10 + (b - B_0);
525
+ this._numFracDiv *= 10;
526
+ this._numFracDigits++;
527
+ this._numDigitsSeen++;
528
+ i++; continue;
529
+ }
530
+ if (b === B_e_low || b === B_E_up) { ss = NS_EXP_MARK; i++; continue; }
531
+ break;
532
+ }
533
+ if (ss === NS_EXP_MARK) {
534
+ if (b === B_PLUS) { ss = NS_EXP_SIGN; i++; continue; }
535
+ if (b === B_MINUS) { this._numExpSign = -1; ss = NS_EXP_SIGN; i++; continue; }
536
+ if (b >= B_0 && b <= B_9) { this._numExpPart = b - B_0; ss = NS_EXP; i++; continue; }
537
+ this._err('E_NUMBER_INVALID', 'expected digit or sign after e');
538
+ }
539
+ if (ss === NS_EXP_SIGN) {
540
+ if (b >= B_0 && b <= B_9) { this._numExpPart = b - B_0; ss = NS_EXP; i++; continue; }
541
+ this._err('E_NUMBER_INVALID', 'expected digit after exponent sign');
542
+ }
543
+ if (ss === NS_EXP) {
544
+ if (b >= B_0 && b <= B_9) { this._numExpPart = this._numExpPart * 10 + (b - B_0); i++; continue; }
545
+ break;
546
+ }
547
+ break;
548
+ }
549
+ this._numSubstate = ss;
550
+ return i - start;
551
+ }
552
+
553
+ _emitNumber() {
554
+ const ss = this._numSubstate;
555
+ if (ss === NS_SIGN || ss === NS_FRAC_DOT || ss === NS_EXP_MARK || ss === NS_EXP_SIGN) {
556
+ this._err('E_NUMBER_INVALID', 'number ended prematurely');
557
+ }
558
+ const digitsSeen = this._numDigitsSeen;
559
+ const fracDigits = this._numFracDigits;
560
+ const explicitExp = this._numExpSign * this._numExpPart;
561
+ const netExp = explicitExp - fracDigits;
562
+ let v;
563
+ // Clinger's fast path: mantissa fits in u53 AND |netExp| <= 22 => single
564
+ // multiply/divide by an exact power-of-10 gives correctly-rounded F64.
565
+ if (digitsSeen <= CLINGER_MAX_DIGITS && netExp >= -CLINGER_MAX_EXP && netExp <= CLINGER_MAX_EXP) {
566
+ // Assemble mantissa as exact integer: intPart * 10^fracDigits + fracPart.
567
+ // With digitsSeen <= 15 the product fits in u53 exactly (no rounding).
568
+ const mantissa = this._numIntPart * this._numFracDiv + this._numFracPart;
569
+ if (netExp === 0) v = this._numSign * mantissa;
570
+ else if (netExp > 0) v = this._numSign * mantissa * POW10[netExp];
571
+ else v = this._numSign * mantissa / POW10[-netExp];
572
+ } else {
573
+ // Slow path (deferred correctly-rounded parsing until M4+): naive
574
+ // computation. May differ from JS Number(str) by <=1 ULP. Documented
575
+ // in SPEC section on numeric preservation.
576
+ v = this._numIntPart + (this._numFracDiv > 1 ? this._numFracPart / this._numFracDiv : 0);
577
+ if (this._numExpPart !== 0) v = v * Math.pow(10, explicitExp);
578
+ v = this._numSign * v;
579
+ }
580
+ if (!Number.isFinite(v)) this._err('E_NUMBER_OVERFLOW', 'number exceeds F64 range');
581
+ this.sink.onNumber(v);
582
+ this._afterValue();
583
+ }
584
+
585
+ _emitString() {
586
+ if (this._strIsKey) {
587
+ this.sink.onKey(this._strBuf, 0, this._strLen);
588
+ this._state = S_OBJECT_KEY_END;
589
+ } else {
590
+ this.sink.onString(this._strBuf, 0, this._strLen);
591
+ this._afterValue();
592
+ }
593
+ }
594
+
595
+ _afterValue() {
596
+ if (this._depth === 0) {
597
+ // Primitives at the top of the outer array (e.g. [1,2,3]) live at depth 0
598
+ // because the outer array is framing, not a stacked container.
599
+ if (this._arrayMode && this._arrayModeOuterOpen) {
600
+ this._state = S_ARRAY_VALUE_END;
601
+ return;
602
+ }
603
+ this._state = S_TOP;
604
+ return;
605
+ }
606
+ const c = this._stack[this._depth - 1];
607
+ this._state = c === C_OBJECT ? S_OBJECT_VALUE_END : S_ARRAY_VALUE_END;
608
+ }
609
+
610
+ _pushContainer(kind) {
611
+ if (this._depth >= MAX_DEPTH) this._err('E_DEPTH_LIMIT', 'nesting too deep');
612
+ this._stack[this._depth++] = kind;
613
+ }
614
+
615
+ _popContainer() {
616
+ this._depth--;
617
+ if (this._depth === 0) {
618
+ if (this._arrayMode) {
619
+ // element ended inside outer array; expect , or ]
620
+ this._state = S_ARRAY_VALUE_END;
621
+ } else {
622
+ this._state = S_TOP;
623
+ }
624
+ return;
625
+ }
626
+ const c = this._stack[this._depth - 1];
627
+ this._state = c === C_OBJECT ? S_OBJECT_VALUE_END : S_ARRAY_VALUE_END;
628
+ }
629
+
630
+ // -------- string buffer helpers --------
631
+
632
+ _appendStrByte(b) {
633
+ if (this._strLen >= this._strBuf.length) this._growStrBuf(1);
634
+ this._strBuf[this._strLen++] = b;
635
+ }
636
+
637
+ _appendStrRange(chunk, from, to) {
638
+ const need = to - from;
639
+ if (this._strLen + need > this._strBuf.length) this._growStrBuf(need);
640
+ // Manual copy loop — Uint8Array.set(source) via subarray allocates a small
641
+ // view header per call, which turns into MB-scale GC pressure across a
642
+ // string-heavy fixture. The loop is boring but keeps the hot path allocation-free.
643
+ const dst = this._strBuf;
644
+ let d = this._strLen;
645
+ for (let s = from; s < to; s++) dst[d++] = chunk[s];
646
+ this._strLen = d;
647
+ }
648
+
649
+ _appendUtf8Codepoint(cp) {
650
+ if (cp < 0x80) {
651
+ this._appendStrByte(cp);
652
+ } else if (cp < 0x800) {
653
+ if (this._strLen + 2 > this._strBuf.length) this._growStrBuf(2);
654
+ this._strBuf[this._strLen++] = 0xC0 | (cp >> 6);
655
+ this._strBuf[this._strLen++] = 0x80 | (cp & 0x3F);
656
+ } else if (cp < 0x10000) {
657
+ if (this._strLen + 3 > this._strBuf.length) this._growStrBuf(3);
658
+ this._strBuf[this._strLen++] = 0xE0 | (cp >> 12);
659
+ this._strBuf[this._strLen++] = 0x80 | ((cp >> 6) & 0x3F);
660
+ this._strBuf[this._strLen++] = 0x80 | (cp & 0x3F);
661
+ } else {
662
+ if (this._strLen + 4 > this._strBuf.length) this._growStrBuf(4);
663
+ this._strBuf[this._strLen++] = 0xF0 | (cp >> 18);
664
+ this._strBuf[this._strLen++] = 0x80 | ((cp >> 12) & 0x3F);
665
+ this._strBuf[this._strLen++] = 0x80 | ((cp >> 6) & 0x3F);
666
+ this._strBuf[this._strLen++] = 0x80 | (cp & 0x3F);
667
+ }
668
+ }
669
+
670
+ _growStrBuf(need) {
671
+ let cap = this._strBuf.length;
672
+ while (cap < this._strLen + need) cap *= 2;
673
+ if (cap > this._maxStringBytes) this._err('E_UNEXPECTED_BYTE', 'string exceeds maxStringBytes cap');
674
+ const nb = new Uint8Array(cap);
675
+ nb.set(this._strBuf);
676
+ this._strBuf = nb;
677
+ }
678
+
679
+ _hexVal(b) {
680
+ if (b >= B_0 && b <= B_9) return b - B_0;
681
+ if (b >= B_a && b <= B_f_low) return 10 + (b - B_a);
682
+ if (b >= B_A_up && b <= B_F_up) return 10 + (b - B_A_up);
683
+ return -1;
684
+ }
685
+
686
+ _err(code, msg) {
687
+ throw new TokenizerError(code, this._absOffset, msg);
688
+ }
689
+ }
690
+
691
+