subscript 9.2.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +115 -169
  2. package/feature/access.js +67 -7
  3. package/feature/accessor.js +49 -0
  4. package/feature/asi.js +15 -0
  5. package/feature/async.js +45 -0
  6. package/feature/block.js +41 -0
  7. package/feature/class.js +69 -0
  8. package/feature/collection.js +40 -0
  9. package/feature/comment.js +25 -5
  10. package/feature/destruct.js +33 -0
  11. package/feature/function.js +44 -0
  12. package/feature/group.js +39 -9
  13. package/feature/if.js +23 -38
  14. package/feature/literal.js +13 -0
  15. package/feature/loop.js +107 -106
  16. package/feature/module.js +42 -0
  17. package/feature/number.js +41 -38
  18. package/feature/op/arithmetic.js +29 -0
  19. package/feature/op/arrow.js +33 -0
  20. package/feature/op/assign-logical.js +33 -0
  21. package/feature/op/assignment.js +47 -0
  22. package/feature/op/bitwise-unsigned.js +17 -0
  23. package/feature/op/bitwise.js +29 -0
  24. package/feature/op/comparison.js +19 -0
  25. package/feature/op/defer.js +15 -0
  26. package/feature/op/equality.js +16 -0
  27. package/feature/op/identity.js +15 -0
  28. package/feature/op/increment.js +23 -0
  29. package/feature/op/logical.js +21 -0
  30. package/feature/op/membership.js +17 -0
  31. package/feature/op/nullish.js +13 -0
  32. package/feature/op/optional.js +61 -0
  33. package/feature/op/pow.js +19 -0
  34. package/feature/op/range.js +26 -0
  35. package/feature/op/spread.js +15 -0
  36. package/feature/op/ternary.js +15 -0
  37. package/feature/op/type.js +18 -0
  38. package/feature/op/unary.js +41 -0
  39. package/feature/prop.js +34 -0
  40. package/feature/regex.js +31 -0
  41. package/feature/seq.js +21 -0
  42. package/feature/string.js +24 -17
  43. package/feature/switch.js +48 -0
  44. package/feature/template.js +39 -0
  45. package/feature/try.js +57 -0
  46. package/feature/unit.js +35 -0
  47. package/feature/var.js +51 -41
  48. package/jessie.js +31 -0
  49. package/jessie.min.js +8 -0
  50. package/justin.js +39 -48
  51. package/justin.min.js +8 -4
  52. package/package.json +15 -16
  53. package/parse.js +153 -0
  54. package/subscript.d.ts +45 -5
  55. package/subscript.js +62 -22
  56. package/subscript.min.js +5 -4
  57. package/util/bundle.js +507 -0
  58. package/util/stringify.js +172 -0
  59. package/feature/add.js +0 -22
  60. package/feature/array.js +0 -11
  61. package/feature/arrow.js +0 -23
  62. package/feature/assign.js +0 -11
  63. package/feature/bitwise.js +0 -11
  64. package/feature/bool.js +0 -5
  65. package/feature/call.js +0 -15
  66. package/feature/compare.js +0 -11
  67. package/feature/control.js +0 -142
  68. package/feature/increment.js +0 -11
  69. package/feature/logic.js +0 -11
  70. package/feature/mult.js +0 -25
  71. package/feature/object.js +0 -17
  72. package/feature/optional.js +0 -23
  73. package/feature/pow.js +0 -5
  74. package/feature/shift.js +0 -12
  75. package/feature/spread.js +0 -6
  76. package/feature/ternary.js +0 -10
  77. package/src/compile.d.ts +0 -17
  78. package/src/compile.js +0 -28
  79. package/src/const.js +0 -45
  80. package/src/parse.d.ts +0 -22
  81. package/src/parse.js +0 -113
  82. package/src/stringify.js +0 -27
  83. /package/{LICENSE → license} +0 -0
