@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.
Files changed (58) hide show
  1. package/LICENSE +22 -0
  2. package/lib/ast.d.ts +341 -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 +42 -0
  19. package/lib/emit/ts.d.ts.map +1 -0
  20. package/lib/emit/ts.js +950 -0
  21. package/lib/emit/ts.js.map +1 -0
  22. package/lib/index.d.ts +18 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +54 -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 +1138 -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/lib/version.d.ts +25 -0
  43. package/lib/version.d.ts.map +1 -0
  44. package/lib/version.js +47 -0
  45. package/lib/version.js.map +1 -0
  46. package/package.json +10 -3
  47. package/src/ast.ts +467 -0
  48. package/src/checker.ts +554 -0
  49. package/src/diagnostics.ts +100 -0
  50. package/src/emit/lua.ts +484 -0
  51. package/src/emit/ts.ts +1024 -0
  52. package/src/index.ts +24 -0
  53. package/src/lexer.ts +301 -0
  54. package/src/parser.ts +1257 -0
  55. package/src/token.ts +34 -0
  56. package/src/types.ts +218 -0
  57. package/src/version.ts +44 -0
  58. package/README.md +0 -3
@@ -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
+ }