@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,486 @@
1
+ // Optimized TRON encoder.
2
+ //
3
+ // Profiling the original showed where the 6x went (tabular-10k, JSON.stringify
4
+ // = 3.98ms baseline):
5
+ // emit via array.push + join('') 15.24 ms <-- dominant
6
+ // shape scan (keys.join sig) 3.48 ms
7
+ // dict statistics pass 1.89 ms
8
+ // + a second shape-signature computation during emit (~6.5 ms)
9
+ // and that plain string concatenation emits the same output in 6.72 ms — V8
10
+ // ropes beat array-join here. Hand-rolled string quoting was *slower* than
11
+ // JSON.stringify, so quoting stays native.
12
+ //
13
+ // The big win is the REVERSE TRAMPOLINE, mirroring the decode side: project
14
+ // rows to plain arrays, let native JSON.stringify serialize them, then rewrite
15
+ // row brackets with native replaceAll:
16
+ //
17
+ // [[1,"Ada"],[2,"Bob"]] -> [C0(1,"Ada"),C0(2,"Bob")]
18
+ //
19
+ // SAFETY. Two properties must hold for that text rewrite to be sound, and both
20
+ // are verified by counting on the serialized text rather than by inspecting
21
+ // every string value (cheaper, and exact):
22
+ // 1. `],[` occurs exactly rows-1 times. Every real row separator contributes
23
+ // one, so any extra means a string contained the sequence.
24
+ // 2. `(` and `)` do not occur at all. Canonical TRON escapes parens inside
25
+ // strings so the decoder can treat every raw paren as structural;
26
+ // JSON.stringify does not escape them, so their absence must be checked.
27
+ // If either fails we fall back to the general encoder, which escapes properly.
28
+
29
+ import * as TBL from './tron-table.js';
30
+
31
+ const MIN_USES = 2;
32
+ // Shape-signature separator. MUST be identical everywhere a signature is built
33
+ // or looked up; a mismatch silently turns lookups into permanent misses.
34
+ const SIG_SEP = '\u0000';
35
+ const DICT_MAX_UNIQUES = 64;
36
+ const MIN_CHUNK = 8; // below this a table declaration costs more than it saves
37
+
38
+ // ---------------------------------------------------------------- helpers
39
+ function isPlainObject(v) {
40
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
41
+ }
42
+ function isScalar(v) {
43
+ return v === null || typeof v !== 'object';
44
+ }
45
+
46
+ // Field name may be bare in a class declaration only if a plain identifier.
47
+ function encFieldName(k) {
48
+ if (k.length === 0) return JSON.stringify(k);
49
+ const f = k.charCodeAt(0);
50
+ if (!((f >= 65 && f <= 90) || (f >= 97 && f <= 122) || f === 95)) return JSON.stringify(k);
51
+ for (let i = 1; i < k.length; i++) {
52
+ const c = k.charCodeAt(i);
53
+ if (!((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95 || c === 46)) {
54
+ return JSON.stringify(k);
55
+ }
56
+ }
57
+ return k;
58
+ }
59
+
60
+ function encEnumVal(v) {
61
+ if (typeof v === 'boolean') return v ? 'true' : 'false';
62
+ if (v.length === 0 || v === 'true' || v === 'false' || v === 'null') return JSON.stringify(v);
63
+ const f = v.charCodeAt(0), l = v.charCodeAt(v.length - 1);
64
+ if (f === 32 || l === 32 || f === 9 || l === 9 || f === 34) return JSON.stringify(v);
65
+ for (let k = 0; k < v.length; k++) {
66
+ const c = v.charCodeAt(k);
67
+ if (c === 44 || c === 34 || c === 92 || c < 32) return JSON.stringify(v);
68
+ }
69
+ if (!Number.isNaN(Number(v))) return JSON.stringify(v);
70
+ return v;
71
+ }
72
+
73
+ // JSON string quoting + paren escaping (canonical TRON).
74
+ function encStr(str) {
75
+ const j = JSON.stringify(str);
76
+ if (str.indexOf('(') < 0 && str.indexOf(')') < 0) return j;
77
+ let o = '';
78
+ for (let k = 0; k < j.length; k++) {
79
+ const c = j.charCodeAt(k);
80
+ if (c === 40) o += '\\u0028';
81
+ else if (c === 41) o += '\\u0029';
82
+ else o += j[k];
83
+ }
84
+ return o;
85
+ }
86
+
87
+ function numStr(v) {
88
+ return Number.isFinite(v) ? String(v) : 'null';
89
+ }
90
+
91
+ // count non-overlapping occurrences (native indexOf loop)
92
+ function countOcc(hay, needle) {
93
+ let n = 0, i = 0;
94
+ const step = needle.length;
95
+ for (;;) {
96
+ const p = hay.indexOf(needle, i);
97
+ if (p === -1) return n;
98
+ n++; i = p + step;
99
+ }
100
+ }
101
+
102
+
103
+ // In table mode rows are serialized by JSON.stringify, which does not escape
104
+ // parens. Canonical TRON requires every RAW paren in a document to be
105
+ // structural (the trampoline relies on it), so escape them here. In plain JSON
106
+ // a paren can only occur inside a string, so a blanket replace is safe and the
107
+ // result is still valid JSON (\u0028 decodes back to "(").
108
+ function escapeParens(t) {
109
+ if (t.indexOf('(') === -1 && t.indexOf(')') === -1) return t;
110
+ return t.replaceAll('(', '\\u0028').replaceAll(')', '\\u0029');
111
+ }
112
+
113
+ // ---------------------------------------------------------------- fast path
114
+ // Root array of >=2 uniform, flat objects. Returns undefined if inapplicable.
115
+ function tryFastTable(value, useDict, useTable, useTableNested) {
116
+ if (!Array.isArray(value)) return undefined;
117
+ const n = value.length;
118
+ if (n < MIN_USES) return undefined;
119
+ const r0 = value[0];
120
+ if (!isPlainObject(r0)) return undefined;
121
+ const keys = Object.keys(r0);
122
+ const nf = keys.length;
123
+ if (nf === 0) return undefined;
124
+
125
+ // Pass 1 — verify uniform + flat, and gather dictionary statistics.
126
+ const stats = useDict ? new Array(nf) : null;
127
+ if (useDict) for (let k = 0; k < nf; k++) stats[k] = { m: new Map(), ok: true, total: 0, bytes: 0, kind: undefined };
128
+
129
+ for (let i = 0; i < n; i++) {
130
+ const r = value[i];
131
+ if (!isPlainObject(r)) return undefined;
132
+ const rk = Object.keys(r);
133
+ if (rk.length !== nf) return undefined;
134
+ for (let k = 0; k < nf; k++) if (rk[k] !== keys[k]) return undefined;
135
+ for (let k = 0; k < nf; k++) {
136
+ const v = r[keys[k]];
137
+ // In table mode rows are plain JSON arrays, so a nested object/array is
138
+ // fine — it just serializes as JSON. Outside table mode a nested value
139
+ // would need the text transform, so it goes to the general path.
140
+ // Nested values are only allowed when the caller opts into nested tables:
141
+ // they serialize as plain JSON, which costs the token savings those inner
142
+ // shapes would have got from their own class. Flat rows are a pure win.
143
+ // Objects with toJSON (Dates) are fine: rows are serialized by native
144
+ // JSON.stringify further down, which resolves them exactly like JSON.
145
+ if (!isScalar(v) && typeof v.toJSON !== 'function' && !useTableNested) return undefined;
146
+ if (useDict) {
147
+ const st = stats[k];
148
+ if (!st.ok) continue;
149
+ const vt = typeof v;
150
+ if (vt !== 'string' && vt !== 'boolean') { st.ok = false; st.m = null; continue; }
151
+ if (st.kind === undefined) st.kind = vt;
152
+ else if (st.kind !== vt) { st.ok = false; st.m = null; continue; }
153
+ st.total++; st.bytes += (vt === 'string' ? v.length : 5);
154
+ const c = st.m.get(v);
155
+ if (c === undefined) {
156
+ if (st.m.size >= DICT_MAX_UNIQUES) { st.ok = false; st.m = null; continue; }
157
+ st.m.set(v, 1);
158
+ } else st.m.set(v, c + 1);
159
+ }
160
+ }
161
+ }
162
+
163
+ // Decide dictionaries.
164
+ let dictMaps = null;
165
+ const enumDecls = [];
166
+ const declFields = new Array(nf);
167
+ const enumBySig = new Map();
168
+ let en = 0;
169
+ for (let k = 0; k < nf; k++) {
170
+ declFields[k] = encFieldName(keys[k]);
171
+ if (!useDict) continue;
172
+ const st = stats[k];
173
+ if (st.ok && st.m && st.m.size >= 1 && n >= 3 * st.m.size && st.total > 0 && (st.bytes / st.total) >= 2) {
174
+ const values = Array.from(st.m.keys());
175
+ const vsig = JSON.stringify(values);
176
+ let ename = enumBySig.get(vsig);
177
+ if (ename === undefined) {
178
+ ename = 'E' + (en++);
179
+ enumBySig.set(vsig, ename);
180
+ enumDecls.push('enum ' + ename + ': ' + values.map(encEnumVal).join(','));
181
+ }
182
+ if (!dictMaps) dictMaps = new Array(nf).fill(null);
183
+ const idx = new Map();
184
+ for (let q = 0; q < values.length; q++) idx.set(values[q], q);
185
+ dictMaps[k] = idx;
186
+ declFields[k] = encFieldName(keys[k]) + '@' + ename;
187
+ }
188
+ }
189
+
190
+ const header = (enumDecls.length ? enumDecls.join('\n') + '\n' : '') +
191
+ 'class C0: ' + declFields.join(',') + '\n';
192
+
193
+ // Pass 2 — project to plain arrays so native JSON.stringify can serialize.
194
+ const proj = new Array(n);
195
+ for (let i = 0; i < n; i++) {
196
+ const r = value[i];
197
+ const a = new Array(nf);
198
+ if (dictMaps === null) {
199
+ for (let k = 0; k < nf; k++) a[k] = r[keys[k]];
200
+ } else {
201
+ for (let k = 0; k < nf; k++) {
202
+ const dm = dictMaps[k];
203
+ a[k] = dm === null ? r[keys[k]] : dm.get(r[keys[k]]);
204
+ }
205
+ }
206
+ proj[i] = a;
207
+ }
208
+
209
+ const t = JSON.stringify(proj);
210
+
211
+ // TABLE MODE: rows stay plain JSON arrays and a `table` declaration says
212
+ // where they live. The decoder then needs no text transform at all — and the
213
+ // repeated class name disappears from every row, so tokens drop too.
214
+ if (useTable) return header + 'table C0: $\n' + escapeParens(t);
215
+
216
+ // Soundness checks on the serialized text (see header comment).
217
+ if (t.indexOf('(') !== -1 || t.indexOf(')') !== -1) return undefined;
218
+ if (countOcc(t, '],[') !== n - 1) return undefined;
219
+
220
+ // [[a,b],[c,d]] -> [C0(a,b),C0(c,d)]
221
+ const body = '[C0(' + t.slice(2, t.length - 2).replaceAll('],[', '),C0(') + ')]';
222
+ return header + body;
223
+ }
224
+
225
+ // ---------------------------------------------------------------- general
226
+ // Same semantics as before, but emitting with string concatenation rather than
227
+ // array push + join (measured 2.3x faster on the emit phase).
228
+ function stringifyGeneral(value, useDict, useTable, useTableNested) {
229
+ const tableDecls = [];
230
+ let curPath = '$'; // path of the value currently being emitted (null = unaddressable)
231
+ const shapeCounts = new Map();
232
+ // Arrays proven uniform+flat during the scan; the emit phase reuses this
233
+ // proof instead of walking every row's keys a second time.
234
+ const uniformArrays = new WeakMap();
235
+
236
+ // A run of identical flat rows shares ONE signature: compute keys.join once
237
+ // for the whole array instead of once per row, and skip recursing into rows
238
+ // whose values are all scalars. On a 10k-row array that removes 10k string
239
+ // joins and 10k recursive calls.
240
+ function scanUniformArray(arr) {
241
+ const n = arr.length;
242
+ if (n < MIN_USES) return false;
243
+ const r0 = arr[0];
244
+ if (r0 === null || typeof r0 !== 'object' || Array.isArray(r0)) return false;
245
+ const keys = Object.keys(r0);
246
+ const nf = keys.length;
247
+ if (nf === 0) return false;
248
+ for (let i = 0; i < n; i++) {
249
+ const r = arr[i];
250
+ if (r === null || typeof r !== 'object' || Array.isArray(r)) return false;
251
+ const rk = Object.keys(r);
252
+ if (rk.length !== nf) return false;
253
+ for (let k = 0; k < nf; k++) if (rk[k] !== keys[k]) return false;
254
+ if (!(useTableNested && n >= MIN_CHUNK)) {
255
+ for (let k = 0; k < nf; k++) { const val = r[keys[k]]; if (val !== null && typeof val === 'object' && typeof val.toJSON !== 'function') return false; }
256
+ }
257
+ }
258
+ const sig = keys.join(SIG_SEP);
259
+ let e = shapeCounts.get(sig);
260
+ if (!e) {
261
+ e = { count: 0, keys, stats: null, cls: null };
262
+ if (useDict) {
263
+ e.stats = new Array(nf);
264
+ for (let k = 0; k < nf; k++) e.stats[k] = { m: new Map(), ok: true, total: 0, bytes: 0, kind: undefined };
265
+ }
266
+ shapeCounts.set(sig, e);
267
+ }
268
+ e.count += n;
269
+ if (useDict) {
270
+ for (let i = 0; i < n; i++) {
271
+ const r = arr[i];
272
+ for (let k = 0; k < nf; k++) {
273
+ const st = e.stats[k];
274
+ if (!st.ok) continue;
275
+ const val = r[keys[k]];
276
+ const vt = typeof val;
277
+ if (vt !== 'string' && vt !== 'boolean') { st.ok = false; st.m = null; continue; }
278
+ if (st.kind === undefined) st.kind = vt;
279
+ else if (st.kind !== vt) { st.ok = false; st.m = null; continue; }
280
+ st.total++; st.bytes += (vt === 'string' ? val.length : 5);
281
+ const c = st.m.get(val);
282
+ if (c === undefined) {
283
+ if (st.m.size >= DICT_MAX_UNIQUES) { st.ok = false; st.m = null; continue; }
284
+ st.m.set(val, 1);
285
+ } else st.m.set(val, c + 1);
286
+ }
287
+ }
288
+ }
289
+ uniformArrays.set(arr, keys);
290
+ return true;
291
+ }
292
+
293
+ (function scan(v) {
294
+ if (v === null || typeof v !== 'object') return;
295
+ // JSON.stringify semantics: an object with toJSON serializes as its
296
+ // toJSON() result (Dates -> ISO strings), so scan the resolved value.
297
+ if (typeof v.toJSON === 'function') { scan(v.toJSON()); return; }
298
+ if (Array.isArray(v)) {
299
+ if (scanUniformArray(v)) return;
300
+ for (let k = 0; k < v.length; k++) scan(v[k]);
301
+ return;
302
+ }
303
+ const keys = Object.keys(v);
304
+ const sig = keys.join(SIG_SEP);
305
+ let e = shapeCounts.get(sig);
306
+ if (!e) {
307
+ e = { count: 0, keys, stats: null, cls: null };
308
+ if (useDict) {
309
+ e.stats = new Array(keys.length);
310
+ for (let k = 0; k < keys.length; k++) e.stats[k] = { m: new Map(), ok: true, total: 0, bytes: 0, kind: undefined };
311
+ }
312
+ shapeCounts.set(sig, e);
313
+ }
314
+ e.count++;
315
+ if (useDict) {
316
+ const keysL = keys.length;
317
+ for (let k = 0; k < keysL; k++) {
318
+ const st = e.stats[k];
319
+ if (!st.ok) continue;
320
+ const val = v[keys[k]];
321
+ const vt = typeof val;
322
+ if (vt !== 'string' && vt !== 'boolean') { st.ok = false; st.m = null; continue; }
323
+ if (st.kind === undefined) st.kind = vt;
324
+ else if (st.kind !== vt) { st.ok = false; st.m = null; continue; }
325
+ st.total++; st.bytes += (vt === 'string' ? val.length : 5);
326
+ const c = st.m.get(val);
327
+ if (c === undefined) {
328
+ if (st.m.size >= DICT_MAX_UNIQUES) { st.ok = false; st.m = null; continue; }
329
+ st.m.set(val, 1);
330
+ } else st.m.set(val, c + 1);
331
+ }
332
+ }
333
+ for (let k = 0; k < keys.length; k++) scan(v[keys[k]]);
334
+ })(value);
335
+
336
+ const classDecls = [];
337
+ const enumDecls = [];
338
+ const enumBySig = new Map();
339
+ let cn = 0, en = 0;
340
+ for (const e of shapeCounts.values()) {
341
+ if (e.count >= MIN_USES && e.keys.length > 0) {
342
+ const name = 'C' + (cn++);
343
+ let dictMaps = null;
344
+ const declFields = new Array(e.keys.length);
345
+ for (let k = 0; k < e.keys.length; k++) {
346
+ declFields[k] = encFieldName(e.keys[k]);
347
+ if (!useDict) continue;
348
+ const st = e.stats[k];
349
+ if (st.ok && st.m && st.m.size >= 1 &&
350
+ e.count >= 3 * st.m.size && st.total > 0 && (st.bytes / st.total) >= 2) {
351
+ const values = Array.from(st.m.keys());
352
+ const vsig = JSON.stringify(values);
353
+ let ename = enumBySig.get(vsig);
354
+ if (ename === undefined) {
355
+ ename = 'E' + (en++);
356
+ enumBySig.set(vsig, ename);
357
+ enumDecls.push('enum ' + ename + ': ' + values.map(encEnumVal).join(','));
358
+ }
359
+ if (!dictMaps) dictMaps = new Array(e.keys.length).fill(null);
360
+ const idx = new Map();
361
+ for (let q = 0; q < values.length; q++) idx.set(values[q], q);
362
+ dictMaps[k] = idx;
363
+ declFields[k] = encFieldName(e.keys[k]) + '@' + ename;
364
+ }
365
+ }
366
+ e.cls = { name, keys: e.keys, dictMaps };
367
+ classDecls.push('class ' + name + ': ' + declFields.join(','));
368
+ }
369
+ }
370
+
371
+ // Apply the reverse trampoline to any sufficiently long run of uniform flat
372
+ // rows found anywhere in the tree — this is what makes the common API
373
+ // envelope {meta:{...}, data:[...10k rows]} fast, since its root is an object
374
+ // and so misses the top-level fast path. Returns null when inapplicable.
375
+ function tryRowChunk(arr) {
376
+ const n = arr.length;
377
+ if (n < MIN_CHUNK) return null;
378
+ // The scan already proved uniformity + flatness for this exact array.
379
+ const keys = uniformArrays.get(arr);
380
+ if (keys === undefined) return null;
381
+ const nf = keys.length;
382
+ const e = shapeCounts.get(keys.join(SIG_SEP));
383
+ const cls = e && e.cls;
384
+ if (!cls) return null;
385
+ const dm = cls.dictMaps;
386
+ const proj = new Array(n);
387
+ for (let i = 0; i < n; i++) {
388
+ const r = arr[i];
389
+ const a = new Array(nf);
390
+ if (dm === null) { for (let k = 0; k < nf; k++) a[k] = r[keys[k]]; }
391
+ else {
392
+ for (let k = 0; k < nf; k++) {
393
+ const dmk = dm[k];
394
+ a[k] = dmk === null ? r[keys[k]] : dmk.get(r[keys[k]]);
395
+ }
396
+ }
397
+ proj[i] = a;
398
+ }
399
+ const t = JSON.stringify(proj);
400
+ if (useTable && curPath !== null) {
401
+ if (t.length === 0) return null;
402
+ tableDecls.push('table ' + cls.name + ': ' + curPath);
403
+ return escapeParens(t); // plain JSON rows; decoder needs no transform
404
+ }
405
+ if (t.indexOf('(') !== -1 || t.indexOf(')') !== -1) return null;
406
+ if (countOcc(t, '],[') !== n - 1) return null;
407
+ const open = cls.name + '(';
408
+ return '[' + open + t.slice(2, t.length - 2).replaceAll('],[', '),' + open) + ')]';
409
+ }
410
+
411
+ let out = '';
412
+ function enc(v) {
413
+ if (v === null) { out += 'null'; return; }
414
+ const t = typeof v;
415
+ if (t === 'string') { out += encStr(v); return; }
416
+ if (t === 'number') { out += numStr(v); return; }
417
+ if (t === 'boolean') { out += v ? 'true' : 'false'; return; }
418
+ if (t === 'undefined') { out += 'null'; return; }
419
+ if (typeof v.toJSON === 'function' && t !== 'function') { enc(v.toJSON()); return; }
420
+ if (Array.isArray(v)) {
421
+ const chunk = tryRowChunk(v);
422
+ if (chunk !== null) { out += chunk; return; }
423
+ const base = curPath;
424
+ out += '[';
425
+ for (let k = 0; k < v.length; k++) {
426
+ if (k) out += ',';
427
+ curPath = base === null ? null : TBL.pathIndex(base, k);
428
+ enc(v[k]);
429
+ }
430
+ curPath = base;
431
+ out += ']';
432
+ return;
433
+ }
434
+ const keys = Object.keys(v);
435
+ const e = shapeCounts.get(keys.join(SIG_SEP));
436
+ const cls = e && e.cls;
437
+ if (cls) {
438
+ out += cls.name + '(';
439
+ const dm = cls.dictMaps;
440
+ const ck = cls.keys;
441
+ const base = curPath;
442
+ for (let k = 0; k < ck.length; k++) {
443
+ if (k) out += ',';
444
+ const dmk = dm === null ? null : dm[k];
445
+ if (dmk) out += dmk.get(v[ck[k]]);
446
+ else { curPath = null; enc(v[ck[k]]); } // inside an instantiation: not addressable
447
+ }
448
+ curPath = base;
449
+ out += ')';
450
+ } else {
451
+ const base = curPath;
452
+ out += '{';
453
+ for (let k = 0; k < keys.length; k++) {
454
+ if (k) out += ',';
455
+ out += encStr(keys[k]) + ':';
456
+ curPath = base === null ? null : TBL.pathKey(base, keys[k]);
457
+ enc(v[keys[k]]);
458
+ }
459
+ curPath = base;
460
+ out += '}';
461
+ }
462
+ }
463
+ enc(value);
464
+
465
+ if (classDecls.length === 0 && enumDecls.length === 0 && tableDecls.length === 0) return out;
466
+ return enumDecls.concat(classDecls).concat(tableDecls).join('\n') + '\n' + out;
467
+ }
468
+
469
+ // opts.table:
470
+ // true -> table declarations for FLAT uniform rows only. Pure win:
471
+ // faster to decode AND fewer tokens than class instantiation.
472
+ // 'nested' -> also table-ify rows containing nested objects/arrays. Faster
473
+ // still, but those inner shapes lose their own class savings,
474
+ // so tokens can get worse. Opt-in for a reason.
475
+ function stringify(value, opts) {
476
+ const useDict = !!(opts && opts.dict);
477
+ const useTableNested = !!(opts && opts.table === 'nested');
478
+ const useTable = !!(opts && opts.table);
479
+ if (!(opts && opts.noFastPath)) {
480
+ const fast = tryFastTable(value, useDict, useTable, useTableNested);
481
+ if (fast !== undefined) return fast;
482
+ }
483
+ return stringifyGeneral(value, useDict, useTable, useTableNested);
484
+ }
485
+
486
+ export { stringify, stringifyGeneral, tryFastTable };