package/util/bundle.js ADDED
@@ -0,0 +1,507 @@
1
+ /**
2
+ * ESM Bundler using subscript's own parser (dogfooding)
3
+ *
4
+ * Thin layer: scope analysis + tree transform
5
+ * Parser comes from the dialect (jessie by default)
6
+ */
7
+ import { parse } from '../jessie.js';
8
+ import { codegen } from './stringify.js';
9
+ import { readFile } from 'fs/promises';
10
+ import { resolve } from 'path';
11
+
12
+ // === AST Utilities ===
13
+
14
+ /** Walk AST, call fn(node, parent, key) for each node */
15
+ const walk = (node, fn, parent = null, key = null) => {
16
+ if (!node || typeof node !== 'object') return;
17
+ fn(node, parent, key);
18
+ if (Array.isArray(node)) {
19
+ for (let i = 0; i < node.length; i++) walk(node[i], fn, node, i);
20
+ }
21
+ };
22
+
23
+ /** Deep clone AST */
24
+ const clone = node =>
25
+ !node ? node :
26
+ Array.isArray(node) ? node.map(clone) :
27
+ node instanceof RegExp ? new RegExp(node.source, node.flags) :
28
+ typeof node === 'object' ? Object.fromEntries(Object.entries(node).map(([k,v]) => [k, clone(v)])) :
29
+ node;
30
+
31
+ /** Rename identifier in AST - skip property access positions */
32
+ const renameId = (ast, old, neu) => {
33
+ walk(ast, (node, parent, key) => {
34
+ if (Array.isArray(node)) {
35
+ for (let i = 0; i < node.length; i++) {
36
+ if (node[i] === old) {
37
+ // Don't rename if this is a property name in a '.' or '?.' access
38
+ if ((node[0] === '.' || node[0] === '?.') && i === 2) continue;
39
+ // Don't rename if this is a property name in object literal {a: b} or shorthand {a}
40
+ if (node[0] === ':' && i === 1 && typeof node[1] === 'string') continue;
41
+ node[i] = neu;
42
+ }
43
+ }
44
+ }
45
+ });
46
+ return ast;
47
+ };
48
+
49
+ /** Flatten comma nodes into array: [',', 'a', 'b'] → ['a', 'b'], 'x' → ['x'] */
50
+ const flattenComma = node =>
51
+ Array.isArray(node) && node[0] === ',' ? node.slice(1) :
52
+ node ? [node] : [];
53
+
54
+ // === Module Analysis ===
55
+
56
+ /** Extract string from path node [null, 'path'] (string literal) */
57
+ const getPath = node => Array.isArray(node) && (node[0] === undefined || node[0] === null) ? node[1] : node;
58
+
59
+ /** Extract imports from AST
60
+ * New AST shapes:
61
+ * import './x.js' → ['import', [null, path]]
62
+ * import X from './x.js' → ['import', ['from', 'X', [null, path]]]
63
+ * import {a,b} from './x' → ['import', ['from', ['{}', ...], [null, path]]]
64
+ * import * as X from './x' → ['import', ['from', ['as', '*', 'X'], [null, path]]]
65
+ */
66
+ const getImports = ast => {
67
+ const imports = [];
68
+ walk(ast, node => {
69
+ if (!Array.isArray(node) || node[0] !== 'import') return;
70
+ const body = node[1];
71
+ const imp = { node };
72
+
73
+ // import './x.js' - bare import: [, 'path'] sparse array with undefined at index 0
74
+ if (Array.isArray(body) && (body[0] === undefined || body[0] === null)) {
75
+ imp.path = body[1];
76
+ imports.push(imp);
77
+ return;
78
+ }
79
+
80
+ // import X from './x.js' or import {...} from './x.js'
81
+ if (Array.isArray(body) && body[0] === 'from') {
82
+ const spec = body[1];
83
+ const pathNode = body[2];
84
+ imp.path = getPath(pathNode);
85
+
86
+ if (typeof spec === 'string') {
87
+ // import X from - default import
88
+ imp.default_ = spec;
89
+ } else if (Array.isArray(spec)) {
90
+ if (spec[0] === '{}') {
91
+ // import { a, b, c as d }
92
+ const items = spec.slice(1).flatMap(flattenComma);
93
+ imp.named = items.map(s =>
94
+ Array.isArray(s) && s[0] === 'as' ? { name: s[1], alias: s[2] } : { name: s, alias: s }
95
+ );
96
+ } else if (spec[0] === 'as' && spec[1] === '*') {
97
+ // import * as X
98
+ imp.namespace = spec[2];
99
+ } else if (spec[0] === '*') {
100
+ // import * as X (alternate shape)
101
+ imp.namespace = spec[1];
102
+ }
103
+ }
104
+ imports.push(imp);
105
+ }
106
+ });
107
+ return imports;
108
+ };
109
+
110
+ /** Extract exports from AST
111
+ * New AST shapes:
112
+ * export const x = 1 → ['export', ['const', ['=', 'x', val]]]
113
+ * export default x → ['export', ['default', 'x']]
114
+ * export { a } → ['export', ['{}', 'a']]
115
+ * export { a } from './x' → ['export', ['from', ['{}', 'a'], [null, path]]]
116
+ * export * from './x' → ['export', ['from', '*', [null, path]]]
117
+ */
118
+ const getExports = ast => {
119
+ const exports = { named: {}, reexports: [], default_: null };
120
+
121
+ walk(ast, node => {
122
+ if (!Array.isArray(node) || node[0] !== 'export') return;
123
+ const spec = node[1];
124
+
125
+ // export { a } from './x' or export * from './x'
126
+ if (Array.isArray(spec) && spec[0] === 'from') {
127
+ const what = spec[1];
128
+ const pathNode = spec[2];
129
+ const path = getPath(pathNode);
130
+
131
+ if (what === '*') {
132
+ exports.reexports.push({ star: true, path });
133
+ } else if (Array.isArray(what) && what[0] === '{}') {
134
+ const items = what.slice(1).flatMap(flattenComma);
135
+ const names = items.map(s =>
136
+ Array.isArray(s) && s[0] === 'as' ? { name: s[1], alias: s[2] } : { name: s, alias: s }
137
+ );
138
+ exports.reexports.push({ names, path });
139
+ }
140
+ return;
141
+ }
142
+
143
+ // export { a, b }
144
+ if (Array.isArray(spec) && spec[0] === '{}') {
145
+ const items = spec.slice(1).flatMap(flattenComma);
146
+ const names = items.map(s =>
147
+ Array.isArray(s) && s[0] === 'as' ? { name: s[1], alias: s[2] } : { name: s, alias: s }
148
+ );
149
+ for (const { name, alias } of names) exports.named[alias] = name;
150
+ return;
151
+ }
152
+
153
+ // export default x
154
+ if (Array.isArray(spec) && spec[0] === 'default') {
155
+ exports.default_ = spec[1];
156
+ return;
157
+ }
158
+
159
+ // export const/let/var x = ... - varargs: ['let', decl1, decl2, ...]
160
+ if (Array.isArray(spec) && (spec[0] === 'const' || spec[0] === 'let' || spec[0] === 'var')) {
161
+ for (let i = 1; i < spec.length; i++) {
162
+ const decl = spec[i];
163
+ if (typeof decl === 'string') {
164
+ exports.named[decl] = decl;
165
+ } else if (Array.isArray(decl) && decl[0] === '=') {
166
+ const name = decl[1];
167
+ if (typeof name === 'string') exports.named[name] = name;
168
+ }
169
+ }
170
+ return;
171
+ }
172
+
173
+ // export function x() {} or export class x {}
174
+ if (Array.isArray(spec) && (spec[0] === 'function' || spec[0] === 'class')) {
175
+ if (typeof spec[1] === 'string') exports.named[spec[1]] = spec[1];
176
+ }
177
+ });
178
+
179
+ return exports;
180
+ };
181
+
182
+ /** Get all declared names in AST
183
+ * New AST shapes:
184
+ * const x = 1 → ['const', ['=', 'x', val]]
185
+ * let x → ['let', 'x']
186
+ * function f() → ['function', 'f', ...]
187
+ * const a = 1, b = 2 → ['const', ['=', 'a', ...], ['=', 'b', ...]] (varargs)
188
+ */
189
+ const getDecls = ast => {
190
+ const decls = new Set();
191
+
192
+ const addDecl = node => {
193
+ if (typeof node === 'string') decls.add(node);
194
+ else if (Array.isArray(node)) {
195
+ if (node[0] === '=') {
196
+ if (typeof node[1] === 'string') decls.add(node[1]);
197
+ } else if (node[0] === ',') {
198
+ // Multiple declarations: const a = 1, b = 2 (older AST shape)
199
+ for (let i = 1; i < node.length; i++) addDecl(node[i]);
200
+ }
201
+ }
202
+ };
203
+
204
+ walk(ast, node => {
205
+ if (!Array.isArray(node)) return;
206
+ const op = node[0];
207
+
208
+ if (op === 'const' || op === 'let' || op === 'var') {
209
+ // Handle varargs: ['const', decl1, decl2, ...] for multiple declarations
210
+ for (let i = 1; i < node.length; i++) addDecl(node[i]);
211
+ }
212
+ if (op === 'function' || op === 'class') {
213
+ if (typeof node[1] === 'string') decls.add(node[1]);
214
+ }
215
+ if (op === 'export') {
216
+ const spec = node[1];
217
+ if (Array.isArray(spec) && (spec[0] === 'const' || spec[0] === 'let' || spec[0] === 'var')) {
218
+ addDecl(spec[1]);
219
+ }
220
+ if (Array.isArray(spec) && (spec[0] === 'function' || spec[0] === 'class') && typeof spec[1] === 'string') {
221
+ decls.add(spec[1]);
222
+ }
223
+ }
224
+ });
225
+
226
+ return decls;
227
+ };
228
+
229
+ // === AST Transforms ===
230
+
231
+ /** Remove import/export nodes, extract declarations */
232
+ /** Remove import/export nodes, extract declarations
233
+ * New AST shapes for export:
234
+ * export const x = 1 → ['export', ['const', ...]] → keep ['const', ...]
235
+ * export default x → ['export', ['default', x]] → keep, or convert to __default
236
+ * export { a } → ['export', ['{}', ...]] → remove
237
+ * export { a } from './x' → ['export', ['from', ['{}', ...], path]] → remove
238
+ * export * from './x' → ['export', ['from', '*', path]] → remove
239
+ */
240
+ const stripModuleSyntax = ast => {
241
+ const defaultExpr = { value: null };
242
+
243
+ const process = node => {
244
+ if (!Array.isArray(node)) return node;
245
+ const op = node[0];
246
+
247
+ if (op === 'import') return null;
248
+
249
+ if (op === 'export') {
250
+ const spec = node[1];
251
+ // Re-exports: export { a } from './x' or export * from './x'
252
+ if (Array.isArray(spec) && spec[0] === 'from') return null;
253
+ // Named exports: export { a, b }
254
+ if (Array.isArray(spec) && spec[0] === '{}') return null;
255
+ // Default export
256
+ if (Array.isArray(spec) && spec[0] === 'default') {
257
+ defaultExpr.value = spec[1];
258
+ if (typeof spec[1] === 'string') return null;
259
+ return ['const', ['=', '__default', spec[1]]];
260
+ }
261
+ // Declaration export: export const x = 1
262
+ return spec;
263
+ }
264
+
265
+ if (op === ';') {
266
+ const parts = node.slice(1).map(process).filter(Boolean);
267
+ if (parts.length === 0) return null;
268
+ if (parts.length === 1) return parts[0];
269
+ return [';', ...parts];
270
+ }
271
+
272
+ return node;
273
+ };
274
+
275
+ return { ast: process(ast), defaultExpr: defaultExpr.value };
276
+ };
277
+
278
+ // === Path Resolution ===
279
+
280
+ const resolvePath = (from, to) => {
281
+ if (!to.startsWith('.')) return to;
282
+ const base = from.split('/').slice(0, -1);
283
+ for (const part of to.split('/')) {
284
+ if (part === '..') base.pop();
285
+ else if (part !== '.') base.push(part);
286
+ }
287
+ let path = base.join('/');
288
+ if (!path.endsWith('.js')) path += '.js';
289
+ return path;
290
+ };
291
+
292
+ // === Bundler ===
293
+
294
+ /**
295
+ * Bundle ES modules into single file
296
+ * @param {string} entry - Entry file path
297
+ * @param {(path: string) => string|Promise<string>} read - File reader
298
+ */
299
+ export async function bundle(entry, read) {
300
+ const modules = new Map();
301
+ const order = [];
302
+
303
+ async function load(path) {
304
+ if (modules.has(path)) return;
305
+ modules.set(path, null);
306
+
307
+ const code = await read(path);
308
+ const ast = parse(code);
309
+ const imports = getImports(ast);
310
+ const exports = getExports(ast);
311
+ const decls = getDecls(ast);
312
+
313
+ for (const imp of imports) {
314
+ imp.resolved = resolvePath(path, imp.path);
315
+ await load(imp.resolved);
316
+ }
317
+ for (const re of exports.reexports) {
318
+ re.resolved = resolvePath(path, re.path);
319
+ await load(re.resolved);
320
+ }
321
+
322
+ modules.set(path, { ast: clone(ast), imports, exports, decls });
323
+ order.push(path);
324
+ }
325
+
326
+ await load(entry);
327
+
328
+ // Detect conflicts
329
+ const allDecls = new Map();
330
+ for (const [path, mod] of modules) {
331
+ const importAliases = new Set();
332
+ for (const imp of mod.imports) {
333
+ if (imp.default_) importAliases.add(imp.default_);
334
+ if (imp.namespace) importAliases.add(imp.namespace);
335
+ if (imp.named) for (const { alias } of imp.named) importAliases.add(alias);
336
+ }
337
+
338
+ for (const name of mod.decls) {
339
+ if (importAliases.has(name)) continue;
340
+ if (!allDecls.has(name)) allDecls.set(name, []);
341
+ allDecls.get(name).push(path);
342
+ }
343
+ }
344
+
345
+ // Build rename maps
346
+ const renames = new Map();
347
+ for (const [name, paths] of allDecls) {
348
+ if (paths.length > 1) {
349
+ for (const path of paths) {
350
+ if (!renames.has(path)) renames.set(path, {});
351
+ // Make valid JS identifier: replace non-alphanumeric with underscore
352
+ const prefix = path.split('/').pop().replace('.js', '').replace(/[^a-zA-Z0-9]/g, '_') + '_';
353
+ renames.get(path)[name] = prefix + name;
354
+ }
355
+ }
356
+ }
357
+
358
+ const traceDefault = path => {
359
+ const mod = modules.get(path);
360
+ if (!mod) return null;
361
+ const def = mod.exports.default_;
362
+ if (!def) return null;
363
+ if (typeof def === 'string') {
364
+ const pathRenames = renames.get(path) || {};
365
+ if (pathRenames[def]) return pathRenames[def];
366
+ const defImp = mod.imports.find(i => i.default_ === def);
367
+ if (defImp) return traceDefault(defImp.resolved);
368
+ return def;
369
+ }
370
+ return '__default';
371
+ };
372
+
373
+ // Transform each module
374
+ const chunks = [];
375
+ for (const path of order) {
376
+ const mod = modules.get(path);
377
+ const pathRenames = renames.get(path) || {};
378
+
379
+ let ast = clone(mod.ast);
380
+ for (const [old, neu] of Object.entries(pathRenames)) {
381
+ renameId(ast, old, neu);
382
+ }
383
+
384
+ for (const imp of mod.imports) {
385
+ const dep = modules.get(imp.resolved);
386
+ if (!dep) continue;
387
+ const depRenames = renames.get(imp.resolved) || {};
388
+
389
+ if (imp.named) {
390
+ for (const { name, alias } of imp.named) {
391
+ // `name` is the exported name, look up what local name it maps to in the dep
392
+ const localName = dep.exports.named[name] || name;
393
+ // Check if that local name was renamed in the dep
394
+ const resolved = depRenames[localName] || localName;
395
+ if (alias !== resolved) renameId(ast, alias, resolved);
396
+ }
397
+ }
398
+
399
+ if (imp.default_) {
400
+ const resolved = traceDefault(imp.resolved);
401
+ if (resolved && imp.default_ !== resolved) {
402
+ renameId(ast, imp.default_, resolved);
403
+ }
404
+ }
405
+
406
+ if (imp.namespace) {
407
+ walk(ast, node => {
408
+ if (Array.isArray(node) && node[0] === '.' && node[1] === imp.namespace) {
409
+ const prop = node[2];
410
+ if (typeof prop === 'string' && dep.exports.named[prop]) {
411
+ const resolved = depRenames[dep.exports.named[prop]] || dep.exports.named[prop];
412
+ node.length = 0;
413
+ node.push(resolved);
414
+ }
415
+ }
416
+ });
417
+ }
418
+ }
419
+
420
+ const { ast: stripped } = stripModuleSyntax(ast);
421
+
422
+ if (stripped) {
423
+ const code = codegen(stripped);
424
+ if (code.trim()) {
425
+ chunks.push(`// === ${path} ===\n${code}`);
426
+ }
427
+ }
428
+ }
429
+
430
+ // Generate exports
431
+ const entryMod = modules.get(entry);
432
+ const entryRenames = renames.get(entry) || {};
433
+ const exportLines = [];
434
+
435
+ // Resolve all named exports including from re-exports
436
+ const resolveExports = (path, seen = new Set()) => {
437
+ if (seen.has(path)) return {};
438
+ seen.add(path);
439
+ const mod = modules.get(path);
440
+ if (!mod) return {};
441
+ const pathRenames = renames.get(path) || {};
442
+ const result = {};
443
+
444
+ // Direct named exports
445
+ for (const [exp, local] of Object.entries(mod.exports.named)) {
446
+ result[exp] = pathRenames[local] || local;
447
+ }
448
+
449
+ // Re-exports
450
+ for (const re of mod.exports.reexports) {
451
+ const depRenames = renames.get(re.resolved) || {};
452
+ if (re.star) {
453
+ // export * from './x' - get all exports from that module
454
+ const depExports = resolveExports(re.resolved, seen);
455
+ for (const [exp, resolved] of Object.entries(depExports)) {
456
+ if (!(exp in result)) result[exp] = resolved; // don't override local exports
457
+ }
458
+ } else if (re.names) {
459
+ // export { a, b } from './x'
460
+ const depMod = modules.get(re.resolved);
461
+ if (depMod) {
462
+ for (const { name, alias } of re.names) {
463
+ const local = depMod.exports.named[name] || name;
464
+ result[alias] = depRenames[local] || local;
465
+ }
466
+ }
467
+ }
468
+ }
469
+ return result;
470
+ };
471
+
472
+ const allExports = resolveExports(entry);
473
+ for (const [exp, resolved] of Object.entries(allExports)) {
474
+ exportLines.push(exp === resolved ? exp : `${resolved} as ${exp}`);
475
+ }
476
+
477
+ if (entryMod.exports.default_) {
478
+ const resolved = traceDefault(entry) || '__default';
479
+ exportLines.push(`${resolved} as default`);
480
+ }
481
+
482
+ let result = chunks.join('\n\n');
483
+ if (exportLines.length) {
484
+ result += `\n\nexport { ${exportLines.join(', ')} }`;
485
+ }
486
+
487
+ return result;
488
+ }
489
+
490
+ /** Bundle with Node.js fs */
491
+ export const bundleFile = (entry) => bundle(resolve(entry), path => readFile(path, 'utf-8'));
492
+
493
+ // CLI
494
+ if (typeof process !== 'undefined' && process.argv[1]?.includes('bundle')) {
495
+ const entry = process.argv[2];
496
+ if (!entry) {
497
+ console.error('Usage: node bundle.js <entry>');
498
+ process.exit(1);
499
+ }
500
+ bundleFile(entry)
501
+ .then(result => console.log(result))
502
+ .catch(e => {
503
+ console.error('Error:', e.message);
504
+ console.error(e.stack);
505
+ process.exit(1);
506
+ });
507
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * AST → JS Source String (codegen)
3
+ *
4
+ * Simple recursive stringifier using pattern matching on AST structure.
5
+ */
6
+
7
+ // Operator-specific overrides
8
+ export const generators = {};
9
+
10
+ // Register custom generator
11
+ export const generator = (op, fn) => generators[op] = fn;
12
+
13
+ // Stringify AST to JS source
14
+ export const codegen = node => {
15
+ // Handle undefined/null
16
+ if (node === undefined) return 'undefined';
17
+ if (node === null) return 'null';
18
+ if (node === '') return '';
19
+
20
+ // Identifier
21
+ if (!Array.isArray(node)) return String(node);
22
+
23
+ const [op, ...args] = node;
24
+
25
+ // Literal: [, value]
26
+ if (op === undefined) return typeof args[0] === 'string' ? JSON.stringify(args[0]) : String(args[0]);
27
+
28
+ // Custom generator
29
+ if (generators[op]) return generators[op](...args);
30
+
31
+ // Brackets: [], {}, ()
32
+ if (op === '[]' || op === '{}' || op === '()') {
33
+ const prefix = args.length > 1 ? codegen(args.shift()) : '';
34
+ // Empty brackets
35
+ if (args[0] === undefined || args[0] === null) return prefix + op;
36
+ // Comma sequence: [',', a, b, c] → a, b, c (null → empty for sparse arrays)
37
+ const inner = args[0]?.[0] === ','
38
+ ? args[0].slice(1).map(a => a === null ? '' : codegen(a)).join(', ')
39
+ : codegen(args[0]);
40
+ return prefix + op[0] + inner + op[1];
41
+ }
42
+
43
+ // Unary: [op, a]
44
+ if (args.length === 1) return op + codegen(args[0]);
45
+
46
+ // N-ary/sequence: comma, semicolon
47
+ if (op === ',' || op === ';') return args.filter(Boolean).map(codegen).join(op === ';' ? '; ' : ', ');
48
+
49
+ // Binary: [op, a, b]
50
+ if (args.length === 2) {
51
+ const [a, b] = args;
52
+ // Postfix: [op, a, null]
53
+ if (b === null) return codegen(a) + op;
54
+ // Property access: no spaces
55
+ if (op === '.') return codegen(a) + '.' + b;
56
+ return codegen(a) + ' ' + op + ' ' + codegen(b);
57
+ }
58
+
59
+ // Ternary: a ? b : c
60
+ if (op === '?' && args.length === 3) {
61
+ return codegen(args[0]) + ' ? ' + codegen(args[1]) + ' : ' + codegen(args[2]);
62
+ }
63
+
64
+ // Fallback n-ary
65
+ return args.filter(Boolean).map(codegen).join(op === ';' ? '; ' : ', ');
66
+ };
67
+
68
+ // --- Statement generators (need structure) ---
69
+
70
+ generator('block', body => body === undefined ? '{}' : '{ ' + codegen(body) + ' }');
71
+
72
+ // Variables: ['let', decl] or ['let', decl1, decl2, ...]
73
+ const varGen = kw => (...args) => kw + ' ' + args.map(codegen).join(', ');
74
+ generator('let', varGen('let'));
75
+ generator('const', varGen('const'));
76
+ generator('var', varGen('var'));
77
+
78
+ // Control flow
79
+ const wrap = s => s?.[0] === 'block' ? codegen(s) : '{ ' + (s ? codegen(s) : '') + ' }';
80
+ generator('if', (cond, then, els) => 'if (' + codegen(cond) + ') ' + wrap(then) + (els ? ' else ' + wrap(els) : ''));
81
+ generator('while', (cond, body) => 'while (' + codegen(cond) + ') ' + wrap(body));
82
+ generator('do', (body, cond) => 'do ' + wrap(body) + ' while (' + codegen(cond) + ')');
83
+ generator('for', (head, body) => {
84
+ if (head?.[0] === ';') {
85
+ const [, init, cond, step] = head;
86
+ return 'for (' + (init ? codegen(init) : '') + '; ' + (cond ? codegen(cond) : '') + '; ' + (step ? codegen(step) : '') + ') ' + wrap(body);
87
+ }
88
+ return 'for (' + codegen(head) + ') ' + wrap(body);
89
+ });
90
+
91
+ generator('return', a => a === undefined ? 'return' : 'return ' + codegen(a));
92
+ generator('break', () => 'break');
93
+ generator('continue', () => 'continue');
94
+ generator('throw', a => 'throw ' + codegen(a));
95
+
96
+ // Try/Catch - nested structure
97
+ generator('try', body => 'try { ' + codegen(body) + ' }');
98
+ generator('catch', (tryExpr, param, body) => codegen(tryExpr) + ' catch (' + codegen(param) + ') { ' + codegen(body) + ' }');
99
+ generator('finally', (expr, body) => codegen(expr) + ' finally { ' + codegen(body) + ' }');
100
+
101
+ // Functions
102
+ generator('function', (name, params, body) => {
103
+ const args = !params ? '' : params[0] === ',' ? params.slice(1).map(codegen).join(', ') : codegen(params);
104
+ const b = body?.[0] === 'block' ? codegen(body) : '{ ' + (body ? codegen(body) : '') + ' }';
105
+ return 'function' + (name ? ' ' + name : '') + '(' + args + ') ' + b;
106
+ });
107
+
108
+ generator('=>', (params, body) => {
109
+ if (params?.[0] === '()') params = params[1];
110
+ const args = !params ? '()' : typeof params === 'string' ? params :
111
+ params[0] === ',' ? '(' + params.slice(1).map(codegen).join(', ') + ')' : '(' + codegen(params) + ')';
112
+ return args + ' => ' + codegen(body);
113
+ });
114
+
115
+ // Class
116
+ generator('class', (name, base, body) =>
117
+ 'class' + (name ? ' ' + name : '') + (base ? ' extends ' + codegen(base) : '') + ' { ' + (body ? codegen(body) : '') + ' }');
118
+
119
+ // Async/Await/Yield
120
+ generator('async', fn => 'async ' + codegen(fn));
121
+ generator('await', a => 'await ' + codegen(a));
122
+ generator('yield', a => a !== undefined ? 'yield ' + codegen(a) : 'yield');
123
+ generator('yield*', a => 'yield* ' + codegen(a));
124
+
125
+ // Switch
126
+ generator('switch', (expr, body) => 'switch (' + codegen(expr) + ') ' + codegen(body));
127
+ generator('case', (test, body) => 'case ' + codegen(test) + ':' + (body ? ' ' + codegen(body) : ''));
128
+ generator('default:', body => 'default:' + (body ? ' ' + codegen(body) : ''));
129
+
130
+ // Keywords
131
+ generator('typeof', a => '(typeof ' + codegen(a) + ')');
132
+ generator('void', a => '(void ' + codegen(a) + ')');
133
+ generator('delete', a => '(delete ' + codegen(a) + ')');
134
+ generator('new', a => 'new ' + codegen(a));
135
+ generator('instanceof', (a, b) => '(' + codegen(a) + ' instanceof ' + codegen(b) + ')');
136
+
137
+ // Optional chaining
138
+ generator('?.', (a, b) => codegen(a) + '?.' + b);
139
+ generator('?.[]', (a, b) => codegen(a) + '?.[' + codegen(b) + ']');
140
+ generator('?.()', (a, b) => codegen(a) + '?.(' + (!b ? '' : b[0] === ',' ? b.slice(1).map(codegen).join(', ') : codegen(b)) + ')');
141
+
142
+ // Object literal
143
+ generator(':', (k, v) => (typeof k === 'string' ? k : '[' + codegen(k) + ']') + ': ' + codegen(v));
144
+
145
+ // Template literals
146
+ generator('`', (...parts) => '`' + parts.map(p => p?.[0] === undefined ? String(p[1]).replace(/`/g, '\\`').replace(/\$/g, '\\$') : '${' + codegen(p) + '}').join('') + '`');
147
+ generator('``', (tag, ...parts) => codegen(tag) + '`' + parts.map(p => p?.[0] === undefined ? String(p[1]).replace(/`/g, '\\`').replace(/\$/g, '\\$') : '${' + codegen(p) + '}').join('') + '`');
148
+
149
+ // Getter/Setter
150
+ generator('get', (name, body) => 'get ' + name + '() { ' + (body ? codegen(body) : '') + ' }');
151
+ generator('set', (name, param, body) => 'set ' + name + '(' + param + ') { ' + (body ? codegen(body) : '') + ' }');
152
+ generator('static', a => 'static ' + codegen(a));
153
+
154
+ // Non-JS operators (emit as helpers)
155
+ generator('..', (a, b) => 'range(' + codegen(a) + ', ' + codegen(b) + ')');
156
+ generator('..<', (a, b) => 'range(' + codegen(a) + ', ' + codegen(b) + ', true)');
157
+ generator('as', (a, b) => b ? codegen(a) + ' as ' + b : codegen(a)); // import alias or type assertion
158
+ generator('is', (a, b) => '(' + codegen(a) + ' instanceof ' + codegen(b) + ')');
159
+ generator('defer', a => '/* defer */ ' + codegen(a));
160
+
161
+ // For-in/of helper
162
+ generator('in', (a, b) => codegen(a) + ' in ' + codegen(b));
163
+ generator('of', (a, b) => codegen(a) + ' of ' + codegen(b));
164
+ generator('for await', (head, body) => 'for await (' + codegen(head) + ') ' + wrap(body));
165
+
166
+ // Module syntax
167
+ generator('export', spec => 'export ' + codegen(spec));
168
+ generator('import', spec => 'import ' + codegen(spec));
169
+ generator('from', (what, path) => codegen(what) + ' from ' + codegen(path));
170
+ generator('default', a => 'default ' + codegen(a));
171
+
172
+ export default codegen;