@starpak/lang 0.0.0-stage → 0.1.2

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 (53) hide show
  1. package/LICENSE +22 -0
  2. package/lib/ast.d.ts +334 -0
  3. package/lib/ast.d.ts.map +1 -0
  4. package/lib/ast.js +9 -0
  5. package/lib/ast.js.map +1 -0
  6. package/lib/checker.d.ts +17 -0
  7. package/lib/checker.d.ts.map +1 -0
  8. package/lib/checker.js +525 -0
  9. package/lib/checker.js.map +1 -0
  10. package/lib/diagnostics.d.ts +46 -0
  11. package/lib/diagnostics.d.ts.map +1 -0
  12. package/lib/diagnostics.js +77 -0
  13. package/lib/diagnostics.js.map +1 -0
  14. package/lib/emit/lua.d.ts +19 -0
  15. package/lib/emit/lua.d.ts.map +1 -0
  16. package/lib/emit/lua.js +451 -0
  17. package/lib/emit/lua.js.map +1 -0
  18. package/lib/emit/ts.d.ts +28 -0
  19. package/lib/emit/ts.d.ts.map +1 -0
  20. package/lib/emit/ts.js +803 -0
  21. package/lib/emit/ts.js.map +1 -0
  22. package/lib/index.d.ts +17 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +49 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/lexer.d.ts +10 -0
  27. package/lib/lexer.d.ts.map +1 -0
  28. package/lib/lexer.js +282 -0
  29. package/lib/lexer.js.map +1 -0
  30. package/lib/parser.d.ts +11 -0
  31. package/lib/parser.d.ts.map +1 -0
  32. package/lib/parser.js +1133 -0
  33. package/lib/parser.js.map +1 -0
  34. package/lib/token.d.ts +32 -0
  35. package/lib/token.d.ts.map +1 -0
  36. package/lib/token.js +18 -0
  37. package/lib/token.js.map +1 -0
  38. package/lib/types.d.ts +63 -0
  39. package/lib/types.d.ts.map +1 -0
  40. package/lib/types.js +177 -0
  41. package/lib/types.js.map +1 -0
  42. package/package.json +10 -3
  43. package/src/ast.ts +459 -0
  44. package/src/checker.ts +554 -0
  45. package/src/diagnostics.ts +100 -0
  46. package/src/emit/lua.ts +484 -0
  47. package/src/emit/ts.ts +861 -0
  48. package/src/index.ts +21 -0
  49. package/src/lexer.ts +301 -0
  50. package/src/parser.ts +1251 -0
  51. package/src/token.ts +34 -0
  52. package/src/types.ts +218 -0
  53. package/README.md +0 -3
