@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
package/src/core/tron.js
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
// TRON — Token Reduced Object Notation (interpretation of the documented format).
|
|
2
|
+
// A superset of JSON that adds class declarations + positional instantiation:
|
|
3
|
+
//
|
|
4
|
+
// class User: id,name,role
|
|
5
|
+
// [User(1,"Ada","admin"), User(2,"Bob","user")]
|
|
6
|
+
//
|
|
7
|
+
// Repeated object shapes are hoisted to a class, so keys are written once.
|
|
8
|
+
// Goal: parse via a single-pass char-code scanner (no regex in the hot path).
|
|
9
|
+
|
|
10
|
+
// ---------- Character codes ----------
|
|
11
|
+
const TAB = 9, LF = 10, CR = 13, SPACE = 32,
|
|
12
|
+
QUOTE = 34, COMMA = 44, MINUS = 45, DOT = 46,
|
|
13
|
+
COLON = 58, LBRACKET = 91, RBRACKET = 93,
|
|
14
|
+
LBRACE = 123, RBRACE = 125, LPAREN = 40, RPAREN = 41,
|
|
15
|
+
BACKSLASH = 92, ZERO = 48, NINE = 57, PLUS = 43,
|
|
16
|
+
n_e = 101, n_E = 69;
|
|
17
|
+
|
|
18
|
+
// Exact powers of ten (10^0..10^22 are all exactly representable as f64).
|
|
19
|
+
const POW10 = new Float64Array(23);
|
|
20
|
+
for (let p = 0; p < 23; p++) POW10[p] = Math.pow(10, p);
|
|
21
|
+
|
|
22
|
+
function isWs(c) { return c === SPACE || c === TAB || c === LF || c === CR; }
|
|
23
|
+
function isDigit(c) { return c >= ZERO && c <= NINE; }
|
|
24
|
+
function isIdentStart(c) {
|
|
25
|
+
return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95;
|
|
26
|
+
}
|
|
27
|
+
function isIdentPart(c) {
|
|
28
|
+
return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95 || isDigit(c) || c === DOT;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ======================================================================
|
|
32
|
+
// PRELUDE — shared reader for `enum E: v1,v2` and `class C: f1,f2@E`
|
|
33
|
+
// declaration lines at the top of a document. Returns { classes, end }
|
|
34
|
+
// where classes maps name -> { f: fields[], d: (values[]|null)[] }.
|
|
35
|
+
// ======================================================================
|
|
36
|
+
function parsePrelude(s) {
|
|
37
|
+
const len = s.length;
|
|
38
|
+
let i = 0;
|
|
39
|
+
const enums = Object.create(null);
|
|
40
|
+
const classes = Object.create(null);
|
|
41
|
+
const tables = [];
|
|
42
|
+
function ws() { let c; while (i < len && ((c = s.charCodeAt(i)) === SPACE || c === TAB || c === LF || c === CR)) i++; }
|
|
43
|
+
function sp() { let c; while (i < len && ((c = s.charCodeAt(i)) === SPACE || c === TAB)) i++; }
|
|
44
|
+
function ident() { const st = i; while (i < len && isIdentPart(s.charCodeAt(i))) i++; return s.slice(st, i); }
|
|
45
|
+
function quotedStr() { // JSON string semantics; i at opening quote
|
|
46
|
+
i++; let out = '', seg = i;
|
|
47
|
+
while (i < len) {
|
|
48
|
+
const c = s.charCodeAt(i);
|
|
49
|
+
if (c === QUOTE) { out += s.slice(seg, i); i++; return out; }
|
|
50
|
+
if (c === BACKSLASH) {
|
|
51
|
+
out += s.slice(seg, i); i++;
|
|
52
|
+
const e = s.charCodeAt(i);
|
|
53
|
+
switch (e) {
|
|
54
|
+
case QUOTE: out += '"'; break; case BACKSLASH: out += '\\'; break;
|
|
55
|
+
case 47: out += '/'; break; case 110: out += '\n'; break;
|
|
56
|
+
case 116: out += '\t'; break; case 114: out += '\r'; break;
|
|
57
|
+
case 98: out += '\b'; break; case 102: out += '\f'; break;
|
|
58
|
+
case 117: out += String.fromCharCode(parseInt(s.slice(i + 1, i + 5), 16)); i += 4; break;
|
|
59
|
+
}
|
|
60
|
+
i++; seg = i;
|
|
61
|
+
} else i++;
|
|
62
|
+
}
|
|
63
|
+
return out + s.slice(seg);
|
|
64
|
+
}
|
|
65
|
+
ws();
|
|
66
|
+
for (;;) {
|
|
67
|
+
if (s.startsWith('enum ', i)) {
|
|
68
|
+
i += 5; sp();
|
|
69
|
+
const name = ident(); sp();
|
|
70
|
+
if (s.charCodeAt(i) !== COLON) break;
|
|
71
|
+
i++;
|
|
72
|
+
const values = [];
|
|
73
|
+
for (;;) {
|
|
74
|
+
sp();
|
|
75
|
+
if (s.charCodeAt(i) === QUOTE) values.push(quotedStr());
|
|
76
|
+
else {
|
|
77
|
+
const st = i; let c;
|
|
78
|
+
while (i < len && (c = s.charCodeAt(i)) !== COMMA && c !== LF && c !== CR) i++;
|
|
79
|
+
let e2 = i;
|
|
80
|
+
while (e2 > st && (s.charCodeAt(e2 - 1) === SPACE || s.charCodeAt(e2 - 1) === TAB)) e2--;
|
|
81
|
+
const raw = s.slice(st, e2);
|
|
82
|
+
// Bare tokens are typed: this is what lets a dictionary hold
|
|
83
|
+
// booleans (and therefore lets boolean columns reach the WASM path).
|
|
84
|
+
if (raw === 'true') values.push(true);
|
|
85
|
+
else if (raw === 'false') values.push(false);
|
|
86
|
+
else if (raw === 'null') values.push(null);
|
|
87
|
+
else values.push(raw);
|
|
88
|
+
}
|
|
89
|
+
sp();
|
|
90
|
+
if (s.charCodeAt(i) === COMMA) { i++; continue; }
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
enums[name] = values; ws();
|
|
94
|
+
} else if (s.startsWith('table ', i)) {
|
|
95
|
+
// `table C0: $.data` — declares that the value at <path> is an array of
|
|
96
|
+
// C0 rows serialized as plain JSON arrays. Lets the decoder skip the
|
|
97
|
+
// text transform entirely: JSON.parse the body, walk to the path, build.
|
|
98
|
+
i += 6; sp();
|
|
99
|
+
const cname = ident(); sp();
|
|
100
|
+
if (s.charCodeAt(i) !== COLON) break;
|
|
101
|
+
i++; sp();
|
|
102
|
+
const st = i;
|
|
103
|
+
while (i < len && s.charCodeAt(i) !== LF && s.charCodeAt(i) !== CR) i++;
|
|
104
|
+
let e2 = i;
|
|
105
|
+
while (e2 > st && (s.charCodeAt(e2 - 1) === SPACE || s.charCodeAt(e2 - 1) === TAB)) e2--;
|
|
106
|
+
tables.push({ cls: cname, path: s.slice(st, e2) });
|
|
107
|
+
ws();
|
|
108
|
+
} else if (s.startsWith('class ', i)) {
|
|
109
|
+
i += 6; sp();
|
|
110
|
+
const name = ident(); sp();
|
|
111
|
+
if (s.charCodeAt(i) !== COLON) break;
|
|
112
|
+
i++;
|
|
113
|
+
const fields = [], dicts = [];
|
|
114
|
+
for (;;) {
|
|
115
|
+
sp();
|
|
116
|
+
// Field names are quoted when they are not plain identifiers (spaces,
|
|
117
|
+
// colons, parens, commas, non-ASCII...). Without this, such keys
|
|
118
|
+
// corrupt the declaration line.
|
|
119
|
+
fields.push(s.charCodeAt(i) === QUOTE ? quotedStr() : ident());
|
|
120
|
+
if (s.charCodeAt(i) === 64 /* @ */) { i++; const ref = ident(); dicts.push(enums[ref] || null); }
|
|
121
|
+
else dicts.push(null);
|
|
122
|
+
sp();
|
|
123
|
+
if (s.charCodeAt(i) === COMMA) { i++; continue; }
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
classes[name] = { f: fields, d: dicts };
|
|
127
|
+
ws();
|
|
128
|
+
} else break;
|
|
129
|
+
}
|
|
130
|
+
return { classes, tables, end: i };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// ======================================================================
|
|
135
|
+
// applyTables — convert `table Ck: <path>` regions from plain JSON arrays
|
|
136
|
+
// into row objects. Shared by every decoder so a table-mode document decodes
|
|
137
|
+
// identically no matter which entry point is used.
|
|
138
|
+
// ======================================================================
|
|
139
|
+
const _tblCtorCache = new Map();
|
|
140
|
+
function _tblCtor(fields, dictFlags, dicts) {
|
|
141
|
+
const key = dictFlags.join('') + '|' + fields.join(',');
|
|
142
|
+
let fac = _tblCtorCache.get(key);
|
|
143
|
+
if (!fac) {
|
|
144
|
+
let body = 'return function(r){return {';
|
|
145
|
+
for (let k = 0; k < fields.length; k++) {
|
|
146
|
+
if (k) body += ',';
|
|
147
|
+
body += JSON.stringify(fields[k]) + ':' + (dictFlags[k] === 1 ? ('D[' + k + '][r[' + k + ']]') : ('r[' + k + ']'));
|
|
148
|
+
}
|
|
149
|
+
body += '}}';
|
|
150
|
+
fac = new Function('D', body);
|
|
151
|
+
_tblCtorCache.set(key, fac);
|
|
152
|
+
}
|
|
153
|
+
return fac(dicts);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseTablePath(p) {
|
|
157
|
+
if (p.charCodeAt(0) !== 36) return null;
|
|
158
|
+
const segs = []; let i = 1; const n = p.length;
|
|
159
|
+
while (i < n) {
|
|
160
|
+
const c = p.charCodeAt(i);
|
|
161
|
+
if (c === 46) {
|
|
162
|
+
i++; const st = i;
|
|
163
|
+
while (i < n) { const d = p.charCodeAt(i); if (d === 46 || d === 91) break; i++; }
|
|
164
|
+
segs.push(p.slice(st, i));
|
|
165
|
+
} else if (c === 91) {
|
|
166
|
+
i++;
|
|
167
|
+
if (p.charCodeAt(i) === 34) {
|
|
168
|
+
let out = '', seg = ++i;
|
|
169
|
+
for (;;) {
|
|
170
|
+
if (i >= n) return null;
|
|
171
|
+
const d = p.charCodeAt(i);
|
|
172
|
+
if (d === 34) { out += p.slice(seg, i); i++; break; }
|
|
173
|
+
if (d === 92) { out += p.slice(seg, i); i++; out += p[i]; i++; seg = i; }
|
|
174
|
+
else i++;
|
|
175
|
+
}
|
|
176
|
+
segs.push(out);
|
|
177
|
+
if (p.charCodeAt(i) !== 93) return null;
|
|
178
|
+
i++;
|
|
179
|
+
} else {
|
|
180
|
+
const st = i;
|
|
181
|
+
while (i < n && p.charCodeAt(i) !== 93) i++;
|
|
182
|
+
if (i >= n) return null;
|
|
183
|
+
segs.push(Number(p.slice(st, i)));
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
} else return null;
|
|
187
|
+
}
|
|
188
|
+
return segs;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function applyTables(root, pre) {
|
|
192
|
+
const tables = pre.tables;
|
|
193
|
+
if (!tables || tables.length === 0) return root;
|
|
194
|
+
for (let t = 0; t < tables.length; t++) {
|
|
195
|
+
const decl = tables[t];
|
|
196
|
+
const cls = pre.classes[decl.cls];
|
|
197
|
+
if (!cls) continue;
|
|
198
|
+
const segs = parseTablePath(decl.path);
|
|
199
|
+
if (segs === null) continue;
|
|
200
|
+
let parent = null, lastSeg = null, cur = root;
|
|
201
|
+
let ok = true;
|
|
202
|
+
for (let q = 0; q < segs.length; q++) {
|
|
203
|
+
if (cur === null || typeof cur !== 'object') { ok = false; break; }
|
|
204
|
+
parent = cur; lastSeg = segs[q]; cur = cur[segs[q]];
|
|
205
|
+
}
|
|
206
|
+
if (!ok || !Array.isArray(cur)) continue;
|
|
207
|
+
const fields = cls.f, dicts = cls.d, nf = fields.length;
|
|
208
|
+
const dictFlags = new Array(nf);
|
|
209
|
+
let hasDict = false;
|
|
210
|
+
for (let k = 0; k < nf; k++) { dictFlags[k] = dicts[k] === null ? 0 : 1; if (dictFlags[k]) hasDict = true; }
|
|
211
|
+
const ctor = _tblCtor(fields, dictFlags, hasDict ? dicts : null);
|
|
212
|
+
const n = cur.length;
|
|
213
|
+
const out = new Array(n);
|
|
214
|
+
let good = true;
|
|
215
|
+
for (let i = 0; i < n; i++) {
|
|
216
|
+
const row = cur[i];
|
|
217
|
+
if (!Array.isArray(row) || row.length !== nf) { good = false; break; }
|
|
218
|
+
out[i] = ctor(row);
|
|
219
|
+
}
|
|
220
|
+
if (!good) continue;
|
|
221
|
+
if (parent === null) root = out; else parent[lastSeg] = out;
|
|
222
|
+
}
|
|
223
|
+
return root;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ======================================================================
|
|
227
|
+
// PARSE
|
|
228
|
+
// ======================================================================
|
|
229
|
+
function parse(input) {
|
|
230
|
+
const s = input;
|
|
231
|
+
const len = s.length;
|
|
232
|
+
let i = 0;
|
|
233
|
+
const classes = Object.create(null); // name -> [fields]
|
|
234
|
+
|
|
235
|
+
function ws() {
|
|
236
|
+
while (i < len) {
|
|
237
|
+
const c = s.charCodeAt(i);
|
|
238
|
+
if (c === SPACE || c === TAB || c === LF || c === CR) i++;
|
|
239
|
+
else break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function err(msg) {
|
|
244
|
+
throw new SyntaxError(`TRON: ${msg} at index ${i}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function readIdent() {
|
|
248
|
+
const start = i;
|
|
249
|
+
if (!isIdentStart(s.charCodeAt(i))) err('expected identifier');
|
|
250
|
+
i++;
|
|
251
|
+
while (i < len && isIdentPart(s.charCodeAt(i))) i++;
|
|
252
|
+
return s.slice(start, i);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readString() {
|
|
256
|
+
// assumes current char is opening quote
|
|
257
|
+
i++; // skip "
|
|
258
|
+
let start = i;
|
|
259
|
+
let result = '';
|
|
260
|
+
let hasEscape = false;
|
|
261
|
+
while (i < len) {
|
|
262
|
+
const c = s.charCodeAt(i);
|
|
263
|
+
if (c === QUOTE) {
|
|
264
|
+
if (!hasEscape) { const out = s.slice(start, i); i++; return out; }
|
|
265
|
+
result += s.slice(start, i); i++; return result;
|
|
266
|
+
}
|
|
267
|
+
if (c === BACKSLASH) {
|
|
268
|
+
hasEscape = true;
|
|
269
|
+
result += s.slice(start, i);
|
|
270
|
+
i++;
|
|
271
|
+
const e = s.charCodeAt(i);
|
|
272
|
+
switch (e) {
|
|
273
|
+
case QUOTE: result += '"'; break;
|
|
274
|
+
case BACKSLASH: result += '\\'; break;
|
|
275
|
+
case 47: result += '/'; break;
|
|
276
|
+
case 110: result += '\n'; break;
|
|
277
|
+
case 116: result += '\t'; break;
|
|
278
|
+
case 114: result += '\r'; break;
|
|
279
|
+
case 98: result += '\b'; break;
|
|
280
|
+
case 102: result += '\f'; break;
|
|
281
|
+
case 117: {
|
|
282
|
+
const hex = s.slice(i + 1, i + 5);
|
|
283
|
+
result += String.fromCharCode(parseInt(hex, 16));
|
|
284
|
+
i += 4; break;
|
|
285
|
+
}
|
|
286
|
+
default: err('bad escape');
|
|
287
|
+
}
|
|
288
|
+
i++;
|
|
289
|
+
start = i;
|
|
290
|
+
} else {
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
err('unterminated string');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Allocation-free number reader with an exact (Clinger) fast path:
|
|
298
|
+
// if the significand has <=15 digits and the decimal exponent is in
|
|
299
|
+
// [-22,22], significand * 10^e (or / 10^-e) is correctly rounded, so we
|
|
300
|
+
// avoid building a substring + Number() on the hot path. Falls back to
|
|
301
|
+
// Number(slice) for the rare wide/large-exponent value.
|
|
302
|
+
function readNumber() {
|
|
303
|
+
const start = i;
|
|
304
|
+
let c = s.charCodeAt(i);
|
|
305
|
+
let neg = false;
|
|
306
|
+
if (c === MINUS) { neg = true; i++; } else if (c === PLUS) { i++; }
|
|
307
|
+
let mant = 0, digits = 0, overflow = false;
|
|
308
|
+
while (i < len) { c = s.charCodeAt(i); if (c < ZERO || c > NINE) break; if (digits < 15) { mant = mant * 10 + (c - ZERO); digits++; } else overflow = true; i++; }
|
|
309
|
+
let fracDigits = 0;
|
|
310
|
+
if (i < len && s.charCodeAt(i) === DOT) {
|
|
311
|
+
i++;
|
|
312
|
+
while (i < len) { c = s.charCodeAt(i); if (c < ZERO || c > NINE) break; if (digits < 15) { mant = mant * 10 + (c - ZERO); digits++; fracDigits++; } else overflow = true; i++; }
|
|
313
|
+
}
|
|
314
|
+
let hasExp = false, expNeg = false, exp = 0;
|
|
315
|
+
if (i < len) { c = s.charCodeAt(i); if (c === n_e || c === n_E) { hasExp = true; i++; c = s.charCodeAt(i); if (c === PLUS) i++; else if (c === MINUS) { expNeg = true; i++; } while (i < len) { c = s.charCodeAt(i); if (c < ZERO || c > NINE) break; exp = exp * 10 + (c - ZERO); i++; } } }
|
|
316
|
+
const e = (hasExp ? (expNeg ? -exp : exp) : 0) - fracDigits;
|
|
317
|
+
if (!overflow && e >= -22 && e <= 22) {
|
|
318
|
+
const val = e >= 0 ? mant * POW10[e] : mant / POW10[-e];
|
|
319
|
+
return neg ? -val : val;
|
|
320
|
+
}
|
|
321
|
+
return +s.slice(start, i);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function readArray() {
|
|
325
|
+
i++; // [
|
|
326
|
+
const arr = [];
|
|
327
|
+
ws();
|
|
328
|
+
if (s.charCodeAt(i) === RBRACKET) { i++; return arr; }
|
|
329
|
+
for (;;) {
|
|
330
|
+
arr.push(readValue());
|
|
331
|
+
ws();
|
|
332
|
+
const c = s.charCodeAt(i);
|
|
333
|
+
if (c === COMMA) { i++; ws(); continue; }
|
|
334
|
+
if (c === RBRACKET) { i++; return arr; }
|
|
335
|
+
err('expected , or ] in array');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function readObject() {
|
|
340
|
+
i++; // {
|
|
341
|
+
const obj = {};
|
|
342
|
+
ws();
|
|
343
|
+
if (s.charCodeAt(i) === RBRACE) { i++; return obj; }
|
|
344
|
+
for (;;) {
|
|
345
|
+
ws();
|
|
346
|
+
let key;
|
|
347
|
+
const c = s.charCodeAt(i);
|
|
348
|
+
if (c === QUOTE) key = readString();
|
|
349
|
+
else key = readIdent(); // TRON allows unquoted keys
|
|
350
|
+
ws();
|
|
351
|
+
if (s.charCodeAt(i) !== COLON) err('expected : after key');
|
|
352
|
+
i++;
|
|
353
|
+
ws();
|
|
354
|
+
obj[key] = readValue();
|
|
355
|
+
ws();
|
|
356
|
+
const d = s.charCodeAt(i);
|
|
357
|
+
if (d === COMMA) { i++; continue; }
|
|
358
|
+
if (d === RBRACE) { i++; return obj; }
|
|
359
|
+
err('expected , or } in object');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function readInstantiation(name) {
|
|
364
|
+
// current char is '(' ; name already read
|
|
365
|
+
const cls = classes[name];
|
|
366
|
+
if (!cls) err(`unknown class "${name}"`);
|
|
367
|
+
const fields = cls.f, dicts = cls.d;
|
|
368
|
+
i++; // (
|
|
369
|
+
const obj = {};
|
|
370
|
+
ws();
|
|
371
|
+
if (s.charCodeAt(i) === RPAREN) { i++; return obj; }
|
|
372
|
+
let fi = 0;
|
|
373
|
+
for (;;) {
|
|
374
|
+
const v = readValue();
|
|
375
|
+
const dd = dicts[fi];
|
|
376
|
+
obj[fields[fi++]] = dd === null ? v : dd[v];
|
|
377
|
+
ws();
|
|
378
|
+
const c = s.charCodeAt(i);
|
|
379
|
+
if (c === COMMA) { i++; ws(); continue; }
|
|
380
|
+
if (c === RPAREN) { i++; return obj; }
|
|
381
|
+
err('expected , or ) in instantiation');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function readValue() {
|
|
386
|
+
ws();
|
|
387
|
+
const c = s.charCodeAt(i);
|
|
388
|
+
switch (c) {
|
|
389
|
+
case QUOTE: return readString();
|
|
390
|
+
case LBRACKET: return readArray();
|
|
391
|
+
case LBRACE: return readObject();
|
|
392
|
+
case MINUS: return readNumber();
|
|
393
|
+
default:
|
|
394
|
+
if (isDigit(c)) return readNumber();
|
|
395
|
+
if (isIdentStart(c)) {
|
|
396
|
+
const id = readIdent();
|
|
397
|
+
// keyword / class instantiation / bare word
|
|
398
|
+
if (id === 'true') return true;
|
|
399
|
+
if (id === 'false') return false;
|
|
400
|
+
if (id === 'null') return null;
|
|
401
|
+
ws();
|
|
402
|
+
if (s.charCodeAt(i) === LPAREN) return readInstantiation(id);
|
|
403
|
+
return id; // bare identifier -> string (lenient)
|
|
404
|
+
}
|
|
405
|
+
err('unexpected character');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---- declarations at the top (shared prelude reader) ----
|
|
410
|
+
const pre = parsePrelude(s);
|
|
411
|
+
for (const cname in pre.classes) classes[cname] = pre.classes[cname];
|
|
412
|
+
i = pre.end;
|
|
413
|
+
let value = readValue();
|
|
414
|
+
value = applyTables(value, pre);
|
|
415
|
+
ws();
|
|
416
|
+
if (i < len) err('trailing content');
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ======================================================================
|
|
421
|
+
// STRINGIFY
|
|
422
|
+
// ======================================================================
|
|
423
|
+
// Strategy: find repeated object "shapes" (ordered key signatures). Any shape
|
|
424
|
+
// used >= MIN_USES times becomes a class, so its keys are emitted once.
|
|
425
|
+
const MIN_USES = 2;
|
|
426
|
+
const DICT_MAX_UNIQUES = 64;
|
|
427
|
+
|
|
428
|
+
// A field name may be written bare in a class declaration only if it is a
|
|
429
|
+
// plain ASCII identifier; otherwise it must be quoted.
|
|
430
|
+
function encFieldName(k) {
|
|
431
|
+
if (k.length === 0) return JSON.stringify(k);
|
|
432
|
+
const f = k.charCodeAt(0);
|
|
433
|
+
if (!((f >= 65 && f <= 90) || (f >= 97 && f <= 122) || f === 95)) return JSON.stringify(k);
|
|
434
|
+
for (let i = 1; i < k.length; i++) {
|
|
435
|
+
const c = k.charCodeAt(i);
|
|
436
|
+
if (!((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95 || c === 46)) {
|
|
437
|
+
return JSON.stringify(k);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return k;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function encEnumVal(v) {
|
|
444
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
|
445
|
+
// bare unless ambiguous on a comma-separated decl line
|
|
446
|
+
if (v.length === 0 || v === 'true' || v === 'false' || v === 'null') return JSON.stringify(v);
|
|
447
|
+
const f = v.charCodeAt(0), l = v.charCodeAt(v.length - 1);
|
|
448
|
+
if (f === 32 || l === 32 || f === 9 || l === 9 || f === QUOTE) return JSON.stringify(v);
|
|
449
|
+
for (let k = 0; k < v.length; k++) {
|
|
450
|
+
const c = v.charCodeAt(k);
|
|
451
|
+
if (c === COMMA || c === QUOTE || c === BACKSLASH || c < 32) return JSON.stringify(v);
|
|
452
|
+
}
|
|
453
|
+
// numeric-looking values must be quoted so decode keeps them as strings
|
|
454
|
+
if (!Number.isNaN(Number(v))) return JSON.stringify(v);
|
|
455
|
+
return v;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function stringify(value, opts) {
|
|
459
|
+
const useDict = !!(opts && opts.dict);
|
|
460
|
+
// Pass 1: count shapes (+ per-field string stats when dict is on).
|
|
461
|
+
const shapeCounts = new Map(); // signature -> {count, keys, stats}
|
|
462
|
+
(function scan(v) {
|
|
463
|
+
if (v === null || typeof v !== 'object') return;
|
|
464
|
+
if (Array.isArray(v)) { for (let k = 0; k < v.length; k++) scan(v[k]); return; }
|
|
465
|
+
const keys = Object.keys(v);
|
|
466
|
+
// only "flat-ish" shapes are worth classing; still class nested, values scanned anyway
|
|
467
|
+
const sig = keys.join('\u0000');
|
|
468
|
+
let e = shapeCounts.get(sig);
|
|
469
|
+
if (!e) {
|
|
470
|
+
e = { count: 0, keys, stats: null };
|
|
471
|
+
if (useDict) {
|
|
472
|
+
e.stats = new Array(keys.length);
|
|
473
|
+
for (let k = 0; k < keys.length; k++) e.stats[k] = { m: new Map(), ok: true, total: 0, bytes: 0 };
|
|
474
|
+
}
|
|
475
|
+
shapeCounts.set(sig, e);
|
|
476
|
+
}
|
|
477
|
+
e.count++;
|
|
478
|
+
if (useDict) {
|
|
479
|
+
for (let k = 0; k < keys.length; k++) {
|
|
480
|
+
const st = e.stats[k];
|
|
481
|
+
if (!st.ok) continue;
|
|
482
|
+
const val = v[keys[k]];
|
|
483
|
+
const vt = typeof val;
|
|
484
|
+
if (vt !== 'string' && vt !== 'boolean') { st.ok = false; st.m = null; continue; }
|
|
485
|
+
if (st.kind === undefined) st.kind = vt;
|
|
486
|
+
else if (st.kind !== vt) { st.ok = false; st.m = null; continue; }
|
|
487
|
+
st.total++; st.bytes += (vt === 'string' ? val.length : 5);
|
|
488
|
+
const c = st.m.get(val);
|
|
489
|
+
if (c === undefined) {
|
|
490
|
+
if (st.m.size >= DICT_MAX_UNIQUES) { st.ok = false; st.m = null; continue; }
|
|
491
|
+
st.m.set(val, 1);
|
|
492
|
+
} else st.m.set(val, c + 1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
for (let k = 0; k < keys.length; k++) scan(v[keys[k]]);
|
|
496
|
+
})(value);
|
|
497
|
+
|
|
498
|
+
// Assign class names to repeated shapes (skip empty objects).
|
|
499
|
+
const sigToClass = new Map();
|
|
500
|
+
const classDecls = [];
|
|
501
|
+
const enumDecls = [];
|
|
502
|
+
const enumBySig = new Map(); // JSON.stringify(values) -> enum name
|
|
503
|
+
let cn = 0, en = 0;
|
|
504
|
+
for (const [sig, e] of shapeCounts) {
|
|
505
|
+
if (e.count >= MIN_USES && e.keys.length > 0) {
|
|
506
|
+
const name = 'C' + (cn++);
|
|
507
|
+
let dictMaps = null;
|
|
508
|
+
const declFields = new Array(e.keys.length);
|
|
509
|
+
for (let k = 0; k < e.keys.length; k++) {
|
|
510
|
+
declFields[k] = encFieldName(e.keys[k]);
|
|
511
|
+
if (!useDict) continue;
|
|
512
|
+
const st = e.stats[k];
|
|
513
|
+
if (st.ok && st.m && st.m.size >= 1 &&
|
|
514
|
+
e.count >= 3 * st.m.size && st.total > 0 && (st.bytes / st.total) >= 2) {
|
|
515
|
+
const values = Array.from(st.m.keys());
|
|
516
|
+
const vsig = JSON.stringify(values);
|
|
517
|
+
let ename = enumBySig.get(vsig);
|
|
518
|
+
if (ename === undefined) {
|
|
519
|
+
ename = 'E' + (en++);
|
|
520
|
+
enumBySig.set(vsig, ename);
|
|
521
|
+
enumDecls.push('enum ' + ename + ': ' + values.map(encEnumVal).join(','));
|
|
522
|
+
}
|
|
523
|
+
if (!dictMaps) dictMaps = new Array(e.keys.length).fill(null);
|
|
524
|
+
const idx = new Map();
|
|
525
|
+
for (let q = 0; q < values.length; q++) idx.set(values[q], q);
|
|
526
|
+
dictMaps[k] = idx;
|
|
527
|
+
declFields[k] = encFieldName(e.keys[k]) + '@' + ename;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
sigToClass.set(sig, { name, keys: e.keys, dictMaps });
|
|
531
|
+
classDecls.push('class ' + name + ': ' + declFields.join(','));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const out = [];
|
|
536
|
+
function encStr(str) {
|
|
537
|
+
// JSON-compatible string quoting. Parens are additionally escaped as
|
|
538
|
+
// (/) so that in canonical TRON every raw "(" / ")" in the
|
|
539
|
+
// document is structural — this is what makes the JSON.parse trampoline
|
|
540
|
+
// (see tron-trampoline.js) a safe pure-text transform.
|
|
541
|
+
const j = JSON.stringify(str);
|
|
542
|
+
if (str.indexOf('(') < 0 && str.indexOf(')') < 0) return j;
|
|
543
|
+
let o = '';
|
|
544
|
+
for (let k = 0; k < j.length; k++) {
|
|
545
|
+
const c = j.charCodeAt(k);
|
|
546
|
+
if (c === 40) o += '\\u0028';
|
|
547
|
+
else if (c === 41) o += '\\u0029';
|
|
548
|
+
else o += j[k];
|
|
549
|
+
}
|
|
550
|
+
return o;
|
|
551
|
+
}
|
|
552
|
+
function enc(v) {
|
|
553
|
+
if (v === null) { out.push('null'); return; }
|
|
554
|
+
const t = typeof v;
|
|
555
|
+
if (t === 'string') { out.push(encStr(v)); return; }
|
|
556
|
+
if (t === 'number') { out.push(Number.isFinite(v) ? String(v) : 'null'); return; }
|
|
557
|
+
if (t === 'boolean') { out.push(v ? 'true' : 'false'); return; }
|
|
558
|
+
if (t === 'undefined') { out.push('null'); return; }
|
|
559
|
+
if (Array.isArray(v)) {
|
|
560
|
+
out.push('[');
|
|
561
|
+
for (let k = 0; k < v.length; k++) { if (k) out.push(','); enc(v[k]); }
|
|
562
|
+
out.push(']');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// object
|
|
566
|
+
const keys = Object.keys(v);
|
|
567
|
+
const sig = keys.join('\u0000');
|
|
568
|
+
const cls = sigToClass.get(sig);
|
|
569
|
+
if (cls) {
|
|
570
|
+
out.push(cls.name, '(');
|
|
571
|
+
const dm = cls.dictMaps;
|
|
572
|
+
for (let k = 0; k < cls.keys.length; k++) {
|
|
573
|
+
if (k) out.push(',');
|
|
574
|
+
const dmk = dm === null ? null : dm[k];
|
|
575
|
+
if (dmk) out.push('' + dmk.get(v[cls.keys[k]]));
|
|
576
|
+
else enc(v[cls.keys[k]]);
|
|
577
|
+
}
|
|
578
|
+
out.push(')');
|
|
579
|
+
} else {
|
|
580
|
+
out.push('{');
|
|
581
|
+
for (let k = 0; k < keys.length; k++) {
|
|
582
|
+
if (k) out.push(',');
|
|
583
|
+
out.push(encStr(keys[k]), ':');
|
|
584
|
+
enc(v[keys[k]]);
|
|
585
|
+
}
|
|
586
|
+
out.push('}');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
enc(value);
|
|
590
|
+
const body = out.join('');
|
|
591
|
+
if (classDecls.length === 0 && enumDecls.length === 0) return body;
|
|
592
|
+
const decls = enumDecls.concat(classDecls);
|
|
593
|
+
return decls.join('\n') + '\n' + body;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ======================================================================
|
|
597
|
+
// parseFast — canonical fast path.
|
|
598
|
+
// Assumes the compact form our stringify() emits (no insignificant
|
|
599
|
+
// whitespace outside strings). Inlines every reader, skips per-token
|
|
600
|
+
// whitespace scanning, and reads class instances positionally. Tolerates
|
|
601
|
+
// incidental spaces/newlines cheaply at structural separators so ordinary
|
|
602
|
+
// JSON (a TRON subset) with spacing still parses.
|
|
603
|
+
// ======================================================================
|
|
604
|
+
function parseFast(input) {
|
|
605
|
+
const s = input;
|
|
606
|
+
const len = s.length;
|
|
607
|
+
let i = 0;
|
|
608
|
+
const classes = Object.create(null);
|
|
609
|
+
|
|
610
|
+
function err(m) { throw new SyntaxError(`TRON: ${m} at ${i}`); }
|
|
611
|
+
|
|
612
|
+
// cheap inline whitespace skip (one comparison in the common no-ws case)
|
|
613
|
+
function sw() {
|
|
614
|
+
let c;
|
|
615
|
+
while (i < len && ((c = s.charCodeAt(i)) === SPACE || c === LF || c === TAB || c === CR)) i++;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function readStr() {
|
|
619
|
+
i++; // "
|
|
620
|
+
let start = i, out = '', esc = false;
|
|
621
|
+
for (;;) {
|
|
622
|
+
const c = s.charCodeAt(i);
|
|
623
|
+
if (c === QUOTE) { if (!esc) { const r = s.slice(start, i); i++; return r; } out += s.slice(start, i); i++; return out; }
|
|
624
|
+
if (c === BACKSLASH) {
|
|
625
|
+
esc = true; out += s.slice(start, i); i++;
|
|
626
|
+
const e = s.charCodeAt(i);
|
|
627
|
+
switch (e) {
|
|
628
|
+
case QUOTE: out += '"'; break; case BACKSLASH: out += '\\'; break;
|
|
629
|
+
case 47: out += '/'; break; case 110: out += '\n'; break;
|
|
630
|
+
case 116: out += '\t'; break; case 114: out += '\r'; break;
|
|
631
|
+
case 98: out += '\b'; break; case 102: out += '\f'; break;
|
|
632
|
+
case 117: out += String.fromCharCode(parseInt(s.slice(i + 1, i + 5), 16)); i += 4; break;
|
|
633
|
+
default: err('bad escape');
|
|
634
|
+
}
|
|
635
|
+
i++; start = i;
|
|
636
|
+
} else if (c !== c) { err('unterminated string'); } else i++;
|
|
637
|
+
if (i > len) err('unterminated string');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function readNum() {
|
|
642
|
+
const start = i;
|
|
643
|
+
let c = s.charCodeAt(i), neg = false;
|
|
644
|
+
if (c === MINUS) { neg = true; i++; } else if (c === PLUS) i++;
|
|
645
|
+
let mant = 0, digits = 0, overflow = false;
|
|
646
|
+
while (i < len) { c = s.charCodeAt(i); if (c < ZERO || c > NINE) break; if (digits < 15) { mant = mant * 10 + (c - ZERO); digits++; } else overflow = true; i++; }
|
|
647
|
+
let frac = 0;
|
|
648
|
+
if (i < len && s.charCodeAt(i) === DOT) { i++; while (i < len) { c = s.charCodeAt(i); if (c < ZERO || c > NINE) break; if (digits < 15) { mant = mant * 10 + (c - ZERO); digits++; frac++; } else overflow = true; i++; } }
|
|
649
|
+
let he = false, en = false, ex = 0;
|
|
650
|
+
if (i < len) { c = s.charCodeAt(i); if (c === n_e || c === n_E) { he = true; i++; c = s.charCodeAt(i); if (c === PLUS) i++; else if (c === MINUS) { en = true; i++; } while (i < len) { c = s.charCodeAt(i); if (c < ZERO || c > NINE) break; ex = ex * 10 + (c - ZERO); i++; } } }
|
|
651
|
+
const e = (he ? (en ? -ex : ex) : 0) - frac;
|
|
652
|
+
if (!overflow && e >= -22 && e <= 22) { const v = e >= 0 ? mant * POW10[e] : mant / POW10[-e]; return neg ? -v : v; }
|
|
653
|
+
return +s.slice(start, i);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function readIdent() {
|
|
657
|
+
const start = i; i++;
|
|
658
|
+
while (i < len && isIdentPart(s.charCodeAt(i))) i++;
|
|
659
|
+
return s.slice(start, i);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function readVal() {
|
|
663
|
+
const c = s.charCodeAt(i);
|
|
664
|
+
if (c === QUOTE) return readStr();
|
|
665
|
+
if (c >= ZERO && c <= NINE) return readNum();
|
|
666
|
+
if (c === MINUS) return readNum();
|
|
667
|
+
if (c === LBRACE) {
|
|
668
|
+
i++; sw(); // {
|
|
669
|
+
if (s.charCodeAt(i) === RBRACE) { i++; return {}; }
|
|
670
|
+
const o = {};
|
|
671
|
+
for (;;) {
|
|
672
|
+
const k = s.charCodeAt(i) === QUOTE ? readStr() : readIdent();
|
|
673
|
+
if (s.charCodeAt(i) !== COLON) { sw(); if (s.charCodeAt(i) !== COLON) err('expected :'); }
|
|
674
|
+
i++; sw();
|
|
675
|
+
o[k] = readVal();
|
|
676
|
+
let d = s.charCodeAt(i);
|
|
677
|
+
if (d !== COMMA && d !== RBRACE) { sw(); d = s.charCodeAt(i); }
|
|
678
|
+
if (d === COMMA) { i++; sw(); continue; }
|
|
679
|
+
if (d === RBRACE) { i++; return o; }
|
|
680
|
+
err('expected , or }');
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (c === LBRACKET) {
|
|
684
|
+
i++; sw(); // [
|
|
685
|
+
if (s.charCodeAt(i) === RBRACKET) { i++; return []; }
|
|
686
|
+
const a = [];
|
|
687
|
+
for (;;) {
|
|
688
|
+
a.push(readVal());
|
|
689
|
+
let d = s.charCodeAt(i);
|
|
690
|
+
if (d !== COMMA && d !== RBRACKET) { sw(); d = s.charCodeAt(i); }
|
|
691
|
+
if (d === COMMA) { i++; sw(); continue; }
|
|
692
|
+
if (d === RBRACKET) { i++; return a; }
|
|
693
|
+
err('expected , or ]');
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (isIdentStart(c)) {
|
|
697
|
+
const id = readIdent();
|
|
698
|
+
if (id === 'true') return true;
|
|
699
|
+
if (id === 'false') return false;
|
|
700
|
+
if (id === 'null') return null;
|
|
701
|
+
// class instantiation?
|
|
702
|
+
let pc = s.charCodeAt(i);
|
|
703
|
+
if (pc !== LPAREN) { const save = i; sw(); if (s.charCodeAt(i) === LPAREN) pc = LPAREN; else { i = save; return id; } }
|
|
704
|
+
const cls = classes[id];
|
|
705
|
+
if (!cls) err(`unknown class ${id}`);
|
|
706
|
+
const flds = cls.f, dcts = cls.d;
|
|
707
|
+
i++; sw(); // (
|
|
708
|
+
const o = {};
|
|
709
|
+
if (s.charCodeAt(i) === RPAREN) { i++; return o; }
|
|
710
|
+
let fi = 0;
|
|
711
|
+
for (;;) {
|
|
712
|
+
const vv = readVal();
|
|
713
|
+
const dd = dcts[fi];
|
|
714
|
+
o[flds[fi++]] = dd === null ? vv : dd[vv];
|
|
715
|
+
let d = s.charCodeAt(i);
|
|
716
|
+
if (d !== COMMA && d !== RPAREN) { sw(); d = s.charCodeAt(i); }
|
|
717
|
+
if (d === COMMA) { i++; sw(); continue; }
|
|
718
|
+
if (d === RPAREN) { i++; return o; }
|
|
719
|
+
err('expected , or )');
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
err('unexpected char');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// declarations (shared prelude reader)
|
|
726
|
+
const pre = parsePrelude(s);
|
|
727
|
+
for (const cname in pre.classes) classes[cname] = pre.classes[cname];
|
|
728
|
+
i = pre.end;
|
|
729
|
+
sw();
|
|
730
|
+
const v = readVal();
|
|
731
|
+
return applyTables(v, pre);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export { parse, parseFast, stringify, parsePrelude, applyTables, parse as parseTolerant };
|