@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.
@@ -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 };