@starpak/lang 0.0.0-stage → 0.1.3
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 +22 -0
- package/lib/ast.d.ts +341 -0
- package/lib/ast.d.ts.map +1 -0
- package/lib/ast.js +9 -0
- package/lib/ast.js.map +1 -0
- package/lib/checker.d.ts +17 -0
- package/lib/checker.d.ts.map +1 -0
- package/lib/checker.js +525 -0
- package/lib/checker.js.map +1 -0
- package/lib/diagnostics.d.ts +46 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +77 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/emit/lua.d.ts +19 -0
- package/lib/emit/lua.d.ts.map +1 -0
- package/lib/emit/lua.js +451 -0
- package/lib/emit/lua.js.map +1 -0
- package/lib/emit/ts.d.ts +42 -0
- package/lib/emit/ts.d.ts.map +1 -0
- package/lib/emit/ts.js +950 -0
- package/lib/emit/ts.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +54 -0
- package/lib/index.js.map +1 -0
- package/lib/lexer.d.ts +10 -0
- package/lib/lexer.d.ts.map +1 -0
- package/lib/lexer.js +282 -0
- package/lib/lexer.js.map +1 -0
- package/lib/parser.d.ts +11 -0
- package/lib/parser.d.ts.map +1 -0
- package/lib/parser.js +1138 -0
- package/lib/parser.js.map +1 -0
- package/lib/token.d.ts +32 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +18 -0
- package/lib/token.js.map +1 -0
- package/lib/types.d.ts +63 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +177 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +25 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +47 -0
- package/lib/version.js.map +1 -0
- package/package.json +10 -3
- package/src/ast.ts +467 -0
- package/src/checker.ts +554 -0
- package/src/diagnostics.ts +100 -0
- package/src/emit/lua.ts +484 -0
- package/src/emit/ts.ts +1024 -0
- package/src/index.ts +24 -0
- package/src/lexer.ts +301 -0
- package/src/parser.ts +1257 -0
- package/src/token.ts +34 -0
- package/src/types.ts +218 -0
- package/src/version.ts +44 -0
- package/README.md +0 -3
package/lib/emit/ts.js
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
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
|
+
* - `fs` named imports stay async when the binding is awaited, so
|
|
16
|
+
* `import { readFile } from "fs"` + `await readFile(...)` works. Imports that
|
|
17
|
+
* are never awaited keep the historical synchronous binding (hello.spk
|
|
18
|
+
* compatibility); `import { readFileSync }` is always explicit.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.typeAnn = typeAnn;
|
|
22
|
+
exports.emitTypeScript = emitTypeScript;
|
|
23
|
+
const version_1 = require("../version");
|
|
24
|
+
/** Lua-style methods that are backed by the runtime shim. */
|
|
25
|
+
const RUNTIME_METHODS = new Set(['push', 'pop', 'sort', 'split', 'len']);
|
|
26
|
+
/** Node builtins whose Spak `fs` surface uses the sync/async compat shim. */
|
|
27
|
+
const FS_SOURCES = new Set(['fs', 'node:fs']);
|
|
28
|
+
/** Node `fs` functions that also exist as `*Sync` and `fs/promises` variants. */
|
|
29
|
+
const FS_SYNC = new Set([
|
|
30
|
+
'access', 'appendFile', 'chmod', 'chown', 'close', 'copyFile', 'cp',
|
|
31
|
+
'exists', 'fchmod', 'fchown', 'fstat', 'fsync', 'ftruncate', 'futimes',
|
|
32
|
+
'glob', 'lchmod', 'lchown', 'link', 'lstat', 'lutimes', 'mkdir', 'mkdtemp',
|
|
33
|
+
'open', 'opendir', 'read', 'readFile', 'readdir', 'readlink', 'readv',
|
|
34
|
+
'realpath', 'rename', 'rm', 'rmdir', 'stat', 'statfs', 'symlink',
|
|
35
|
+
'truncate', 'unlink', 'utimes', 'write', 'writeFile', 'writev',
|
|
36
|
+
]);
|
|
37
|
+
const RUNTIME = `// spak runtime (inlined helpers, no external dependencies)
|
|
38
|
+
const __spak = {
|
|
39
|
+
len(x) {
|
|
40
|
+
if (x === null || x === undefined) return 0
|
|
41
|
+
if (typeof x === "string" || Array.isArray(x)) return x.length
|
|
42
|
+
if (x instanceof Map || x instanceof Set) return x.size
|
|
43
|
+
if (typeof x === "object") return Object.keys(x).length
|
|
44
|
+
return 0
|
|
45
|
+
},
|
|
46
|
+
range(from, to, inclusive = true) {
|
|
47
|
+
const out = []
|
|
48
|
+
const step = from <= to ? 1 : -1
|
|
49
|
+
const end = inclusive ? to : to - step
|
|
50
|
+
for (let i = from; step > 0 ? i <= end : i >= end; i += step) out.push(i)
|
|
51
|
+
return out
|
|
52
|
+
},
|
|
53
|
+
slice(x, from, to, inclusive = true) {
|
|
54
|
+
// 1-based, end-inclusive bounds. Empty ranges (1..0, 3..2) must be empty,
|
|
55
|
+
// so clamp instead of letting a negative end mean "from the tail".
|
|
56
|
+
const length = typeof x === "string" || Array.isArray(x) ? x.length : 0
|
|
57
|
+
const rawStart = Number(from) - 1
|
|
58
|
+
const rawEnd = inclusive ? Number(to) : Number(to) - 1
|
|
59
|
+
const start = Math.max(0, Number.isFinite(rawStart) ? rawStart : 0)
|
|
60
|
+
const end = Number.isFinite(rawEnd) ? rawEnd : -1
|
|
61
|
+
if (end < start || start >= length) return x.slice(0, 0)
|
|
62
|
+
return x.slice(start, Math.min(end, length))
|
|
63
|
+
},
|
|
64
|
+
idx(x, i) {
|
|
65
|
+
// Lua-style 1-based indexing for sequences; plain maps keep their keys.
|
|
66
|
+
if (Array.isArray(x) || typeof x === "string") return x[Number(i) - 1]
|
|
67
|
+
return x[i]
|
|
68
|
+
},
|
|
69
|
+
split(s, sep) {
|
|
70
|
+
return String(s).split(sep)
|
|
71
|
+
},
|
|
72
|
+
push(t, v) {
|
|
73
|
+
if (Array.isArray(t)) t.push(v)
|
|
74
|
+
else if (typeof t.length === "number") t[t.length] = v
|
|
75
|
+
else t[Object.keys(t).length] = v
|
|
76
|
+
return t
|
|
77
|
+
},
|
|
78
|
+
pop(t) {
|
|
79
|
+
if (Array.isArray(t)) return t.pop()
|
|
80
|
+
const keys = Object.keys(t)
|
|
81
|
+
if (!keys.length) return null
|
|
82
|
+
const key = keys[keys.length - 1]
|
|
83
|
+
const value = t[key]
|
|
84
|
+
delete t[key]
|
|
85
|
+
return value
|
|
86
|
+
},
|
|
87
|
+
sort(t, cmp) {
|
|
88
|
+
if (!cmp) {
|
|
89
|
+
t.sort()
|
|
90
|
+
return t
|
|
91
|
+
}
|
|
92
|
+
t.sort((a, b) => {
|
|
93
|
+
const result = cmp(a, b)
|
|
94
|
+
if (typeof result === "number") return result
|
|
95
|
+
if (result) return -1
|
|
96
|
+
return cmp(b, a) ? 1 : 0
|
|
97
|
+
})
|
|
98
|
+
return t
|
|
99
|
+
},
|
|
100
|
+
entries(t) {
|
|
101
|
+
if (t instanceof Map) return Array.from(t.entries())
|
|
102
|
+
if (t instanceof Set) return Array.from(t.entries())
|
|
103
|
+
if (Array.isArray(t) || typeof t === "string") return Array.from(t.entries())
|
|
104
|
+
return Object.entries(t)
|
|
105
|
+
},
|
|
106
|
+
iter(t) {
|
|
107
|
+
if (t === null || t === undefined) return []
|
|
108
|
+
if (typeof t === "string" || Array.isArray(t)) return t
|
|
109
|
+
if (t instanceof Map || t instanceof Set) return t
|
|
110
|
+
if (typeof t[Symbol.iterator] === "function") return t
|
|
111
|
+
return Object.keys(t)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function len(x) { return __spak.len(x) }
|
|
115
|
+
function range(from, to) { return __spak.range(from, to, true) }
|
|
116
|
+
function print(...args) { console.log(...args) }
|
|
117
|
+
function tostring(x) { return String(x) }
|
|
118
|
+
`;
|
|
119
|
+
class Emitter {
|
|
120
|
+
lines = [];
|
|
121
|
+
level = 0;
|
|
122
|
+
scopes = [new Set()];
|
|
123
|
+
temp = 0;
|
|
124
|
+
/** Identifiers used directly under `await`; drives the async `fs` surface. */
|
|
125
|
+
awaited = new Set();
|
|
126
|
+
options;
|
|
127
|
+
constructor(options = {}) {
|
|
128
|
+
this.options = options;
|
|
129
|
+
}
|
|
130
|
+
get unit() {
|
|
131
|
+
return this.options.indent ?? ' ';
|
|
132
|
+
}
|
|
133
|
+
pad(extra = 0) {
|
|
134
|
+
return this.unit.repeat(Math.max(0, this.level + extra));
|
|
135
|
+
}
|
|
136
|
+
line(text = '') {
|
|
137
|
+
this.lines.push(text ? this.pad() + text : '');
|
|
138
|
+
}
|
|
139
|
+
withScope(fn) {
|
|
140
|
+
this.scopes.push(new Set());
|
|
141
|
+
this.level++;
|
|
142
|
+
try {
|
|
143
|
+
fn();
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
this.level--;
|
|
147
|
+
this.scopes.pop();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
declare(name) {
|
|
151
|
+
this.scopes[this.scopes.length - 1].add(name);
|
|
152
|
+
}
|
|
153
|
+
declared(name) {
|
|
154
|
+
return this.scopes.some(scope => scope.has(name));
|
|
155
|
+
}
|
|
156
|
+
/* ---------------------------------------------------------------- */
|
|
157
|
+
/* Program */
|
|
158
|
+
/* ---------------------------------------------------------------- */
|
|
159
|
+
emitProgram(program) {
|
|
160
|
+
this.awaited = collectAwaitedIdentifiers(program);
|
|
161
|
+
for (const statement of program.body)
|
|
162
|
+
this.emitStatement(statement);
|
|
163
|
+
const chunks = [];
|
|
164
|
+
const defaultBanner = this.options.version
|
|
165
|
+
? (0, version_1.versionBanner)(this.options.name ?? 'spak', this.options.version)
|
|
166
|
+
: '// Generated by Spak emitTypeScript';
|
|
167
|
+
const banner = this.options.banner ?? defaultBanner;
|
|
168
|
+
if (banner)
|
|
169
|
+
chunks.push(banner);
|
|
170
|
+
if (this.options.runtime !== false)
|
|
171
|
+
chunks.push(RUNTIME.replace(/\n$/, ''));
|
|
172
|
+
chunks.push(this.lines.join('\n'));
|
|
173
|
+
return chunks.filter(Boolean).join('\n\n') + '\n';
|
|
174
|
+
}
|
|
175
|
+
emitStatement(statement, exported = false) {
|
|
176
|
+
switch (statement.kind) {
|
|
177
|
+
case 'VarDecl':
|
|
178
|
+
this.emitVarDecl(statement, exported);
|
|
179
|
+
return;
|
|
180
|
+
case 'Assign':
|
|
181
|
+
this.emitAssign(statement);
|
|
182
|
+
return;
|
|
183
|
+
case 'FnDecl':
|
|
184
|
+
this.emitFnDecl(statement, exported);
|
|
185
|
+
return;
|
|
186
|
+
case 'ClassDecl':
|
|
187
|
+
this.emitClassDecl(statement, exported);
|
|
188
|
+
return;
|
|
189
|
+
case 'IfStmt':
|
|
190
|
+
this.emitIf(statement);
|
|
191
|
+
return;
|
|
192
|
+
case 'UnlessStmt':
|
|
193
|
+
this.line(`if (!(${this.expr(statement.cond)})) {`);
|
|
194
|
+
this.withScope(() => this.emitBlock(statement.body));
|
|
195
|
+
this.line('}');
|
|
196
|
+
return;
|
|
197
|
+
case 'WhileStmt':
|
|
198
|
+
this.line(`while (${this.expr(statement.cond)}) {`);
|
|
199
|
+
this.withScope(() => this.emitBlock(statement.body));
|
|
200
|
+
this.line('}');
|
|
201
|
+
return;
|
|
202
|
+
case 'RepeatStmt':
|
|
203
|
+
this.line('do {');
|
|
204
|
+
this.withScope(() => this.emitBlock(statement.body));
|
|
205
|
+
this.line(`} while (!(${this.expr(statement.cond)}));`);
|
|
206
|
+
return;
|
|
207
|
+
case 'ForNumeric':
|
|
208
|
+
this.emitForNumeric(statement);
|
|
209
|
+
return;
|
|
210
|
+
case 'ForIn':
|
|
211
|
+
this.emitForIn(statement);
|
|
212
|
+
return;
|
|
213
|
+
case 'ReturnStmt': {
|
|
214
|
+
if (statement.values.length === 0)
|
|
215
|
+
this.line('return;');
|
|
216
|
+
else if (statement.values.length === 1)
|
|
217
|
+
this.line(`return ${this.expr(statement.values[0])};`);
|
|
218
|
+
else
|
|
219
|
+
this.line(`return [${statement.values.map(value => this.expr(value)).join(', ')}];`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
case 'BreakStmt':
|
|
223
|
+
this.line('break;');
|
|
224
|
+
return;
|
|
225
|
+
case 'ContinueStmt':
|
|
226
|
+
this.line('continue;');
|
|
227
|
+
return;
|
|
228
|
+
case 'MatchStmt':
|
|
229
|
+
this.emitMatch(statement);
|
|
230
|
+
return;
|
|
231
|
+
case 'TryStmt':
|
|
232
|
+
this.emitTry(statement);
|
|
233
|
+
return;
|
|
234
|
+
case 'ImportDecl':
|
|
235
|
+
this.emitImport(statement);
|
|
236
|
+
return;
|
|
237
|
+
case 'ExportDecl':
|
|
238
|
+
this.emitStatement(statement.declaration, true);
|
|
239
|
+
return;
|
|
240
|
+
case 'ExportDefault':
|
|
241
|
+
this.line(`export default ${this.expr(statement.expr)};`);
|
|
242
|
+
return;
|
|
243
|
+
case 'TypeAliasDecl':
|
|
244
|
+
this.emitTypeAlias(statement, exported);
|
|
245
|
+
return;
|
|
246
|
+
case 'ThrowStmt':
|
|
247
|
+
this.line(`throw ${this.expr(statement.value)};`);
|
|
248
|
+
return;
|
|
249
|
+
case 'SpawnStmt': {
|
|
250
|
+
const value = this.expr(statement.value);
|
|
251
|
+
if (statement.value.kind === 'FunctionExpr')
|
|
252
|
+
this.line(`void Promise.resolve((${value})());`);
|
|
253
|
+
else
|
|
254
|
+
this.line(`void Promise.resolve(${value});`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
case 'ExprStmt':
|
|
258
|
+
this.line(`${this.expr(statement.expr)};`);
|
|
259
|
+
return;
|
|
260
|
+
default:
|
|
261
|
+
this.line(`// unsupported statement: ${statement.kind}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
emitBlock(block) {
|
|
265
|
+
if (!block)
|
|
266
|
+
return;
|
|
267
|
+
for (const statement of block.body)
|
|
268
|
+
this.emitStatement(statement);
|
|
269
|
+
}
|
|
270
|
+
/** Comments only: used for positions that cannot be wrapped at runtime. */
|
|
271
|
+
emitDecorators(decorators) {
|
|
272
|
+
if (!decorators?.length)
|
|
273
|
+
return;
|
|
274
|
+
for (const decorator of decorators) {
|
|
275
|
+
this.line(`// @${decorator.name}${this.decoratorArgs(decorator)}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
decoratorArgs(decorator) {
|
|
279
|
+
const args = decorator.args.map(arg => this.expr(arg)).join(', ');
|
|
280
|
+
return args ? `(${args})` : '';
|
|
281
|
+
}
|
|
282
|
+
/** Decorator as a callable expression: `memo`, `route("GET", "/x")`. */
|
|
283
|
+
decoratorCall(decorator) {
|
|
284
|
+
return decorator.args.length
|
|
285
|
+
? `${decorator.name}(${decorator.args.map(arg => this.expr(arg)).join(', ')})`
|
|
286
|
+
: decorator.name;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Emits executable decorator wrappers (`target = dec(target)`), innermost
|
|
290
|
+
* decorator first, matching the docs' `@decorator fn` => `f = decorator(f)`.
|
|
291
|
+
*/
|
|
292
|
+
applyDecorators(target, decorators) {
|
|
293
|
+
if (!decorators?.length)
|
|
294
|
+
return;
|
|
295
|
+
for (const decorator of [...decorators].reverse()) {
|
|
296
|
+
this.line(`${target} = ${this.decoratorCall(decorator)}(${target});`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/* ---------------------------------------------------------------- */
|
|
300
|
+
/* Declarations */
|
|
301
|
+
/* ---------------------------------------------------------------- */
|
|
302
|
+
emitVarDecl(node, exported) {
|
|
303
|
+
if (node.doc)
|
|
304
|
+
this.emitDoc(node.doc);
|
|
305
|
+
this.emitDecorators(node.decorators);
|
|
306
|
+
const pairs = node.names.map((entry, index) => {
|
|
307
|
+
const value = node.values[index];
|
|
308
|
+
return { name: entry.name, value: value ? this.expr(value) : undefined };
|
|
309
|
+
});
|
|
310
|
+
if (node.scope === 'global') {
|
|
311
|
+
for (const pair of pairs) {
|
|
312
|
+
const value = pair.value ?? 'undefined';
|
|
313
|
+
this.line(`globalThis.${pair.name} = ${value};`);
|
|
314
|
+
if (exported)
|
|
315
|
+
this.line(`export const ${pair.name} = globalThis.${pair.name};`);
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const declaredHere = pairs.some(pair => this.declared(pair.name));
|
|
320
|
+
if (!declaredHere) {
|
|
321
|
+
const keyword = node.scope === 'const' ? 'const' : 'let';
|
|
322
|
+
const prefix = exported ? 'export ' : '';
|
|
323
|
+
const assign = pairs.map(pair => pair.value === undefined ? pair.name : `${pair.name} = ${pair.value}`);
|
|
324
|
+
this.line(`${prefix}${keyword} ${assign.join(', ')};`);
|
|
325
|
+
for (const pair of pairs)
|
|
326
|
+
this.declare(pair.name);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
for (const pair of pairs) {
|
|
330
|
+
if (!this.declared(pair.name)) {
|
|
331
|
+
const keyword = node.scope === 'const' ? 'const' : 'let';
|
|
332
|
+
this.declare(pair.name);
|
|
333
|
+
this.line(pair.value === undefined ? `${keyword} ${pair.name};` : `${keyword} ${pair.name} = ${pair.value};`);
|
|
334
|
+
}
|
|
335
|
+
else if (pair.value !== undefined) {
|
|
336
|
+
this.line(`${pair.name} = ${pair.value};`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
emitDoc(doc) {
|
|
341
|
+
const lines = doc.split(/\r?\n/);
|
|
342
|
+
if (lines.length === 1) {
|
|
343
|
+
this.line(`/** ${lines[0]} */`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this.line('/**');
|
|
347
|
+
for (const text of lines)
|
|
348
|
+
this.line(` * ${text}`);
|
|
349
|
+
this.line(' */');
|
|
350
|
+
}
|
|
351
|
+
emitAssign(node) {
|
|
352
|
+
const values = node.values;
|
|
353
|
+
node.targets.forEach((target, index) => {
|
|
354
|
+
const value = values[index] ? this.expr(values[index]) : 'undefined';
|
|
355
|
+
if (this.isPattern(target)) {
|
|
356
|
+
this.emitPatternAssign(target, value);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.line(`${this.expr(target)} = ${value};`);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
isPattern(target) {
|
|
363
|
+
return target.kind === 'TablePattern' || target.kind === 'ArrayPattern';
|
|
364
|
+
}
|
|
365
|
+
emitPatternAssign(pattern, value) {
|
|
366
|
+
if (pattern.kind === 'ArrayPattern') {
|
|
367
|
+
const names = pattern.elements;
|
|
368
|
+
this.line(`let [${names.join(', ')}] = ${value};`);
|
|
369
|
+
for (const name of names)
|
|
370
|
+
this.declare(name);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const table = pattern;
|
|
374
|
+
const entries = table.entries.map((entry) => {
|
|
375
|
+
const target = entry.target ?? entry.key;
|
|
376
|
+
if (entry.default)
|
|
377
|
+
return `${entry.key}: ${target} = ${this.expr(entry.default)}`;
|
|
378
|
+
return entry.target ? `${entry.key}: ${target}` : entry.key;
|
|
379
|
+
});
|
|
380
|
+
this.line(`let { ${entries.join(', ')} } = ${value};`);
|
|
381
|
+
for (const entry of table.entries)
|
|
382
|
+
this.declare(entry.target ?? entry.key);
|
|
383
|
+
}
|
|
384
|
+
emitFnDecl(node, exported) {
|
|
385
|
+
if (node.doc)
|
|
386
|
+
this.emitDoc(node.doc);
|
|
387
|
+
this.emitDecorators(node.decorators);
|
|
388
|
+
if (node.receiver) {
|
|
389
|
+
const fn = this.renderFunction(node);
|
|
390
|
+
const target = `${node.receiver}.${propertyName(node.name)}`;
|
|
391
|
+
this.line(`${target} = ${fn};`);
|
|
392
|
+
this.applyDecorators(target, node.decorators);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.declare(node.name);
|
|
396
|
+
const prefix = exported ? 'export ' : '';
|
|
397
|
+
this.line(`${prefix}${node.async ? 'async ' : ''}function ${node.name}(${this.emitParams(node.params)}) {`);
|
|
398
|
+
this.withScope(() => {
|
|
399
|
+
this.declareParams(node.params);
|
|
400
|
+
this.emitFunctionBody(node);
|
|
401
|
+
});
|
|
402
|
+
this.line('}');
|
|
403
|
+
this.applyDecorators(node.name, node.decorators);
|
|
404
|
+
}
|
|
405
|
+
emitClassDecl(node, exported) {
|
|
406
|
+
if (node.doc)
|
|
407
|
+
this.emitDoc(node.doc);
|
|
408
|
+
this.emitDecorators(node.decorators);
|
|
409
|
+
this.declare(node.name);
|
|
410
|
+
const prefix = exported ? 'export ' : '';
|
|
411
|
+
const parent = node.superClass ? ` extends ${node.superClass}` : '';
|
|
412
|
+
this.line(`${prefix}class ${node.name}${parent} {`);
|
|
413
|
+
const methodWrappers = [];
|
|
414
|
+
this.withScope(() => {
|
|
415
|
+
for (const member of node.members) {
|
|
416
|
+
if (member.kind === 'VarDecl') {
|
|
417
|
+
if (member.doc)
|
|
418
|
+
this.emitDoc(member.doc);
|
|
419
|
+
this.emitDecorators(member.decorators);
|
|
420
|
+
member.names.forEach((entry, index) => {
|
|
421
|
+
const value = member.values[index];
|
|
422
|
+
this.line(`${entry.name} = ${value ? this.expr(value) : 'undefined'};`);
|
|
423
|
+
this.declare(entry.name);
|
|
424
|
+
});
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
this.emitClassMethod(member);
|
|
428
|
+
if (member.decorators.length) {
|
|
429
|
+
if (member.name === 'init') {
|
|
430
|
+
// Constructors cannot be reassigned after the class is built.
|
|
431
|
+
this.emitDecorators(member.decorators);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
methodWrappers.push({
|
|
435
|
+
target: `${node.name}.prototype.${propertyName(member.name)}`,
|
|
436
|
+
decorators: member.decorators,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
this.line('}');
|
|
443
|
+
for (const wrapper of methodWrappers) {
|
|
444
|
+
this.emitDecorators(wrapper.decorators);
|
|
445
|
+
this.applyDecorators(wrapper.target, wrapper.decorators);
|
|
446
|
+
}
|
|
447
|
+
this.applyDecorators(node.name, node.decorators);
|
|
448
|
+
}
|
|
449
|
+
emitClassMethod(node) {
|
|
450
|
+
if (node.doc)
|
|
451
|
+
this.emitDoc(node.doc);
|
|
452
|
+
const name = node.name === 'init' ? 'constructor' : node.name;
|
|
453
|
+
const asyncKeyword = node.async && name !== 'constructor' ? 'async ' : '';
|
|
454
|
+
this.line(`${asyncKeyword}${name}(${this.emitParams(node.params)}) {`);
|
|
455
|
+
this.withScope(() => {
|
|
456
|
+
this.declareParams(node.params);
|
|
457
|
+
this.emitFunctionBody(node);
|
|
458
|
+
});
|
|
459
|
+
this.line('}');
|
|
460
|
+
}
|
|
461
|
+
emitFunctionBody(node) {
|
|
462
|
+
if (node.exprBody) {
|
|
463
|
+
this.line(`return ${this.expr(node.exprBody)};`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
this.emitBlock(node.body);
|
|
467
|
+
}
|
|
468
|
+
declareParams(params) {
|
|
469
|
+
for (const param of params)
|
|
470
|
+
this.declare(rawParamName(param));
|
|
471
|
+
}
|
|
472
|
+
emitParams(params) {
|
|
473
|
+
return params.map((param) => {
|
|
474
|
+
if (param.rest)
|
|
475
|
+
return `...${rawParamName(param)}`;
|
|
476
|
+
const value = param.default ? ` = ${this.expr(param.default)}` : '';
|
|
477
|
+
return `${param.name}${value}`;
|
|
478
|
+
}).join(', ');
|
|
479
|
+
}
|
|
480
|
+
renderFunction(node) {
|
|
481
|
+
const sub = new Emitter(this.options);
|
|
482
|
+
sub.scopes = [new Set()];
|
|
483
|
+
const params = sub.emitParams(node.params);
|
|
484
|
+
sub.withScope(() => {
|
|
485
|
+
sub.declareParams(node.params);
|
|
486
|
+
sub.emitFunctionBody(node);
|
|
487
|
+
});
|
|
488
|
+
const body = sub.lines.map(text => (text ? this.pad() + text : text)).join('\n');
|
|
489
|
+
const asyncKeyword = node.async ? 'async ' : '';
|
|
490
|
+
return `${asyncKeyword}function (${params}) {\n${body}\n${this.pad()}}`;
|
|
491
|
+
}
|
|
492
|
+
emitTypeAlias(node, exported) {
|
|
493
|
+
if (node.doc)
|
|
494
|
+
this.emitDoc(node.doc);
|
|
495
|
+
this.line(`${exported ? 'export ' : ''}type ${node.name} = ${typeAnn(node.type)};`);
|
|
496
|
+
}
|
|
497
|
+
/* ---------------------------------------------------------------- */
|
|
498
|
+
/* Control flow */
|
|
499
|
+
/* ---------------------------------------------------------------- */
|
|
500
|
+
emitIf(node) {
|
|
501
|
+
this.line(`if (${this.expr(node.cond)}) {`);
|
|
502
|
+
this.withScope(() => this.emitBlock(node.then));
|
|
503
|
+
for (const branch of node.elifs) {
|
|
504
|
+
this.line(`} else if (${this.expr(branch.cond)}) {`);
|
|
505
|
+
this.withScope(() => this.emitBlock(branch.body));
|
|
506
|
+
}
|
|
507
|
+
if (node.else) {
|
|
508
|
+
this.line('} else {');
|
|
509
|
+
this.withScope(() => this.emitBlock(node.else));
|
|
510
|
+
}
|
|
511
|
+
this.line('}');
|
|
512
|
+
}
|
|
513
|
+
emitForNumeric(node) {
|
|
514
|
+
const from = this.expr(node.from);
|
|
515
|
+
const to = this.expr(node.to);
|
|
516
|
+
const step = node.step ? this.expr(node.step) : '1';
|
|
517
|
+
const descending = isNegative(node.step);
|
|
518
|
+
const compare = descending ? '>=' : '<=';
|
|
519
|
+
this.line(`for (let ${node.varName} = ${from}; ${node.varName} ${compare} ${to}; ${node.varName} += ${step}) {`);
|
|
520
|
+
this.withScope(() => {
|
|
521
|
+
this.declare(node.varName);
|
|
522
|
+
this.emitBlock(node.body);
|
|
523
|
+
});
|
|
524
|
+
this.line('}');
|
|
525
|
+
}
|
|
526
|
+
emitForIn(node) {
|
|
527
|
+
let source;
|
|
528
|
+
if (node.iter.kind === 'Range') {
|
|
529
|
+
source = `__spak.range(${this.expr(node.iter.from)}, ${this.expr(node.iter.to)}, ${node.iter.inclusive ? 'true' : 'false'})`;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
source = this.expr(node.iter);
|
|
533
|
+
}
|
|
534
|
+
if (node.vars.length > 1) {
|
|
535
|
+
this.line(`for (const [${node.vars.join(', ')}] of __spak.entries(${source})) {`);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
this.line(`for (const ${node.vars[0]} of ${source}) {`);
|
|
539
|
+
}
|
|
540
|
+
this.withScope(() => {
|
|
541
|
+
for (const name of node.vars)
|
|
542
|
+
this.declare(name);
|
|
543
|
+
this.emitBlock(node.body);
|
|
544
|
+
});
|
|
545
|
+
this.line('}');
|
|
546
|
+
}
|
|
547
|
+
emitMatch(node) {
|
|
548
|
+
const subject = `__match${++this.temp}`;
|
|
549
|
+
this.line(`const ${subject} = ${this.expr(node.subject)};`);
|
|
550
|
+
node.arms.forEach((arm, index) => {
|
|
551
|
+
const bindings = [];
|
|
552
|
+
const conditions = arm.patterns.map((pattern) => {
|
|
553
|
+
if (pattern.kind === 'Ident') {
|
|
554
|
+
bindings.push(pattern.name);
|
|
555
|
+
return 'true';
|
|
556
|
+
}
|
|
557
|
+
return this.matchCondition(pattern, subject);
|
|
558
|
+
});
|
|
559
|
+
let condition = conditions.length ? conditions.join(' || ') : 'true';
|
|
560
|
+
if (arm.guard)
|
|
561
|
+
condition = `(${condition} && (${this.expr(arm.guard)}))`;
|
|
562
|
+
for (const name of bindings)
|
|
563
|
+
condition = `((${name}) => ${condition})(${subject})`;
|
|
564
|
+
this.line(`${index === 0 ? 'if' : '} else if'} (${condition}) {`);
|
|
565
|
+
this.withScope(() => {
|
|
566
|
+
for (const name of bindings) {
|
|
567
|
+
this.line(`const ${name} = ${subject};`);
|
|
568
|
+
this.declare(name);
|
|
569
|
+
}
|
|
570
|
+
this.emitBlock(arm.body);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
if (node.elseBody) {
|
|
574
|
+
const hasArms = node.arms.length > 0;
|
|
575
|
+
this.line(hasArms ? '} else {' : 'if (true) {');
|
|
576
|
+
this.withScope(() => this.emitBlock(node.elseBody));
|
|
577
|
+
}
|
|
578
|
+
if (node.arms.length || node.elseBody)
|
|
579
|
+
this.line('}');
|
|
580
|
+
}
|
|
581
|
+
matchCondition(pattern, subject) {
|
|
582
|
+
if (pattern.kind === 'Range') {
|
|
583
|
+
const from = this.expr(pattern.from);
|
|
584
|
+
const to = this.expr(pattern.to);
|
|
585
|
+
const op = pattern.inclusive ? '<=' : '<';
|
|
586
|
+
return `(${subject} >= ${from} && ${subject} ${op} ${to})`;
|
|
587
|
+
}
|
|
588
|
+
return `${subject} === ${this.expr(pattern)}`;
|
|
589
|
+
}
|
|
590
|
+
emitTry(node) {
|
|
591
|
+
this.line('try {');
|
|
592
|
+
this.withScope(() => this.emitBlock(node.body));
|
|
593
|
+
if (node.catchBody) {
|
|
594
|
+
this.line(node.catchVar ? `} catch (${node.catchVar}) {` : '} catch {');
|
|
595
|
+
this.withScope(() => {
|
|
596
|
+
if (node.catchVar)
|
|
597
|
+
this.declare(node.catchVar);
|
|
598
|
+
this.emitBlock(node.catchBody);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
if (node.finallyBody) {
|
|
602
|
+
this.line('} finally {');
|
|
603
|
+
this.withScope(() => this.emitBlock(node.finallyBody));
|
|
604
|
+
}
|
|
605
|
+
this.line('}');
|
|
606
|
+
}
|
|
607
|
+
/* ---------------------------------------------------------------- */
|
|
608
|
+
/* Modules */
|
|
609
|
+
/* ---------------------------------------------------------------- */
|
|
610
|
+
emitImport(node) {
|
|
611
|
+
if (node.doc)
|
|
612
|
+
this.emitDoc(node.doc);
|
|
613
|
+
if (FS_SOURCES.has(node.source)) {
|
|
614
|
+
this.emitFsImport(node);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const source = JSON.stringify(node.source);
|
|
618
|
+
const form = node.form ?? inferImportForm(node);
|
|
619
|
+
if (node.clauses.length === 0 || form === 'side-effect') {
|
|
620
|
+
this.line(`import ${source};`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (form === 'namespace' || node.clauses[0]?.name === '*') {
|
|
624
|
+
const clause = node.clauses[0];
|
|
625
|
+
this.line(`import * as ${clause.alias ?? 'ns'} from ${source};`);
|
|
626
|
+
this.declare(clause.alias ?? 'ns');
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (form === 'default' && node.clauses.length === 1) {
|
|
630
|
+
const clause = node.clauses[0];
|
|
631
|
+
this.line(`import ${clause.name} from ${source};`);
|
|
632
|
+
this.declare(clause.name);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const specifiers = node.clauses.map(clause => clause.alias ? `${clause.name} as ${clause.alias}` : clause.name);
|
|
636
|
+
this.line(`import { ${specifiers.join(', ')} } from ${source};`);
|
|
637
|
+
for (const clause of node.clauses)
|
|
638
|
+
this.declare(clause.alias ?? clause.name);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Spak's `fs` keeps the Lua-flavoured synchronous surface for backwards
|
|
642
|
+
* compatibility (`for name in readdir(dir)` in hello.spk), but named imports
|
|
643
|
+
* that are awaited bind to `node:fs/promises` instead:
|
|
644
|
+
*
|
|
645
|
+
* import { readFile } from "fs"
|
|
646
|
+
* text = await readFile("a.txt") -> node:fs/promises
|
|
647
|
+
*
|
|
648
|
+
* Synchronous access is explicit via `import { readFileSync } from "fs"`.
|
|
649
|
+
*/
|
|
650
|
+
emitFsImport(node) {
|
|
651
|
+
const form = node.form ?? inferImportForm(node);
|
|
652
|
+
if (node.clauses.length === 0 || form === 'side-effect') {
|
|
653
|
+
this.line('import "node:fs";');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const namespace = `__fs${++this.temp}`;
|
|
657
|
+
if (form === 'namespace' || node.clauses[0]?.name === '*') {
|
|
658
|
+
const clause = node.clauses[0];
|
|
659
|
+
const alias = clause.alias ?? 'fs';
|
|
660
|
+
this.line(`import * as ${namespace} from "node:fs";`);
|
|
661
|
+
this.line(`const ${alias} = ${namespace};`);
|
|
662
|
+
this.declare(alias);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
// `import fs from "fs"` historically exposed the whole namespace.
|
|
666
|
+
if (form === 'default' && node.clauses.length === 1) {
|
|
667
|
+
const clause = node.clauses[0];
|
|
668
|
+
this.line(`import * as ${namespace} from "node:fs";`);
|
|
669
|
+
this.line(`const ${clause.name} = ${namespace};`);
|
|
670
|
+
this.declare(clause.name);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const bindings = [];
|
|
674
|
+
const asyncSpecifiers = [];
|
|
675
|
+
for (const clause of node.clauses) {
|
|
676
|
+
if (clause.name === '*') {
|
|
677
|
+
bindings.push(`const ${clause.alias ?? 'fs'} = ${namespace};`);
|
|
678
|
+
this.declare(clause.alias ?? 'fs');
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const local = clause.alias ?? clause.name;
|
|
682
|
+
const member = fsMember(clause.name, this.awaited.has(local));
|
|
683
|
+
if (member.namespace === 'async') {
|
|
684
|
+
asyncSpecifiers.push(clause.alias ? `${clause.name} as ${clause.alias}` : clause.name);
|
|
685
|
+
this.declare(local);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (member.renamed) {
|
|
689
|
+
bindings.push(`// fs compat: never awaited, bound to ${member.name}; use import { ${member.name} } for an explicit sync API`);
|
|
690
|
+
}
|
|
691
|
+
bindings.push(`const ${local} = ${namespace}.${member.name};`);
|
|
692
|
+
this.declare(local);
|
|
693
|
+
}
|
|
694
|
+
if (bindings.length)
|
|
695
|
+
this.line(`import * as ${namespace} from "node:fs";`);
|
|
696
|
+
if (asyncSpecifiers.length)
|
|
697
|
+
this.line(`import { ${asyncSpecifiers.join(', ')} } from "node:fs/promises";`);
|
|
698
|
+
for (const binding of bindings)
|
|
699
|
+
this.line(binding);
|
|
700
|
+
}
|
|
701
|
+
/* ---------------------------------------------------------------- */
|
|
702
|
+
/* Expressions */
|
|
703
|
+
/* ---------------------------------------------------------------- */
|
|
704
|
+
expr(node) {
|
|
705
|
+
switch (node.kind) {
|
|
706
|
+
case 'NumberLit':
|
|
707
|
+
return Number.isFinite(node.value) ? String(node.value) : '0';
|
|
708
|
+
case 'StringLit':
|
|
709
|
+
return this.string(node);
|
|
710
|
+
case 'BoolLit':
|
|
711
|
+
return node.value ? 'true' : 'false';
|
|
712
|
+
case 'NilLit':
|
|
713
|
+
return 'null';
|
|
714
|
+
case 'Ident':
|
|
715
|
+
return node.name;
|
|
716
|
+
case 'SelfExpr':
|
|
717
|
+
return 'this';
|
|
718
|
+
case 'Member':
|
|
719
|
+
return `${this.expr(node.object)}${node.optional ? '?.' : '.'}${propertyName(node.property)}`;
|
|
720
|
+
case 'Index':
|
|
721
|
+
return this.index(node);
|
|
722
|
+
case 'Call':
|
|
723
|
+
return this.call(node);
|
|
724
|
+
case 'Binary':
|
|
725
|
+
return this.binary(node);
|
|
726
|
+
case 'Unary':
|
|
727
|
+
if (node.op === 'not')
|
|
728
|
+
return `(!(${this.expr(node.argument)}))`;
|
|
729
|
+
if (node.op === '#')
|
|
730
|
+
return `__spak.len(${this.expr(node.argument)})`;
|
|
731
|
+
return `(-(${this.expr(node.argument)}))`;
|
|
732
|
+
case 'Table':
|
|
733
|
+
return this.table(node);
|
|
734
|
+
case 'FunctionExpr':
|
|
735
|
+
return this.renderFunction(node);
|
|
736
|
+
case 'Range':
|
|
737
|
+
return `__spak.range(${this.expr(node.from)}, ${this.expr(node.to)}, ${node.inclusive ? 'true' : 'false'})`;
|
|
738
|
+
case 'AwaitExpr':
|
|
739
|
+
return `(await ${this.expr(node.argument)})`;
|
|
740
|
+
case 'Spread':
|
|
741
|
+
return node.argument ? `...${this.expr(node.argument)}` : '...[]';
|
|
742
|
+
default:
|
|
743
|
+
return 'undefined';
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
string(node) {
|
|
747
|
+
if (node.fstring && node.parts) {
|
|
748
|
+
const body = node.parts.map((part) => {
|
|
749
|
+
if (typeof part === 'string')
|
|
750
|
+
return escapeTemplate(part);
|
|
751
|
+
return `\${${this.expr(part)}}`;
|
|
752
|
+
}).join('');
|
|
753
|
+
return `\`${body}\``;
|
|
754
|
+
}
|
|
755
|
+
return JSON.stringify(node.value);
|
|
756
|
+
}
|
|
757
|
+
index(node) {
|
|
758
|
+
const object = this.expr(node.object);
|
|
759
|
+
if (node.index.kind === 'Range') {
|
|
760
|
+
const range = node.index;
|
|
761
|
+
// `list[1..n]` is the common case: a plain 0-based slice. Everything
|
|
762
|
+
// else (including `..<` and empty `1..0`) goes through the clamped
|
|
763
|
+
// runtime helper so an off-by-one end can never wrap from the tail.
|
|
764
|
+
if (range.from.kind === 'NumberLit' && range.from.value === 1 && range.inclusive) {
|
|
765
|
+
return `${object}.slice(0, ${this.expr(range.to)})`;
|
|
766
|
+
}
|
|
767
|
+
return `__spak.slice(${object}, ${this.expr(range.from)}, ${this.expr(range.to)}, ${range.inclusive ? 'true' : 'false'})`;
|
|
768
|
+
}
|
|
769
|
+
return `__spak.idx(${object}, ${this.expr(node.index)})`;
|
|
770
|
+
}
|
|
771
|
+
call(node, prepend = []) {
|
|
772
|
+
const args = [...prepend, ...node.args].map(arg => this.expr(arg)).join(', ');
|
|
773
|
+
if (node.callee.kind === 'Member') {
|
|
774
|
+
const member = node.callee;
|
|
775
|
+
const object = this.expr(member.object);
|
|
776
|
+
if (!node.isMethod && member.property === 'new') {
|
|
777
|
+
return `new ${object}(${args})`;
|
|
778
|
+
}
|
|
779
|
+
if (node.isMethod && RUNTIME_METHODS.has(member.property)) {
|
|
780
|
+
const prefix = args ? `${object}, ${args}` : object;
|
|
781
|
+
return `__spak.${member.property}(${prefix})`;
|
|
782
|
+
}
|
|
783
|
+
const access = `${object}${member.optional ? '?.' : '.'}${propertyName(member.property)}`;
|
|
784
|
+
return `${access}(${args})`;
|
|
785
|
+
}
|
|
786
|
+
if (node.callee.kind === 'Ident') {
|
|
787
|
+
if (node.callee.name === 'len')
|
|
788
|
+
return `__spak.len(${args})`;
|
|
789
|
+
if (node.callee.name === 'range')
|
|
790
|
+
return `__spak.range(${args})`;
|
|
791
|
+
if (node.callee.name === 'pairs' || node.callee.name === 'ipairs')
|
|
792
|
+
return `__spak.entries(${args})`;
|
|
793
|
+
}
|
|
794
|
+
return `${this.expr(node.callee)}(${args})`;
|
|
795
|
+
}
|
|
796
|
+
binary(node) {
|
|
797
|
+
const left = this.expr(node.left);
|
|
798
|
+
const right = this.expr(node.right);
|
|
799
|
+
if (node.op === '|>') {
|
|
800
|
+
if (node.right.kind === 'Call')
|
|
801
|
+
return this.call(node.right, [node.left]);
|
|
802
|
+
return `${this.expr(node.right)}(${left})`;
|
|
803
|
+
}
|
|
804
|
+
if (node.op === '//')
|
|
805
|
+
return `Math.floor((${left}) / (${right}))`;
|
|
806
|
+
if (node.op === '..')
|
|
807
|
+
return `(String(${left}) + String(${right}))`;
|
|
808
|
+
const operators = {
|
|
809
|
+
and: '&&',
|
|
810
|
+
or: '||',
|
|
811
|
+
'==': '===',
|
|
812
|
+
'~=': '!==',
|
|
813
|
+
'^': '**',
|
|
814
|
+
};
|
|
815
|
+
const operator = operators[node.op] ?? node.op;
|
|
816
|
+
return `(${left} ${operator} ${right})`;
|
|
817
|
+
}
|
|
818
|
+
table(node) {
|
|
819
|
+
if (node.fields.length === 0)
|
|
820
|
+
return '[]';
|
|
821
|
+
const hasKeys = node.fields.some(field => field.key);
|
|
822
|
+
if (!hasKeys) {
|
|
823
|
+
return `[${node.fields.map(field => this.expr(field.value)).join(', ')}]`;
|
|
824
|
+
}
|
|
825
|
+
const parts = [];
|
|
826
|
+
let position = 0;
|
|
827
|
+
for (const field of node.fields) {
|
|
828
|
+
if (field.key?.kind === 'Ident') {
|
|
829
|
+
parts.push(`${propertyName(field.key.name)}: ${this.expr(field.value)}`);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (field.key) {
|
|
833
|
+
parts.push(`[${this.expr(field.key)}]: ${this.expr(field.value)}`);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
parts.push(`${position}: ${this.expr(field.value)}`);
|
|
837
|
+
position++;
|
|
838
|
+
}
|
|
839
|
+
return `{ ${parts.join(', ')} }`;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/* ------------------------------------------------------------------ */
|
|
843
|
+
/* Helpers */
|
|
844
|
+
/* ------------------------------------------------------------------ */
|
|
845
|
+
/** Falls back to the historical clause-shape inference for hand-built ASTs. */
|
|
846
|
+
function inferImportForm(node) {
|
|
847
|
+
if (!node.clauses.length)
|
|
848
|
+
return 'side-effect';
|
|
849
|
+
if (node.clauses.length > 1)
|
|
850
|
+
return 'named';
|
|
851
|
+
if (node.clauses[0].name === '*')
|
|
852
|
+
return 'namespace';
|
|
853
|
+
return 'default';
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Resolves how an `fs` named import is bound:
|
|
857
|
+
* - `readFileSync` -> `readFileSync` on `node:fs` (explicit sync)
|
|
858
|
+
* - `readFile` awaited -> `readFile` on `node:fs/promises`
|
|
859
|
+
* - `readFile` not awaited -> `readFileSync` on `node:fs` (compat)
|
|
860
|
+
*/
|
|
861
|
+
function fsMember(name, awaited) {
|
|
862
|
+
if (name.endsWith('Sync') || !FS_SYNC.has(name)) {
|
|
863
|
+
return { name, namespace: 'sync', renamed: false };
|
|
864
|
+
}
|
|
865
|
+
if (awaited)
|
|
866
|
+
return { name, namespace: 'async', renamed: false };
|
|
867
|
+
return { name: `${name}Sync`, namespace: 'sync', renamed: true };
|
|
868
|
+
}
|
|
869
|
+
/** Collects identifiers used directly under `await` (`await readFile(...)`). */
|
|
870
|
+
function collectAwaitedIdentifiers(program) {
|
|
871
|
+
const names = new Set();
|
|
872
|
+
// Every identifier inside the awaited expression is a candidate; only fs
|
|
873
|
+
// names are consulted later, so unrelated identifiers are harmless.
|
|
874
|
+
const collectFrom = (node) => {
|
|
875
|
+
if (!node || typeof node !== 'object')
|
|
876
|
+
return;
|
|
877
|
+
if (Array.isArray(node)) {
|
|
878
|
+
for (const item of node)
|
|
879
|
+
collectFrom(item);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const candidate = node;
|
|
883
|
+
if (candidate.kind === 'Ident' && candidate.name)
|
|
884
|
+
names.add(candidate.name);
|
|
885
|
+
for (const child of Object.values(node))
|
|
886
|
+
collectFrom(child);
|
|
887
|
+
};
|
|
888
|
+
const visit = (value) => {
|
|
889
|
+
if (!value || typeof value !== 'object')
|
|
890
|
+
return;
|
|
891
|
+
if (Array.isArray(value)) {
|
|
892
|
+
for (const item of value)
|
|
893
|
+
visit(item);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const node = value;
|
|
897
|
+
if (node.kind === 'AwaitExpr')
|
|
898
|
+
collectFrom(node.argument);
|
|
899
|
+
for (const child of Object.values(value))
|
|
900
|
+
visit(child);
|
|
901
|
+
};
|
|
902
|
+
visit(program);
|
|
903
|
+
return names;
|
|
904
|
+
}
|
|
905
|
+
function rawParamName(param) {
|
|
906
|
+
return param.rest ? (param.name === '...' ? 'args' : param.name) : param.name;
|
|
907
|
+
}
|
|
908
|
+
function propertyName(name) {
|
|
909
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
910
|
+
}
|
|
911
|
+
function isNegative(expr) {
|
|
912
|
+
if (!expr)
|
|
913
|
+
return false;
|
|
914
|
+
if (expr.kind === 'NumberLit')
|
|
915
|
+
return expr.value < 0;
|
|
916
|
+
return expr.kind === 'Unary' && expr.op === '-' && expr.argument.kind === 'NumberLit';
|
|
917
|
+
}
|
|
918
|
+
function escapeTemplate(text) {
|
|
919
|
+
return text.replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
920
|
+
}
|
|
921
|
+
/** Converts a Spak type annotation into TypeScript syntax (types only). */
|
|
922
|
+
function typeAnn(node) {
|
|
923
|
+
switch (node.kind) {
|
|
924
|
+
case 'named':
|
|
925
|
+
return node.name === 'nil' ? 'null' : node.name;
|
|
926
|
+
case 'array': {
|
|
927
|
+
const element = node.element.kind === 'union' || node.element.kind === 'fn'
|
|
928
|
+
? `(${typeAnn(node.element)})`
|
|
929
|
+
: typeAnn(node.element);
|
|
930
|
+
return `${element}[]`;
|
|
931
|
+
}
|
|
932
|
+
case 'dict':
|
|
933
|
+
return `Record<${typeAnn(node.key)}, ${typeAnn(node.value)}>`;
|
|
934
|
+
case 'record':
|
|
935
|
+
return `{ ${node.fields.map(field => `${propertyName(field.name)}: ${typeAnn(field.type)}`).join(', ')} }`;
|
|
936
|
+
case 'union':
|
|
937
|
+
return node.types.map(typeAnn).join(' | ');
|
|
938
|
+
case 'fn':
|
|
939
|
+
return `(${node.params.map(typeAnn).join(', ')}) => ${typeAnn(node.returns)}`;
|
|
940
|
+
case 'optional':
|
|
941
|
+
return `${typeAnn(node.type)} | null`;
|
|
942
|
+
default:
|
|
943
|
+
return 'unknown';
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/** Lowers a Spak program to an executable ESM TypeScript module. */
|
|
947
|
+
function emitTypeScript(program, options = {}) {
|
|
948
|
+
return new Emitter(options).emitProgram(program);
|
|
949
|
+
}
|
|
950
|
+
//# sourceMappingURL=ts.js.map
|