lt-script 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,748 @@
1
+ import * as AST from '../parser/AST.js';
2
+ /**
3
+ * LT → Lua Code Emitter
4
+ * Generates clean, readable Lua 5.4 code
5
+ */
6
+ export class LuaEmitter {
7
+ indent = 0;
8
+ output = [];
9
+ tempId = 0;
10
+ usedVars = new Set();
11
+ nextTemp(prefix = '_v') {
12
+ return `${prefix}${++this.tempId}`;
13
+ }
14
+ emit(program) {
15
+ this.analyzeUsages(program);
16
+ for (const stmt of program.body) {
17
+ this.emitStatement(stmt);
18
+ }
19
+ return this.output.join('\n');
20
+ }
21
+ analyzeUsages(node) {
22
+ if (!node || typeof node !== 'object')
23
+ return;
24
+ if (Array.isArray(node)) {
25
+ node.forEach(n => this.analyzeUsages(n));
26
+ return;
27
+ }
28
+ switch (node.kind) {
29
+ case AST.NodeType.Identifier:
30
+ this.usedVars.add(node.name);
31
+ break;
32
+ case AST.NodeType.VariableDecl:
33
+ if (node.values)
34
+ node.values.forEach((v) => this.analyzeUsages(v));
35
+ break;
36
+ case AST.NodeType.AssignmentStmt:
37
+ if (node.values)
38
+ node.values.forEach((v) => this.analyzeUsages(v));
39
+ node.targets.forEach((t) => this.analyzeUsages(t));
40
+ break;
41
+ case AST.NodeType.ReturnStmt:
42
+ if (node.values)
43
+ node.values.forEach((v) => this.analyzeUsages(v));
44
+ break;
45
+ case AST.NodeType.FunctionDecl:
46
+ this.analyzeUsages(node.body);
47
+ if (node.params) {
48
+ node.params.forEach((p) => {
49
+ if (p.defaultValue)
50
+ this.analyzeUsages(p.defaultValue);
51
+ });
52
+ }
53
+ break;
54
+ case AST.NodeType.MemberExpr:
55
+ case AST.NodeType.OptionalChainExpr:
56
+ this.analyzeUsages(node.object);
57
+ if (node.computed)
58
+ this.analyzeUsages(node.property);
59
+ break;
60
+ case AST.NodeType.TableLiteral:
61
+ node.fields.forEach((f) => {
62
+ if (f.key && f.key.kind !== AST.NodeType.Identifier)
63
+ this.analyzeUsages(f.key);
64
+ this.analyzeUsages(f.value);
65
+ });
66
+ break;
67
+ default:
68
+ for (const key in node) {
69
+ if (Object.prototype.hasOwnProperty.call(node, key)) {
70
+ const val = node[key];
71
+ if (val && typeof val === 'object')
72
+ this.analyzeUsages(val);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ emitStatement(stmt) {
78
+ switch (stmt.kind) {
79
+ case AST.NodeType.VariableDecl:
80
+ this.emitVariableDecl(stmt);
81
+ break;
82
+ case AST.NodeType.AssignmentStmt:
83
+ this.emitAssignment(stmt);
84
+ break;
85
+ case AST.NodeType.CompoundAssignment:
86
+ this.emitCompoundAssignment(stmt);
87
+ break;
88
+ case AST.NodeType.IfStmt:
89
+ this.emitIfStmt(stmt);
90
+ break;
91
+ case AST.NodeType.ForStmt:
92
+ this.emitForStmt(stmt);
93
+ break;
94
+ case AST.NodeType.RangeForStmt:
95
+ this.emitRangeForStmt(stmt);
96
+ break;
97
+ case AST.NodeType.WhileStmt:
98
+ this.emitWhileStmt(stmt);
99
+ break;
100
+ case AST.NodeType.ReturnStmt:
101
+ this.emitReturnStmt(stmt);
102
+ break;
103
+ case AST.NodeType.BreakStmt:
104
+ this.line('break');
105
+ break;
106
+ case AST.NodeType.ContinueStmt:
107
+ this.line('goto continue');
108
+ break;
109
+ case AST.NodeType.GuardStmt:
110
+ this.emitGuardStmt(stmt);
111
+ break;
112
+ case AST.NodeType.SafeCallStmt:
113
+ this.emitSafeCall(stmt);
114
+ break;
115
+ case AST.NodeType.ThreadStmt:
116
+ this.emitThreadStmt(stmt);
117
+ break;
118
+ case AST.NodeType.LoopStmt:
119
+ this.emitLoopStmt(stmt);
120
+ break;
121
+ case AST.NodeType.WaitStmt:
122
+ this.emitWaitStmt(stmt);
123
+ break;
124
+ case AST.NodeType.TimeoutStmt:
125
+ this.emitTimeoutStmt(stmt);
126
+ break;
127
+ case AST.NodeType.IntervalStmt:
128
+ this.emitIntervalStmt(stmt);
129
+ break;
130
+ case AST.NodeType.TryCatchStmt:
131
+ this.emitTryCatch(stmt);
132
+ break;
133
+ case AST.NodeType.EmitStmt:
134
+ this.emitEmitStmt(stmt);
135
+ break;
136
+ case AST.NodeType.EventHandler:
137
+ this.emitEventHandler(stmt);
138
+ break;
139
+ case AST.NodeType.FunctionDecl:
140
+ this.emitFunctionDecl(stmt);
141
+ break;
142
+ case AST.NodeType.SwitchStmt:
143
+ this.emitSwitchStmt(stmt);
144
+ break;
145
+ case AST.NodeType.ExportDecl:
146
+ this.emitExportDecl(stmt);
147
+ break;
148
+ case AST.NodeType.CommandStmt:
149
+ this.emitCommandStmt(stmt);
150
+ break;
151
+ default:
152
+ // Expression statement
153
+ // Optimize UpdateExpr used as statement (i++)
154
+ {
155
+ const expr = stmt;
156
+ if (expr.kind === AST.NodeType.UpdateExpr) {
157
+ const update = expr;
158
+ const arg = this.emitExpr(update.argument);
159
+ const op = update.operator === '++' ? '+' : '-';
160
+ this.line(`${arg} = ${arg} ${op} 1`);
161
+ return;
162
+ }
163
+ }
164
+ this.line(this.emitExpr(stmt));
165
+ }
166
+ }
167
+ emitVariableDecl(decl) {
168
+ const prefix = decl.scope === 'var' ? '' : 'local ';
169
+ const globalConst = decl.scope === 'const' ? ' <const>' : '';
170
+ // Single Name Case (supports destructuring)
171
+ if (decl.names.length === 1) {
172
+ const name = decl.names[0];
173
+ const valEmitted = (decl.values && decl.values[0]) ? this.emitExpr(decl.values[0]) : 'nil';
174
+ if (name.kind === AST.NodeType.Identifier) {
175
+ const id = name;
176
+ const attr = id.attribute ? ` <${id.attribute}>` : globalConst;
177
+ this.line(`${prefix}${id.name}${attr} = ${valEmitted}`);
178
+ return;
179
+ }
180
+ if (name.kind === AST.NodeType.ObjectDestructure) {
181
+ const obj = name;
182
+ const tempVar = this.nextTemp('_obj');
183
+ this.line(`local ${tempVar} = ${valEmitted}`);
184
+ obj.properties.forEach(p => {
185
+ this.line(`${prefix}${p.name}${globalConst} = ${tempVar}.${p.name}`);
186
+ });
187
+ return;
188
+ }
189
+ if (name.kind === AST.NodeType.ArrayDestructure) {
190
+ const arr = name;
191
+ const tempVar = this.nextTemp('_t');
192
+ this.line(`local ${tempVar} = ${valEmitted}`);
193
+ arr.elements.forEach((el, i) => {
194
+ this.line(`${prefix}${el.name}${globalConst} = ${tempVar}[${i + 1}]`);
195
+ });
196
+ return;
197
+ }
198
+ }
199
+ // Multiple Names (Standard Lua / Multiple variable LT)
200
+ const namesEmitted = decl.names.map(n => {
201
+ const id = n;
202
+ const attr = id.attribute ? ` <${id.attribute}>` : globalConst;
203
+ return id.name + attr;
204
+ }).join(', ');
205
+ // Dead Code Elimination
206
+ if (decl.names.length === 1 && decl.scope !== 'var' && !this.usedVars.has(decl.names[0].name)) {
207
+ const val = (decl.values && decl.values[0]);
208
+ if (val && this.hasSideEffects(val)) {
209
+ this.line(`${this.emitExpr(val)} -- dead code(unused)`);
210
+ }
211
+ return;
212
+ }
213
+ const valsEmitted = decl.values && decl.values.length > 0
214
+ ? ` = ${decl.values.map(v => this.emitExpr(v)).join(', ')}`
215
+ : '';
216
+ this.line(`${prefix}${namesEmitted}${valsEmitted}`);
217
+ }
218
+ hasSideEffects(node) {
219
+ switch (node.kind) {
220
+ case AST.NodeType.CallExpr:
221
+ return true;
222
+ case AST.NodeType.BinaryExpr:
223
+ return this.hasSideEffects(node.left) || this.hasSideEffects(node.right);
224
+ case AST.NodeType.UnaryExpr:
225
+ return this.hasSideEffects(node.operand);
226
+ case AST.NodeType.CallExpr:
227
+ return true; // Calls ALWAYS have side effects
228
+ case AST.NodeType.TableLiteral:
229
+ return node.fields.some(f => this.hasSideEffects(f.value) || (f.key && f.key.kind !== AST.NodeType.Identifier && this.hasSideEffects(f.key)));
230
+ case AST.NodeType.VectorLiteral:
231
+ return node.components.some(c => this.hasSideEffects(c));
232
+ case AST.NodeType.SpreadExpr:
233
+ return this.hasSideEffects(node.argument);
234
+ default:
235
+ return false;
236
+ }
237
+ }
238
+ emitArrayLiteral(expr) {
239
+ const elements = expr.fields.map(f => this.emitExpr(f.value));
240
+ return `{ ${elements.join(', ')} }`;
241
+ }
242
+ emitAssignment(stmt) {
243
+ const targets = stmt.targets.map(t => this.emitExpr(t)).join(', ');
244
+ const values = stmt.values.map(v => this.emitExpr(v)).join(', ');
245
+ this.line(`${targets} = ${values}`);
246
+ }
247
+ emitCompoundAssignment(stmt) {
248
+ const target = this.emitExpr(stmt.target);
249
+ const opMap = {
250
+ '+=': '+', '-=': '-', '*=': '*', '/=': '/', '%=': '%', '..=': '..'
251
+ };
252
+ this.line(`${target} = ${target} ${opMap[stmt.operator]} ${this.emitExpr(stmt.value)}`);
253
+ }
254
+ emitIfStmt(stmt) {
255
+ this.line(`if ${this.emitExpr(stmt.condition)} then`);
256
+ this.emitBlock(stmt.thenBody);
257
+ for (const elseIf of stmt.elseIfClauses || []) {
258
+ this.line(`elseif ${this.emitExpr(elseIf.condition)} then`);
259
+ this.emitBlock(elseIf.body);
260
+ }
261
+ if (stmt.elseBody) {
262
+ this.line('else');
263
+ this.emitBlock(stmt.elseBody);
264
+ }
265
+ this.line('end');
266
+ }
267
+ emitForStmt(stmt) {
268
+ const iterators = stmt.iterators.map(i => i.name).join(', ');
269
+ const iterable = this.emitExpr(stmt.iterable);
270
+ this.line(`for ${iterators} in ${iterable} do`);
271
+ this.emitBlock(stmt.body);
272
+ this.line('end');
273
+ }
274
+ emitRangeForStmt(stmt) {
275
+ const step = stmt.step ? `, ${this.emitExpr(stmt.step)}` : '';
276
+ this.line(`for ${stmt.counter.name} = ${this.emitExpr(stmt.start)}, ${this.emitExpr(stmt.end)}${step} do`);
277
+ this.emitBlockWithContinue(stmt.body);
278
+ this.line('end');
279
+ }
280
+ emitWhileStmt(stmt) {
281
+ this.line(`while ${this.emitExpr(stmt.condition)} do`);
282
+ this.emitBlockWithContinue(stmt.body);
283
+ this.line('end');
284
+ }
285
+ emitReturnStmt(stmt) {
286
+ if (stmt.values && stmt.values.length > 0) {
287
+ const values = stmt.values.map(v => this.emitExpr(v)).join(', ');
288
+ this.line(`return ${values}`);
289
+ }
290
+ else {
291
+ this.line('return');
292
+ }
293
+ }
294
+ emitGuardStmt(stmt) {
295
+ this.line(`if not (${this.emitExpr(stmt.condition)}) then`);
296
+ this.indent++;
297
+ if (stmt.elseBody) {
298
+ for (const s of stmt.elseBody) {
299
+ this.emitStatement(s);
300
+ }
301
+ }
302
+ this.line('return');
303
+ this.indent--;
304
+ this.line('end');
305
+ }
306
+ emitSafeCall(stmt) {
307
+ const call = stmt.call;
308
+ const callee = this.emitExpr(call.callee);
309
+ const args = call.args.map(a => this.emitExpr(a)).join(', ');
310
+ this.line(`pcall(${callee}, ${args})`);
311
+ }
312
+ emitThreadStmt(stmt) {
313
+ this.line('Citizen.CreateThread(function()');
314
+ this.emitBlock(stmt.body);
315
+ this.line('end)');
316
+ }
317
+ emitLoopStmt(stmt) {
318
+ const conds = stmt.conditions.map(c => this.emitExpr(c)).join(' and ');
319
+ this.line(`while ${conds} do`);
320
+ this.emitBlockWithContinue(stmt.body);
321
+ this.line('end');
322
+ }
323
+ emitWaitStmt(stmt) {
324
+ this.line(`Citizen.Wait(${this.emitExpr(stmt.time)})`);
325
+ }
326
+ emitTimeoutStmt(stmt) {
327
+ this.line(`Citizen.SetTimeout(${this.emitExpr(stmt.delay)}, function()`);
328
+ this.emitBlock(stmt.body);
329
+ this.line('end)');
330
+ }
331
+ emitIntervalStmt(stmt) {
332
+ this.line('Citizen.CreateThread(function()');
333
+ this.indent++;
334
+ this.line('while true do');
335
+ this.indent++;
336
+ this.line(`Citizen.Wait(${this.emitExpr(stmt.interval)})`);
337
+ for (const s of stmt.body.statements) {
338
+ this.emitStatement(s);
339
+ }
340
+ this.indent--;
341
+ this.line('end');
342
+ this.indent--;
343
+ this.line('end)');
344
+ }
345
+ emitTryCatch(stmt) {
346
+ this.line('local _ok, _err = pcall(function()');
347
+ this.emitBlock(stmt.tryBody);
348
+ this.line('end)');
349
+ this.line('if not _ok then');
350
+ this.indent++;
351
+ this.line(`local ${stmt.catchParam.name} = _err`);
352
+ for (const s of stmt.catchBody.statements) {
353
+ this.emitStatement(s);
354
+ }
355
+ this.indent--;
356
+ this.line('end');
357
+ }
358
+ emitEmitStmt(stmt) {
359
+ const fnName = stmt.emitType === 'emit' ? 'TriggerEvent' :
360
+ stmt.emitType === 'emitClient' ? 'TriggerClientEvent' : 'TriggerServerEvent';
361
+ const args = [this.emitExpr(stmt.eventName), ...stmt.args.map(a => this.emitExpr(a))].join(', ');
362
+ this.line(`${fnName}(${args})`);
363
+ }
364
+ emitEventHandler(stmt) {
365
+ const eventName = this.emitExpr(stmt.eventName);
366
+ const params = stmt.params.map(p => p.name.name).join(', ');
367
+ if (stmt.isNet) {
368
+ this.line(`RegisterNetEvent(${eventName})`);
369
+ }
370
+ this.line(`AddEventHandler(${eventName}, function(${params})`);
371
+ this.emitBlock(stmt.body);
372
+ this.line('end)');
373
+ }
374
+ emitFunctionDecl(stmt) {
375
+ const params = stmt.params.map(p => p.name.name).join(', ');
376
+ const prefix = stmt.isLocal ? 'local ' : '';
377
+ const name = stmt.name ? this.emitExpr(stmt.name) : '';
378
+ this.line(`${prefix}function ${name}(${params})`);
379
+ this.indent++;
380
+ // Default Values
381
+ for (const p of stmt.params) {
382
+ if (p.defaultValue) {
383
+ const pname = p.name.name;
384
+ const val = this.emitExpr(p.defaultValue);
385
+ this.line(`if ${pname} == nil then ${pname} = ${val} end`);
386
+ }
387
+ }
388
+ // Explicitly call emitStatement for each to avoid redundant indent++ if we use emitBlock
389
+ for (const s of stmt.body.statements) {
390
+ this.emitStatement(s);
391
+ }
392
+ this.indent--;
393
+ this.line('end');
394
+ }
395
+ emitSwitchStmt(stmt) {
396
+ // Generate temp variable for discriminant to evaluate only once
397
+ const tempVar = this.nextTemp('_s');
398
+ this.line(`local ${tempVar} = ${this.emitExpr(stmt.discriminant)}`);
399
+ let first = true;
400
+ for (const caseClause of stmt.cases) {
401
+ const conditions = caseClause.values.map(v => `${tempVar} == ${this.emitExpr(v)}`).join(' or ');
402
+ if (first) {
403
+ this.line(`if ${conditions} then`);
404
+ first = false;
405
+ }
406
+ else {
407
+ this.line(`elseif ${conditions} then`);
408
+ }
409
+ this.emitBlock(caseClause.body);
410
+ }
411
+ if (stmt.defaultCase) {
412
+ if (first) {
413
+ // Only default case
414
+ this.emitBlock(stmt.defaultCase);
415
+ }
416
+ else {
417
+ this.line('else');
418
+ this.emitBlock(stmt.defaultCase);
419
+ }
420
+ }
421
+ if (!first) {
422
+ this.line('end');
423
+ }
424
+ }
425
+ emitExportDecl(stmt) {
426
+ const decl = stmt.declaration;
427
+ const nameStr = decl.name ? this.emitExpr(decl.name) : 'anonymous';
428
+ const params = decl.params.map(p => p.name.name).join(', ');
429
+ this.line(`exports(${nameStr.startsWith('"') ? nameStr : `"${nameStr}"`}, function(${params})`);
430
+ this.emitBlock(decl.body);
431
+ this.line('end)');
432
+ }
433
+ emitCommandStmt(stmt) {
434
+ const name = stmt.commandName;
435
+ const params = stmt.params.map(p => p.name.name).join(', ');
436
+ this.line(`RegisterCommand("${name}", function(${params})`);
437
+ this.emitBlock(stmt.body);
438
+ this.line('end, false)');
439
+ }
440
+ // ============ Expression Emission ============
441
+ emitExpr(expr) {
442
+ switch (expr.kind) {
443
+ case AST.NodeType.NumberLiteral:
444
+ return String(expr.value);
445
+ case AST.NodeType.StringLiteral: {
446
+ const str = expr;
447
+ if (str.isLong)
448
+ return `[[${str.value}]]`;
449
+ // Use original quote type for FiveM Lua compatibility
450
+ const quote = str.quote || '"';
451
+ return `${quote}${str.value}${quote}`;
452
+ }
453
+ case AST.NodeType.InterpolatedString:
454
+ return this.emitInterpolatedString(expr);
455
+ case AST.NodeType.BooleanLiteral:
456
+ return expr.value ? 'true' : 'false';
457
+ case AST.NodeType.NilLiteral:
458
+ return 'nil';
459
+ case AST.NodeType.Identifier:
460
+ return expr.name;
461
+ case AST.NodeType.BinaryExpr:
462
+ return this.emitBinaryExpr(expr);
463
+ case AST.NodeType.UnaryExpr:
464
+ return this.emitUnaryExpr(expr);
465
+ case AST.NodeType.CallExpr:
466
+ return this.emitCallExpr(expr);
467
+ case AST.NodeType.MemberExpr:
468
+ return this.emitMemberExpr(expr);
469
+ case AST.NodeType.OptionalChainExpr:
470
+ return this.emitOptionalChain(expr);
471
+ case AST.NodeType.NullCoalesceExpr:
472
+ return this.emitNullCoalesce(expr);
473
+ case AST.NodeType.ArrowFunc:
474
+ return this.emitArrowFunc(expr);
475
+ case AST.NodeType.TableLiteral:
476
+ return this.emitTableLiteral(expr);
477
+ case AST.NodeType.VectorLiteral:
478
+ return this.emitVectorLiteral(expr);
479
+ case AST.NodeType.UpdateExpr:
480
+ return this.emitUpdateExpr(expr);
481
+ case AST.NodeType.ConditionalExpr:
482
+ return this.emitConditionalExpr(expr);
483
+ case AST.NodeType.SpreadExpr:
484
+ return `unpack(${this.emitExpr(expr.argument)})`;
485
+ case AST.NodeType.FunctionDecl:
486
+ return this.emitFunctionExpr(expr);
487
+ default:
488
+ return `--[[ unknown: ${expr.kind} ]]`;
489
+ }
490
+ }
491
+ emitInterpolatedString(str) {
492
+ const quote = str.quote || '"';
493
+ const parts = str.parts.map(p => {
494
+ if (typeof p === 'string')
495
+ return `${quote}${p}${quote}`;
496
+ const expr = p;
497
+ const emitted = this.emitExpr(expr);
498
+ // Optimize: No need to tostring literals or already-string expressions
499
+ if (expr.kind === AST.NodeType.StringLiteral || expr.kind === AST.NodeType.NumberLiteral) {
500
+ return emitted;
501
+ }
502
+ return `tostring(${emitted})`;
503
+ });
504
+ return parts.join(' .. ');
505
+ }
506
+ emitBinaryExpr(expr) {
507
+ const left = this.emitExpr(expr.left);
508
+ const right = this.emitExpr(expr.right);
509
+ let op = expr.operator;
510
+ if (op === '!=')
511
+ op = '~=';
512
+ return `(${left} ${op} ${right})`;
513
+ }
514
+ emitUnaryExpr(expr) {
515
+ const space = expr.operator === 'not' ? ' ' : '';
516
+ return `${expr.operator}${space}${this.emitExpr(expr.operand)}`;
517
+ }
518
+ emitCallExpr(expr) {
519
+ const callee = this.emitExpr(expr.callee);
520
+ const args = expr.args.map(a => this.emitExpr(a)).join(', ');
521
+ return `${callee}(${args})`;
522
+ }
523
+ emitMemberExpr(expr) {
524
+ let obj = this.emitExpr(expr.object);
525
+ // Lua Requirement: Literal strings must be wrapped in parens for method calls/access
526
+ if (expr.object.kind === AST.NodeType.StringLiteral || expr.object.kind === AST.NodeType.TableLiteral) {
527
+ obj = `(${obj})`;
528
+ }
529
+ const sep = expr.isMethod ? ':' : '.';
530
+ if (expr.computed) {
531
+ return `${obj}[${this.emitExpr(expr.property)}]`;
532
+ }
533
+ return `${obj}${sep}${expr.property.name}`;
534
+ }
535
+ emitOptionalChain(expr) {
536
+ const chain = [];
537
+ let current = expr;
538
+ // Unroll the chain (e.g., a?.b?.c is nested as (a?.b)?.c)
539
+ while (current.kind === AST.NodeType.OptionalChainExpr) {
540
+ const oc = current;
541
+ const prop = oc.computed
542
+ ? `[${this.emitExpr(oc.property)}]`
543
+ : `.${oc.property.name}`;
544
+ chain.unshift(prop);
545
+ current = oc.object;
546
+ }
547
+ const base = this.emitExpr(current);
548
+ let result = `(${base}`;
549
+ let fullPath = base;
550
+ for (const p of chain) {
551
+ fullPath += p;
552
+ result += ` and ${fullPath}`;
553
+ }
554
+ result += `)`;
555
+ return result;
556
+ }
557
+ emitNullCoalesce(expr) {
558
+ const left = this.emitExpr(expr.left);
559
+ const right = this.emitExpr(expr.right);
560
+ // Optimization: If left is simple (identifier or member access), repetition is okay and shorter.
561
+ if (this.isSimpleExpr(expr.left)) {
562
+ return `(${left} ~= nil and ${left} or ${right})`;
563
+ }
564
+ // For complex expressions (optional chains, calls), use parameter trick to evaluate only once.
565
+ // This is safer and prevents massive repetition in the output.
566
+ return `(function(v) if v == nil then return ${right} end return v end)(${left})`;
567
+ }
568
+ emitArrowFunc(expr) {
569
+ const params = expr.params.map(p => p.name.name).join(', ');
570
+ if ('kind' in expr.body && expr.body.kind === AST.NodeType.Block) {
571
+ const block = expr.body;
572
+ const bodyLines = [];
573
+ for (const s of block.statements) {
574
+ bodyLines.push(this.emitStatementInline(s));
575
+ }
576
+ return `function(${params})\n${bodyLines.join('\n')}\nend`;
577
+ }
578
+ else {
579
+ return `function(${params}) return ${this.emitExpr(expr.body)} end`;
580
+ }
581
+ }
582
+ emitFunctionExpr(stmt) {
583
+ const params = stmt.params.map(p => p.name.name).join(', ');
584
+ const name = stmt.name ? ` ${this.emitExpr(stmt.name)}` : '';
585
+ const prevOutput = this.output;
586
+ this.output = [];
587
+ this.indent++;
588
+ // Default Values
589
+ for (const p of stmt.params) {
590
+ if (p.defaultValue) {
591
+ const pname = p.name.name;
592
+ const val = this.emitExpr(p.defaultValue);
593
+ this.line(`if ${pname} == nil then ${pname} = ${val} end`);
594
+ }
595
+ }
596
+ for (const s of stmt.body.statements) {
597
+ this.emitStatement(s);
598
+ }
599
+ this.indent--;
600
+ const body = this.output.join('\n');
601
+ this.output = prevOutput;
602
+ return `function${name}(${params})\n${body}${body ? '\n' : ''}${' '.repeat(this.indent)}end`;
603
+ }
604
+ emitTableLiteral(expr) {
605
+ const hasSpread = expr.fields.some(f => f.value.kind === AST.NodeType.SpreadExpr);
606
+ const hasComputedKeys = expr.fields.some(f => f.key && f.key.kind !== AST.NodeType.Identifier);
607
+ const hasMixedFields = expr.fields.some(f => f.key) && expr.fields.some(f => !f.key);
608
+ // Simple array-like table: {1, 2, 3}
609
+ if (!hasSpread && !hasComputedKeys && !hasMixedFields && expr.fields.every(f => !f.key)) {
610
+ const fields = expr.fields.map(f => this.emitExpr(f.value));
611
+ return `{ ${fields.join(', ')} }`;
612
+ }
613
+ if (!hasSpread) {
614
+ const fields = expr.fields.map(f => {
615
+ if (f.key) {
616
+ const key = f.key.kind === AST.NodeType.Identifier
617
+ ? f.key.name
618
+ : `[${this.emitExpr(f.key)}]`;
619
+ return `${key} = ${this.emitExpr(f.value)}`;
620
+ }
621
+ return this.emitExpr(f.value);
622
+ });
623
+ return `{ ${fields.join(', ')} }`;
624
+ }
625
+ // Table with spread: Use an IIFE for merging
626
+ const temp = this.nextTemp('_tab');
627
+ const lines = [`local ${temp} = {}`];
628
+ for (const f of expr.fields) {
629
+ if (f.value.kind === AST.NodeType.SpreadExpr) {
630
+ const spreadArg = this.emitExpr(f.value.argument);
631
+ const k = this.nextTemp('_k');
632
+ const v = this.nextTemp('_v');
633
+ lines.push(`for ${k},${v} in pairs(${spreadArg}) do`);
634
+ lines.push(` if type(${k}) == "number" then table.insert(${temp}, ${v}) else ${temp}[${k}] = ${v} end`);
635
+ lines.push(`end`);
636
+ }
637
+ else if (f.key) {
638
+ const key = f.key.kind === AST.NodeType.Identifier
639
+ ? `"${f.key.name}"`
640
+ : this.emitExpr(f.key);
641
+ lines.push(`${temp}[${key}] = ${this.emitExpr(f.value)}`);
642
+ }
643
+ else {
644
+ lines.push(`table.insert(${temp}, ${this.emitExpr(f.value)})`);
645
+ }
646
+ }
647
+ return `(function() ${lines.join('; ')} return ${temp} end)()`;
648
+ }
649
+ emitVectorLiteral(expr) {
650
+ const args = expr.components.map(c => this.emitExpr(c)).join(', ');
651
+ const fn = expr.components.length === 2 ? 'vector2' :
652
+ expr.components.length === 3 ? 'vector3' : 'vector4';
653
+ return `${fn}(${args})`;
654
+ }
655
+ emitUpdateExpr(expr) {
656
+ const op = expr.operator === '++' ? '+' : '-';
657
+ const arg = this.emitExpr(expr.argument);
658
+ if (expr.prefix) {
659
+ return `(function() ${arg} = ${arg} ${op} 1; return ${arg} end)()`;
660
+ }
661
+ else {
662
+ const temp = this.nextTemp('_old');
663
+ return `(function() local ${temp} = ${arg}; ${arg} = ${arg} ${op} 1; return ${temp} end)()`;
664
+ }
665
+ }
666
+ emitConditionalExpr(expr) {
667
+ const test = this.emitExpr(expr.test);
668
+ const cons = this.emitExpr(expr.consequent);
669
+ const alt = this.emitExpr(expr.alternate);
670
+ // Optimization: (test ? true : false) -> (not not test) or just (test) for comparisons.
671
+ if (expr.consequent.kind === AST.NodeType.BooleanLiteral && expr.consequent.value === true &&
672
+ expr.alternate.kind === AST.NodeType.BooleanLiteral && expr.alternate.value === false) {
673
+ if (expr.test.kind === AST.NodeType.BinaryExpr) {
674
+ const op = expr.test.operator;
675
+ if (['>', '<', '>=', '<=', '==', '!=', '==='].includes(op)) {
676
+ return `(${test})`;
677
+ }
678
+ }
679
+ return `(not not ${test})`;
680
+ }
681
+ // Optimization: (test ? false : true) -> (not test)
682
+ if (expr.consequent.kind === AST.NodeType.BooleanLiteral && expr.consequent.value === false &&
683
+ expr.alternate.kind === AST.NodeType.BooleanLiteral && expr.alternate.value === true) {
684
+ return `(not ${test})`;
685
+ }
686
+ // Optimization: Use (a and b or c) if b is a non-falsy literal
687
+ const isTruthyLiteral = expr.consequent.kind === AST.NodeType.NumberLiteral ||
688
+ expr.consequent.kind === AST.NodeType.StringLiteral ||
689
+ expr.consequent.kind === AST.NodeType.TableLiteral ||
690
+ expr.consequent.kind === AST.NodeType.VectorLiteral ||
691
+ (expr.consequent.kind === AST.NodeType.BooleanLiteral && expr.consequent.value === true);
692
+ if (isTruthyLiteral) {
693
+ return `(${test} and ${cons} or ${alt})`;
694
+ }
695
+ return `(function() if ${test} then return ${cons} else return ${alt} end end)()`;
696
+ }
697
+ emitStatementInline(stmt) {
698
+ const prev = this.output;
699
+ this.output = [];
700
+ this.emitStatement(stmt);
701
+ const result = this.output.join('\n');
702
+ this.output = prev;
703
+ return result;
704
+ }
705
+ // ============ Helpers ============
706
+ hasContinue(block) {
707
+ return block.statements.some(s => s.kind === AST.NodeType.ContinueStmt);
708
+ }
709
+ emitBlock(block) {
710
+ this.indent++;
711
+ for (const stmt of block.statements) {
712
+ this.emitStatement(stmt);
713
+ }
714
+ this.indent--;
715
+ }
716
+ emitBlockWithContinue(block) {
717
+ this.indent++;
718
+ for (const stmt of block.statements) {
719
+ this.emitStatement(stmt);
720
+ }
721
+ if (this.hasContinue(block)) {
722
+ this.line('::continue::');
723
+ }
724
+ this.indent--;
725
+ }
726
+ line(code) {
727
+ this.output.push(' '.repeat(this.indent) + code);
728
+ }
729
+ isSimpleExpr(expr) {
730
+ if (expr.kind === AST.NodeType.Identifier ||
731
+ expr.kind === AST.NodeType.NumberLiteral ||
732
+ expr.kind === AST.NodeType.BooleanLiteral ||
733
+ expr.kind === AST.NodeType.NilLiteral ||
734
+ expr.kind === AST.NodeType.StringLiteral) {
735
+ return true;
736
+ }
737
+ if (expr.kind === AST.NodeType.MemberExpr) {
738
+ const mem = expr;
739
+ return this.isSimpleExpr(mem.object) && (mem.computed ? this.isSimpleExpr(mem.property) : true);
740
+ }
741
+ if (expr.kind === AST.NodeType.OptionalChainExpr) {
742
+ // Optional chains are NOT simple for repetition because they expand into long and-chains.
743
+ // We return false here so that NullCoalescing uses a parameter trick instead of repeating the path.
744
+ return false;
745
+ }
746
+ return false;
747
+ }
748
+ }