@human-synthesis/norns-tron 0.0.1
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/LICENSE +21 -0
- package/PERFORMANCE.md +179 -0
- package/README.md +108 -0
- package/package.json +43 -0
- package/src/client.d.ts +39 -0
- package/src/client.js +86 -0
- package/src/core/tron-auto.js +85 -0
- package/src/core/tron-encode.js +486 -0
- package/src/core/tron-schema.js +227 -0
- package/src/core/tron-table.js +159 -0
- package/src/core/tron-trampoline.js +396 -0
- package/src/core/tron-wasm.js +169 -0
- package/src/core/tron.js +734 -0
- package/src/core/wasm-bytes.js +4 -0
- package/src/index.d.ts +105 -0
- package/src/index.js +165 -0
- package/src/server.d.ts +19 -0
- package/src/server.js +99 -0
- package/src/valibot.d.ts +14 -0
- package/src/valibot.js +84 -0
- package/wasm/parserTron2.wasm +0 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// The JSON.parse TRAMPOLINE — the "innovative" decode path.
|
|
2
|
+
//
|
|
3
|
+
// Insight: JSON.parse is native C++, and V8's string replaceAll is also native
|
|
4
|
+
// C++. So instead of scanning TRON char-by-char in JS, we:
|
|
5
|
+
//
|
|
6
|
+
// 1. Rewrite class instantiations with native replaceAll (~GB/s).
|
|
7
|
+
// 2. Feed the result to JSON.parse (native C++).
|
|
8
|
+
// 3. Rebuild rows with per-class constructors compiled via new Function —
|
|
9
|
+
// a monomorphic {k1:r[0],k2:r[1],...} literal that V8 turns into a single
|
|
10
|
+
// hidden-class allocation. Dictionary columns bake the lookup into the
|
|
11
|
+
// constructor: D[k][r[i]].
|
|
12
|
+
//
|
|
13
|
+
// JS never scans the payload character-by-character; it only orchestrates
|
|
14
|
+
// native passes and materializes objects.
|
|
15
|
+
//
|
|
16
|
+
// Two transform modes:
|
|
17
|
+
// ANONYMOUS (single-class docs): C0(a,b) -> [a,b]
|
|
18
|
+
// JSON.parse allocates no marker string per row. Requires that no
|
|
19
|
+
// instance is nested inside another — guarded by counting C0( against
|
|
20
|
+
// the number of top-level rows.
|
|
21
|
+
// MARKER (general): C0(a,b) -> ["\u0001C0",a,b]
|
|
22
|
+
// Handles multi-class and nested instances.
|
|
23
|
+
//
|
|
24
|
+
// Safe because canonical TRON escapes "(" and ")" inside strings (see encStr
|
|
25
|
+
// in tron.js), so every raw paren in the document is structural.
|
|
26
|
+
//
|
|
27
|
+
// Caveats: needs new Function (CSP-restricted environments fall back to
|
|
28
|
+
// parseFast); a literal U+0001 in the source falls back to the scanner.
|
|
29
|
+
|
|
30
|
+
import { parsePrelude, parseFast, applyTables } from './tron.js';
|
|
31
|
+
|
|
32
|
+
const MARK = '\u0001';
|
|
33
|
+
const MARK_ESC = '\\u0001'; // the 6-character JSON escape, not the raw char
|
|
34
|
+
|
|
35
|
+
// Path-coverage counters. Used by stress.js to prove the risky fast paths are
|
|
36
|
+
// actually exercised by the corpus rather than silently falling back.
|
|
37
|
+
const stats = {
|
|
38
|
+
anonRoot: 0, anonDeep: 0, anonReject: 0, deepReject: 0,
|
|
39
|
+
markerTight: 0, markerWalk: 0, plainJson: 0, fallbackScanner: 0, lazyRows: 0,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ---- compiled-constructor cache (shared across calls) ----
|
|
43
|
+
// Dict *contents* are bound per-document via the outer function, so the
|
|
44
|
+
// compiled code itself is reusable across documents with the same shape.
|
|
45
|
+
const ctorFactoryCache = new Map();
|
|
46
|
+
|
|
47
|
+
function getCtorFactory(fields, dictFlags, offset) {
|
|
48
|
+
const key = offset + '|' + dictFlags.join('') + '|' + fields.join(',');
|
|
49
|
+
let fac = ctorFactoryCache.get(key);
|
|
50
|
+
if (fac) return fac;
|
|
51
|
+
let body = 'return function(r){return {';
|
|
52
|
+
for (let k = 0; k < fields.length; k++) {
|
|
53
|
+
if (k) body += ',';
|
|
54
|
+
body += JSON.stringify(fields[k]) + ':';
|
|
55
|
+
body += dictFlags[k] === 1 ? ('D[' + k + '][r[' + (k + offset) + ']]') : ('r[' + (k + offset) + ']');
|
|
56
|
+
}
|
|
57
|
+
body += '}}';
|
|
58
|
+
fac = new Function('D', body);
|
|
59
|
+
ctorFactoryCache.set(key, fac);
|
|
60
|
+
return fac;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildCtors(classes, offset) {
|
|
64
|
+
const ctors = Object.create(null);
|
|
65
|
+
for (const name in classes) {
|
|
66
|
+
const cls = classes[name];
|
|
67
|
+
const dictFlags = cls.d.map(d => (d === null ? 0 : 1));
|
|
68
|
+
ctors[name] = getCtorFactory(cls.f, dictFlags, offset)(cls.d);
|
|
69
|
+
}
|
|
70
|
+
return ctors;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- lazy row classes (simdjson "On Demand" style) ----
|
|
74
|
+
const lazyClassCache = new Map();
|
|
75
|
+
|
|
76
|
+
function getLazyClass(fields, dictFlags, offset) {
|
|
77
|
+
const key = offset + '|' + dictFlags.join('') + '|' + fields.join(',');
|
|
78
|
+
let fac = lazyClassCache.get(key);
|
|
79
|
+
if (fac) return fac;
|
|
80
|
+
let body = 'return class{constructor(r){this._r=r}';
|
|
81
|
+
for (let k = 0; k < fields.length; k++) {
|
|
82
|
+
const fname = JSON.stringify(fields[k]);
|
|
83
|
+
const expr = dictFlags[k] === 1
|
|
84
|
+
? ('D[' + k + '][this._r[' + (k + offset) + ']]')
|
|
85
|
+
: ('this._r[' + (k + offset) + ']');
|
|
86
|
+
body += 'get ' + fname + '(){return ' + expr + '}';
|
|
87
|
+
}
|
|
88
|
+
body += 'toJSON(){return {';
|
|
89
|
+
for (let k = 0; k < fields.length; k++) {
|
|
90
|
+
if (k) body += ',';
|
|
91
|
+
body += JSON.stringify(fields[k]) + ':this[' + JSON.stringify(fields[k]) + ']';
|
|
92
|
+
}
|
|
93
|
+
body += '}}}';
|
|
94
|
+
fac = new Function('D', body);
|
|
95
|
+
lazyClassCache.set(key, fac);
|
|
96
|
+
return fac;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// canonical class names C<digits> are safe for plain string replaceAll;
|
|
100
|
+
// arbitrary names need a boundary-guarded regex to avoid substring hits.
|
|
101
|
+
function isCanonicalName(n) {
|
|
102
|
+
if (n.charCodeAt(0) !== 67 /* C */) return false;
|
|
103
|
+
for (let k = 1; k < n.length; k++) { const c = n.charCodeAt(k); if (c < 48 || c > 57) return false; }
|
|
104
|
+
return n.length > 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function transform(text, pre) {
|
|
108
|
+
let t = text.slice(pre.end);
|
|
109
|
+
const names = Object.keys(pre.classes);
|
|
110
|
+
for (let k = 0; k < names.length; k++) {
|
|
111
|
+
const name = names[k];
|
|
112
|
+
// JSON forbids RAW control characters inside strings, so the marker must
|
|
113
|
+
// be written as the escape sequence \u0001; JSON.parse decodes it back to
|
|
114
|
+
// U+0001. Emitting it raw made JSON.parse throw, which silently killed
|
|
115
|
+
// this entire path (every multi-class doc fell back to the scanner).
|
|
116
|
+
const rep = '["' + MARK_ESC + name + '",';
|
|
117
|
+
if (isCanonicalName(name)) {
|
|
118
|
+
t = t.replaceAll(name + '(', rep);
|
|
119
|
+
} else {
|
|
120
|
+
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
121
|
+
t = t.replace(new RegExp('(?<![A-Za-z0-9_.])' + esc + '\\(', 'g'), rep);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (names.length > 0) t = t.replaceAll(')', ']');
|
|
125
|
+
return t;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A data string that itself begins with U+0001 would be indistinguishable from
|
|
129
|
+
// a marker after parsing. The encoder writes such a character as the escape
|
|
130
|
+
// \u0001, so BOTH forms must be checked.
|
|
131
|
+
function hasMarkerCollision(text) {
|
|
132
|
+
return text.indexOf(MARK) !== -1 || text.indexOf(MARK_ESC) !== -1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Count non-overlapping occurrences using native indexOf (one linear scan).
|
|
136
|
+
function countOcc(hay, needle) {
|
|
137
|
+
let n = 0, i = 0;
|
|
138
|
+
const step = needle.length;
|
|
139
|
+
for (;;) {
|
|
140
|
+
const p = hay.indexOf(needle, i);
|
|
141
|
+
if (p === -1) return n;
|
|
142
|
+
n++; i = p + step;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ANONYMOUS fast path — returns undefined if not applicable.
|
|
147
|
+
function tryAnonymous(text, pre, name, lazy) {
|
|
148
|
+
const body = text.slice(pre.end);
|
|
149
|
+
// Cheap pre-check: this path only handles a root array of instances.
|
|
150
|
+
// Without it we would pay a full transform + JSON.parse before finding out.
|
|
151
|
+
if (body.charCodeAt(0) !== 91 /* [ */ || body.charCodeAt(1) !== name.charCodeAt(0)) return undefined;
|
|
152
|
+
const open = name + '(';
|
|
153
|
+
const t = body.replaceAll(open, '[').replaceAll(')', ']');
|
|
154
|
+
let raw;
|
|
155
|
+
try { raw = JSON.parse(t); } catch (e) { return undefined; }
|
|
156
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
157
|
+
if (countOcc(body, open) !== raw.length) { stats.anonReject++; return undefined; } // nested instances
|
|
158
|
+
const cls = pre.classes[name];
|
|
159
|
+
const nf = cls.f.length;
|
|
160
|
+
const dictFlags = cls.d.map(d => (d === null ? 0 : 1));
|
|
161
|
+
const out = new Array(raw.length);
|
|
162
|
+
if (lazy) {
|
|
163
|
+
const L = getLazyClass(cls.f, dictFlags, 0)(cls.d);
|
|
164
|
+
for (let k = 0; k < raw.length; k++) {
|
|
165
|
+
const row = raw[k];
|
|
166
|
+
if (!Array.isArray(row) || row.length !== nf) { stats.anonReject++; return undefined; }
|
|
167
|
+
out[k] = new L(row);
|
|
168
|
+
}
|
|
169
|
+
stats.anonRoot++; stats.lazyRows++;
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
const ctor = getCtorFactory(cls.f, dictFlags, 0)(cls.d);
|
|
173
|
+
for (let k = 0; k < raw.length; k++) {
|
|
174
|
+
const row = raw[k];
|
|
175
|
+
if (!Array.isArray(row) || row.length !== nf) { stats.anonReject++; return undefined; }
|
|
176
|
+
out[k] = ctor(row);
|
|
177
|
+
}
|
|
178
|
+
stats.anonRoot++;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ANONYMOUS-DEEP — same marker-free transform, but for documents whose rows
|
|
183
|
+
// live somewhere inside the tree (the very common API envelope
|
|
184
|
+
// {meta:{...}, data:[...rows]}). After the transform a row is just an array,
|
|
185
|
+
// so we find "row runs" (arrays whose every element is an array of exactly
|
|
186
|
+
// nFields) and convert them. Soundness comes from a global count guard:
|
|
187
|
+
// every real instance is necessarily a candidate, so if the number of
|
|
188
|
+
// converted rows equals the number of `C0(` occurrences in the source, the
|
|
189
|
+
// candidate set is exactly the real set. Any mismatch -> caller falls back.
|
|
190
|
+
function tryAnonymousDeep(text, pre, name, lazy) {
|
|
191
|
+
const body = text.slice(pre.end);
|
|
192
|
+
const open = name + '(';
|
|
193
|
+
const expect = countOcc(body, open);
|
|
194
|
+
if (expect === 0) return undefined;
|
|
195
|
+
const t = body.replaceAll(open, '[').replaceAll(')', ']');
|
|
196
|
+
let raw;
|
|
197
|
+
try { raw = JSON.parse(t); } catch (e) { return undefined; }
|
|
198
|
+
const cls = pre.classes[name];
|
|
199
|
+
const nf = cls.f.length;
|
|
200
|
+
const dictFlags = cls.d.map(d => (d === null ? 0 : 1));
|
|
201
|
+
const make = lazy
|
|
202
|
+
? (() => { const L = getLazyClass(cls.f, dictFlags, 0)(cls.d); return (r) => new L(r); })()
|
|
203
|
+
: getCtorFactory(cls.f, dictFlags, 0)(cls.d);
|
|
204
|
+
let converted = 0;
|
|
205
|
+
|
|
206
|
+
function walk(v) {
|
|
207
|
+
if (Array.isArray(v)) {
|
|
208
|
+
const n = v.length;
|
|
209
|
+
// is this a run of rows?
|
|
210
|
+
let isRun = n > 0;
|
|
211
|
+
for (let k = 0; k < n; k++) {
|
|
212
|
+
const e = v[k];
|
|
213
|
+
if (!Array.isArray(e) || e.length !== nf) { isRun = false; break; }
|
|
214
|
+
}
|
|
215
|
+
if (isRun) {
|
|
216
|
+
const out = new Array(n);
|
|
217
|
+
for (let k = 0; k < n; k++) { out[k] = make(v[k]); converted++; }
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
for (let k = 0; k < n; k++) {
|
|
221
|
+
const e = v[k];
|
|
222
|
+
if (e !== null && typeof e === 'object') v[k] = walk(e);
|
|
223
|
+
}
|
|
224
|
+
return v;
|
|
225
|
+
}
|
|
226
|
+
for (const k in v) {
|
|
227
|
+
const e = v[k];
|
|
228
|
+
if (e !== null && typeof e === 'object') v[k] = walk(e);
|
|
229
|
+
}
|
|
230
|
+
return v;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (raw === null || typeof raw !== 'object') return undefined;
|
|
234
|
+
const out = walk(raw);
|
|
235
|
+
if (converted !== expect) { stats.deepReject++; return undefined; } // ambiguous -> fall back
|
|
236
|
+
stats.anonDeep++;
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function decode(text) {
|
|
241
|
+
const pre = parsePrelude(text);
|
|
242
|
+
// Table-mode documents are plain JSON and must never be text-transformed:
|
|
243
|
+
// their rows are serialized by JSON.stringify, so the transform would corrupt
|
|
244
|
+
// any string containing a bracket.
|
|
245
|
+
if (pre.tables && pre.tables.length > 0) {
|
|
246
|
+
try { return applyTables(JSON.parse(text.slice(pre.end)), pre); }
|
|
247
|
+
catch (e) { return parseFast(text); }
|
|
248
|
+
}
|
|
249
|
+
const names = Object.keys(pre.classes);
|
|
250
|
+
if (names.length === 0) {
|
|
251
|
+
// plain JSON body — hand it straight to the native parser
|
|
252
|
+
stats.plainJson++;
|
|
253
|
+
return JSON.parse(pre.end === 0 ? text : text.slice(pre.end));
|
|
254
|
+
}
|
|
255
|
+
if (hasMarkerCollision(text)) { stats.fallbackScanner++; return parseFast(text); }
|
|
256
|
+
if (names.length === 1) {
|
|
257
|
+
const fastOut = tryAnonymous(text, pre, names[0], false);
|
|
258
|
+
if (fastOut !== undefined) return fastOut;
|
|
259
|
+
const deepOut = tryAnonymousDeep(text, pre, names[0], false);
|
|
260
|
+
if (deepOut !== undefined) return deepOut;
|
|
261
|
+
}
|
|
262
|
+
const t = transform(text, pre);
|
|
263
|
+
let raw;
|
|
264
|
+
try { raw = JSON.parse(t); }
|
|
265
|
+
catch (e) { stats.fallbackScanner++; return parseFast(text); } // non-canonical body
|
|
266
|
+
const ctors = buildCtors(pre.classes, 1);
|
|
267
|
+
|
|
268
|
+
// uniform-root fast path: all top-level elements are flat marker rows of the
|
|
269
|
+
// same class -> tight ctor loop instead of the generic recursive walk.
|
|
270
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
271
|
+
const h0 = raw[0];
|
|
272
|
+
if (Array.isArray(h0) && typeof h0[0] === 'string' && h0[0].charCodeAt(0) === 1) {
|
|
273
|
+
const marker = h0[0];
|
|
274
|
+
const ctor = ctors[marker.slice(1)];
|
|
275
|
+
let flat = true;
|
|
276
|
+
for (let k = 0; k < raw.length; k++) {
|
|
277
|
+
const row = raw[k];
|
|
278
|
+
if (!Array.isArray(row) || row[0] !== marker) { flat = false; break; }
|
|
279
|
+
for (let q = 1; q < row.length; q++) {
|
|
280
|
+
const e = row[q];
|
|
281
|
+
if (e !== null && typeof e === 'object') { flat = false; break; }
|
|
282
|
+
}
|
|
283
|
+
if (!flat) break;
|
|
284
|
+
}
|
|
285
|
+
if (flat) {
|
|
286
|
+
const out = new Array(raw.length);
|
|
287
|
+
for (let k = 0; k < raw.length; k++) out[k] = ctor(raw[k]);
|
|
288
|
+
stats.markerTight++;
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Tight loop for a homogeneous run of flat marker rows at ANY depth —
|
|
295
|
+
// this is what makes the common API envelope {data:[...rows], meta:{}}
|
|
296
|
+
// as fast as a bare root array. Returns null when not applicable.
|
|
297
|
+
function tightRows(v) {
|
|
298
|
+
const n = v.length;
|
|
299
|
+
if (n === 0) return null;
|
|
300
|
+
const h0 = v[0];
|
|
301
|
+
if (!Array.isArray(h0)) return null;
|
|
302
|
+
const marker = h0[0];
|
|
303
|
+
if (typeof marker !== 'string' || marker.charCodeAt(0) !== 1) return null;
|
|
304
|
+
for (let k = 0; k < n; k++) {
|
|
305
|
+
const row = v[k];
|
|
306
|
+
if (!Array.isArray(row) || row[0] !== marker) return null;
|
|
307
|
+
for (let q = 1; q < row.length; q++) {
|
|
308
|
+
const e = row[q];
|
|
309
|
+
if (e !== null && typeof e === 'object') return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const ctor = ctors[marker.slice(1)];
|
|
313
|
+
const out = new Array(n);
|
|
314
|
+
for (let k = 0; k < n; k++) out[k] = ctor(v[k]);
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// generic post-order fixup
|
|
319
|
+
function fix(v) {
|
|
320
|
+
if (Array.isArray(v)) {
|
|
321
|
+
const n = v.length;
|
|
322
|
+
if (n > 0) {
|
|
323
|
+
const h = v[0];
|
|
324
|
+
if (typeof h === 'string' && h.charCodeAt(0) === 1) {
|
|
325
|
+
for (let k = 1; k < n; k++) {
|
|
326
|
+
const e = v[k];
|
|
327
|
+
if (e !== null && typeof e === 'object') v[k] = fix(e);
|
|
328
|
+
}
|
|
329
|
+
return ctors[h.slice(1)](v);
|
|
330
|
+
}
|
|
331
|
+
const tight = tightRows(v);
|
|
332
|
+
if (tight !== null) return tight;
|
|
333
|
+
}
|
|
334
|
+
for (let k = 0; k < n; k++) {
|
|
335
|
+
const e = v[k];
|
|
336
|
+
if (e !== null && typeof e === 'object') v[k] = fix(e);
|
|
337
|
+
}
|
|
338
|
+
return v;
|
|
339
|
+
}
|
|
340
|
+
for (const k in v) {
|
|
341
|
+
const e = v[k];
|
|
342
|
+
if (e !== null && typeof e === 'object') v[k] = fix(e);
|
|
343
|
+
}
|
|
344
|
+
return v;
|
|
345
|
+
}
|
|
346
|
+
if (raw !== null && typeof raw === 'object') { stats.markerWalk++; return fix(raw); }
|
|
347
|
+
return raw;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// LAZY variant: rows are thin wrappers with prototype getters. Parse cost is
|
|
351
|
+
// transform + JSON.parse + one wrapper alloc per row; field access pays a
|
|
352
|
+
// getter indirection later. NOTE: fields live on the prototype, so Object.keys
|
|
353
|
+
// on a row returns ['_r'] — use toJSON()/JSON.stringify for a plain object.
|
|
354
|
+
function decodeLazy(text) {
|
|
355
|
+
const pre = parsePrelude(text);
|
|
356
|
+
if (pre.tables && pre.tables.length > 0) return decode(text);
|
|
357
|
+
const names = Object.keys(pre.classes);
|
|
358
|
+
if (names.length === 0) return JSON.parse(pre.end === 0 ? text : text.slice(pre.end));
|
|
359
|
+
if (hasMarkerCollision(text)) return parseFast(text);
|
|
360
|
+
if (names.length === 1) {
|
|
361
|
+
const fastOut = tryAnonymous(text, pre, names[0], true);
|
|
362
|
+
if (fastOut !== undefined) return fastOut;
|
|
363
|
+
}
|
|
364
|
+
const t = transform(text, pre);
|
|
365
|
+
let raw;
|
|
366
|
+
try { raw = JSON.parse(t); } catch (e) { return parseFast(text); }
|
|
367
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
368
|
+
const h0 = raw[0];
|
|
369
|
+
if (Array.isArray(h0) && typeof h0[0] === 'string' && h0[0].charCodeAt(0) === 1) {
|
|
370
|
+
const cname = h0[0].slice(1);
|
|
371
|
+
const cls = pre.classes[cname];
|
|
372
|
+
const dictFlags = cls.d.map(d => (d === null ? 0 : 1));
|
|
373
|
+
const L = getLazyClass(cls.f, dictFlags, 1)(cls.d);
|
|
374
|
+
const marker = h0[0];
|
|
375
|
+
let uniform = true;
|
|
376
|
+
const out = new Array(raw.length);
|
|
377
|
+
for (let k = 0; k < raw.length; k++) {
|
|
378
|
+
const row = raw[k];
|
|
379
|
+
if (!Array.isArray(row) || row[0] !== marker) { uniform = false; break; }
|
|
380
|
+
// Rows must be FLAT. A nested class instance inside a field is still a
|
|
381
|
+
// raw marker array at this point, and the lazy wrapper does not walk
|
|
382
|
+
// into fields — returning it would leak ["\u0001C1",...] to the caller.
|
|
383
|
+
for (let q = 1; q < row.length; q++) {
|
|
384
|
+
const e = row[q];
|
|
385
|
+
if (e !== null && typeof e === 'object') { uniform = false; break; }
|
|
386
|
+
}
|
|
387
|
+
if (!uniform) break;
|
|
388
|
+
out[k] = new L(row);
|
|
389
|
+
}
|
|
390
|
+
if (uniform) { stats.lazyRows++; return out; }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return decode(text);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export { decode, decodeLazy, stats };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// WASM decode path for all-numeric TRON class rows.
|
|
2
|
+
//
|
|
3
|
+
// The WASM scanner validates as it goes and returns a negative code for
|
|
4
|
+
// anything it will not represent exactly (strings, booleans, null, nesting,
|
|
5
|
+
// numbers wider than the exact Clinger range). So JS never has to *prove* a
|
|
6
|
+
// payload is numeric in advance — it tries, and any rejection falls back.
|
|
7
|
+
// That makes the fast path correct by construction rather than by heuristic.
|
|
8
|
+
//
|
|
9
|
+
// Dictionary-encoded columns are integers on the wire, so enum-encoded string
|
|
10
|
+
// and boolean columns pass straight through and are mapped back by index here.
|
|
11
|
+
//
|
|
12
|
+
// Exposes:
|
|
13
|
+
// available() -> bool
|
|
14
|
+
// decode(text) -> array of row objects, or undefined if not eligible
|
|
15
|
+
// decodeColumnar(text) -> {fields, rows, cols, tape:Float64Array} or undefined
|
|
16
|
+
// (zero-copy-ish typed array; the 4x win)
|
|
17
|
+
|
|
18
|
+
import { parsePrelude } from './tron.js';
|
|
19
|
+
import { WASM_BASE64 } from './wasm-bytes.js';
|
|
20
|
+
|
|
21
|
+
// The binary is tiny (~1.7 KB), so it ships embedded as base64 — no `fs`, no
|
|
22
|
+
// asset plumbing, works identically in Node, Bun, and browser bundles.
|
|
23
|
+
// setWasmBinary() still overrides it (e.g. to test a newer scanner build).
|
|
24
|
+
let _wasmBytes = null;
|
|
25
|
+
function setWasmBinary(bytes) { _wasmBytes = bytes; initTried = false; ready = false; }
|
|
26
|
+
|
|
27
|
+
let mod = null, inst = null, exp = null, ready = false, initTried = false;
|
|
28
|
+
let inPtr = 0, inCap = 0, outPtr = 0, outCap = 0;
|
|
29
|
+
const encoder = new TextEncoder();
|
|
30
|
+
|
|
31
|
+
function init() {
|
|
32
|
+
if (initTried) return ready;
|
|
33
|
+
initTried = true;
|
|
34
|
+
try {
|
|
35
|
+
let buf = _wasmBytes;
|
|
36
|
+
if (buf === null) buf = decodeBase64(WASM_BASE64);
|
|
37
|
+
if (typeof WebAssembly === 'undefined') { ready = false; return false; }
|
|
38
|
+
mod = new WebAssembly.Module(buf);
|
|
39
|
+
inst = new WebAssembly.Instance(mod, { env: { abort() { throw new Error('wasm abort'); } } });
|
|
40
|
+
exp = inst.exports;
|
|
41
|
+
ready = true;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
ready = false;
|
|
44
|
+
}
|
|
45
|
+
return ready;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function decodeBase64(b64) {
|
|
49
|
+
if (typeof Buffer !== 'undefined') return Buffer.from(b64, 'base64');
|
|
50
|
+
const bin = atob(b64);
|
|
51
|
+
const out = new Uint8Array(bin.length);
|
|
52
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function available() { return init(); }
|
|
57
|
+
|
|
58
|
+
function ensureIn(bytes) {
|
|
59
|
+
if (bytes > inCap) {
|
|
60
|
+
const want = Math.max(bytes, inCap * 2, 1 << 16);
|
|
61
|
+
inPtr = exp.allocate(want);
|
|
62
|
+
inCap = want;
|
|
63
|
+
}
|
|
64
|
+
return inPtr;
|
|
65
|
+
}
|
|
66
|
+
function ensureOut(floats) {
|
|
67
|
+
if (floats > outCap) {
|
|
68
|
+
const want = Math.max(floats, outCap * 2, 1 << 14);
|
|
69
|
+
outPtr = exp.allocate(want * 8);
|
|
70
|
+
outCap = want;
|
|
71
|
+
}
|
|
72
|
+
return outPtr;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// True if the first row contains only characters that can appear in a numeric
|
|
76
|
+
// row. Conservative: any quote/letter/bracket makes this false.
|
|
77
|
+
function probeNumericRow(body) {
|
|
78
|
+
const open = body.indexOf('(');
|
|
79
|
+
if (open === -1) return false;
|
|
80
|
+
const close = body.indexOf(')', open);
|
|
81
|
+
if (close === -1) return false;
|
|
82
|
+
for (let i = open + 1; i < close; i++) {
|
|
83
|
+
const c = body.charCodeAt(i);
|
|
84
|
+
if (c >= 48 && c <= 57) continue; // 0-9
|
|
85
|
+
if (c === 43 || c === 45 || c === 46) continue; // + - .
|
|
86
|
+
if (c === 101 || c === 69) continue; // e E
|
|
87
|
+
if (c === 44 || c === 32) continue; // , space
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return close > open + 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Shared core: returns {cls, tape, count} or undefined.
|
|
94
|
+
function scan(text) {
|
|
95
|
+
if (!init()) return undefined;
|
|
96
|
+
const pre = parsePrelude(text);
|
|
97
|
+
const names = Object.keys(pre.classes);
|
|
98
|
+
if (names.length !== 1) return undefined; // single-class documents only
|
|
99
|
+
const cls = pre.classes[names[0]];
|
|
100
|
+
const nf = cls.f.length;
|
|
101
|
+
if (nf === 0) return undefined;
|
|
102
|
+
|
|
103
|
+
const body = text.slice(pre.end);
|
|
104
|
+
if (body.charCodeAt(0) !== 91 /* [ */) return undefined;
|
|
105
|
+
// Cheap pre-check on the FIRST row only. The WASM validator is what
|
|
106
|
+
// guarantees correctness; this exists purely to avoid paying for a UTF-8
|
|
107
|
+
// encode of the whole body on payloads that obviously contain strings.
|
|
108
|
+
if (!probeNumericRow(body)) return undefined;
|
|
109
|
+
|
|
110
|
+
const bytes = encoder.encode(body);
|
|
111
|
+
const ip = ensureIn(bytes.length);
|
|
112
|
+
new Uint8Array(exp.memory.buffer, ip, bytes.length).set(bytes);
|
|
113
|
+
|
|
114
|
+
// capacity guess: at most one float per 2 bytes of body
|
|
115
|
+
const cap = Math.max(nf, Math.ceil(bytes.length / 2));
|
|
116
|
+
const op = ensureOut(cap);
|
|
117
|
+
|
|
118
|
+
const count = exp.parseTronTape(ip, bytes.length, op, outCap);
|
|
119
|
+
if (count < 0) return undefined; // rejected -> caller falls back
|
|
120
|
+
if (count % nf !== 0) return undefined;
|
|
121
|
+
const tape = new Float64Array(exp.memory.buffer, op, count);
|
|
122
|
+
return { cls, nf, tape, count, rows: count / nf };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decode(text) {
|
|
126
|
+
const r = scan(text);
|
|
127
|
+
if (r === undefined) return undefined;
|
|
128
|
+
const { cls, nf, tape, rows } = r;
|
|
129
|
+
const fields = cls.f, dicts = cls.d;
|
|
130
|
+
const out = new Array(rows);
|
|
131
|
+
let hasDict = false;
|
|
132
|
+
for (let k = 0; k < nf; k++) if (dicts[k] !== null) { hasDict = true; break; }
|
|
133
|
+
let p = 0;
|
|
134
|
+
if (!hasDict) {
|
|
135
|
+
for (let i = 0; i < rows; i++) {
|
|
136
|
+
const o = {};
|
|
137
|
+
for (let k = 0; k < nf; k++) o[fields[k]] = tape[p++];
|
|
138
|
+
out[i] = o;
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
for (let i = 0; i < rows; i++) {
|
|
142
|
+
const o = {};
|
|
143
|
+
for (let k = 0; k < nf; k++) {
|
|
144
|
+
const d = dicts[k];
|
|
145
|
+
const v = tape[p++];
|
|
146
|
+
o[fields[k]] = d === null ? v : d[v];
|
|
147
|
+
}
|
|
148
|
+
out[i] = o;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Columnar view — the mode where WASM is ~4x faster than JSON.parse, because
|
|
155
|
+
// nothing is materialized as JS objects at all. `tape` aliases WASM memory and
|
|
156
|
+
// is invalidated by the next decode call; pass copy=true to detach it.
|
|
157
|
+
function decodeColumnar(text, copy) {
|
|
158
|
+
const r = scan(text);
|
|
159
|
+
if (r === undefined) return undefined;
|
|
160
|
+
return {
|
|
161
|
+
fields: r.cls.f.slice(),
|
|
162
|
+
dicts: r.cls.d,
|
|
163
|
+
rows: r.rows,
|
|
164
|
+
cols: r.nf,
|
|
165
|
+
tape: copy ? new Float64Array(r.tape) : r.tape,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { available, decode, decodeColumnar, setWasmBinary };
|