package/lib/emit/ts.js ADDED
@@ -0,0 +1,803 @@
1
+ "use strict";
2
+ /**
3
+ * Spak Lang - TypeScript emitter
4
+ *
5
+ * Lowers a checked Spak `Program` into standalone ESM TypeScript that can be
6
+ * executed directly with `node --import tsx <file>`.
7
+ *
8
+ * Design notes:
9
+ * - Types are erased from runtime positions; `type` aliases are kept.
10
+ * - The output embeds a tiny runtime shim (`// spak runtime`) so generated
11
+ * files do not depend on any external package.
12
+ * - Spak's Lua-flavoured method calls (`list:sort`, `s:split`, ...) lower to
13
+ * calls on that shim; unknown methods keep normal JS method syntax.
14
+ * - Slices are 1-based and inclusive: `list[1..n]` -> `list.slice(0, n)`.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.typeAnn = typeAnn;
18
+ exports.emitTypeScript = emitTypeScript;
19
+ /** Lua-style methods that are backed by the runtime shim. */
20
+ const RUNTIME_METHODS = new Set(['push', 'pop', 'sort', 'split', 'len']);
21
+ /** Node builtins whose Spak `fs` surface is synchronous. */
22
+ const FS_SOURCES = new Set(['fs', 'node:fs']);
23
+ const FS_SYNC = new Set([
24
+ 'access', 'appendFile', 'chmod', 'chown', 'close', 'copyFile', 'cp',
25
+ 'exists', 'fchmod', 'fchown', 'fstat', 'fsync', 'ftruncate', 'futimes',
26
+ 'glob', 'lchmod', 'lchown', 'link', 'lstat', 'lutimes', 'mkdir', 'mkdtemp',
27
+ 'open', 'opendir', 'read', 'readFile', 'readdir', 'readlink', 'readv',
28
+ 'realpath', 'rename', 'rm', 'rmdir', 'stat', 'statfs', 'symlink',
29
+ 'truncate', 'unlink', 'utimes', 'write', 'writeFile', 'writev',
30
+ ]);
31
+ const RUNTIME = `// spak runtime (inlined helpers, no external dependencies)
32
+ const __spak = {
33
+ len(x) {
34
+ if (x === null || x === undefined) return 0
35
+ if (typeof x === "string" || Array.isArray(x)) return x.length
36
+ if (x instanceof Map || x instanceof Set) return x.size
37
+ if (typeof x === "object") return Object.keys(x).length
38
+ return 0
39
+ },
40
+ range(from, to, inclusive = true) {
41
+ const out = []
42
+ const step = from <= to ? 1 : -1
43
+ const end = inclusive ? to : to - step
44
+ for (let i = from; step > 0 ? i <= end : i >= end; i += step) out.push(i)
45
+ return out
46
+ },
47
+ slice(x, from, to, inclusive = true) {
48
+ const start = Number(from) - 1
49
+ const end = inclusive ? Number(to) : Number(to) - 1
50
+ return x.slice(start, end)
51
+ },
52
+ idx(x, i) {
53
+ // Lua-style 1-based indexing for sequences; plain maps keep their keys.
54
+ if (Array.isArray(x) || typeof x === "string") return x[Number(i) - 1]
55
+ return x[i]
56
+ },
57
+ split(s, sep) {
58
+ return String(s).split(sep)
59
+ },
60
+ push(t, v) {
61
+ if (Array.isArray(t)) t.push(v)
62
+ else if (typeof t.length === "number") t[t.length] = v
63
+ else t[Object.keys(t).length] = v
64
+ return t
65
+ },
66
+ pop(t) {
67
+ if (Array.isArray(t)) return t.pop()
68
+ const keys = Object.keys(t)
69
+ if (!keys.length) return null
70
+ const key = keys[keys.length - 1]
71
+ const value = t[key]
72
+ delete t[key]
73
+ return value
74
+ },
75
+ sort(t, cmp) {
76
+ if (!cmp) {
77
+ t.sort()
78
+ return t
79
+ }
80
+ t.sort((a, b) => {
81
+ const result = cmp(a, b)
82
+ if (typeof result === "number") return result
83
+ if (result) return -1
84
+ return cmp(b, a) ? 1 : 0
85
+ })
86
+ return t
87
+ },
88
+ entries(t) {
89
+ if (t instanceof Map) return Array.from(t.entries())
90
+ if (t instanceof Set) return Array.from(t.entries())
91
+ if (Array.isArray(t) || typeof t === "string") return Array.from(t.entries())
92
+ return Object.entries(t)
93
+ },
94
+ iter(t) {
95
+ if (t === null || t === undefined) return []
96
+ if (typeof t === "string" || Array.isArray(t)) return t
97
+ if (t instanceof Map || t instanceof Set) return t
98
+ if (typeof t[Symbol.iterator] === "function") return t
99
+ return Object.keys(t)
100
+ }
101
+ }
102
+ function len(x) { return __spak.len(x) }
103
+ function range(from, to) { return __spak.range(from, to, true) }
104
+ function print(...args) { console.log(...args) }
105
+ function tostring(x) { return String(x) }
106
+ `;
107
+ class Emitter {
108
+ lines = [];
109
+ level = 0;
110
+ scopes = [new Set()];
111
+ temp = 0;
112
+ options;
113
+ constructor(options = {}) {
114
+ this.options = options;
115
+ }
116
+ get unit() {
117
+ return this.options.indent ?? ' ';
118
+ }
119
+ pad(extra = 0) {
120
+ return this.unit.repeat(Math.max(0, this.level + extra));
121
+ }
122
+ line(text = '') {
123
+ this.lines.push(text ? this.pad() + text : '');
124
+ }
125
+ withScope(fn) {
126
+ this.scopes.push(new Set());
127
+ this.level++;
128
+ try {
129
+ fn();
130
+ }
131
+ finally {
132
+ this.level--;
133
+ this.scopes.pop();
134
+ }
135
+ }
136
+ declare(name) {
137
+ this.scopes[this.scopes.length - 1].add(name);
138
+ }
139
+ declared(name) {
140
+ return this.scopes.some(scope => scope.has(name));
141
+ }
142
+ /* ---------------------------------------------------------------- */
143
+ /* Program */
144
+ /* ---------------------------------------------------------------- */
145
+ emitProgram(program) {
146
+ for (const statement of program.body)
147
+ this.emitStatement(statement);
148
+ const chunks = [];
149
+ const banner = this.options.banner ?? '// Generated by Spak emitTypeScript';
150
+ if (banner)
151
+ chunks.push(banner);
152
+ if (this.options.runtime !== false)
153
+ chunks.push(RUNTIME.replace(/\n$/, ''));
154
+ chunks.push(this.lines.join('\n'));
155
+ return chunks.filter(Boolean).join('\n\n') + '\n';
156
+ }
157
+ emitStatement(statement, exported = false) {
158
+ switch (statement.kind) {
159
+ case 'VarDecl':
160
+ this.emitVarDecl(statement, exported);
161
+ return;
162
+ case 'Assign':
163
+ this.emitAssign(statement);
164
+ return;
165
+ case 'FnDecl':
166
+ this.emitFnDecl(statement, exported);
167
+ return;
168
+ case 'ClassDecl':
169
+ this.emitClassDecl(statement, exported);
170
+ return;
171
+ case 'IfStmt':
172
+ this.emitIf(statement);
173
+ return;
174
+ case 'UnlessStmt':
175
+ this.line(`if (!(${this.expr(statement.cond)})) {`);
176
+ this.withScope(() => this.emitBlock(statement.body));
177
+ this.line('}');
178
+ return;
179
+ case 'WhileStmt':
180
+ this.line(`while (${this.expr(statement.cond)}) {`);
181
+ this.withScope(() => this.emitBlock(statement.body));
182
+ this.line('}');
183
+ return;
184
+ case 'RepeatStmt':
185
+ this.line('do {');
186
+ this.withScope(() => this.emitBlock(statement.body));
187
+ this.line(`} while (!(${this.expr(statement.cond)}));`);
188
+ return;
189
+ case 'ForNumeric':
190
+ this.emitForNumeric(statement);
191
+ return;
192
+ case 'ForIn':
193
+ this.emitForIn(statement);
194
+ return;
195
+ case 'ReturnStmt': {
196
+ if (statement.values.length === 0)
197
+ this.line('return;');
198
+ else if (statement.values.length === 1)
199
+ this.line(`return ${this.expr(statement.values[0])};`);
200
+ else
201
+ this.line(`return [${statement.values.map(value => this.expr(value)).join(', ')}];`);
202
+ return;
203
+ }
204
+ case 'BreakStmt':
205
+ this.line('break;');
206
+ return;
207
+ case 'ContinueStmt':
208
+ this.line('continue;');
209
+ return;
210
+ case 'MatchStmt':
211
+ this.emitMatch(statement);
212
+ return;
213
+ case 'TryStmt':
214
+ this.emitTry(statement);
215
+ return;
216
+ case 'ImportDecl':
217
+ this.emitImport(statement);
218
+ return;
219
+ case 'ExportDecl':
220
+ this.emitStatement(statement.declaration, true);
221
+ return;
222
+ case 'ExportDefault':
223
+ this.line(`export default ${this.expr(statement.expr)};`);
224
+ return;
225
+ case 'TypeAliasDecl':
226
+ this.emitTypeAlias(statement, exported);
227
+ return;
228
+ case 'ThrowStmt':
229
+ this.line(`throw ${this.expr(statement.value)};`);
230
+ return;
231
+ case 'SpawnStmt': {
232
+ const value = this.expr(statement.value);
233
+ if (statement.value.kind === 'FunctionExpr')
234
+ this.line(`void Promise.resolve((${value})());`);
235
+ else
236
+ this.line(`void Promise.resolve(${value});`);
237
+ return;
238
+ }
239
+ case 'ExprStmt':
240
+ this.line(`${this.expr(statement.expr)};`);
241
+ return;
242
+ default:
243
+ this.line(`// unsupported statement: ${statement.kind}`);
244
+ }
245
+ }
246
+ emitBlock(block) {
247
+ if (!block)
248
+ return;
249
+ for (const statement of block.body)
250
+ this.emitStatement(statement);
251
+ }
252
+ emitDecorators(decorators) {
253
+ if (!decorators?.length)
254
+ return;
255
+ for (const decorator of decorators) {
256
+ const args = decorator.args.map(arg => this.expr(arg)).join(', ');
257
+ this.line(`// @${decorator.name}${args ? `(${args})` : ''}`);
258
+ }
259
+ }
260
+ /* ---------------------------------------------------------------- */
261
+ /* Declarations */
262
+ /* ---------------------------------------------------------------- */
263
+ emitVarDecl(node, exported) {
264
+ if (node.doc)
265
+ this.emitDoc(node.doc);
266
+ this.emitDecorators(node.decorators);
267
+ const pairs = node.names.map((entry, index) => {
268
+ const value = node.values[index];
269
+ return { name: entry.name, value: value ? this.expr(value) : undefined };
270
+ });
271
+ if (node.scope === 'global') {
272
+ for (const pair of pairs) {
273
+ const value = pair.value ?? 'undefined';
274
+ this.line(`globalThis.${pair.name} = ${value};`);
275
+ if (exported)
276
+ this.line(`export const ${pair.name} = globalThis.${pair.name};`);
277
+ }
278
+ return;
279
+ }
280
+ const declaredHere = pairs.some(pair => this.declared(pair.name));
281
+ if (!declaredHere) {
282
+ const keyword = node.scope === 'const' ? 'const' : 'let';
283
+ const prefix = exported ? 'export ' : '';
284
+ const assign = pairs.map(pair => pair.value === undefined ? pair.name : `${pair.name} = ${pair.value}`);
285
+ this.line(`${prefix}${keyword} ${assign.join(', ')};`);
286
+ for (const pair of pairs)
287
+ this.declare(pair.name);
288
+ return;
289
+ }
290
+ for (const pair of pairs) {
291
+ if (!this.declared(pair.name)) {
292
+ const keyword = node.scope === 'const' ? 'const' : 'let';
293
+ this.declare(pair.name);
294
+ this.line(pair.value === undefined ? `${keyword} ${pair.name};` : `${keyword} ${pair.name} = ${pair.value};`);
295
+ }
296
+ else if (pair.value !== undefined) {
297
+ this.line(`${pair.name} = ${pair.value};`);
298
+ }
299
+ }
300
+ }
301
+ emitDoc(doc) {
302
+ const lines = doc.split(/\r?\n/);
303
+ if (lines.length === 1) {
304
+ this.line(`/** ${lines[0]} */`);
305
+ return;
306
+ }
307
+ this.line('/**');
308
+ for (const text of lines)
309
+ this.line(` * ${text}`);
310
+ this.line(' */');
311
+ }
312
+ emitAssign(node) {
313
+ const values = node.values;
314
+ node.targets.forEach((target, index) => {
315
+ const value = values[index] ? this.expr(values[index]) : 'undefined';
316
+ if (this.isPattern(target)) {
317
+ this.emitPatternAssign(target, value);
318
+ return;
319
+ }
320
+ this.line(`${this.expr(target)} = ${value};`);
321
+ });
322
+ }
323
+ isPattern(target) {
324
+ return target.kind === 'TablePattern' || target.kind === 'ArrayPattern';
325
+ }
326
+ emitPatternAssign(pattern, value) {
327
+ if (pattern.kind === 'ArrayPattern') {
328
+ const names = pattern.elements;
329
+ this.line(`let [${names.join(', ')}] = ${value};`);
330
+ for (const name of names)
331
+ this.declare(name);
332
+ return;
333
+ }
334
+ const table = pattern;
335
+ const entries = table.entries.map((entry) => {
336
+ const target = entry.target ?? entry.key;
337
+ if (entry.default)
338
+ return `${entry.key}: ${target} = ${this.expr(entry.default)}`;
339
+ return entry.target ? `${entry.key}: ${target}` : entry.key;
340
+ });
341
+ this.line(`let { ${entries.join(', ')} } = ${value};`);
342
+ for (const entry of table.entries)
343
+ this.declare(entry.target ?? entry.key);
344
+ }
345
+ emitFnDecl(node, exported) {
346
+ if (node.doc)
347
+ this.emitDoc(node.doc);
348
+ this.emitDecorators(node.decorators);
349
+ if (node.receiver) {
350
+ const fn = this.renderFunction(node);
351
+ this.line(`${node.receiver}.${node.name} = ${fn};`);
352
+ return;
353
+ }
354
+ this.declare(node.name);
355
+ const prefix = exported ? 'export ' : '';
356
+ this.line(`${prefix}${node.async ? 'async ' : ''}function ${node.name}(${this.emitParams(node.params)}) {`);
357
+ this.withScope(() => {
358
+ this.declareParams(node.params);
359
+ this.emitFunctionBody(node);
360
+ });
361
+ this.line('}');
362
+ }
363
+ emitClassDecl(node, exported) {
364
+ if (node.doc)
365
+ this.emitDoc(node.doc);
366
+ this.emitDecorators(node.decorators);
367
+ this.declare(node.name);
368
+ const prefix = exported ? 'export ' : '';
369
+ const parent = node.superClass ? ` extends ${node.superClass}` : '';
370
+ this.line(`${prefix}class ${node.name}${parent} {`);
371
+ this.withScope(() => {
372
+ for (const member of node.members) {
373
+ if (member.kind === 'VarDecl') {
374
+ if (member.doc)
375
+ this.emitDoc(member.doc);
376
+ this.emitDecorators(member.decorators);
377
+ member.names.forEach((entry, index) => {
378
+ const value = member.values[index];
379
+ this.line(`${entry.name} = ${value ? this.expr(value) : 'undefined'};`);
380
+ this.declare(entry.name);
381
+ });
382
+ continue;
383
+ }
384
+ this.emitClassMethod(member);
385
+ }
386
+ });
387
+ this.line('}');
388
+ }
389
+ emitClassMethod(node) {
390
+ if (node.doc)
391
+ this.emitDoc(node.doc);
392
+ this.emitDecorators(node.decorators);
393
+ const name = node.name === 'init' ? 'constructor' : node.name;
394
+ const asyncKeyword = node.async && name !== 'constructor' ? 'async ' : '';
395
+ this.line(`${asyncKeyword}${name}(${this.emitParams(node.params)}) {`);
396
+ this.withScope(() => {
397
+ this.declareParams(node.params);
398
+ this.emitFunctionBody(node);
399
+ });
400
+ this.line('}');
401
+ }
402
+ emitFunctionBody(node) {
403
+ if (node.exprBody) {
404
+ this.line(`return ${this.expr(node.exprBody)};`);
405
+ return;
406
+ }
407
+ this.emitBlock(node.body);
408
+ }
409
+ declareParams(params) {
410
+ for (const param of params)
411
+ this.declare(rawParamName(param));
412
+ }
413
+ emitParams(params) {
414
+ return params.map((param) => {
415
+ if (param.rest)
416
+ return `...${rawParamName(param)}`;
417
+ const value = param.default ? ` = ${this.expr(param.default)}` : '';
418
+ return `${param.name}${value}`;
419
+ }).join(', ');
420
+ }
421
+ renderFunction(node) {
422
+ const sub = new Emitter(this.options);
423
+ sub.scopes = [new Set()];
424
+ const params = sub.emitParams(node.params);
425
+ sub.withScope(() => {
426
+ sub.declareParams(node.params);
427
+ sub.emitFunctionBody(node);
428
+ });
429
+ const body = sub.lines.map(text => (text ? this.pad() + text : text)).join('\n');
430
+ const asyncKeyword = node.async ? 'async ' : '';
431
+ return `${asyncKeyword}function (${params}) {\n${body}\n${this.pad()}}`;
432
+ }
433
+ emitTypeAlias(node, exported) {
434
+ if (node.doc)
435
+ this.emitDoc(node.doc);
436
+ this.line(`${exported ? 'export ' : ''}type ${node.name} = ${typeAnn(node.type)};`);
437
+ }
438
+ /* ---------------------------------------------------------------- */
439
+ /* Control flow */
440
+ /* ---------------------------------------------------------------- */
441
+ emitIf(node) {
442
+ this.line(`if (${this.expr(node.cond)}) {`);
443
+ this.withScope(() => this.emitBlock(node.then));
444
+ for (const branch of node.elifs) {
445
+ this.line(`} else if (${this.expr(branch.cond)}) {`);
446
+ this.withScope(() => this.emitBlock(branch.body));
447
+ }
448
+ if (node.else) {
449
+ this.line('} else {');
450
+ this.withScope(() => this.emitBlock(node.else));
451
+ }
452
+ this.line('}');
453
+ }
454
+ emitForNumeric(node) {
455
+ const from = this.expr(node.from);
456
+ const to = this.expr(node.to);
457
+ const step = node.step ? this.expr(node.step) : '1';
458
+ const descending = isNegative(node.step);
459
+ const compare = descending ? '>=' : '<=';
460
+ this.line(`for (let ${node.varName} = ${from}; ${node.varName} ${compare} ${to}; ${node.varName} += ${step}) {`);
461
+ this.withScope(() => {
462
+ this.declare(node.varName);
463
+ this.emitBlock(node.body);
464
+ });
465
+ this.line('}');
466
+ }
467
+ emitForIn(node) {
468
+ let source;
469
+ if (node.iter.kind === 'Range') {
470
+ source = `__spak.range(${this.expr(node.iter.from)}, ${this.expr(node.iter.to)}, ${node.iter.inclusive ? 'true' : 'false'})`;
471
+ }
472
+ else {
473
+ source = this.expr(node.iter);
474
+ }
475
+ if (node.vars.length > 1) {
476
+ this.line(`for (const [${node.vars.join(', ')}] of __spak.entries(${source})) {`);
477
+ }
478
+ else {
479
+ this.line(`for (const ${node.vars[0]} of ${source}) {`);
480
+ }
481
+ this.withScope(() => {
482
+ for (const name of node.vars)
483
+ this.declare(name);
484
+ this.emitBlock(node.body);
485
+ });
486
+ this.line('}');
487
+ }
488
+ emitMatch(node) {
489
+ const subject = `__match${++this.temp}`;
490
+ this.line(`const ${subject} = ${this.expr(node.subject)};`);
491
+ node.arms.forEach((arm, index) => {
492
+ const bindings = [];
493
+ const conditions = arm.patterns.map((pattern) => {
494
+ if (pattern.kind === 'Ident') {
495
+ bindings.push(pattern.name);
496
+ return 'true';
497
+ }
498
+ return this.matchCondition(pattern, subject);
499
+ });
500
+ let condition = conditions.length ? conditions.join(' || ') : 'true';
501
+ if (arm.guard)
502
+ condition = `(${condition} && (${this.expr(arm.guard)}))`;
503
+ for (const name of bindings)
504
+ condition = `((${name}) => ${condition})(${subject})`;
505
+ this.line(`${index === 0 ? 'if' : '} else if'} (${condition}) {`);
506
+ this.withScope(() => {
507
+ for (const name of bindings) {
508
+ this.line(`const ${name} = ${subject};`);
509
+ this.declare(name);
510
+ }
511
+ this.emitBlock(arm.body);
512
+ });
513
+ });
514
+ if (node.elseBody) {
515
+ const hasArms = node.arms.length > 0;
516
+ this.line(hasArms ? '} else {' : 'if (true) {');
517
+ this.withScope(() => this.emitBlock(node.elseBody));
518
+ }
519
+ if (node.arms.length || node.elseBody)
520
+ this.line('}');
521
+ }
522
+ matchCondition(pattern, subject) {
523
+ if (pattern.kind === 'Range') {
524
+ const from = this.expr(pattern.from);
525
+ const to = this.expr(pattern.to);
526
+ const op = pattern.inclusive ? '<=' : '<';
527
+ return `(${subject} >= ${from} && ${subject} ${op} ${to})`;
528
+ }
529
+ return `${subject} === ${this.expr(pattern)}`;
530
+ }
531
+ emitTry(node) {
532
+ this.line('try {');
533
+ this.withScope(() => this.emitBlock(node.body));
534
+ if (node.catchBody) {
535
+ this.line(node.catchVar ? `} catch (${node.catchVar}) {` : '} catch {');
536
+ this.withScope(() => {
537
+ if (node.catchVar)
538
+ this.declare(node.catchVar);
539
+ this.emitBlock(node.catchBody);
540
+ });
541
+ }
542
+ if (node.finallyBody) {
543
+ this.line('} finally {');
544
+ this.withScope(() => this.emitBlock(node.finallyBody));
545
+ }
546
+ this.line('}');
547
+ }
548
+ /* ---------------------------------------------------------------- */
549
+ /* Modules */
550
+ /* ---------------------------------------------------------------- */
551
+ emitImport(node) {
552
+ if (node.doc)
553
+ this.emitDoc(node.doc);
554
+ if (FS_SOURCES.has(node.source)) {
555
+ this.emitFsImport(node);
556
+ return;
557
+ }
558
+ const source = JSON.stringify(node.source);
559
+ if (node.clauses.length === 0) {
560
+ this.line(`import ${source};`);
561
+ return;
562
+ }
563
+ if (node.clauses.length === 1) {
564
+ const clause = node.clauses[0];
565
+ if (clause.name === '*') {
566
+ this.line(`import * as ${clause.alias ?? 'ns'} from ${source};`);
567
+ return;
568
+ }
569
+ if (!clause.alias) {
570
+ this.line(`import ${clause.name} from ${source};`);
571
+ this.declare(clause.name);
572
+ return;
573
+ }
574
+ this.line(`import { ${clause.name} as ${clause.alias} } from ${source};`);
575
+ this.declare(clause.alias);
576
+ return;
577
+ }
578
+ const specifiers = node.clauses.map(clause => clause.alias ? `${clause.name} as ${clause.alias}` : clause.name);
579
+ this.line(`import { ${specifiers.join(', ')} } from ${source};`);
580
+ for (const clause of node.clauses)
581
+ this.declare(clause.alias ?? clause.name);
582
+ }
583
+ /**
584
+ * Spak's `fs` is synchronous (`for name in readdir(dir)`), so imports from
585
+ * `fs` are bound to the `*Sync` variants through a namespace import.
586
+ */
587
+ emitFsImport(node) {
588
+ const namespace = `__fs${++this.temp}`;
589
+ this.line(`import * as ${namespace} from "node:fs";`);
590
+ if (node.clauses.length === 0)
591
+ return;
592
+ if (node.clauses.length === 1) {
593
+ const clause = node.clauses[0];
594
+ if (clause.name === '*') {
595
+ this.line(`const ${clause.alias ?? 'fs'} = ${namespace};`);
596
+ return;
597
+ }
598
+ if (!clause.alias && !FS_SYNC.has(clause.name)) {
599
+ this.line(`const ${clause.name} = ${namespace};`);
600
+ this.declare(clause.name);
601
+ return;
602
+ }
603
+ }
604
+ for (const clause of node.clauses) {
605
+ if (clause.name === '*') {
606
+ this.line(`const ${clause.alias ?? 'fs'} = ${namespace};`);
607
+ continue;
608
+ }
609
+ const local = clause.alias ?? clause.name;
610
+ const member = FS_SYNC.has(clause.name) ? `${clause.name}Sync` : clause.name;
611
+ this.line(`const ${local} = ${namespace}.${member};`);
612
+ this.declare(local);
613
+ }
614
+ }
615
+ /* ---------------------------------------------------------------- */
616
+ /* Expressions */
617
+ /* ---------------------------------------------------------------- */
618
+ expr(node) {
619
+ switch (node.kind) {
620
+ case 'NumberLit':
621
+ return Number.isFinite(node.value) ? String(node.value) : '0';
622
+ case 'StringLit':
623
+ return this.string(node);
624
+ case 'BoolLit':
625
+ return node.value ? 'true' : 'false';
626
+ case 'NilLit':
627
+ return 'null';
628
+ case 'Ident':
629
+ return node.name;
630
+ case 'SelfExpr':
631
+ return 'this';
632
+ case 'Member':
633
+ return `${this.expr(node.object)}${node.optional ? '?.' : '.'}${propertyName(node.property)}`;
634
+ case 'Index':
635
+ return this.index(node);
636
+ case 'Call':
637
+ return this.call(node);
638
+ case 'Binary':
639
+ return this.binary(node);
640
+ case 'Unary':
641
+ if (node.op === 'not')
642
+ return `(!(${this.expr(node.argument)}))`;
643
+ if (node.op === '#')
644
+ return `__spak.len(${this.expr(node.argument)})`;
645
+ return `(-(${this.expr(node.argument)}))`;
646
+ case 'Table':
647
+ return this.table(node);
648
+ case 'FunctionExpr':
649
+ return this.renderFunction(node);
650
+ case 'Range':
651
+ return `__spak.range(${this.expr(node.from)}, ${this.expr(node.to)}, ${node.inclusive ? 'true' : 'false'})`;
652
+ case 'AwaitExpr':
653
+ return `(await ${this.expr(node.argument)})`;
654
+ case 'Spread':
655
+ return node.argument ? `...${this.expr(node.argument)}` : '...[]';
656
+ default:
657
+ return 'undefined';
658
+ }
659
+ }
660
+ string(node) {
661
+ if (node.fstring && node.parts) {
662
+ const body = node.parts.map((part) => {
663
+ if (typeof part === 'string')
664
+ return escapeTemplate(part);
665
+ return `\${${this.expr(part)}}`;
666
+ }).join('');
667
+ return `\`${body}\``;
668
+ }
669
+ return JSON.stringify(node.value);
670
+ }
671
+ index(node) {
672
+ const object = this.expr(node.object);
673
+ if (node.index.kind === 'Range') {
674
+ const range = node.index;
675
+ if (range.from.kind === 'NumberLit' && range.from.value === 1) {
676
+ if (range.inclusive)
677
+ return `${object}.slice(0, ${this.expr(range.to)})`;
678
+ return `${object}.slice(0, (${this.expr(range.to)}) - 1)`;
679
+ }
680
+ return `__spak.slice(${object}, ${this.expr(range.from)}, ${this.expr(range.to)}, ${range.inclusive ? 'true' : 'false'})`;
681
+ }
682
+ return `__spak.idx(${object}, ${this.expr(node.index)})`;
683
+ }
684
+ call(node, prepend = []) {
685
+ const args = [...prepend, ...node.args].map(arg => this.expr(arg)).join(', ');
686
+ if (node.callee.kind === 'Member') {
687
+ const member = node.callee;
688
+ const object = this.expr(member.object);
689
+ if (!node.isMethod && member.property === 'new') {
690
+ return `new ${object}(${args})`;
691
+ }
692
+ if (node.isMethod && RUNTIME_METHODS.has(member.property)) {
693
+ const prefix = args ? `${object}, ${args}` : object;
694
+ return `__spak.${member.property}(${prefix})`;
695
+ }
696
+ const access = `${object}${member.optional ? '?.' : '.'}${propertyName(member.property)}`;
697
+ return `${access}(${args})`;
698
+ }
699
+ if (node.callee.kind === 'Ident') {
700
+ if (node.callee.name === 'len')
701
+ return `__spak.len(${args})`;
702
+ if (node.callee.name === 'range')
703
+ return `__spak.range(${args})`;
704
+ if (node.callee.name === 'pairs' || node.callee.name === 'ipairs')
705
+ return `__spak.entries(${args})`;
706
+ }
707
+ return `${this.expr(node.callee)}(${args})`;
708
+ }
709
+ binary(node) {
710
+ const left = this.expr(node.left);
711
+ const right = this.expr(node.right);
712
+ if (node.op === '|>') {
713
+ if (node.right.kind === 'Call')
714
+ return this.call(node.right, [node.left]);
715
+ return `${this.expr(node.right)}(${left})`;
716
+ }
717
+ if (node.op === '//')
718
+ return `Math.floor((${left}) / (${right}))`;
719
+ if (node.op === '..')
720
+ return `(String(${left}) + String(${right}))`;
721
+ const operators = {
722
+ and: '&&',
723
+ or: '||',
724
+ '==': '===',
725
+ '~=': '!==',
726
+ '^': '**',
727
+ };
728
+ const operator = operators[node.op] ?? node.op;
729
+ return `(${left} ${operator} ${right})`;
730
+ }
731
+ table(node) {
732
+ if (node.fields.length === 0)
733
+ return '[]';
734
+ const hasKeys = node.fields.some(field => field.key);
735
+ if (!hasKeys) {
736
+ return `[${node.fields.map(field => this.expr(field.value)).join(', ')}]`;
737
+ }
738
+ const parts = [];
739
+ let position = 0;
740
+ for (const field of node.fields) {
741
+ if (field.key?.kind === 'Ident') {
742
+ parts.push(`${propertyName(field.key.name)}: ${this.expr(field.value)}`);
743
+ continue;
744
+ }
745
+ if (field.key) {
746
+ parts.push(`[${this.expr(field.key)}]: ${this.expr(field.value)}`);
747
+ continue;
748
+ }
749
+ parts.push(`${position}: ${this.expr(field.value)}`);
750
+ position++;
751
+ }
752
+ return `{ ${parts.join(', ')} }`;
753
+ }
754
+ }
755
+ /* ------------------------------------------------------------------ */
756
+ /* Helpers */
757
+ /* ------------------------------------------------------------------ */
758
+ function rawParamName(param) {
759
+ return param.rest ? (param.name === '...' ? 'args' : param.name) : param.name;
760
+ }
761
+ function propertyName(name) {
762
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
763
+ }
764
+ function isNegative(expr) {
765
+ if (!expr)
766
+ return false;
767
+ if (expr.kind === 'NumberLit')
768
+ return expr.value < 0;
769
+ return expr.kind === 'Unary' && expr.op === '-' && expr.argument.kind === 'NumberLit';
770
+ }
771
+ function escapeTemplate(text) {
772
+ return text.replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
773
+ }
774
+ /** Converts a Spak type annotation into TypeScript syntax (types only). */
775
+ function typeAnn(node) {
776
+ switch (node.kind) {
777
+ case 'named':
778
+ return node.name === 'nil' ? 'null' : node.name;
779
+ case 'array': {
780
+ const element = node.element.kind === 'union' || node.element.kind === 'fn'
781
+ ? `(${typeAnn(node.element)})`
782
+ : typeAnn(node.element);
783
+ return `${element}[]`;
784
+ }
785
+ case 'dict':
786
+ return `Record<${typeAnn(node.key)}, ${typeAnn(node.value)}>`;
787
+ case 'record':
788
+ return `{ ${node.fields.map(field => `${propertyName(field.name)}: ${typeAnn(field.type)}`).join(', ')} }`;
789
+ case 'union':
790
+ return node.types.map(typeAnn).join(' | ');
791
+ case 'fn':
792
+ return `(${node.params.map(typeAnn).join(', ')}) => ${typeAnn(node.returns)}`;
793
+ case 'optional':
794
+ return `${typeAnn(node.type)} | null`;
795
+ default:
796
+ return 'unknown';
797
+ }
798
+ }
799
+ /** Lowers a Spak program to an executable ESM TypeScript module. */
800
+ function emitTypeScript(program, options = {}) {
801
+ return new Emitter(options).emitProgram(program);
802
+ }
803
+ //# sourceMappingURL=ts.js.map