@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.
- package/CHANGELOG.md +457 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/SPEC.md +364 -0
- package/llms.txt +81 -0
- package/package.json +117 -0
- package/src/FileIngest.js +104 -0
- package/src/MultiReader.js +160 -0
- package/src/PreserveReader.js +180 -0
- package/src/PreserveTokenizer.js +172 -0
- package/src/PreserveWriter.js +218 -0
- package/src/RangeReader.js +470 -0
- package/src/Reader.js +349 -0
- package/src/Split.js +359 -0
- package/src/StringTable.js +225 -0
- package/src/Tokenizer.js +691 -0
- package/src/Writer.js +713 -0
- package/src/index.js +193 -0
- package/types/FileIngest.d.ts +44 -0
- package/types/MultiReader.d.ts +36 -0
- package/types/PreserveReader.d.ts +44 -0
- package/types/PreserveTokenizer.d.ts +27 -0
- package/types/PreserveWriter.d.ts +35 -0
- package/types/RangeReader.d.ts +93 -0
- package/types/Reader.d.ts +85 -0
- package/types/Split.d.ts +66 -0
- package/types/StringTable.d.ts +23 -0
- package/types/Tokenizer.d.ts +42 -0
- package/types/Writer.d.ts +92 -0
- package/types/index.d.ts +58 -0
package/src/Tokenizer.js
ADDED
|
@@ -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
|
+
|