@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/src/emit/lua.ts
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Lang - Lua emitter (basic)
|
|
3
|
+
*
|
|
4
|
+
* A deliberately small lowering from the Spak AST to Lua 5.4 source. It
|
|
5
|
+
* covers the everyday core (locals, functions, if/while/for, tables, calls,
|
|
6
|
+
* string concatenation) and leaves the heavier sugar (`match`, `try`,
|
|
7
|
+
* classes, decorators, slicing) as plain, best-effort Lua. It is not used by
|
|
8
|
+
* the runtime and exists for tooling / round-trip experiments.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
Assign,
|
|
13
|
+
Binary,
|
|
14
|
+
Block,
|
|
15
|
+
Call,
|
|
16
|
+
ClassDecl,
|
|
17
|
+
Expr,
|
|
18
|
+
FnDecl,
|
|
19
|
+
ForIn,
|
|
20
|
+
ForNumeric,
|
|
21
|
+
FunctionExpr,
|
|
22
|
+
ImportDecl,
|
|
23
|
+
Index,
|
|
24
|
+
MatchStmt,
|
|
25
|
+
Param,
|
|
26
|
+
Pattern,
|
|
27
|
+
Program,
|
|
28
|
+
Statement,
|
|
29
|
+
StringLit,
|
|
30
|
+
Table,
|
|
31
|
+
TablePattern,
|
|
32
|
+
TryStmt,
|
|
33
|
+
TypeAliasDecl,
|
|
34
|
+
TypeAnn,
|
|
35
|
+
VarDecl,
|
|
36
|
+
} from '../ast'
|
|
37
|
+
|
|
38
|
+
export interface EmitLuaOptions {
|
|
39
|
+
/** Header comment; set to an empty string to omit it. */
|
|
40
|
+
banner?: string
|
|
41
|
+
/** Indentation unit. Defaults to two spaces. */
|
|
42
|
+
indent?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class LuaEmitter {
|
|
46
|
+
private lines: string[] = []
|
|
47
|
+
private level = 0
|
|
48
|
+
private temp = 0
|
|
49
|
+
private readonly options: EmitLuaOptions
|
|
50
|
+
|
|
51
|
+
constructor(options: EmitLuaOptions = {}) {
|
|
52
|
+
this.options = options
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private get unit(): string {
|
|
56
|
+
return this.options.indent ?? ' '
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private line(text = ''): void {
|
|
60
|
+
this.lines.push(text ? this.unit.repeat(this.level) + text : '')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private withLevel(fn: () => void): void {
|
|
64
|
+
this.level++
|
|
65
|
+
try {
|
|
66
|
+
fn()
|
|
67
|
+
} finally {
|
|
68
|
+
this.level--
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
emitProgram(program: Program): string {
|
|
73
|
+
for (const statement of program.body) this.statement(statement)
|
|
74
|
+
const chunks: string[] = []
|
|
75
|
+
const banner = this.options.banner ?? '-- Generated by Spak emitLua'
|
|
76
|
+
if (banner) chunks.push(banner)
|
|
77
|
+
chunks.push(this.lines.join('\n'))
|
|
78
|
+
return chunks.filter(Boolean).join('\n\n') + '\n'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private statement(node: Statement): void {
|
|
82
|
+
switch (node.kind) {
|
|
83
|
+
case 'VarDecl':
|
|
84
|
+
this.varDecl(node)
|
|
85
|
+
return
|
|
86
|
+
case 'Assign':
|
|
87
|
+
this.assign(node)
|
|
88
|
+
return
|
|
89
|
+
case 'FnDecl':
|
|
90
|
+
this.fnDecl(node)
|
|
91
|
+
return
|
|
92
|
+
case 'ClassDecl':
|
|
93
|
+
this.classDecl(node)
|
|
94
|
+
return
|
|
95
|
+
case 'IfStmt': {
|
|
96
|
+
this.line(`if ${this.expr(node.cond)} then`)
|
|
97
|
+
this.withLevel(() => this.block(node.then))
|
|
98
|
+
for (const branch of node.elifs) {
|
|
99
|
+
this.line(`elseif ${this.expr(branch.cond)} then`)
|
|
100
|
+
this.withLevel(() => this.block(branch.body))
|
|
101
|
+
}
|
|
102
|
+
if (node.else) {
|
|
103
|
+
this.line('else')
|
|
104
|
+
this.withLevel(() => this.block(node.else))
|
|
105
|
+
}
|
|
106
|
+
this.line('end')
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
case 'UnlessStmt':
|
|
110
|
+
this.line(`if not (${this.expr(node.cond)}) then`)
|
|
111
|
+
this.withLevel(() => this.block(node.body))
|
|
112
|
+
this.line('end')
|
|
113
|
+
return
|
|
114
|
+
case 'WhileStmt':
|
|
115
|
+
this.line(`while ${this.expr(node.cond)} do`)
|
|
116
|
+
this.withLevel(() => this.block(node.body))
|
|
117
|
+
this.line('end')
|
|
118
|
+
return
|
|
119
|
+
case 'RepeatStmt':
|
|
120
|
+
this.line('repeat')
|
|
121
|
+
this.withLevel(() => this.block(node.body))
|
|
122
|
+
this.line(`until ${this.expr(node.cond)}`)
|
|
123
|
+
return
|
|
124
|
+
case 'ForNumeric':
|
|
125
|
+
this.forNumeric(node)
|
|
126
|
+
return
|
|
127
|
+
case 'ForIn':
|
|
128
|
+
this.forIn(node)
|
|
129
|
+
return
|
|
130
|
+
case 'ReturnStmt':
|
|
131
|
+
this.line(node.values.length ? `return ${node.values.map(value => this.expr(value)).join(', ')}` : 'return')
|
|
132
|
+
return
|
|
133
|
+
case 'BreakStmt':
|
|
134
|
+
this.line('break')
|
|
135
|
+
return
|
|
136
|
+
case 'ContinueStmt':
|
|
137
|
+
this.line('-- continue (Lua 5.4 has no continue)')
|
|
138
|
+
return
|
|
139
|
+
case 'MatchStmt':
|
|
140
|
+
this.match(node)
|
|
141
|
+
return
|
|
142
|
+
case 'TryStmt':
|
|
143
|
+
this.try(node)
|
|
144
|
+
return
|
|
145
|
+
case 'ImportDecl':
|
|
146
|
+
this.importDecl(node)
|
|
147
|
+
return
|
|
148
|
+
case 'ExportDecl':
|
|
149
|
+
this.statement(node.declaration)
|
|
150
|
+
return
|
|
151
|
+
case 'ExportDefault':
|
|
152
|
+
this.line(`return ${this.expr(node.expr)}`)
|
|
153
|
+
return
|
|
154
|
+
case 'TypeAliasDecl':
|
|
155
|
+
this.typeAlias(node)
|
|
156
|
+
return
|
|
157
|
+
case 'ThrowStmt':
|
|
158
|
+
this.line(`error(${this.expr(node.value)})`)
|
|
159
|
+
return
|
|
160
|
+
case 'SpawnStmt': {
|
|
161
|
+
const value = this.expr(node.value)
|
|
162
|
+
this.line(node.value.kind === 'FunctionExpr'
|
|
163
|
+
? `coroutine.wrap(${value})()`
|
|
164
|
+
: `coroutine.wrap(${value})()`)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
case 'ExprStmt':
|
|
168
|
+
this.line(this.expr(node.expr))
|
|
169
|
+
return
|
|
170
|
+
default:
|
|
171
|
+
this.line(`-- unsupported statement: ${(node as Statement).kind}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private block(block: Block | undefined): void {
|
|
176
|
+
if (!block) return
|
|
177
|
+
for (const statement of block.body) this.statement(statement)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private varDecl(node: VarDecl): void {
|
|
181
|
+
if (node.doc) this.line(`-- ${node.doc.split(/\r?\n/).join(' ')}`)
|
|
182
|
+
const names = node.names.map(entry => entry.name).join(', ')
|
|
183
|
+
const values = node.values.map(value => this.expr(value)).join(', ')
|
|
184
|
+
const keyword = node.scope === 'global' ? '' : 'local '
|
|
185
|
+
this.line(values ? `${keyword}${names} = ${values}` : `${keyword}${names}`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private assign(node: Assign): void {
|
|
189
|
+
node.targets.forEach((target, index) => {
|
|
190
|
+
const value = node.values[index] ? this.expr(node.values[index]) : 'nil'
|
|
191
|
+
if (this.isPattern(target)) {
|
|
192
|
+
this.patternAssign(target, value)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
this.line(`${this.expr(target)} = ${value}`)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private isPattern(target: Expr | Pattern): target is Pattern {
|
|
200
|
+
return target.kind === 'TablePattern' || target.kind === 'ArrayPattern'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private patternAssign(pattern: Pattern, value: string): void {
|
|
204
|
+
if (pattern.kind === 'ArrayPattern') {
|
|
205
|
+
this.line(`local ${pattern.elements.join(', ')} = table.unpack(${value})`)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
for (const entry of (pattern as TablePattern).entries) {
|
|
209
|
+
const target = entry.target ?? entry.key
|
|
210
|
+
this.line(`local ${target} = ${value}${this.indexSuffix(entry.key)}`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private indexSuffix(key: string): string {
|
|
215
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private fnDecl(node: FnDecl): void {
|
|
219
|
+
if (node.doc) this.line(`-- ${node.doc.split(/\r?\n/).join(' ')}`)
|
|
220
|
+
const name = node.receiver ? `${node.receiver}:${node.name}` : node.name
|
|
221
|
+
this.line(`function ${name}(${this.params(node.params)})`)
|
|
222
|
+
this.withLevel(() => {
|
|
223
|
+
if (node.exprBody) this.line(`return ${this.expr(node.exprBody)}`)
|
|
224
|
+
else this.block(node.body)
|
|
225
|
+
})
|
|
226
|
+
this.end()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private classDecl(node: ClassDecl): void {
|
|
230
|
+
if (node.doc) this.line(`-- ${node.doc.split(/\r?\n/).join(' ')}`)
|
|
231
|
+
this.line(`local ${node.name} = {}`)
|
|
232
|
+
if (node.superClass) this.line(`setmetatable(${node.name}, { __index = ${node.superClass} })`)
|
|
233
|
+
this.line(`${node.name}.__index = ${node.name}`)
|
|
234
|
+
|
|
235
|
+
for (const member of node.members) {
|
|
236
|
+
if (member.kind === 'VarDecl') {
|
|
237
|
+
member.names.forEach((entry, index) => {
|
|
238
|
+
const value = member.values[index]
|
|
239
|
+
this.line(`${node.name}.${entry.name} = ${value ? this.expr(value) : 'nil'}`)
|
|
240
|
+
})
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
const name = member.name === 'init' ? 'new' : member.name
|
|
244
|
+
if (member.name === 'init') {
|
|
245
|
+
this.line(`function ${node.name}.${name}(${this.params(member.params)})`)
|
|
246
|
+
} else {
|
|
247
|
+
this.line(`function ${node.name}:${name}(${this.params(member.params)})`)
|
|
248
|
+
}
|
|
249
|
+
this.withLevel(() => {
|
|
250
|
+
if (member.exprBody) this.line(`return ${this.expr(member.exprBody)}`)
|
|
251
|
+
else this.block(member.body)
|
|
252
|
+
})
|
|
253
|
+
this.end()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private forNumeric(node: ForNumeric): void {
|
|
258
|
+
const from = this.expr(node.from)
|
|
259
|
+
const to = node.inclusive ? this.expr(node.to) : `(${this.expr(node.to)}) - 1`
|
|
260
|
+
const step = node.step ? `, ${this.expr(node.step)}` : ''
|
|
261
|
+
this.line(`for ${node.varName} = ${from}, ${to}${step} do`)
|
|
262
|
+
this.withLevel(() => this.block(node.body))
|
|
263
|
+
this.end()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private forIn(node: ForIn): void {
|
|
267
|
+
const vars = node.vars.join(', ')
|
|
268
|
+
if (node.iter.kind === 'Range') {
|
|
269
|
+
const to = node.iter.inclusive ? this.expr(node.iter.to) : `(${this.expr(node.iter.to)}) - 1`
|
|
270
|
+
this.line(`for ${vars} = ${this.expr(node.iter.from)}, ${to} do`)
|
|
271
|
+
} else {
|
|
272
|
+
this.line(`for ${vars} in ${this.expr(node.iter)} do`)
|
|
273
|
+
}
|
|
274
|
+
this.withLevel(() => this.block(node.body))
|
|
275
|
+
this.end()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private match(node: MatchStmt): void {
|
|
279
|
+
const subject = `__match${++this.temp}`
|
|
280
|
+
this.line(`local ${subject} = ${this.expr(node.subject)}`)
|
|
281
|
+
node.arms.forEach((arm, index) => {
|
|
282
|
+
const conditions = arm.patterns.map((pattern) => {
|
|
283
|
+
if (pattern.kind === 'Ident') return 'true'
|
|
284
|
+
if (pattern.kind === 'Range') {
|
|
285
|
+
return `${subject} >= ${this.expr(pattern.from)} and ${subject} ${pattern.inclusive ? '<=' : '<'} ${this.expr(pattern.to)}`
|
|
286
|
+
}
|
|
287
|
+
return `${subject} == ${this.expr(pattern)}`
|
|
288
|
+
}).filter(Boolean)
|
|
289
|
+
const condition = conditions.join(' or ') || 'true'
|
|
290
|
+
this.line(`${index === 0 ? 'if' : 'elseif'} ${condition} then`)
|
|
291
|
+
this.withLevel(() => {
|
|
292
|
+
for (const pattern of arm.patterns) {
|
|
293
|
+
if (pattern.kind === 'Ident') this.line(`local ${pattern.name} = ${subject}`)
|
|
294
|
+
}
|
|
295
|
+
this.block(arm.body)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
if (node.elseBody) {
|
|
299
|
+
this.line(node.arms.length ? 'else' : 'do')
|
|
300
|
+
this.withLevel(() => this.block(node.elseBody))
|
|
301
|
+
if (!node.arms.length) this.end()
|
|
302
|
+
}
|
|
303
|
+
if (node.arms.length) this.line('end')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private try(node: TryStmt): void {
|
|
307
|
+
this.line('local __ok, __err = pcall(function()')
|
|
308
|
+
this.withLevel(() => this.block(node.body))
|
|
309
|
+
this.line('end)')
|
|
310
|
+
if (node.catchBody) {
|
|
311
|
+
this.line('if not __ok then')
|
|
312
|
+
this.withLevel(() => {
|
|
313
|
+
if (node.catchVar) this.line(`local ${node.catchVar} = __err`)
|
|
314
|
+
this.block(node.catchBody)
|
|
315
|
+
})
|
|
316
|
+
this.line('end')
|
|
317
|
+
}
|
|
318
|
+
if (node.finallyBody) this.withLevel(() => this.block(node.finallyBody))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private importDecl(node: ImportDecl): void {
|
|
322
|
+
if (!node.clauses.length) {
|
|
323
|
+
this.line(`require(${JSON.stringify(node.source)})`)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
for (const clause of node.clauses) {
|
|
327
|
+
if (clause.name === '*') {
|
|
328
|
+
this.line(`local ${clause.alias ?? 'ns'} = require(${JSON.stringify(node.source)})`)
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
const local = clause.alias ?? clause.name
|
|
332
|
+
this.line(`local ${local} = require(${JSON.stringify(node.source)}).${clause.name}`)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private typeAlias(node: TypeAliasDecl): void {
|
|
337
|
+
if (node.doc) this.line(`-- ${node.doc.split(/\r?\n/).join(' ')}`)
|
|
338
|
+
this.line(`-- type ${node.name} = ${luaType(node.type)}`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private end(): void {
|
|
342
|
+
this.line('end')
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private params(params: Param[]): string {
|
|
346
|
+
return params.map(param => param.rest ? '...' : param.name).join(', ')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* ---------------------------------------------------------------- */
|
|
350
|
+
/* Expressions */
|
|
351
|
+
/* ---------------------------------------------------------------- */
|
|
352
|
+
|
|
353
|
+
private expr(node: Expr): string {
|
|
354
|
+
switch (node.kind) {
|
|
355
|
+
case 'NumberLit':
|
|
356
|
+
return Number.isFinite(node.value) ? String(node.value) : '0'
|
|
357
|
+
case 'StringLit':
|
|
358
|
+
return this.string(node)
|
|
359
|
+
case 'BoolLit':
|
|
360
|
+
return node.value ? 'true' : 'false'
|
|
361
|
+
case 'NilLit':
|
|
362
|
+
return 'nil'
|
|
363
|
+
case 'Ident':
|
|
364
|
+
return node.name
|
|
365
|
+
case 'SelfExpr':
|
|
366
|
+
return 'self'
|
|
367
|
+
case 'Member':
|
|
368
|
+
return `${this.expr(node.object)}${node.optional ? ' and ' : '.'}${node.property}`
|
|
369
|
+
case 'Index':
|
|
370
|
+
return this.index(node)
|
|
371
|
+
case 'Call':
|
|
372
|
+
return this.call(node)
|
|
373
|
+
case 'Binary':
|
|
374
|
+
return this.binary(node)
|
|
375
|
+
case 'Unary':
|
|
376
|
+
if (node.op === 'not') return `not (${this.expr(node.argument)})`
|
|
377
|
+
if (node.op === '#') return `#${this.expr(node.argument)}`
|
|
378
|
+
return `-(${this.expr(node.argument)})`
|
|
379
|
+
case 'Table':
|
|
380
|
+
return this.table(node)
|
|
381
|
+
case 'FunctionExpr':
|
|
382
|
+
return this.functionExpr(node)
|
|
383
|
+
case 'Range':
|
|
384
|
+
return `${this.expr(node.from)}, ${this.expr(node.to)}`
|
|
385
|
+
case 'AwaitExpr':
|
|
386
|
+
return this.expr(node.argument)
|
|
387
|
+
case 'Spread':
|
|
388
|
+
return node.argument ? `table.unpack(${this.expr(node.argument)})` : 'table.unpack({})'
|
|
389
|
+
default:
|
|
390
|
+
return 'nil'
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private string(node: StringLit): string {
|
|
395
|
+
if (node.fstring && node.parts) {
|
|
396
|
+
const parts = node.parts.map(part => typeof part === 'string'
|
|
397
|
+
? JSON.stringify(part)
|
|
398
|
+
: `tostring(${this.expr(part)})`)
|
|
399
|
+
return parts.length ? parts.join(' .. ') : '""'
|
|
400
|
+
}
|
|
401
|
+
return JSON.stringify(node.value)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private index(node: Index): string {
|
|
405
|
+
const object = this.expr(node.object)
|
|
406
|
+
if (node.index.kind === 'Range') {
|
|
407
|
+
// Basic mapping only: Lua has no slice operator, so the range is erased
|
|
408
|
+
// to a plain reference. Callers that need slicing should use `table.move`.
|
|
409
|
+
return object
|
|
410
|
+
}
|
|
411
|
+
return `${object}[${this.expr(node.index)}]`
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private call(node: Call): string {
|
|
415
|
+
const args = node.args.map(arg => this.expr(arg)).join(', ')
|
|
416
|
+
if (node.callee.kind === 'Member') {
|
|
417
|
+
const member = node.callee
|
|
418
|
+
const object = this.expr(member.object)
|
|
419
|
+
if (!node.isMethod && member.property === 'new') return `${object}.new(${args})`
|
|
420
|
+
if (node.isMethod) return `${object}:${member.property}(${args})`
|
|
421
|
+
return `${object}.${member.property}(${args})`
|
|
422
|
+
}
|
|
423
|
+
return `${this.expr(node.callee)}(${args})`
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private binary(node: Binary): string {
|
|
427
|
+
if (node.op === '|>') {
|
|
428
|
+
if (node.right.kind === 'Call') {
|
|
429
|
+
const args = [node.left, ...node.right.args].map(arg => this.expr(arg)).join(', ')
|
|
430
|
+
if (node.right.callee.kind === 'Member' && node.right.isMethod) {
|
|
431
|
+
return `${this.expr(node.right.callee.object)}:${node.right.callee.property}(${args})`
|
|
432
|
+
}
|
|
433
|
+
return `${this.expr(node.right.callee)}(${args})`
|
|
434
|
+
}
|
|
435
|
+
return `${this.expr(node.right)}(${this.expr(node.left)})`
|
|
436
|
+
}
|
|
437
|
+
if (node.op === '??') return `(${this.expr(node.left)} or ${this.expr(node.right)})`
|
|
438
|
+
return `(${this.expr(node.left)} ${node.op} ${this.expr(node.right)})`
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private table(node: Table): string {
|
|
442
|
+
const parts = node.fields.map((field) => {
|
|
443
|
+
if (field.key?.kind === 'Ident') return `${field.key.name} = ${this.expr(field.value)}`
|
|
444
|
+
if (field.key) return `[${this.expr(field.key)}] = ${this.expr(field.value)}`
|
|
445
|
+
return this.expr(field.value)
|
|
446
|
+
})
|
|
447
|
+
return `{ ${parts.join(', ')} }`
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private functionExpr(node: FunctionExpr): string {
|
|
451
|
+
const sub = new LuaEmitter(this.options)
|
|
452
|
+
sub.level = 0
|
|
453
|
+
if (node.exprBody) sub.line(`return ${sub.expr(node.exprBody)}`)
|
|
454
|
+
else sub.block(node.body)
|
|
455
|
+
const body = sub.lines.map(text => (text ? this.unit + text : text)).join('\n')
|
|
456
|
+
return `function(${this.params(node.params)})\n${body}\n${this.unit.repeat(this.level)}end`
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function luaType(node: TypeAnn): string {
|
|
461
|
+
switch (node.kind) {
|
|
462
|
+
case 'named':
|
|
463
|
+
return node.name
|
|
464
|
+
case 'array':
|
|
465
|
+
return `[${luaType(node.element)}]`
|
|
466
|
+
case 'dict':
|
|
467
|
+
return `{ [${luaType(node.key)}]: ${luaType(node.value)} }`
|
|
468
|
+
case 'record':
|
|
469
|
+
return `{ ${node.fields.map(field => `${field.name}: ${luaType(field.type)}`).join(', ')} }`
|
|
470
|
+
case 'union':
|
|
471
|
+
return node.types.map(luaType).join(' | ')
|
|
472
|
+
case 'fn':
|
|
473
|
+
return `(${node.params.map(luaType).join(', ')}) -> ${luaType(node.returns)}`
|
|
474
|
+
case 'optional':
|
|
475
|
+
return `${luaType(node.type)}?`
|
|
476
|
+
default:
|
|
477
|
+
return 'any'
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Lowers a Spak program to basic Lua 5.4 source. */
|
|
482
|
+
export function emitLua(program: Program, options: EmitLuaOptions = {}): string {
|
|
483
|
+
return new LuaEmitter(options).emitProgram(program)
|
|
484
|
+
}
|