luau2ts 0.2.2 → 0.2.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 (140) hide show
  1. package/dist/cli/args.d.ts +23 -0
  2. package/dist/cli/args.d.ts.map +1 -0
  3. package/dist/cli/args.js +177 -0
  4. package/dist/cli/args.js.map +1 -0
  5. package/dist/cli/bin.d.ts +3 -0
  6. package/dist/cli/bin.d.ts.map +1 -0
  7. package/dist/cli/bin.js +71 -0
  8. package/dist/cli/bin.js.map +1 -0
  9. package/dist/cli/modes.d.ts +20 -0
  10. package/dist/cli/modes.d.ts.map +1 -0
  11. package/dist/cli/modes.js +145 -0
  12. package/dist/cli/modes.js.map +1 -0
  13. package/dist/compile/class-shape.d.ts +31 -0
  14. package/dist/compile/class-shape.d.ts.map +1 -0
  15. package/dist/compile/class-shape.js +291 -0
  16. package/dist/compile/class-shape.js.map +1 -0
  17. package/dist/compile/context.d.ts +86 -0
  18. package/dist/compile/context.d.ts.map +1 -0
  19. package/dist/compile/context.js +144 -0
  20. package/dist/compile/context.js.map +1 -0
  21. package/dist/compile/index.d.ts +58 -0
  22. package/dist/compile/index.d.ts.map +1 -0
  23. package/dist/compile/index.js +2155 -0
  24. package/dist/compile/index.js.map +1 -0
  25. package/dist/compile/macros/datatypes.d.ts +2 -0
  26. package/dist/compile/macros/datatypes.d.ts.map +1 -0
  27. package/dist/compile/macros/datatypes.js +76 -0
  28. package/dist/compile/macros/datatypes.js.map +1 -0
  29. package/dist/compile/macros/index.d.ts +33 -0
  30. package/dist/compile/macros/index.d.ts.map +1 -0
  31. package/dist/compile/macros/index.js +71 -0
  32. package/dist/compile/macros/index.js.map +1 -0
  33. package/dist/compile/macros/instance.d.ts +2 -0
  34. package/dist/compile/macros/instance.d.ts.map +1 -0
  35. package/dist/compile/macros/instance.js +58 -0
  36. package/dist/compile/macros/instance.js.map +1 -0
  37. package/dist/compile/macros/stdlib.d.ts +2 -0
  38. package/dist/compile/macros/stdlib.d.ts.map +1 -0
  39. package/dist/compile/macros/stdlib.js +140 -0
  40. package/dist/compile/macros/stdlib.js.map +1 -0
  41. package/dist/compile/rbxts-runtime.d.ts +2 -0
  42. package/dist/compile/rbxts-runtime.d.ts.map +1 -0
  43. package/dist/compile/rbxts-runtime.js +163 -0
  44. package/dist/compile/rbxts-runtime.js.map +1 -0
  45. package/dist/compile/sourcemap.d.ts +25 -0
  46. package/dist/compile/sourcemap.d.ts.map +1 -0
  47. package/dist/compile/sourcemap.js +71 -0
  48. package/dist/compile/sourcemap.js.map +1 -0
  49. package/dist/compile/type.d.ts +6 -0
  50. package/dist/compile/type.d.ts.map +1 -0
  51. package/dist/compile/type.js +122 -0
  52. package/dist/compile/type.js.map +1 -0
  53. package/dist/compile/util.d.ts +38 -0
  54. package/dist/compile/util.d.ts.map +1 -0
  55. package/dist/compile/util.js +153 -0
  56. package/dist/compile/util.js.map +1 -0
  57. package/dist/index.d.ts +3 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +7 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/parser/index.d.ts +4 -0
  62. package/dist/parser/index.d.ts.map +1 -0
  63. package/dist/parser/index.js +227 -0
  64. package/dist/parser/index.js.map +1 -0
  65. package/dist/parser/types.d.ts +430 -0
  66. package/dist/parser/types.d.ts.map +1 -0
  67. package/dist/parser/types.js +14 -0
  68. package/dist/parser/types.js.map +1 -0
  69. package/dist/parser/wasm/luau-parser.d.mts +21 -0
  70. package/dist/parser/wasm/luau-parser.mjs +2 -0
  71. package/dist/parser/wasm/luau-parser.wasm +0 -0
  72. package/dist/rojo/index.d.ts +4 -0
  73. package/dist/rojo/index.d.ts.map +1 -0
  74. package/dist/rojo/index.js +3 -0
  75. package/dist/rojo/index.js.map +1 -0
  76. package/dist/rojo/load-project.d.ts +12 -0
  77. package/dist/rojo/load-project.d.ts.map +1 -0
  78. package/dist/rojo/load-project.js +35 -0
  79. package/dist/rojo/load-project.js.map +1 -0
  80. package/dist/rojo/types.d.ts +39 -0
  81. package/dist/rojo/types.d.ts.map +1 -0
  82. package/dist/rojo/types.js +2 -0
  83. package/dist/rojo/types.js.map +1 -0
  84. package/dist/rojo/walk-tree.d.ts +40 -0
  85. package/dist/rojo/walk-tree.d.ts.map +1 -0
  86. package/dist/rojo/walk-tree.js +164 -0
  87. package/dist/rojo/walk-tree.js.map +1 -0
  88. package/dist/runtime/arith.d.ts +13 -0
  89. package/dist/runtime/arith.d.ts.map +1 -0
  90. package/dist/runtime/arith.js +151 -0
  91. package/dist/runtime/arith.js.map +1 -0
  92. package/dist/runtime/index-helper.d.ts +3 -0
  93. package/dist/runtime/index-helper.d.ts.map +1 -0
  94. package/dist/runtime/index-helper.js +40 -0
  95. package/dist/runtime/index-helper.js.map +1 -0
  96. package/dist/runtime/index.d.ts +13 -0
  97. package/dist/runtime/index.d.ts.map +1 -0
  98. package/dist/runtime/index.js +13 -0
  99. package/dist/runtime/index.js.map +1 -0
  100. package/dist/runtime/iterator.d.ts +58 -0
  101. package/dist/runtime/iterator.d.ts.map +1 -0
  102. package/dist/runtime/iterator.js +181 -0
  103. package/dist/runtime/iterator.js.map +1 -0
  104. package/dist/runtime/length.d.ts +2 -0
  105. package/dist/runtime/length.d.ts.map +1 -0
  106. package/dist/runtime/length.js +15 -0
  107. package/dist/runtime/length.js.map +1 -0
  108. package/dist/runtime/lua-stdlib.d.ts +186 -0
  109. package/dist/runtime/lua-stdlib.d.ts.map +1 -0
  110. package/dist/runtime/lua-stdlib.js +502 -0
  111. package/dist/runtime/lua-stdlib.js.map +1 -0
  112. package/dist/runtime/metatable.d.ts +16 -0
  113. package/dist/runtime/metatable.d.ts.map +1 -0
  114. package/dist/runtime/metatable.js +129 -0
  115. package/dist/runtime/metatable.js.map +1 -0
  116. package/dist/runtime/pattern.d.ts +21 -0
  117. package/dist/runtime/pattern.d.ts.map +1 -0
  118. package/dist/runtime/pattern.js +375 -0
  119. package/dist/runtime/pattern.js.map +1 -0
  120. package/dist/runtime/pcall.d.ts +12 -0
  121. package/dist/runtime/pcall.d.ts.map +1 -0
  122. package/dist/runtime/pcall.js +54 -0
  123. package/dist/runtime/pcall.js.map +1 -0
  124. package/dist/runtime/string-lib.d.ts +31 -0
  125. package/dist/runtime/string-lib.d.ts.map +1 -0
  126. package/dist/runtime/string-lib.js +296 -0
  127. package/dist/runtime/string-lib.js.map +1 -0
  128. package/dist/runtime/table-lib.d.ts +18 -0
  129. package/dist/runtime/table-lib.d.ts.map +1 -0
  130. package/dist/runtime/table-lib.js +133 -0
  131. package/dist/runtime/table-lib.js.map +1 -0
  132. package/dist/runtime/tostring.d.ts +3 -0
  133. package/dist/runtime/tostring.d.ts.map +1 -0
  134. package/dist/runtime/tostring.js +82 -0
  135. package/dist/runtime/tostring.js.map +1 -0
  136. package/dist/runtime/truthy.d.ts +13 -0
  137. package/dist/runtime/truthy.d.ts.map +1 -0
  138. package/dist/runtime/truthy.js +26 -0
  139. package/dist/runtime/truthy.js.map +1 -0
  140. package/package.json +1 -1
@@ -0,0 +1,2155 @@
1
+ import { parse, } from '../parser/index.js';
2
+ import ts from 'typescript';
3
+ import { format as prettierFormat } from 'prettier';
4
+ import { ARITH_DATATYPES, CompileContext, RUNTIME_MODULE } from './context.js';
5
+ import { lookupMacro } from './macros/index.js';
6
+ // Side-effect imports — each module's top-level `registerMacro` calls
7
+ // populate the global registry consulted by `lookupMacro` above.
8
+ import './macros/datatypes.js';
9
+ import './macros/instance.js';
10
+ import './macros/stdlib.js';
11
+ import './rbxts-runtime.js';
12
+ import { detectClasses, compileClassPattern } from './class-shape.js';
13
+ import { compileType, compileTypePack } from './type.js';
14
+ import { buildSourceMap, inlineSourceMapURL, } from './sourcemap.js';
15
+ import { propertyName, isRepeatableExpression, safeIdentifier, throwUnsupported, truthify, unsupportedExpr, } from './util.js';
16
+ const { factory } = ts;
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+ // Statements
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+ function compileBlock(block, ctx) {
21
+ return ctx.withScope(() => factory.createBlock(compileBlockBody(block, ctx), true));
22
+ }
23
+ function compileBlockBody(block, ctx) {
24
+ if (block.type !== 'Block')
25
+ return statementsOf(block, ctx);
26
+ // — class-shape recognition. In rbxts compat mode, walk the
27
+ // block body once to detect roblox-ts's metatable-OOP class emit pattern
28
+ // and replace each detected group with a synthesized TS class
29
+ // declaration. The constituent statements are dropped from the output.
30
+ const classes = ctx.compatMode === 'rbxts' ? detectClasses(block.body) : [];
31
+ if (classes.length === 0) {
32
+ return block.body.flatMap((s) => statementsOf(s, ctx));
33
+ }
34
+ const consumed = new Set();
35
+ const classByLeadIndex = new Map();
36
+ for (const c of classes) {
37
+ // Register the class name with the context so subsequent
38
+ // `<Class>.new(...)` calls in the same file are rewritten to
39
+ // `new <Class>(...)` by the macro registry.
40
+ ctx.recordDetectedClass(c.name);
41
+ let lead = Infinity;
42
+ for (const idx of c.consumed) {
43
+ if (idx < lead)
44
+ lead = idx;
45
+ consumed.add(idx);
46
+ }
47
+ classByLeadIndex.set(lead, c);
48
+ }
49
+ const out = [];
50
+ for (let i = 0; i < block.body.length; i++) {
51
+ if (classByLeadIndex.has(i)) {
52
+ out.push(compileClassPattern(classByLeadIndex.get(i), ctx, compileBlockBody, compileExpr));
53
+ continue;
54
+ }
55
+ if (consumed.has(i))
56
+ continue;
57
+ out.push(...statementsOf(block.body[i], ctx));
58
+ }
59
+ return out;
60
+ }
61
+ function statementsOf(stat, ctx) {
62
+ switch (stat.type) {
63
+ case 'Local':
64
+ return compileLocal(stat, ctx);
65
+ case 'LocalFunction':
66
+ return [compileLocalFunction(stat, ctx)];
67
+ case 'Function':
68
+ return [compileFunctionStat(stat, ctx)];
69
+ case 'Expr':
70
+ return [factory.createExpressionStatement(compileExpr(stat.expr, ctx))];
71
+ case 'Return': {
72
+ if (stat.values.length === 0) {
73
+ return [factory.createReturnStatement(undefined)];
74
+ }
75
+ if (stat.values.length === 1) {
76
+ return [factory.createReturnStatement(compileExpr(stat.values[0], ctx))];
77
+ }
78
+ // Multi-value return. In native mode we emit a JS array, which is
79
+ // what `[a, b] = f()` destructure expects on the consumer side.
80
+ // In rbxts mode we emit roblox-ts's `$tuple(...)` macro instead, so
81
+ // re-feeding the output through roblox-ts produces native Luau
82
+ // multi-return (`return a, b`) rather than a Lua table (`return {a, b}`).
83
+ const compiled = stat.values.map((v) => compileExpr(v, ctx));
84
+ if (ctx.compatMode === 'rbxts') {
85
+ return [factory.createReturnStatement(factory.createCallExpression(factory.createIdentifier('$tuple'), undefined, compiled))];
86
+ }
87
+ return [factory.createReturnStatement(factory.createArrayLiteralExpression(compiled, false))];
88
+ }
89
+ case 'If':
90
+ return [compileIf(stat, ctx)];
91
+ case 'While':
92
+ return [
93
+ factory.createWhileStatement(truthify(compileExpr(stat.condition, ctx), ctx, staticTypeOfExpr(stat.condition, ctx)), compileBlock(stat.body, ctx)),
94
+ ];
95
+ case 'Repeat':
96
+ return [
97
+ factory.createDoStatement(compileBlock(stat.body, ctx), factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, truthify(compileExpr(stat.condition, ctx), ctx, staticTypeOfExpr(stat.condition, ctx)))),
98
+ ];
99
+ case 'For':
100
+ return [compileFor(stat, ctx)];
101
+ case 'ForIn':
102
+ return compileForIn(stat, ctx);
103
+ case 'Break':
104
+ return [factory.createBreakStatement()];
105
+ case 'Continue':
106
+ return [factory.createContinueStatement()];
107
+ case 'Block':
108
+ return [compileBlock(stat, ctx)];
109
+ case 'Assign':
110
+ return compileAssign(stat, ctx);
111
+ case 'CompoundAssign':
112
+ return [compileCompoundAssign(stat, ctx)];
113
+ case 'TypeAlias':
114
+ return [compileTypeAlias(stat)];
115
+ case 'TypeFunction':
116
+ // Luau type functions evaluate at type-check time — emit nothing.
117
+ return [];
118
+ case 'DeclareGlobal':
119
+ return [compileDeclareGlobal(stat)];
120
+ case 'DeclareFunction':
121
+ return [compileDeclareFunction(stat)];
122
+ case 'DeclareExternType':
123
+ // Declare-extern-types are .d.ts-style class declarations; we drop
124
+ // them in JS output for now.
125
+ return [];
126
+ case 'StatError':
127
+ return [throwUnsupported(`luau-to-ts: parse error in statement`)];
128
+ case 'UnknownStat':
129
+ default:
130
+ return [throwUnsupported(`luau-to-ts: unsupported statement '${stat.type}'`)];
131
+ }
132
+ }
133
+ /** Does `node` contain a free reference to `name` (binding-position-aware)? */
134
+ function containsFreeRef(node, name) {
135
+ let found = false;
136
+ const check = (n) => {
137
+ if (found)
138
+ return;
139
+ if (ts.isIdentifier(n) && n.text === name) {
140
+ found = true;
141
+ return;
142
+ }
143
+ if (ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)
144
+ || ts.isArrowFunction(n) || ts.isMethodDeclaration(n)) {
145
+ const params = n.parameters;
146
+ if (params.some((p) => ts.isIdentifier(p.name) && p.name.text === name))
147
+ return;
148
+ }
149
+ if (ts.isPropertyAccessExpression(n)) {
150
+ check(n.expression);
151
+ return;
152
+ }
153
+ ts.forEachChild(n, check);
154
+ };
155
+ check(node);
156
+ return found;
157
+ }
158
+ function compileLocal(stat, ctx) {
159
+ // Beautification: drop `local X = <expr>` when <expr> is exactly the
160
+ // identifier `X`. This happens when a macro rewrites the RHS to the
161
+ // same name as the LHS — e.g. `local Workspace = game:GetService("Workspace")`
162
+ // becomes `let Workspace = Workspace` after the GetService macro fires,
163
+ // shadowing the import. Suppressing the local lets the import stay
164
+ // visible to subsequent uses.
165
+ if (stat.vars.length === 1 && stat.values.length === 1) {
166
+ const v = stat.vars[0];
167
+ const init = stat.values[0];
168
+ const compiledInit = compileExpr(init, ctx);
169
+ if (ts.isIdentifier(compiledInit)
170
+ && compiledInit.text === safeIdentifier(v.name)) {
171
+ ctx.suppressLocal(v.name);
172
+ ctx.defineLocal(v.name, typeFromAnnotation(v.annotation, init, ctx));
173
+ return [];
174
+ }
175
+ }
176
+ // Single RHS call with multiple LHS targets → destructuring (multi-return).
177
+ if (stat.vars.length > 1 && stat.values.length === 1 && stat.values[0]?.type === 'Call') {
178
+ const rawInit = compileExpr(stat.values[0], ctx);
179
+ const init = factory.createCallExpression(factory.createIdentifier(ctx.use('multiret')), undefined, [rawInit]);
180
+ const anyShadow = stat.vars.some((v) => ctx.hasLocalInCurrentScope(v.name));
181
+ for (const v of stat.vars)
182
+ ctx.assignLocal(v.name, typeFromAnnotation(v.annotation));
183
+ if (anyShadow) {
184
+ // Any var already in scope means `let [a, b] = …` would TS-error on the
185
+ // shadowed name. Emit bare destructuring assignment instead — Luau
186
+ // semantics for `local x, y = f()` when x or y already exist is "reuse
187
+ // the in-scope binding" in this same scope.
188
+ return [factory.createExpressionStatement(factory.createAssignment(factory.createArrayLiteralExpression(stat.vars.map((v) => factory.createIdentifier(safeIdentifier(v.name)))), init))];
189
+ }
190
+ // Luau lets you write `local _, _, _, xx, ...` with duplicate `_` to
191
+ // discard values. JS forbids duplicate names in one destructure, so
192
+ // rewrite duplicates (including the standard `_` placeholder) to fresh
193
+ // names like `_skip_0`. Single `_` keeps its original name.
194
+ const seen = new Set();
195
+ const bindingNames = stat.vars.map((v) => {
196
+ let name = safeIdentifier(v.name);
197
+ if (seen.has(name)) {
198
+ name = ctx.freshIdentifier(`${name}_skip`);
199
+ }
200
+ seen.add(name);
201
+ return name;
202
+ });
203
+ return [factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
204
+ factory.createVariableDeclaration(factory.createArrayBindingPattern(bindingNames.map((name) => factory.createBindingElement(undefined, undefined, factory.createIdentifier(name), undefined))), undefined, v_typeForLocal(stat.vars), init),
205
+ ], stat.isConst ? ts.NodeFlags.Const : ts.NodeFlags.Let))];
206
+ }
207
+ // Luau lets you write `local X = ...` after a previous `local X = ...` in
208
+ // the same block; the second declaration shadows the first. TS `let`
209
+ // can't redeclare in the same scope, so a same-scope shadow becomes an
210
+ // assignment to the existing binding. (Cross-block shadow stays a real
211
+ // `let` since `withScope` opens a fresh JS block scope.)
212
+ const newDecls = [];
213
+ const reassignments = [];
214
+ for (let i = 0; i < stat.vars.length; i += 1) {
215
+ const v = stat.vars[i];
216
+ const init = stat.values[i];
217
+ const initExpr = init ? compileExpr(init, ctx) : undefined;
218
+ const safeName = safeIdentifier(v.name);
219
+ // Pick the JS name for the new binding.
220
+ //
221
+ // Luau: in `local X = expr`, free references to `X` inside `expr` bind to
222
+ // the OUTER `X`. JS `let X = function () { X() }` infinite-recurses because
223
+ // the inner `X` is the new local (TDZ-shadowed at parse time, then the new
224
+ // local at runtime). To stay faithful we rename the new local to a fresh
225
+ // JS name when init captures the same name; the inner reference then
226
+ // resolves to the still-unshadowed outer binding. Future Luau-side reads
227
+ // of `X` in this scope go through `ctx.getLocalJsName` and pick up the
228
+ // fresh name.
229
+ let jsName = safeName;
230
+ if (initExpr && !ctx.hasLocalInCurrentScope(v.name) && containsFreeRef(initExpr, safeName)) {
231
+ jsName = ctx.freshIdentifier(`${safeName}_local`);
232
+ ctx.setLocalJsName(v.name, jsName);
233
+ }
234
+ else if (ctx.hasLocalInCurrentScope(v.name)) {
235
+ jsName = ctx.getLocalJsName(v.name) ?? safeName;
236
+ }
237
+ else if (ctx.hasLocalInOuterScope(v.name)) {
238
+ // Cross-block shadow: outer scope already binds `X`, and Luau lets
239
+ // earlier statements in this block read the outer value before this
240
+ // `local X = ...` line introduces the inner one. JS `let` hoists the
241
+ // inner binding to the top of the block, so those earlier reads
242
+ // hit TDZ. Rename the inner to a fresh JS name; reads up to this
243
+ // point resolve to the outer, reads after this line go through the
244
+ // jsNameOverride to the new name.
245
+ jsName = ctx.freshIdentifier(`${safeName}_local`);
246
+ ctx.setLocalJsName(v.name, jsName);
247
+ }
248
+ if (ctx.hasLocalInCurrentScope(v.name)) {
249
+ if (initExpr) {
250
+ reassignments.push(factory.createExpressionStatement(factory.createBinaryExpression(factory.createIdentifier(jsName), factory.createToken(ts.SyntaxKind.EqualsToken), initExpr)));
251
+ }
252
+ ctx.assignLocal(v.name, typeFromAnnotation(v.annotation, init, ctx));
253
+ }
254
+ else {
255
+ newDecls.push(factory.createVariableDeclaration(factory.createIdentifier(jsName), undefined, v.annotation ? compileType(v.annotation) : undefined, initExpr));
256
+ ctx.defineLocal(v.name, typeFromAnnotation(v.annotation, init, ctx));
257
+ }
258
+ }
259
+ const out = [];
260
+ if (newDecls.length > 0) {
261
+ out.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList(newDecls, stat.isConst ? ts.NodeFlags.Const : ts.NodeFlags.Let)));
262
+ }
263
+ out.push(...reassignments);
264
+ return out;
265
+ }
266
+ function v_typeForLocal(vars) {
267
+ // For destructured locals, only emit a tuple type if every var has an
268
+ // annotation. Mixed-annotation cases drop the type to keep noise down.
269
+ if (!vars.every((v) => v.annotation))
270
+ return undefined;
271
+ return factory.createTupleTypeNode(vars.map((v) => compileType(v.annotation)));
272
+ }
273
+ // Every compiled Luau function is emitted `async` so the compiler can
274
+ // freely insert `await` for yielding APIs (wait, task.wait, WaitForChild,
275
+ // :*Async). Roblox scripts can yield from any nested function — pcall
276
+ // callbacks, signal handlers, dropper-tycoon while-loops — so making
277
+ // only the script body async wouldn't be enough.
278
+ const ASYNC_MOD = [factory.createModifier(ts.SyntaxKind.AsyncKeyword)];
279
+ /**
280
+ * Walk a compiled function body looking for an `await` expression. If the
281
+ * body has no awaits, the function doesn't need to be async — and emitting
282
+ * non-async lets call sites that forgot to `await` (i.e. all of them, since
283
+ * Lua doesn't have an explicit await marker) still get the unwrapped value.
284
+ *
285
+ * Skip nested function bodies: an inner `await` inside a closure is the
286
+ * inner closure's concern, not ours.
287
+ */
288
+ function bodyContainsAwait(body) {
289
+ return nodeContainsAwait(body);
290
+ }
291
+ function asyncModIfNeeded(body) {
292
+ return bodyContainsAwait(body) ? ASYNC_MOD : undefined;
293
+ }
294
+ /** Walk a Luau function body looking for multi-value `return` statements.
295
+ * Returns the largest number of values across any return; null if every
296
+ * return has 0 or 1 value. Skips nested functions since each one's
297
+ * return shape is its own concern. */
298
+ function maxMultiReturnArity(body) {
299
+ let max = null;
300
+ function walk(stat) {
301
+ if (!stat)
302
+ return;
303
+ if (stat.type === 'Function' || stat.type === 'LocalFunction')
304
+ return;
305
+ if (stat.type === 'Return') {
306
+ if (stat.values.length > 1) {
307
+ max = Math.max(max ?? 0, stat.values.length);
308
+ }
309
+ return;
310
+ }
311
+ if (stat.type === 'Block') {
312
+ for (const s of stat.body)
313
+ walk(s);
314
+ return;
315
+ }
316
+ if (stat.type === 'If') {
317
+ walk(stat.thenBody);
318
+ walk(stat.elseBody);
319
+ return;
320
+ }
321
+ if (stat.type === 'While' || stat.type === 'Repeat') {
322
+ walk(stat.body);
323
+ return;
324
+ }
325
+ if (stat.type === 'For' || stat.type === 'ForIn') {
326
+ walk(stat.body);
327
+ return;
328
+ }
329
+ // Other statement kinds (Local, Assign, Expr, etc.) can't contain a
330
+ // direct return, so we don't need to descend into them.
331
+ }
332
+ walk(body);
333
+ return max;
334
+ }
335
+ function nodeContainsAwait(node) {
336
+ let found = false;
337
+ function visit(next) {
338
+ if (found)
339
+ return;
340
+ if (ts.isFunctionDeclaration(next) ||
341
+ ts.isFunctionExpression(next) ||
342
+ ts.isArrowFunction(next) ||
343
+ ts.isMethodDeclaration(next)) {
344
+ // Don't descend — that nested function manages its own async-ness.
345
+ return;
346
+ }
347
+ if (ts.isAwaitExpression(next)) {
348
+ found = true;
349
+ return;
350
+ }
351
+ ts.forEachChild(next, visit);
352
+ }
353
+ visit(node);
354
+ return found;
355
+ }
356
+ function compileLocalFunction(stat, ctx) {
357
+ // `local function foo() end` → `async function foo() {}` (when needed)
358
+ // with hoisting parity. Use a function declaration so `foo` is callable
359
+ // before its line in TS.
360
+ const { params, returnType, body } = compileFunctionShape(stat.func, ctx);
361
+ // If the name was already declared as a `let` in this scope (e.g. an
362
+ // earlier `local foo = …`), a function declaration would conflict. Emit
363
+ // assignment to the existing binding instead.
364
+ if (ctx.hasLocalInCurrentScope(stat.name.name)) {
365
+ return factory.createExpressionStatement(factory.createAssignment(factory.createIdentifier(safeIdentifier(stat.name.name)), factory.createFunctionExpression(asyncModIfNeeded(body), undefined, undefined, undefined, params, returnType, body)));
366
+ }
367
+ ctx.defineLocal(stat.name.name, 'unknown');
368
+ return factory.createFunctionDeclaration(asyncModIfNeeded(body), undefined, factory.createIdentifier(safeIdentifier(stat.name.name)), undefined, params, returnType, body);
369
+ }
370
+ function compileFunctionStat(stat, ctx) {
371
+ // `function name() end` (global) or `function obj.m() end` (member).
372
+ // Lua's name is an expression resolving to where the function gets stored.
373
+ // Parser-recovery ExprError as the name would emit `IIFE() = fn` which is
374
+ // not a valid lvalue — skip the whole statement.
375
+ if (stat.name.type === 'ExprError' || stat.name.type === 'UnknownExpr') {
376
+ return factory.createEmptyStatement();
377
+ }
378
+ const fn = compileFunctionExpr(stat.func, ctx);
379
+ if (stat.name.type === 'Global') {
380
+ // A previously declared local with the same name (e.g. `local scrollUp`
381
+ // then later `function scrollUp(args)`) makes the JS function-declaration
382
+ // form a redeclaration conflict. Emit assignment instead. Same for names
383
+ // the script wrapper provides as parameters (`script`, `plugin`, ...):
384
+ // `function script(s) {...}` would hoist past the wrapper's binding,
385
+ // breaking every earlier `script.Method()` call in the body (Lua's
386
+ // dynamic-global lookup matches the assignment's *position*; JS's
387
+ // lexical hoisting would not).
388
+ if (ctx.hasLocalInCurrentScope(stat.name.name)
389
+ || HOST_PROVIDED_GLOBALS.has(stat.name.name)) {
390
+ return factory.createExpressionStatement(factory.createAssignment(factory.createIdentifier(safeIdentifier(stat.name.name)), fn));
391
+ }
392
+ // Register the binding so future declarations of the same name in this
393
+ // scope (e.g. Build to Survive's player-list builder redefining
394
+ // `onClick` per player) emit assignment, not redeclaration.
395
+ ctx.defineLocal(stat.name.name, 'unknown');
396
+ return factory.createFunctionDeclaration(asyncModIfNeeded(fn.body), undefined, factory.createIdentifier(safeIdentifier(stat.name.name)), undefined, paramsFromLocals(stat.func.args), stat.func.returnAnnotation ? compileTypePack(stat.func.returnAnnotation) : undefined, fn.body);
397
+ }
398
+ // Member: `obj.x = function ...` — use the parsed name as an lvalue.
399
+ return factory.createExpressionStatement(factory.createAssignment(compileExpr(stat.name, ctx), fn));
400
+ }
401
+ function compileIf(stat, ctx) {
402
+ const condition = truthify(compileExpr(stat.condition, ctx), ctx, staticTypeOfExpr(stat.condition, ctx));
403
+ const thenBranch = compileBlock(stat.thenBody, ctx);
404
+ const elseBranch = stat.elseBody === null
405
+ ? undefined
406
+ : stat.elseBody.type === 'If'
407
+ ? compileIf(stat.elseBody, ctx)
408
+ : compileBlock(stat.elseBody, ctx);
409
+ return factory.createIfStatement(condition, thenBranch, elseBranch);
410
+ }
411
+ function compileFor(stat, ctx) {
412
+ // Lua: `for i = from, to, step do … end`. step defaults to 1.
413
+ const idName = safeIdentifier(stat.var.name);
414
+ const id = factory.createIdentifier(idName);
415
+ const fromExpr = compileExpr(stat.from, ctx);
416
+ const toExpr = compileExpr(stat.to, ctx);
417
+ const stepExpr = stat.step ? compileExpr(stat.step, ctx) : null;
418
+ const body = ctx.withScope(() => {
419
+ ctx.defineLocal(stat.var.name, 'number');
420
+ const inner = compileBlockBody(stat.body, ctx);
421
+ // Anti-exploit scripts use `for i=-math.huge, math.huge, 1 do …` to lock
422
+ // up Roblox. Without a guard the same loop hangs every browser tab. Bail
423
+ // out as soon as the loop variable is non-finite — Infinity/NaN bounds
424
+ // could never make forward progress anyway.
425
+ const guard = factory.createIfStatement(factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier('Number'), factory.createIdentifier('isFinite')), undefined, [id])), factory.createBreakStatement(), undefined);
426
+ return factory.createBlock([guard, ...inner], true);
427
+ });
428
+ // Fast path: if `step` is a numeric literal (or absent — Lua default
429
+ // is 1), emit a clean `for (let i = from; i <= to; i += step)`. The
430
+ // step direction is statically known so we don't need the runtime guard.
431
+ // Runtime-expression steps (e.g. `for i = 1, 10, x`) fall through to
432
+ // the slow path since direction can flip per-call.
433
+ const stepLiteral = stepExpr === null ? 1 : literalNumber(stepExpr);
434
+ if (stepLiteral !== null && Number.isFinite(stepLiteral) && stepLiteral > 0) {
435
+ const initializer = factory.createVariableDeclarationList([factory.createVariableDeclaration(id, undefined, undefined, fromExpr)], ts.NodeFlags.Let);
436
+ const condition = factory.createBinaryExpression(id, ts.SyntaxKind.LessThanEqualsToken, toExpr);
437
+ const incrementor = stepLiteral === 1
438
+ ? factory.createPostfixUnaryExpression(id, ts.SyntaxKind.PlusPlusToken)
439
+ : factory.createBinaryExpression(id, ts.SyntaxKind.PlusEqualsToken, factory.createNumericLiteral(stepLiteral));
440
+ return factory.createForStatement(initializer, condition, incrementor, body);
441
+ }
442
+ if (stepLiteral !== null && Number.isFinite(stepLiteral) && stepLiteral < 0) {
443
+ const initializer = factory.createVariableDeclarationList([factory.createVariableDeclaration(id, undefined, undefined, fromExpr)], ts.NodeFlags.Let);
444
+ const condition = factory.createBinaryExpression(id, ts.SyntaxKind.GreaterThanEqualsToken, toExpr);
445
+ // TS factory rejects negative numeric literals — `i -= |step|` reads
446
+ // cleaner anyway. Special-case the common -1 step into `i--`.
447
+ const incrementor = stepLiteral === -1
448
+ ? factory.createPostfixUnaryExpression(id, ts.SyntaxKind.MinusMinusToken)
449
+ : factory.createBinaryExpression(id, ts.SyntaxKind.MinusEqualsToken, factory.createNumericLiteral(Math.abs(stepLiteral)));
450
+ return factory.createForStatement(initializer, condition, incrementor, body);
451
+ }
452
+ // Slow path: step is a runtime expression (variable, computed). Hoist
453
+ // `to` and `step` and use the conditional guard so the direction can
454
+ // flip without breaking the loop.
455
+ const toLocal = factory.createIdentifier(`__for_${idName}_to`);
456
+ const stepLocal = factory.createIdentifier(`__for_${idName}_step`);
457
+ const stepInit = stepExpr ?? factory.createNumericLiteral(1);
458
+ const initializer = factory.createVariableDeclarationList([
459
+ factory.createVariableDeclaration(id, undefined, undefined, fromExpr),
460
+ factory.createVariableDeclaration(toLocal, undefined, undefined, toExpr),
461
+ factory.createVariableDeclaration(stepLocal, undefined, undefined, stepInit),
462
+ ], ts.NodeFlags.Let);
463
+ const condition = factory.createConditionalExpression(factory.createBinaryExpression(stepLocal, ts.SyntaxKind.GreaterThanToken, factory.createNumericLiteral(0)), factory.createToken(ts.SyntaxKind.QuestionToken), factory.createBinaryExpression(id, ts.SyntaxKind.LessThanEqualsToken, toLocal), factory.createToken(ts.SyntaxKind.ColonToken), factory.createBinaryExpression(id, ts.SyntaxKind.GreaterThanEqualsToken, toLocal));
464
+ const incrementor = factory.createBinaryExpression(id, ts.SyntaxKind.PlusEqualsToken, stepLocal);
465
+ return factory.createForStatement(initializer, condition, incrementor, body);
466
+ }
467
+ /** Pull a literal numeric value out of a TS expression, or `null` if it
468
+ * isn't a recognizable number-form. Handles `<n>`, `-<n>`, and `+<n>`. */
469
+ function literalNumber(expr) {
470
+ if (!expr)
471
+ return null;
472
+ if (ts.isNumericLiteral(expr))
473
+ return Number(expr.text);
474
+ if (ts.isPrefixUnaryExpression(expr)) {
475
+ if (expr.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(expr.operand)) {
476
+ return -Number(expr.operand.text);
477
+ }
478
+ if (expr.operator === ts.SyntaxKind.PlusToken && ts.isNumericLiteral(expr.operand)) {
479
+ return Number(expr.operand.text);
480
+ }
481
+ }
482
+ return null;
483
+ }
484
+ function compileForIn(stat, ctx) {
485
+ // Lua: `for k, v in pairs(t) do … end`. Lua's iteration triple is
486
+ // (iter_fn, state, init_value)
487
+ // The full iterator protocol desugars to a while loop calling iter_fn
488
+ // (see slow path below). For the two overwhelmingly common cases — a
489
+ // single-call RHS of `ipairs(arr)` or `pairs(t)` — we emit the much
490
+ // shorter TS-native equivalents instead.
491
+ if (stat.values.length === 1 && stat.values[0].type === 'Call') {
492
+ const call = stat.values[0];
493
+ const callee = call.func;
494
+ if (callee.type === 'Global' && (callee.name === 'ipairs' || callee.name === 'pairs') && call.args.length === 1) {
495
+ const fast = compileForInFastPath(stat, callee.name, call.args[0], ctx);
496
+ if (fast)
497
+ return fast;
498
+ }
499
+ }
500
+ const iterTriple = factory.createIdentifier(ctx.freshIdentifier('__iter'));
501
+ const stateName = factory.createIdentifier(ctx.freshIdentifier('__state'));
502
+ const ctrlName = factory.createIdentifier(ctx.freshIdentifier('__ctrl'));
503
+ const rawValuesExpr = stat.values.length === 1
504
+ ? compileExpr(stat.values[0], ctx)
505
+ : factory.createArrayLiteralExpression(stat.values.map((v) => compileExpr(v, ctx)));
506
+ // Wrap in `genericIter(expr)` so generic-for works regardless of
507
+ // whether the RHS is an iterator triple, a callable, a metatable
508
+ // with `__iter`, or a plain table/array (Luau's no-pairs shorthand).
509
+ // The raw destructure used to break silently on arrays — Roblox code
510
+ // like `for _, x in CollectionService:GetTagged("Tag") do` would
511
+ // iterate zero times, leaving derived state (tycoon button labels,
512
+ // tag-bound systems) at their defaults.
513
+ const valuesExpr = factory.createCallExpression(factory.createIdentifier(ctx.use('genericIter')), undefined, [rawValuesExpr]);
514
+ const initStmt = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
515
+ factory.createVariableDeclaration(factory.createArrayBindingPattern([
516
+ factory.createBindingElement(undefined, undefined, iterTriple),
517
+ factory.createBindingElement(undefined, undefined, stateName),
518
+ factory.createBindingElement(undefined, undefined, ctrlName),
519
+ ]), undefined, undefined, valuesExpr),
520
+ ], ts.NodeFlags.Let));
521
+ // while (true) { const __step = __iter(__state, __ctrl); if (!__step) break;
522
+ // __ctrl = __step[0]; const [k, v, ...] = __step; body; }
523
+ const stepName = factory.createIdentifier(ctx.freshIdentifier('__step'));
524
+ const stepDecl = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
525
+ factory.createVariableDeclaration(stepName, undefined, undefined,
526
+ // Tolerate `for ... in expr` where `expr` evaluates to a single
527
+ // non-iterator value (nil, a plain table, or a closure). Lua would
528
+ // error anyway, but our scripts come from a binary place file we
529
+ // don't fully model, so bail the loop cleanly instead of throwing.
530
+ factory.createConditionalExpression(factory.createBinaryExpression(factory.createTypeOfExpression(iterTriple), factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), factory.createStringLiteral('function')), factory.createToken(ts.SyntaxKind.QuestionToken), factory.createCallExpression(iterTriple, undefined, [stateName, ctrlName]), factory.createToken(ts.SyntaxKind.ColonToken), factory.createIdentifier('undefined'))),
531
+ ], ts.NodeFlags.Const));
532
+ const breakIfDone = factory.createIfStatement(factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, factory.createBinaryExpression(stepName, ts.SyntaxKind.AmpersandAmpersandToken, factory.createBinaryExpression(factory.createElementAccessExpression(stepName, factory.createNumericLiteral(0)), ts.SyntaxKind.ExclamationEqualsEqualsToken, factory.createIdentifier('undefined')))), factory.createBreakStatement());
533
+ const updateCtrl = factory.createExpressionStatement(factory.createAssignment(ctrlName, factory.createElementAccessExpression(stepName, factory.createNumericLiteral(0))));
534
+ // Dedup destructure names (Luau allows multiple `_` placeholders; JS
535
+ // forbids duplicates in one destructure) and emit `let` so the loop body
536
+ // can reassign the iteration vars (Black Magic's `for c in gmatch do
537
+ // c = c:gsub(...)` pattern).
538
+ const seenForIn = new Set();
539
+ const forInNames = stat.vars.map((v) => {
540
+ let name = safeIdentifier(v.name);
541
+ if (seenForIn.has(name))
542
+ name = ctx.freshIdentifier(`${name}_skip`);
543
+ seenForIn.add(name);
544
+ return name;
545
+ });
546
+ const destructure = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
547
+ factory.createVariableDeclaration(factory.createArrayBindingPattern(forInNames.map((name) => factory.createBindingElement(undefined, undefined, factory.createIdentifier(name)))), undefined, undefined, stepName),
548
+ ], ts.NodeFlags.Let));
549
+ const bodyStatements = ctx.withScope(() => {
550
+ for (const v of stat.vars)
551
+ ctx.defineLocal(v.name, 'unknown');
552
+ return compileBlockBody(stat.body, ctx);
553
+ });
554
+ const loop = factory.createWhileStatement(factory.createTrue(), factory.createBlock([stepDecl, breakIfDone, updateCtrl, destructure, ...bodyStatements], true));
555
+ // Slow path needs the iterator-triple binding to live alongside the
556
+ // while loop in the same scope; a leading block-scoped `let` plus the
557
+ // while is the right shape, returned as two sibling statements so the
558
+ // emitted file doesn't get a redundant `{ … }` wrapper.
559
+ return [initStmt, loop];
560
+ }
561
+ /** Fast path for `for k, v in ipairs(arr) do … end` and `for k, v in
562
+ * pairs(t) do … end` — the two cases that account for ~all real-world
563
+ * ForIn loops. Returns a TS for-of/for-in statement; null on mismatch
564
+ * so the caller falls back to the iterator-protocol expansion. */
565
+ function compileForInFastPath(stat, kind, iterableLuauExpr, ctx) {
566
+ const iterable = compileExpr(iterableLuauExpr, ctx);
567
+ const bodyStatements = ctx.withScope(() => {
568
+ for (const v of stat.vars)
569
+ ctx.defineLocal(v.name, 'unknown');
570
+ return compileBlockBody(stat.body, ctx);
571
+ });
572
+ const block = factory.createBlock(bodyStatements, true);
573
+ if (kind === 'ipairs') {
574
+ // Do not emit JS `for-of` here. Runtime arrays use Symbol.iterator to
575
+ // expose Luau's iterator triple for generic `for`, so JS `for-of` would
576
+ // visit `[iterFn, state, ctrl]` instead of array values.
577
+ if (stat.vars.length < 1 || stat.vars.length > 2)
578
+ return null;
579
+ const arrayName = ts.isIdentifier(iterable)
580
+ ? iterable
581
+ : factory.createIdentifier(ctx.freshIdentifier('__ipairs'));
582
+ const indexName = factory.createIdentifier(ctx.freshIdentifier('__i'));
583
+ const loopBody = [];
584
+ const pushConst = (name, initializer) => {
585
+ if (name === '_')
586
+ return;
587
+ // `let`, not `const`: Luau permits reassigning the loop variable in
588
+ // the body (e.g. Knit's Promise.all does `value = value:await()`).
589
+ loopBody.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
590
+ factory.createVariableDeclaration(factory.createIdentifier(safeIdentifier(name)), undefined, undefined, initializer),
591
+ ], ts.NodeFlags.Let)));
592
+ };
593
+ if (stat.vars.length === 1) {
594
+ // `for i in ipairs(arr)` receives the 1-based index.
595
+ pushConst(stat.vars[0].name, factory.createBinaryExpression(indexName, ts.SyntaxKind.PlusToken, factory.createNumericLiteral(1)));
596
+ }
597
+ else {
598
+ const idxVar = stat.vars[0];
599
+ const valVar = stat.vars[1];
600
+ pushConst(idxVar.name, factory.createBinaryExpression(indexName, ts.SyntaxKind.PlusToken, factory.createNumericLiteral(1)));
601
+ pushConst(valVar.name, factory.createElementAccessExpression(arrayName, indexName));
602
+ }
603
+ loopBody.push(...bodyStatements);
604
+ const loop = factory.createForStatement(factory.createVariableDeclarationList([
605
+ factory.createVariableDeclaration(indexName, undefined, undefined, factory.createNumericLiteral(0)),
606
+ ], ts.NodeFlags.Let), factory.createBinaryExpression(indexName, ts.SyntaxKind.LessThanToken, factory.createPropertyAccessExpression(arrayName, factory.createIdentifier('length'))), factory.createPostfixIncrement(indexName), factory.createBlock(loopBody, true));
607
+ if (ts.isIdentifier(iterable))
608
+ return [loop];
609
+ const hoist = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(arrayName, undefined, undefined, iterable)], ts.NodeFlags.Const));
610
+ return [hoist, loop];
611
+ // `for _, v in ipairs(arr) do …` → `for (const v of arr) { … }`
612
+ // `for i, v in ipairs(arr) do …` → `for (const [i_zero, v] of arr.entries()) { const i = i_zero + 1; … }`
613
+ if (stat.vars.length === 1) {
614
+ const valVar = stat.vars[0];
615
+ return [factory.createForOfStatement(undefined, factory.createVariableDeclarationList([
616
+ factory.createVariableDeclaration(factory.createIdentifier(safeIdentifier(valVar.name))),
617
+ ], ts.NodeFlags.Const), iterable, block)];
618
+ }
619
+ if (stat.vars.length === 2) {
620
+ const idxVar = stat.vars[0];
621
+ const valVar = stat.vars[1];
622
+ // If the index var is `_`, omit it entirely — `for (const v of arr)`.
623
+ if (idxVar.name === '_') {
624
+ return [factory.createForOfStatement(undefined, factory.createVariableDeclarationList([
625
+ factory.createVariableDeclaration(factory.createIdentifier(safeIdentifier(valVar.name))),
626
+ ], ts.NodeFlags.Const), iterable, block)];
627
+ }
628
+ // Otherwise destructure with .entries() and rebase to 1-indexed via
629
+ // a single prelude `const i = __i + 1` so the user's `i` variable
630
+ // matches Lua semantics.
631
+ const zeroIdx = factory.createIdentifier(`__${safeIdentifier(idxVar.name)}_zero`);
632
+ const userIdx = factory.createIdentifier(safeIdentifier(idxVar.name));
633
+ const prelude = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
634
+ factory.createVariableDeclaration(userIdx, undefined, undefined, factory.createBinaryExpression(zeroIdx, ts.SyntaxKind.PlusToken, factory.createNumericLiteral(1))),
635
+ ], ts.NodeFlags.Const));
636
+ return [factory.createForOfStatement(undefined, factory.createVariableDeclarationList([
637
+ factory.createVariableDeclaration(factory.createArrayBindingPattern([
638
+ factory.createBindingElement(undefined, undefined, zeroIdx),
639
+ factory.createBindingElement(undefined, undefined, factory.createIdentifier(safeIdentifier(valVar.name))),
640
+ ]), undefined, undefined, undefined),
641
+ ], ts.NodeFlags.Const), factory.createCallExpression(factory.createPropertyAccessExpression(iterable, factory.createIdentifier('entries')), undefined, []), factory.createBlock([prelude, ...bodyStatements], true))];
642
+ }
643
+ return null;
644
+ }
645
+ // pairs — iterates every (key, value) pair on a record/object.
646
+ // `for k, v in pairs(t) do …` → `for (const k of pairKeys(t)) { const v = pairValue(t, k); … }`
647
+ // `for k in pairs(t) do …` → `for (const k of pairKeys(t)) { … }`
648
+ // Uses the runtime helper instead of `Object.keys` so Instance-keyed
649
+ // tables (e.g. `offsets[part] = x`) yield the actual Instance back as
650
+ // the key rather than its `.toString()` string artifact. The runtime
651
+ // installs a key reifier; without it pairKeys behaves identically to
652
+ // Object.keys (with proper numeric-index handling).
653
+ const pairKeysFn = ctx.use('pairKeys');
654
+ const pairValueFn = ctx.use('pairValue');
655
+ if (stat.vars.length === 1) {
656
+ const keyVar = stat.vars[0];
657
+ return [factory.createForOfStatement(undefined, factory.createVariableDeclarationList([
658
+ factory.createVariableDeclaration(factory.createIdentifier(safeIdentifier(keyVar.name))),
659
+ ], ts.NodeFlags.Let), factory.createCallExpression(factory.createIdentifier(pairKeysFn), undefined, [iterable]), block)];
660
+ }
661
+ if (stat.vars.length === 2) {
662
+ const keyVar = stat.vars[0];
663
+ const valVar = stat.vars[1];
664
+ // Hoist the table to a local so pairKeys(t) and pairValue(t, k)
665
+ // reference the same expression. Use a `__t` synthetic name; if
666
+ // `iterable` is already a plain identifier we can skip the hoist.
667
+ const tableName = ts.isIdentifier(iterable)
668
+ ? iterable
669
+ : factory.createIdentifier(ctx.freshIdentifier('__t'));
670
+ const valDecl = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
671
+ factory.createVariableDeclaration(factory.createIdentifier(safeIdentifier(valVar.name)), undefined, undefined, factory.createCallExpression(factory.createIdentifier(pairValueFn), undefined, [tableName, factory.createIdentifier(safeIdentifier(keyVar.name))])),
672
+ ], ts.NodeFlags.Let));
673
+ const loop = factory.createForOfStatement(undefined, factory.createVariableDeclarationList([
674
+ factory.createVariableDeclaration(factory.createIdentifier(safeIdentifier(keyVar.name))),
675
+ ], ts.NodeFlags.Let), factory.createCallExpression(factory.createIdentifier(pairKeysFn), undefined, [tableName]), factory.createBlock([valDecl, ...bodyStatements], true));
676
+ if (ts.isIdentifier(iterable))
677
+ return [loop];
678
+ // Hoist `__t = <iterable>` as a sibling const decl rather than
679
+ // wrapping the loop in a TS block — the visual noise of the extra
680
+ // braces isn't worth the local-scoping gain since `__t` only refs
681
+ // here and inside the loop body anyway.
682
+ const hoist = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(tableName, undefined, undefined, iterable)], ts.NodeFlags.Const));
683
+ return [hoist, loop];
684
+ }
685
+ return null;
686
+ }
687
+ function compileAssign(stat, ctx) {
688
+ // ExprError on either side means the parser couldn't recover. Skip the
689
+ // whole statement — emitting it as JS would produce "invalid assignment
690
+ // target" since the placeholder IIFE isn't a valid lvalue.
691
+ const containsErr = (node) => {
692
+ if (!node || typeof node !== 'object')
693
+ return false;
694
+ const t = node.type;
695
+ if (t === 'Error' || t === 'ExprError' || t === 'UnknownExpr')
696
+ return true;
697
+ for (const v of Object.values(node)) {
698
+ if (Array.isArray(v)) {
699
+ if (v.some(containsErr))
700
+ return true;
701
+ }
702
+ else if (v && typeof v === 'object') {
703
+ if (containsErr(v))
704
+ return true;
705
+ }
706
+ }
707
+ return false;
708
+ };
709
+ if (stat.vars.some(containsErr) || stat.values.some(containsErr)) {
710
+ return [];
711
+ }
712
+ // Single RHS call with multiple LHS → destructuring assignment.
713
+ if (stat.vars.length > 1 && stat.values.length === 1 && stat.values[0]?.type === 'Call') {
714
+ const targets = stat.vars.map((v) => compileExpr(v, ctx));
715
+ const valueExpr = factory.createCallExpression(factory.createIdentifier(ctx.use('multiret')), undefined, [compileExpr(stat.values[0], ctx)]);
716
+ for (const target of stat.vars) {
717
+ if (target.type === 'Local')
718
+ ctx.assignLocal(target.name, 'unknown');
719
+ }
720
+ return [
721
+ factory.createExpressionStatement(factory.createAssignment(factory.createArrayLiteralExpression(targets), valueExpr)),
722
+ ];
723
+ }
724
+ const stmts = [];
725
+ for (let i = 0; i < stat.vars.length; i += 1) {
726
+ const target = stat.vars[i];
727
+ const value = stat.values[i];
728
+ if (!value)
729
+ continue;
730
+ const targetExpr = compileExpr(target, ctx);
731
+ const valueExpr = compileExpr(value, ctx);
732
+ if (target.type === 'Local')
733
+ ctx.assignLocal(target.name, staticTypeOfExpr(value, ctx));
734
+ stmts.push(factory.createExpressionStatement(factory.createAssignment(targetExpr, valueExpr)));
735
+ // `_G["X"] = expr` in Lua sets the global `X`. In JS our `_G` is a
736
+ // plain object, so the assignment only updates _G.X — a later bare
737
+ // `X(...)` reads the implicit-global predecl (undefined) and crashes.
738
+ // Mirror to the matching local binding when the key is a valid identifier.
739
+ // Use `_G["X"]` as the RHS so any side effects in `expr` only run once.
740
+ const tgt = target;
741
+ if (tgt.type === 'IndexExpr'
742
+ && tgt.expr?.type === 'Global'
743
+ && tgt.expr.name === '_G'
744
+ && tgt.index?.type === 'ConstantString'
745
+ && typeof tgt.index.value === 'string'
746
+ && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(tgt.index.value)) {
747
+ stmts.push(factory.createExpressionStatement(factory.createAssignment(factory.createIdentifier(safeIdentifier(tgt.index.value)), factory.createElementAccessExpression(factory.createIdentifier('_G'), factory.createStringLiteral(tgt.index.value)))));
748
+ }
749
+ }
750
+ return stmts;
751
+ }
752
+ function compileCompoundAssign(stat, ctx) {
753
+ const target = compileExpr(stat.var, ctx);
754
+ const value = compileExpr(stat.value, ctx);
755
+ if (stat.var.type === 'Local') {
756
+ const numericOps = new Set(['+', '-', '*', '/', '%', '^', '//']);
757
+ const nextType = numericOps.has(stat.op)
758
+ && staticTypeOfExpr(stat.var, ctx) === 'number'
759
+ && staticTypeOfExpr(stat.value, ctx) === 'number'
760
+ ? 'number'
761
+ : 'unknown';
762
+ ctx.assignLocal(stat.var.name, nextType);
763
+ }
764
+ const op = compoundAssignToken(stat.op);
765
+ if (op !== undefined) {
766
+ return factory.createExpressionStatement(factory.createBinaryExpression(target, op, value));
767
+ }
768
+ if (stat.op === '..') {
769
+ const concat = ctx.use('luaConcat');
770
+ return factory.createExpressionStatement(factory.createAssignment(target, factory.createCallExpression(factory.createIdentifier(concat), undefined, [target, value])));
771
+ }
772
+ if (stat.op === '//') {
773
+ const idiv = ctx.use('luaIdiv');
774
+ return factory.createExpressionStatement(factory.createAssignment(target, factory.createCallExpression(factory.createIdentifier(idiv), undefined, [target, value])));
775
+ }
776
+ return factory.createExpressionStatement(factory.createAssignment(target, compileBinary(stat.op, target, value, ctx)));
777
+ }
778
+ function compoundAssignToken(op) {
779
+ switch (op) {
780
+ case '+':
781
+ return ts.SyntaxKind.PlusEqualsToken;
782
+ case '-':
783
+ return ts.SyntaxKind.MinusEqualsToken;
784
+ case '*':
785
+ return ts.SyntaxKind.AsteriskEqualsToken;
786
+ case '/':
787
+ return ts.SyntaxKind.SlashEqualsToken;
788
+ case '%':
789
+ return ts.SyntaxKind.PercentEqualsToken;
790
+ case '^':
791
+ return ts.SyntaxKind.AsteriskAsteriskEqualsToken;
792
+ default:
793
+ return undefined;
794
+ }
795
+ }
796
+ function compileTypeAlias(stat) {
797
+ // `type X<T> = ...` → `type X<T> = ...;` with same generics.
798
+ const typeParams = stat.generics.map((g) => factory.createTypeParameterDeclaration(undefined, factory.createIdentifier(g.name), undefined, g.defaultValue ? compileType(g.defaultValue) : undefined));
799
+ return factory.createTypeAliasDeclaration(stat.exported ? [factory.createToken(ts.SyntaxKind.ExportKeyword)] : undefined, factory.createIdentifier(stat.name), typeParams.length > 0 ? typeParams : undefined, compileType(stat.aliasType));
800
+ }
801
+ function compileDeclareGlobal(stat) {
802
+ return factory.createVariableStatement([factory.createToken(ts.SyntaxKind.DeclareKeyword)], factory.createVariableDeclarationList([
803
+ factory.createVariableDeclaration(factory.createIdentifier(safeIdentifier(stat.name)), undefined, compileType(stat.declType), undefined),
804
+ ], ts.NodeFlags.Const));
805
+ }
806
+ function compileDeclareFunction(stat) {
807
+ const params = stat.params.types.map((t, i) => {
808
+ const paramName = stat.paramNames[i]?.name ?? `arg${i}`;
809
+ return factory.createParameterDeclaration(undefined, undefined, factory.createIdentifier(safeIdentifier(paramName)), undefined, compileType(t));
810
+ });
811
+ if (stat.params.tailType || stat.vararg) {
812
+ const tail = stat.params.tailType;
813
+ const restType = tail?.type === 'TypePackVariadic'
814
+ ? factory.createArrayTypeNode(compileType(tail.variadicType))
815
+ : factory.createArrayTypeNode(factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
816
+ params.push(factory.createParameterDeclaration(undefined, factory.createToken(ts.SyntaxKind.DotDotDotToken), factory.createIdentifier('rest'), undefined, restType));
817
+ }
818
+ return factory.createFunctionDeclaration([factory.createToken(ts.SyntaxKind.DeclareKeyword)], undefined, factory.createIdentifier(safeIdentifier(stat.name)), undefined, params, compileTypePack(stat.retTypes), undefined);
819
+ }
820
+ function compileFunctionShape(fn, ctx) {
821
+ const params = [];
822
+ // Treat any function whose first explicit argument is literally named
823
+ // `self` as a method, even if defined via dot syntax. Roblox places
824
+ // ship plenty of dot-defined methods like
825
+ // `Invisicam.SetMode = function(self, newMode) end` paired with
826
+ // colon calls (`Invisicam:SetMode(x)`); without this, the function
827
+ // signature would consume the call's first real argument into `self`
828
+ // and shift everything else down by one. `self` is reserved by
829
+ // convention in Lua, so the false-positive risk is negligible.
830
+ const implicitSelf = fn.self === null
831
+ && fn.args.length > 0
832
+ && fn.args[0].name === 'self';
833
+ const hasSelf = fn.self !== null || implicitSelf;
834
+ if (hasSelf) {
835
+ params.push(factory.createParameterDeclaration(undefined, undefined, factory.createIdentifier('this'), undefined, factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)));
836
+ }
837
+ const realArgs = implicitSelf ? fn.args.slice(1) : fn.args;
838
+ for (const p of paramsFromLocals(realArgs)) {
839
+ params.push(p);
840
+ }
841
+ if (fn.vararg) {
842
+ params.push(factory.createParameterDeclaration(undefined, factory.createToken(ts.SyntaxKind.DotDotDotToken), factory.createIdentifier('__varargs'), undefined, factory.createArrayTypeNode(factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword))));
843
+ }
844
+ let returnType = fn.returnAnnotation ? compileTypePack(fn.returnAnnotation) : undefined;
845
+ // In rbxts mode, scan the function body for any multi-value `return`
846
+ // statement. If we find one, wrap the return annotation as
847
+ // `LuaTuple<[t1, t2, ...]>` so roblox-ts's macro recognizer kicks in
848
+ // and emits native Lua multi-return instead of a wrapped table.
849
+ // (Component types we don't statically know fall back to `unknown`.)
850
+ if (ctx.compatMode === 'rbxts') {
851
+ const tupleArity = maxMultiReturnArity(fn.body);
852
+ if (tupleArity !== null) {
853
+ ctx.useImport('@rbxts/types', 'LuaTuple');
854
+ const componentTypes = Array.from({ length: tupleArity }, () => factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
855
+ returnType = factory.createTypeReferenceNode('LuaTuple', [
856
+ factory.createTupleTypeNode(componentTypes),
857
+ ]);
858
+ }
859
+ }
860
+ const innerStatements = ctx.withScope(() => {
861
+ for (const arg of realArgs)
862
+ ctx.defineLocal(arg.name, typeFromAnnotation(arg.annotation));
863
+ if (fn.vararg)
864
+ ctx.defineLocal('__varargs', 'unknown');
865
+ if (hasSelf)
866
+ ctx.defineLocal('self', 'unknown');
867
+ const bodyStatements = compileBlockBody(fn.body, ctx);
868
+ if (hasSelf) {
869
+ bodyStatements.unshift(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
870
+ factory.createVariableDeclaration(factory.createIdentifier('self'), undefined, undefined, factory.createIdentifier('this')),
871
+ ], ts.NodeFlags.Const)));
872
+ }
873
+ return bodyStatements;
874
+ });
875
+ return {
876
+ params,
877
+ returnType,
878
+ body: factory.createBlock(innerStatements, true),
879
+ };
880
+ }
881
+ function paramsFromLocals(locals) {
882
+ const seen = new Set();
883
+ const out = [];
884
+ locals.forEach((local, i) => {
885
+ const base = safeIdentifier(local.name);
886
+ let name = base;
887
+ if (seen.has(name))
888
+ name = `_dup_${i}`;
889
+ seen.add(name);
890
+ out.push(factory.createParameterDeclaration(undefined, undefined, factory.createIdentifier(name), undefined, local.annotation ? compileType(local.annotation) : undefined));
891
+ });
892
+ return out;
893
+ }
894
+ function compileFunctionExpr(fn, ctx) {
895
+ const { params, returnType, body } = compileFunctionShape(fn, ctx);
896
+ return factory.createFunctionExpression(asyncModIfNeeded(body), undefined, undefined, undefined, params, returnType, body);
897
+ }
898
+ // ═══════════════════════════════════════════════════════════════════════════
899
+ // Expressions
900
+ // ═══════════════════════════════════════════════════════════════════════════
901
+ function typeFromAnnotation(annotation, fallbackExpr, ctx) {
902
+ if (!annotation)
903
+ return fallbackExpr && ctx ? staticTypeOfExpr(fallbackExpr, ctx) : 'unknown';
904
+ switch (annotation.type) {
905
+ case 'TypeReference':
906
+ if (annotation.prefix === null) {
907
+ if (annotation.name === 'number')
908
+ return 'number';
909
+ if (annotation.name === 'boolean')
910
+ return 'boolean';
911
+ if (annotation.name === 'string')
912
+ return 'string';
913
+ if (ARITH_DATATYPES.has(annotation.name)) {
914
+ return `datatype:${annotation.name}`;
915
+ }
916
+ }
917
+ return 'unknown';
918
+ case 'TypeSingletonBool':
919
+ return 'boolean';
920
+ case 'TypeSingletonString':
921
+ return 'string';
922
+ case 'TypeGroup':
923
+ return typeFromAnnotation(annotation.groupType, fallbackExpr, ctx);
924
+ default:
925
+ return 'unknown';
926
+ }
927
+ }
928
+ function staticTypeOfExpr(expr, ctx) {
929
+ switch (expr.type) {
930
+ case 'ConstantInteger':
931
+ case 'ConstantNumber':
932
+ return 'number';
933
+ case 'Call': {
934
+ // Constructor calls — `Vector3.new(…)`, `CFrame.new(…)`, etc.
935
+ // narrow the result to the datatype so subsequent arithmetic can
936
+ // fast-path `a + b` to `a.add(b)`.
937
+ const f = expr.func;
938
+ if (f.type === 'IndexName' && f.expr.type === 'Global' && ARITH_DATATYPES.has(f.expr.name)) {
939
+ return `datatype:${f.expr.name}`;
940
+ }
941
+ return 'unknown';
942
+ }
943
+ case 'ConstantBool':
944
+ return 'boolean';
945
+ case 'ConstantString':
946
+ return 'string';
947
+ case 'ConstantNil':
948
+ return 'nil';
949
+ case 'Local':
950
+ return ctx.lookupLocal(expr.name);
951
+ case 'Group':
952
+ return staticTypeOfExpr(expr.expr, ctx);
953
+ case 'TypeAssertion':
954
+ return typeFromAnnotation(expr.annotation, expr.expr, ctx);
955
+ case 'Unary':
956
+ if (expr.op === 'not')
957
+ return 'boolean';
958
+ if (expr.op === '#' || expr.op === '-')
959
+ return 'number';
960
+ return 'unknown';
961
+ case 'Binary':
962
+ if (['+', '-', '*', '/', '%', '^', '//'].includes(expr.op)) {
963
+ return staticTypeOfExpr(expr.left, ctx) === 'number'
964
+ && staticTypeOfExpr(expr.right, ctx) === 'number'
965
+ ? 'number'
966
+ : 'unknown';
967
+ }
968
+ if (['==', '~=', '<', '<=', '>', '>='].includes(expr.op))
969
+ return 'boolean';
970
+ // Lua `..` produces a string when either side is statically string-
971
+ // or-number — the runtime coerces both sides to strings.
972
+ if (expr.op === '..') {
973
+ const lt = staticTypeOfExpr(expr.left, ctx);
974
+ const rt = staticTypeOfExpr(expr.right, ctx);
975
+ if (lt === 'string' || rt === 'string')
976
+ return 'string';
977
+ }
978
+ return 'unknown';
979
+ case 'IfElse': {
980
+ const trueType = staticTypeOfExpr(expr.trueExpr, ctx);
981
+ const falseType = staticTypeOfExpr(expr.falseExpr, ctx);
982
+ return trueType === falseType ? trueType : 'unknown';
983
+ }
984
+ default:
985
+ return 'unknown';
986
+ }
987
+ }
988
+ function isPrimitiveStaticType(type) {
989
+ return type === 'number' || type === 'boolean' || type === 'string' || type === 'nil';
990
+ }
991
+ function compileExpr(expr, ctx) {
992
+ switch (expr.type) {
993
+ case 'ConstantNil':
994
+ return factory.createIdentifier('undefined');
995
+ case 'ConstantBool':
996
+ return expr.value ? factory.createTrue() : factory.createFalse();
997
+ case 'ConstantInteger':
998
+ case 'ConstantNumber':
999
+ if (Number.isFinite(expr.value)) {
1000
+ if (expr.value < 0) {
1001
+ return factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(expr.value)));
1002
+ }
1003
+ return factory.createNumericLiteral(expr.value);
1004
+ }
1005
+ if (Number.isNaN(expr.value))
1006
+ return factory.createIdentifier('NaN');
1007
+ return factory.createIdentifier(expr.value > 0 ? 'Infinity' : '-Infinity');
1008
+ case 'ConstantString':
1009
+ return factory.createStringLiteral(expr.value);
1010
+ case 'Local':
1011
+ return factory.createIdentifier(ctx.getLocalJsName(expr.name) ?? safeIdentifier(expr.name));
1012
+ case 'Global':
1013
+ return factory.createIdentifier(ctx.getLocalJsName(expr.name) ?? safeIdentifier(expr.name));
1014
+ case 'Varargs':
1015
+ // Single-value default; call-arg / table-list use compileExprAsArg.
1016
+ return factory.createElementAccessExpression(factory.createIdentifier('__varargs'), factory.createNumericLiteral(0));
1017
+ case 'Group':
1018
+ return factory.createParenthesizedExpression(compileExpr(expr.expr, ctx));
1019
+ case 'Binary':
1020
+ return compileBinaryExpr(expr, ctx);
1021
+ case 'Unary':
1022
+ return compileUnary(expr, ctx);
1023
+ case 'Call':
1024
+ return compileCall(expr, ctx);
1025
+ case 'IndexName':
1026
+ // Property names allow reserved words (`obj.new` is fine) — use
1027
+ // propertyName, not safeIdentifier, so `Instance.new("Part")` stays
1028
+ // as-written instead of becoming `Instance.new_("Part")`.
1029
+ return factory.createPropertyAccessExpression(compileExpr(expr.expr, ctx), factory.createIdentifier(propertyName(expr.index)));
1030
+ case 'IndexExpr': {
1031
+ // Lua tables are 1-indexed; JS arrays are 0-indexed. For numeric
1032
+ // literals we statically translate (`arr[1]` → `arr[0]`). For runtime
1033
+ // values we used to emit an inline `typeof i === 'number' ? i-1 : i`
1034
+ // conditional, but that silently broke dictionary lookups keyed by
1035
+ // large numeric values (Roblox developer-product/asset IDs etc.):
1036
+ // `productCash[3582943767]` would compile to a lookup of `3582943766`
1037
+ // and return undefined.
1038
+ //
1039
+ // We now emit a call to `luaIndex(t, k)`, which checks
1040
+ // `Array.isArray(t)` at runtime and only subtracts 1 when t is an
1041
+ // actual JS array. Sequence tables still index correctly; dictionary
1042
+ // tables pass through unchanged.
1043
+ const target = compileExpr(expr.expr, ctx);
1044
+ const indexExpr = expr.index;
1045
+ const index = compileExpr(indexExpr, ctx);
1046
+ if ((indexExpr.type === 'ConstantNumber' || indexExpr.type === 'ConstantInteger')
1047
+ && typeof indexExpr.value === 'number') {
1048
+ const n = indexExpr.value - 1;
1049
+ const lit = n < 0
1050
+ ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(n)))
1051
+ : factory.createNumericLiteral(n);
1052
+ return factory.createElementAccessExpression(target, lit);
1053
+ }
1054
+ if (indexExpr.type === 'ConstantString') {
1055
+ return factory.createElementAccessExpression(target, index);
1056
+ }
1057
+ const luaIndexFn = ctx.use('luaIndex');
1058
+ return factory.createCallExpression(factory.createIdentifier(luaIndexFn), undefined, [target, index]);
1059
+ }
1060
+ case 'Function':
1061
+ return compileFunctionExpr(expr, ctx);
1062
+ case 'Table':
1063
+ return compileTableExpr(expr, ctx);
1064
+ case 'TypeAssertion':
1065
+ return factory.createAsExpression(compileExpr(expr.expr, ctx), compileType(expr.annotation));
1066
+ case 'IfElse':
1067
+ return compileIfElseExpr(expr, ctx);
1068
+ case 'InterpString':
1069
+ return compileInterpString(expr, ctx);
1070
+ case 'Instantiate':
1071
+ // f<<T>> — TS doesn't expose generic instantiation values; drop type args.
1072
+ return compileExpr(expr.expr, ctx);
1073
+ case 'ExprError':
1074
+ case 'UnknownExpr':
1075
+ default:
1076
+ return unsupportedExpr(`luau-to-ts: unsupported expression '${expr.type}'`);
1077
+ }
1078
+ }
1079
+ function compileUnary(expr, ctx) {
1080
+ const inner = compileExpr(expr.expr, ctx);
1081
+ switch (expr.op) {
1082
+ case '-':
1083
+ return factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, inner);
1084
+ case '#': {
1085
+ const innerType = staticTypeOfExpr(expr.expr, ctx);
1086
+ // Type-narrowed length: strings have a native .length property
1087
+ // with byte-equivalent semantics under our parser's UTF-8 strings.
1088
+ // For unknown / table types we still need lualen, which walks the
1089
+ // array part of mixed list+dict tables the way Lua does.
1090
+ if (innerType === 'string') {
1091
+ return factory.createPropertyAccessExpression(inner, 'length');
1092
+ }
1093
+ const fn = ctx.use('lualen');
1094
+ return factory.createCallExpression(factory.createIdentifier(fn), undefined, [inner]);
1095
+ }
1096
+ case 'not': {
1097
+ const innerType = staticTypeOfExpr(expr.expr, ctx);
1098
+ // `not <expr>` where the operand is statically boolean → just `!expr`.
1099
+ // For other repeatable operands, fall back to the inline truthiness
1100
+ // check (same shape as `if (truthy(x))` but negated).
1101
+ if (innerType === 'boolean') {
1102
+ return factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, inner);
1103
+ }
1104
+ if (isRepeatableExpression(inner)) {
1105
+ return factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, factory.createParenthesizedExpression(truthify(inner, ctx, innerType)));
1106
+ }
1107
+ const fn = ctx.use('luaNot');
1108
+ return factory.createCallExpression(factory.createIdentifier(fn), undefined, [inner]);
1109
+ }
1110
+ }
1111
+ }
1112
+ function compileBinaryExpr(expr, ctx) {
1113
+ const leftType = staticTypeOfExpr(expr.left, ctx);
1114
+ const rightType = staticTypeOfExpr(expr.right, ctx);
1115
+ const left = compileExpr(expr.left, ctx);
1116
+ const right = compileExpr(expr.right, ctx);
1117
+ if (expr.op === 'and' || expr.op === 'or') {
1118
+ return compileLogicalBinary(expr.op, left, right, ctx, leftType);
1119
+ }
1120
+ if (leftType === 'number' && rightType === 'number') {
1121
+ const directNumeric = {
1122
+ '+': ts.SyntaxKind.PlusToken,
1123
+ '-': ts.SyntaxKind.MinusToken,
1124
+ '*': ts.SyntaxKind.AsteriskToken,
1125
+ '/': ts.SyntaxKind.SlashToken,
1126
+ '^': ts.SyntaxKind.AsteriskAsteriskToken,
1127
+ };
1128
+ const op = directNumeric[expr.op];
1129
+ if (op !== undefined)
1130
+ return factory.createBinaryExpression(left, op, right);
1131
+ if (expr.op === '//') {
1132
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier('Math'), 'floor'), undefined, [factory.createBinaryExpression(left, ts.SyntaxKind.SlashToken, right)]);
1133
+ }
1134
+ }
1135
+ // Datatype-arithmetic fast path. When the LEFT operand is statically a
1136
+ // Roblox datatype (Vector3, CFrame, etc.), emit a direct method call
1137
+ // instead of routing through `luaAdd`/`luaSub`/etc. The instance method
1138
+ // handles the typed overload internally (Vector3.mul accepts both
1139
+ // Vector3 and number, etc.), so this is safe for both same-datatype
1140
+ // arithmetic (`v1 + v2`) and scaling by scalar (`v1 * 2`).
1141
+ //
1142
+ // We only fast-path when the LEFT side is the datatype, not the right —
1143
+ // `2 * v1` keeps the helper since you can't call `.mul()` on a JS number.
1144
+ // The helper dispatches to the right operand's __mul anyway.
1145
+ if (typeof leftType === 'string' && leftType.startsWith('datatype:')) {
1146
+ const methodMap = {
1147
+ '+': 'add',
1148
+ '-': 'sub',
1149
+ '*': 'mul',
1150
+ '/': 'div',
1151
+ };
1152
+ const method = methodMap[expr.op];
1153
+ if (method) {
1154
+ return factory.createCallExpression(factory.createPropertyAccessExpression(left, factory.createIdentifier(method)), undefined, [right]);
1155
+ }
1156
+ }
1157
+ if ((expr.op === '==' || expr.op === '~=')
1158
+ && isPrimitiveStaticType(leftType)
1159
+ && isPrimitiveStaticType(rightType)) {
1160
+ return factory.createBinaryExpression(left, expr.op === '=='
1161
+ ? ts.SyntaxKind.EqualsEqualsEqualsToken
1162
+ : ts.SyntaxKind.ExclamationEqualsEqualsToken, right);
1163
+ }
1164
+ // Beautify `..` concat — Lua's string-concat operator. When at least
1165
+ // one side is statically string, JS template literals capture the
1166
+ // semantics directly: nested `${expr}` interpolations call `.toString()`
1167
+ // on non-string operands, matching Lua's `..` behavior.
1168
+ if (expr.op === '..' && (leftType === 'string' || rightType === 'string')) {
1169
+ return buildTemplateLiteral([
1170
+ { value: left, type: leftType },
1171
+ { value: right, type: rightType },
1172
+ ]);
1173
+ }
1174
+ return compileBinary(expr.op, left, right, ctx);
1175
+ }
1176
+ /** Build a TS template literal from a sequence of string-or-other values.
1177
+ * Each consecutive run of string literals is folded into a single template
1178
+ * span; everything else becomes an interpolation. */
1179
+ function buildTemplateLiteral(parts) {
1180
+ // Flatten nested template literals from prior `..` concatenations so
1181
+ // `("a" .. b) .. c` becomes one template instead of nested ones.
1182
+ const flat = [];
1183
+ for (const p of parts) {
1184
+ if (ts.isTemplateExpression(p.value)) {
1185
+ // Decompose: head + spans → [string, expr, string, expr, ..., string].
1186
+ flat.push({ value: factory.createStringLiteral(p.value.head.text), type: 'string' });
1187
+ for (const span of p.value.templateSpans) {
1188
+ flat.push({ value: span.expression, type: 'unknown' });
1189
+ flat.push({ value: factory.createStringLiteral(span.literal.text), type: 'string' });
1190
+ }
1191
+ continue;
1192
+ }
1193
+ if (ts.isNoSubstitutionTemplateLiteral(p.value)) {
1194
+ flat.push({ value: factory.createStringLiteral(p.value.text), type: 'string' });
1195
+ continue;
1196
+ }
1197
+ flat.push(p);
1198
+ }
1199
+ // Walk: emit head string, then alternating expr/string spans.
1200
+ let head = '';
1201
+ let i = 0;
1202
+ while (i < flat.length && flat[i].type === 'string' && ts.isStringLiteral(flat[i].value)) {
1203
+ head += flat[i].value.text;
1204
+ i++;
1205
+ }
1206
+ if (i >= flat.length) {
1207
+ // Pure literal — return `${head}` as a single string (no template needed),
1208
+ // wrapped to keep the return type consistent.
1209
+ return factory.createNoSubstitutionTemplateLiteral(head);
1210
+ }
1211
+ const spans = [];
1212
+ while (i < flat.length) {
1213
+ const exprPart = flat[i];
1214
+ i++;
1215
+ let between = '';
1216
+ while (i < flat.length && flat[i].type === 'string' && ts.isStringLiteral(flat[i].value)) {
1217
+ between += flat[i].value.text;
1218
+ i++;
1219
+ }
1220
+ const literal = i >= flat.length
1221
+ ? factory.createTemplateTail(between)
1222
+ : factory.createTemplateMiddle(between);
1223
+ spans.push(factory.createTemplateSpan(exprPart.value, literal));
1224
+ }
1225
+ return factory.createTemplateExpression(factory.createTemplateHead(head), spans);
1226
+ }
1227
+ function compileBinary(op, left, right, ctx) {
1228
+ if (op === 'and' || op === 'or') {
1229
+ return compileLogicalBinary(op, left, right, ctx);
1230
+ }
1231
+ // Arithmetic / concat / and-or / equality route through runtime helpers
1232
+ // because JS has no operator overloading: `cf * cf2` on Roblox CFrames
1233
+ // must call into our luaMul, which dispatches to .mul()/__mul. The
1234
+ // helpers fast-path numeric operands so `1 + 2` stays cheap.
1235
+ // Comparison ops on numbers/strings stay direct.
1236
+ const helperName = {
1237
+ '+': 'luaAdd',
1238
+ '-': 'luaSub',
1239
+ '*': 'luaMul',
1240
+ '/': 'luaDiv',
1241
+ '%': 'luaMod',
1242
+ '^': 'luaPow',
1243
+ '//': 'luaIdiv',
1244
+ '..': 'luaConcat',
1245
+ '==': 'luaEq',
1246
+ };
1247
+ if (op === '~=') {
1248
+ const fn = ctx.use('luaEq');
1249
+ return factory.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, factory.createCallExpression(factory.createIdentifier(fn), undefined, [left, right]));
1250
+ }
1251
+ if (helperName[op]) {
1252
+ const fn = ctx.use(helperName[op]);
1253
+ return factory.createCallExpression(factory.createIdentifier(fn), undefined, [left, right]);
1254
+ }
1255
+ const direct = {
1256
+ '<': ts.SyntaxKind.LessThanToken,
1257
+ '<=': ts.SyntaxKind.LessThanEqualsToken,
1258
+ '>': ts.SyntaxKind.GreaterThanToken,
1259
+ '>=': ts.SyntaxKind.GreaterThanEqualsToken,
1260
+ };
1261
+ const directOp = direct[op];
1262
+ if (directOp !== undefined)
1263
+ return factory.createBinaryExpression(left, directOp, right);
1264
+ throw new Error(`luau-to-ts: unhandled binary operator '${op}'`);
1265
+ }
1266
+ function compileLogicalBinary(op, left, right, ctx, leftType = 'unknown') {
1267
+ // Lua-faithful `a and b` / `a or b` semantics — these aren't pure
1268
+ // boolean operators; they short-circuit and return the chosen operand
1269
+ // (potentially non-boolean). When left is statically boolean and side-
1270
+ // effect-free, JS's `&&` / `||` already match the semantics exactly,
1271
+ // so we can emit the native operator without the truthify dance.
1272
+ if (leftType === 'boolean' && isRepeatableExpression(left)) {
1273
+ return factory.createBinaryExpression(left, op === 'and' ? ts.SyntaxKind.AmpersandAmpersandToken : ts.SyntaxKind.BarBarToken, right);
1274
+ }
1275
+ if (isRepeatableExpression(left)) {
1276
+ return factory.createConditionalExpression(truthify(left, ctx, leftType), factory.createToken(ts.SyntaxKind.QuestionToken), op === 'and' ? right : left, factory.createToken(ts.SyntaxKind.ColonToken), op === 'and' ? left : right);
1277
+ }
1278
+ // Non-repeatable LHS — bind it once via a single-expression arrow IIFE
1279
+ // so the truthiness test and the chosen-operand both reference the
1280
+ // same evaluation: `(__l => isTruthy(__l) ? right : __l)(left)`.
1281
+ const isTruthyFn = ctx.use('isTruthy');
1282
+ const leftId = factory.createIdentifier('__l');
1283
+ const needsAsync = nodeContainsAwait(right);
1284
+ const choose = factory.createConditionalExpression(factory.createCallExpression(factory.createIdentifier(isTruthyFn), undefined, [leftId]), factory.createToken(ts.SyntaxKind.QuestionToken), op === 'and' ? right : leftId, factory.createToken(ts.SyntaxKind.ColonToken), op === 'and' ? leftId : right);
1285
+ const arrow = factory.createArrowFunction(needsAsync ? ASYNC_MOD : undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, leftId)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), choose);
1286
+ const call = factory.createCallExpression(factory.createParenthesizedExpression(arrow), undefined, [left]);
1287
+ return needsAsync ? factory.createAwaitExpression(call) : call;
1288
+ }
1289
+ // Roblox functions that yield the calling thread until they complete —
1290
+ // the compiled equivalent has to be awaited or the place enters tight
1291
+ // busy loops on `while wait(1) do …`. Known by name so we can wrap the
1292
+ // resulting CallExpression in `await`.
1293
+ // pcall / xpcall might forward through to a yielding function, so
1294
+ // always await — we don't know statically whether the wrapped fn yields.
1295
+ // require() now returns a Promise<export> because all ModuleScript factories
1296
+ // are async; callers must await it to get the resolved module value.
1297
+ // `delay` and `task.delay` / `task.spawn` are fire-and-forget in Lua: they
1298
+ // schedule a function and return immediately. Treating them as yielding made
1299
+ // the caller async, which transitively forced every call site to be awaited
1300
+ // — which then broke async-function-returns-Promise issues (e.g. ProfileService
1301
+ // .GetProfileStore returned `Promise<profile_store>` instead of profile_store
1302
+ // because its body had `await task.spawn(...)` and the call sites weren't
1303
+ // awaited).
1304
+ const YIELDING_FREE_FUNCS = new Set(['wait', 'pcall', 'xpcall', 'require']);
1305
+ const YIELDING_TASK_FUNCS = new Set(['wait']);
1306
+ // WaitForChild is intentionally NOT here — our runtime makes it synchronous
1307
+ // (returns NullProxy on miss) so the compiler emits a plain call. Awaiting it
1308
+ // would force every caller to be async, which transitively poisons Lua-style
1309
+ // "synchronous-looking" call sites that the script doesn't await.
1310
+ // Legacy lowercase aliases (`event:wait()`, `:invokeServer()`) appear in old
1311
+ // Roblox places; treat them as yielding too. The runtime exposes both casings.
1312
+ // LoadAsset / LoadAssetVersion / LoadAssetWithFormat are documented as
1313
+ // yielding even without an `*Async` suffix; same for the legacy lowercase.
1314
+ const YIELDING_METHODS = new Set([
1315
+ 'Wait', 'wait',
1316
+ 'InvokeServer', 'invokeServer', 'InvokeClient', 'invokeClient',
1317
+ 'LoadAsset', 'loadAsset',
1318
+ 'LoadAssetVersion', 'LoadAssetWithFormat',
1319
+ ]);
1320
+ function isYieldingCall(expr, ctx) {
1321
+ // Old admin scripts snapshot Lua globals into locals at startup
1322
+ // (`local wait,pcall,...=wait,pcall,...`). The reference type flips
1323
+ // from Global to Local but the binding still points at the yielding
1324
+ // implementation, so the await wrap must still fire.
1325
+ if ((expr.func.type === 'Global' || expr.func.type === 'Local') && YIELDING_FREE_FUNCS.has(expr.func.name))
1326
+ return true;
1327
+ if (expr.func.type === 'IndexName' && expr.func.expr.type === 'Global') {
1328
+ if (expr.func.expr.name === 'task' && YIELDING_TASK_FUNCS.has(expr.func.index))
1329
+ return true;
1330
+ }
1331
+ // Match both colon-call (`event:wait()`) and dot-call (`event.wait()`) —
1332
+ // the dot form shows up in scripts that already adapted to JS-style.
1333
+ if (expr.func.type === 'IndexName' && YIELDING_METHODS.has(expr.func.index)) {
1334
+ return true;
1335
+ }
1336
+ // User-defined yielding functions discovered by the pre-pass scanner.
1337
+ // Direct calls match Local/Global names; member calls (Server.Init())
1338
+ // match the trailing index name — the scanner catalogs both forms.
1339
+ if (ctx && (expr.func.type === 'Local' || expr.func.type === 'Global')) {
1340
+ if (ctx.yieldingFunctions.has(expr.func.name))
1341
+ return true;
1342
+ }
1343
+ if (ctx && expr.func.type === 'IndexName') {
1344
+ if (ctx.yieldingFunctions.has(expr.func.index))
1345
+ return true;
1346
+ }
1347
+ // *Async* naming convention — substring match catches both `LoadProfileAsync`
1348
+ // and `StandardProfileUpdateAsyncDataStore`. ProfileService and similar Lua
1349
+ // libs use the suffix loosely; missing one breaks Promise chains.
1350
+ const nameOf = expr.func.type === 'IndexName' ? expr.func.index :
1351
+ expr.func.type === 'Global' ? expr.func.name :
1352
+ expr.func.type === 'Local' ? expr.func.name :
1353
+ undefined;
1354
+ if (nameOf && /Async/.test(nameOf))
1355
+ return true;
1356
+ return false;
1357
+ }
1358
+ /** Walk every node in a tree. Calls `visit(node)` for each node-typed object
1359
+ * (anything with a string `.type` property). Used by the yield-inference
1360
+ * pre-pass to find function definitions and call expressions without
1361
+ * enumerating every Luau AST shape by hand. */
1362
+ function walkLuauNodes(node, visit) {
1363
+ if (!node || typeof node !== 'object')
1364
+ return;
1365
+ const obj = node;
1366
+ if (typeof obj.type === 'string')
1367
+ visit(obj);
1368
+ for (const key of Object.keys(obj)) {
1369
+ if (key === 'loc' || key === 'argLocation')
1370
+ continue;
1371
+ const value = obj[key];
1372
+ if (Array.isArray(value)) {
1373
+ for (const item of value)
1374
+ walkLuauNodes(item, visit);
1375
+ }
1376
+ else if (value && typeof value === 'object') {
1377
+ walkLuauNodes(value, visit);
1378
+ }
1379
+ }
1380
+ }
1381
+ /** Pre-pass: populate `ctx.yieldingFunctions` with every named function whose
1382
+ * body transitively yields. Fixed-point iteration so a caller picks up its
1383
+ * callee's status across forward references. */
1384
+ function scanYieldingFunctions(root, ctx) {
1385
+ // 1. Catalog every named function definition reachable from the root.
1386
+ const funcBodies = new Map();
1387
+ walkLuauNodes(root, (n) => {
1388
+ if (n.type === 'LocalFunction') {
1389
+ const s = n;
1390
+ funcBodies.set(s.name.name, s.func.body);
1391
+ }
1392
+ else if (n.type === 'Function' && 'func' in n && 'name' in n) {
1393
+ // FunctionStat and FunctionExpr share `type: 'Function'`. Only the
1394
+ // statement form has `func` and `name` — distinguish by structural shape.
1395
+ const s = n;
1396
+ if (s.name && (s.name.type === 'Global' || s.name.type === 'Local')) {
1397
+ funcBodies.set(s.name.name, s.func.body);
1398
+ }
1399
+ else if (s.name && s.name.type === 'IndexName' && 'index' in s.name) {
1400
+ // `function Obj.Method() … end` — catalog by the trailing member
1401
+ // name. Callers like `Server.InitializeFishingZones()` resolve
1402
+ // through this when the call's IndexName-tail matches.
1403
+ funcBodies.set(s.name.index, s.func.body);
1404
+ }
1405
+ }
1406
+ else if (n.type === 'Local' && 'vars' in n && 'values' in n) {
1407
+ // `local foo = function() ... end` — function-valued local declarations.
1408
+ // Distinguish LocalStat (has `vars`/`values`) from the bare `Local`
1409
+ // variable-name node (used inside LocalStat.vars and function args).
1410
+ const s = n;
1411
+ for (let i = 0; i < s.vars.length; i += 1) {
1412
+ const init = s.values[i];
1413
+ if (init && init.type === 'Function' && 'body' in init) {
1414
+ funcBodies.set(s.vars[i].name, init.body);
1415
+ }
1416
+ }
1417
+ }
1418
+ else if (n.type === 'Assign' && 'vars' in n && 'values' in n) {
1419
+ // `foo = function() ... end` — assignment with function rhs. Catches
1420
+ // old Roblox Animate scripts that declare yielding helpers as plain
1421
+ // global assignments instead of `function foo` or `local function foo`.
1422
+ const s = n;
1423
+ for (let i = 0; i < s.vars.length; i += 1) {
1424
+ const target = s.vars[i];
1425
+ const init = s.values[i];
1426
+ if (!init || init.type !== 'Function' || !('body' in init))
1427
+ continue;
1428
+ const body = init.body;
1429
+ if (!target)
1430
+ continue;
1431
+ if (target.type === 'Global' || target.type === 'Local') {
1432
+ funcBodies.set(target.name, body);
1433
+ }
1434
+ else if (target.type === 'IndexName' && 'index' in target) {
1435
+ // `Server.Init = async function() … end` — catalog by trailing
1436
+ // member name so call sites like `Server.Init()` get awaited.
1437
+ funcBodies.set(target.index, body);
1438
+ }
1439
+ }
1440
+ }
1441
+ });
1442
+ // 2. Iterate to fixed point: a function yields if its body contains any
1443
+ // call already classified as yielding (built-in or previously-marked user
1444
+ // function). Each round may newly mark a function whose callee just got
1445
+ // marked, so keep going until nothing changes.
1446
+ let changed = true;
1447
+ while (changed) {
1448
+ changed = false;
1449
+ for (const [name, body] of funcBodies) {
1450
+ if (ctx.yieldingFunctions.has(name))
1451
+ continue;
1452
+ let bodyYields = false;
1453
+ walkLuauNodes(body, (n) => {
1454
+ if (bodyYields)
1455
+ return;
1456
+ if (n.type === 'Call') {
1457
+ if (isYieldingCall(n, ctx)) {
1458
+ bodyYields = true;
1459
+ }
1460
+ }
1461
+ });
1462
+ if (bodyYields) {
1463
+ ctx.yieldingFunctions.add(name);
1464
+ changed = true;
1465
+ }
1466
+ }
1467
+ }
1468
+ }
1469
+ function compileExprAsArg(a, ctx) {
1470
+ if (a.type === 'Varargs') {
1471
+ return factory.createSpreadElement(factory.createIdentifier('__varargs'));
1472
+ }
1473
+ return compileExpr(a, ctx);
1474
+ }
1475
+ function compileCall(expr, ctx) {
1476
+ const args = expr.args.map((a) => compileExprAsArg(a, ctx));
1477
+ // Macro registry — interception point for compatMode='rbxts' rewrites
1478
+ // (Vector3.new → new Vector3, Instance.new("Part") → new Part(),
1479
+ // game:GetService("X") → imported X singleton, TS.async → async fn, …).
1480
+ // Macros may decline (return undefined) and fall through to default emit.
1481
+ const macroResult = lookupMacro({ call: expr, compiledArgs: args, ctx });
1482
+ if (macroResult !== undefined) {
1483
+ if (isYieldingCall(expr, ctx))
1484
+ return factory.createAwaitExpression(macroResult);
1485
+ return macroResult;
1486
+ }
1487
+ let call;
1488
+ if (expr.self && expr.func.type === 'IndexName') {
1489
+ call = factory.createCallExpression(factory.createPropertyAccessExpression(compileExpr(expr.func.expr, ctx), factory.createIdentifier(propertyName(expr.func.index))), undefined, args);
1490
+ }
1491
+ else {
1492
+ call = factory.createCallExpression(compileExpr(expr.func, ctx), undefined, args);
1493
+ }
1494
+ if (isYieldingCall(expr, ctx)) {
1495
+ return factory.createAwaitExpression(call);
1496
+ }
1497
+ return call;
1498
+ }
1499
+ function compileTableExpr(expr, ctx) {
1500
+ const allList = expr.items.every((i) => i.kind === 'List');
1501
+ const allRecord = expr.items.every((i) => i.kind === 'Record');
1502
+ if (allList) {
1503
+ return factory.createArrayLiteralExpression(expr.items.map((i) => compileExprAsArg(i.value, ctx)), false);
1504
+ }
1505
+ // Explicit `{[1] = a, [2] = b, [3] = c}` (TableItemKind: 'General') that
1506
+ // is dense and 1-indexed is semantically an array in Lua; emit as JS
1507
+ // array so `t[idx - 1]` (our compiler's Lua→JS index conversion) lines
1508
+ // up with `arr[0]` for `t[1]`. Without this, the table is an object
1509
+ // with string-numeric keys "1"/"2"/... and `t["0"]` returns undefined —
1510
+ // which is what blew up Meepcity's FISH_DICTIONARY[FishIndex] lookup
1511
+ // for FishIndex=1.
1512
+ const allGeneralNumeric = expr.items.length > 0 && expr.items.every((i, idx) => {
1513
+ if (i.kind !== 'General' || !i.key)
1514
+ return false;
1515
+ const t = i.key.type;
1516
+ if (t !== 'ConstantNumber' && t !== 'ConstantInteger')
1517
+ return false;
1518
+ return i.key.value === idx + 1;
1519
+ });
1520
+ if (allGeneralNumeric) {
1521
+ return factory.createArrayLiteralExpression(expr.items.map((i) => compileExprAsArg(i.value, ctx)), true);
1522
+ }
1523
+ if (allRecord) {
1524
+ return factory.createObjectLiteralExpression(expr.items.map((i) => compileTableProp(i, ctx)), true);
1525
+ }
1526
+ // Mixed: emit object literal with numeric keys for List items (1-indexed).
1527
+ return factory.createObjectLiteralExpression(expr.items.map((i, idx) => {
1528
+ if (i.kind === 'List') {
1529
+ return factory.createPropertyAssignment(factory.createNumericLiteral(idx + 1), compileExpr(i.value, ctx));
1530
+ }
1531
+ return compileTableProp(i, ctx);
1532
+ }), true);
1533
+ }
1534
+ function compileTableProp(item, ctx) {
1535
+ const value = compileExpr(item.value, ctx);
1536
+ if (item.key === null) {
1537
+ return factory.createPropertyAssignment(factory.createIdentifier('_'), value);
1538
+ }
1539
+ if (item.kind === 'Record' && item.key.type === 'ConstantString') {
1540
+ return factory.createPropertyAssignment(propNameFromString(item.key.value), value);
1541
+ }
1542
+ return factory.createPropertyAssignment(factory.createComputedPropertyName(compileExpr(item.key, ctx)), value);
1543
+ }
1544
+ function propNameFromString(s) {
1545
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s))
1546
+ return factory.createIdentifier(s);
1547
+ return factory.createStringLiteral(s);
1548
+ }
1549
+ function compileIfElseExpr(expr, ctx) {
1550
+ return factory.createConditionalExpression(truthify(compileExpr(expr.condition, ctx), ctx, staticTypeOfExpr(expr.condition, ctx)), factory.createToken(ts.SyntaxKind.QuestionToken), compileExpr(expr.trueExpr, ctx), factory.createToken(ts.SyntaxKind.ColonToken), compileExpr(expr.falseExpr, ctx));
1551
+ }
1552
+ function compileInterpString(expr, ctx) {
1553
+ const head = factory.createTemplateHead(expr.strings[0] ?? '');
1554
+ const spans = [];
1555
+ for (let i = 0; i < expr.expressions.length; i += 1) {
1556
+ const literal = expr.strings[i + 1] ?? '';
1557
+ const isLast = i === expr.expressions.length - 1;
1558
+ spans.push(factory.createTemplateSpan(compileExpr(expr.expressions[i], ctx), isLast ? factory.createTemplateTail(literal) : factory.createTemplateMiddle(literal)));
1559
+ }
1560
+ return factory.createTemplateExpression(head, spans);
1561
+ }
1562
+ // ═══════════════════════════════════════════════════════════════════════════
1563
+ // Public API
1564
+ // ═══════════════════════════════════════════════════════════════════════════
1565
+ import pkgJson from '../../package.json' with { type: 'json' };
1566
+ const COMPILER_VERSION = pkgJson.version;
1567
+ const COMPILER_NAME = pkgJson.name;
1568
+ // One-line header. The "DO NOT EDIT" warning was redundant — every dev
1569
+ // looking at the file already knows what `// Compiled by …` means.
1570
+ const COMPILER_HEADER = `// Compiled by ${COMPILER_NAME} v${COMPILER_VERSION} (do not edit).\n`;
1571
+ export async function compile(source, options = {}) {
1572
+ const sourceFileName = options.sourceFile ?? 'input.luau';
1573
+ const parsed = await parse(source);
1574
+ const ctx = new CompileContext(options.compatMode ?? 'native');
1575
+ // Route the top-level body through compileBlockBody so the class-shape
1576
+ // detector (R.9) sees it. Synthesize a Block-shaped wrapper since the
1577
+ // root body is a flat statement array.
1578
+ const rootBlock = parsed.root
1579
+ ? {
1580
+ type: 'Block',
1581
+ body: parsed.root.body,
1582
+ hasEnd: true,
1583
+ loc: { start: { line: 0, col: 0 }, end: { line: 0, col: 0 } },
1584
+ }
1585
+ : null;
1586
+ if (rootBlock) {
1587
+ // Pre-pass: infer which user-defined functions yield. Codegen below
1588
+ // uses this set so call sites get `await` even when the helper is a
1589
+ // local function rather than a built-in (`waitForChild`, custom event
1590
+ // wrappers, anything that internally calls `task.wait`, etc.).
1591
+ scanYieldingFunctions(rootBlock, ctx);
1592
+ }
1593
+ const stmts = rootBlock ? compileBlockBody(rootBlock, ctx) : [];
1594
+ const helpers = ctx.importedHelpers();
1595
+ // Track each top-level Luau stmt → output TS stmt for source maps.
1596
+ const stmtToLuauLoc = new WeakMap();
1597
+ if (parsed.root) {
1598
+ let outIndex = helpers.length > 0 ? 1 : 0;
1599
+ for (const luauStmt of parsed.root.body) {
1600
+ const out = stmts[outIndex];
1601
+ if (out && luauStmt.loc) {
1602
+ stmtToLuauLoc.set(out, {
1603
+ line: luauStmt.loc.start.line,
1604
+ col: luauStmt.loc.start.col,
1605
+ });
1606
+ }
1607
+ outIndex++;
1608
+ }
1609
+ }
1610
+ // Lua creates a global on first assignment: `deb = true` works even if
1611
+ // `deb` was never declared. JS strict mode throws. We can't tell if the
1612
+ // user *intended* a global vs. forgot a `local`, but we can pre-declare
1613
+ // every Global name that gets written to so the script keeps running.
1614
+ // Names already supplied by the host (game, workspace, math, table,
1615
+ // print, …) are excluded — those come from the script wrapper.
1616
+ const implicitGlobals = collectImplicitGlobals(parsed);
1617
+ const implicitGlobalDecls = [];
1618
+ for (const name of implicitGlobals) {
1619
+ // Initialize the predecl from `_G[name]` so cross-script globals set
1620
+ // earlier in the place's startup are visible at this script's start.
1621
+ // (Best-effort: still doesn't see later updates from other scripts —
1622
+ // matching Lua semantics would need every read to consult _G, which is
1623
+ // a deeper refactor.)
1624
+ implicitGlobalDecls.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(safeIdentifier(name)), undefined, undefined, factory.createElementAccessExpression(factory.createIdentifier('_G'), factory.createStringLiteral(name)))], ts.NodeFlags.Let)));
1625
+ }
1626
+ const allStatements = [];
1627
+ if (helpers.length > 0)
1628
+ allStatements.push(buildRuntimeImport(helpers));
1629
+ // Macro-registered extras — `@rbxts/services`, `@rbxts/types`, `@rbxts/promise`,
1630
+ // `@rbxts/roact`, etc. Each macro that fired called `ctx.useImport(module, name)`;
1631
+ // the bookkeeping is reified into one import declaration per module.
1632
+ for (const { module, names } of ctx.extraImportEntries()) {
1633
+ allStatements.push(buildNamedImport(module, names));
1634
+ }
1635
+ allStatements.push(...implicitGlobalDecls);
1636
+ allStatements.push(...stmts);
1637
+ const sourceFile = factory.updateSourceFile(ts.createSourceFile('output.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS), allStatements);
1638
+ const printer = ts.createPrinter({
1639
+ newLine: ts.NewLineKind.LineFeed,
1640
+ removeComments: false,
1641
+ });
1642
+ let printed = printer.printFile(sourceFile);
1643
+ printed = beautifyOutput(printed);
1644
+ // Comment preservation: prepend the source's leading comment block
1645
+ // (file header). Inline comments live in the WASM CST data we don't
1646
+ // currently surface, so we keep the most-useful slice — the file
1647
+ // header — by extracting any `--` / `--[[ ]]` comments that appear
1648
+ // before the first non-comment, non-whitespace token in the source.
1649
+ if (options.preserveComments) {
1650
+ const header = extractFileHeaderComments(source);
1651
+ if (header)
1652
+ printed = `${header}\n${printed}`;
1653
+ }
1654
+ // Always prepend a "compiled by" header so users know the file is
1655
+ // generated and from which version of the compiler. Goes first so it's
1656
+ // visible above whatever the source author wrote.
1657
+ printed = `${COMPILER_HEADER}\n${printed}`;
1658
+ // Pretty-print the final output via Prettier. The TS factory printer
1659
+ // produces correct but ugly TypeScript: 4-space indents, no blank lines
1660
+ // between top-level blocks, no consistent quoting. Prettier with the
1661
+ // baked-in rules brings it in line with how humans write TS. Source-map
1662
+ // building runs AFTER this step so generated-line numbers match the
1663
+ // final output the user sees.
1664
+ if (options.pretty !== false) {
1665
+ try {
1666
+ printed = await prettierFormat(printed, {
1667
+ parser: 'typescript',
1668
+ semi: true,
1669
+ singleQuote: true,
1670
+ trailingComma: 'all',
1671
+ printWidth: 100,
1672
+ arrowParens: 'always',
1673
+ endOfLine: 'lf',
1674
+ });
1675
+ }
1676
+ catch {
1677
+ // If Prettier can't parse the output (parser bug, unusual syntax),
1678
+ // fall back to the printer's raw output rather than failing the
1679
+ // whole compile. Users can see exactly what we emitted.
1680
+ }
1681
+ }
1682
+ // Layer A: post-emit TypeScript type check. Defaults to on; set
1683
+ // postEmitCheck: false (or --no-check-ts at the CLI) to skip. Runs
1684
+ // the bundled tsc over the emitted .ts source via an in-memory
1685
+ // CompilerHost so we don't touch the filesystem or pull in external
1686
+ // @types. strict:false so any-laden translations of untyped Luau
1687
+ // don't drown the user in noise; users who want strict can run
1688
+ // tsc --strict on the output.
1689
+ const runLayerA = options.typeCheck === true || options.postEmitCheck !== false;
1690
+ if (runLayerA) {
1691
+ const postEmitDiags = runPostEmitCheck(printed, sourceFileName);
1692
+ for (const d of postEmitDiags)
1693
+ parsed.errors.push(d);
1694
+ }
1695
+ // Layer B: pre-emit Luau check via @luau2ts/analyzer. Defaults to on
1696
+ // when the package is installed; soft-fails silently otherwise (no
1697
+ // soft warning, since "default-on" means we run when we can and
1698
+ // skip when we can't, similar to Prettier formatting). Users can
1699
+ // force-enable with typeCheck:true to get the soft warning when
1700
+ // they're expecting the analyzer to be there.
1701
+ const runLayerB = options.typeCheck === true || options.preEmitCheck === true || options.preEmitCheck !== false;
1702
+ if (runLayerB) {
1703
+ let analyzerMod;
1704
+ try {
1705
+ // Resolve the package name through an indirected variable so the
1706
+ // TypeScript checker doesn't try to resolve it at compile time.
1707
+ // The analyzer is an OPTIONAL peer; if it isn't installed we
1708
+ // soft-fail below.
1709
+ const analyzerPath = '@luau2ts' + '/analyzer';
1710
+ analyzerMod = (await import(/* @vite-ignore */ analyzerPath));
1711
+ }
1712
+ catch {
1713
+ // Analyzer not installed. If the user explicitly asked for the
1714
+ // check (typeCheck:true or preEmitCheck:true), surface a single
1715
+ // soft warning so they know it's silently skipped. Otherwise
1716
+ // stay quiet because preEmitCheck defaults to "run if available".
1717
+ if (options.typeCheck === true || options.preEmitCheck === true) {
1718
+ parsed.errors.push({
1719
+ loc: { start: { line: 0, col: 0 }, end: { line: 0, col: 0 } },
1720
+ message: '[luau2ts] preEmitCheck requested but @luau2ts/analyzer is not installed. Install with: pnpm add -D @luau2ts/analyzer',
1721
+ });
1722
+ }
1723
+ }
1724
+ if (analyzerMod) {
1725
+ const diags = await analyzerMod.analyze(source);
1726
+ for (const d of diags) {
1727
+ // Normalize the analyzer's flat {line, col} into the
1728
+ // parser's nested loc shape so the union of errors in
1729
+ // CompileResult stays uniform.
1730
+ parsed.errors.push({
1731
+ message: `[luau:${d.code}] ${d.message}`,
1732
+ loc: {
1733
+ start: { line: d.line, col: d.col },
1734
+ end: { line: d.endLine, col: d.endCol },
1735
+ },
1736
+ });
1737
+ }
1738
+ }
1739
+ }
1740
+ let sourceMap;
1741
+ if (options.sourceMap || options.inlineSourceMap) {
1742
+ const mappings = [];
1743
+ // Walk each line of the printed output. The compiler header (and
1744
+ // optional source comment header) we prepended produce lines that
1745
+ // don't correspond to any input statement, so skip past them first.
1746
+ const lines = printed.split('\n');
1747
+ let cursor = 0;
1748
+ while (cursor < lines.length) {
1749
+ const ln = lines[cursor];
1750
+ if (ln.startsWith('//') || ln.startsWith('/*') || ln.trim() === '') {
1751
+ cursor++;
1752
+ continue;
1753
+ }
1754
+ break;
1755
+ }
1756
+ for (const stmt of allStatements) {
1757
+ const loc = stmtToLuauLoc.get(stmt);
1758
+ if (!loc)
1759
+ continue;
1760
+ while (cursor < lines.length && lines[cursor].trim() === '')
1761
+ cursor++;
1762
+ if (cursor >= lines.length)
1763
+ break;
1764
+ mappings.push({
1765
+ generatedLine: cursor,
1766
+ generatedColumn: 0,
1767
+ originalLine: loc.line,
1768
+ originalColumn: loc.col,
1769
+ });
1770
+ cursor++;
1771
+ }
1772
+ sourceMap = buildSourceMap(`${sourceFileName}.ts`, sourceFileName, source, mappings);
1773
+ if (options.inlineSourceMap) {
1774
+ printed += `\n${inlineSourceMapURL(sourceMap)}`;
1775
+ }
1776
+ }
1777
+ return {
1778
+ source: printed,
1779
+ helpers,
1780
+ errors: parsed.errors,
1781
+ ...(sourceMap ? { sourceMap } : {}),
1782
+ };
1783
+ }
1784
+ /** Extract the file's leading comment block — every `--` line comment or
1785
+ * `--[[ ]]` block comment that appears before the first non-comment,
1786
+ * non-whitespace character of the source. */
1787
+ function extractFileHeaderComments(source) {
1788
+ const out = [];
1789
+ let i = 0;
1790
+ const len = source.length;
1791
+ while (i < len) {
1792
+ // Skip whitespace.
1793
+ while (i < len && /\s/.test(source[i]))
1794
+ i++;
1795
+ if (i >= len)
1796
+ break;
1797
+ if (source[i] !== '-' || source[i + 1] !== '-')
1798
+ break;
1799
+ if (source[i + 2] === '[' && source[i + 3] === '[') {
1800
+ // Block comment.
1801
+ const end = source.indexOf(']]', i + 4);
1802
+ if (end < 0)
1803
+ break;
1804
+ const block = source.slice(i, end + 2);
1805
+ // Convert --[[ ... ]] to /* ... */ for TS.
1806
+ out.push(`/* ${block.slice(4, -2).trim()} */`);
1807
+ i = end + 2;
1808
+ }
1809
+ else {
1810
+ // Line comment until newline.
1811
+ const end = source.indexOf('\n', i);
1812
+ const stop = end < 0 ? len : end;
1813
+ out.push(`// ${source.slice(i + 2, stop).trim()}`);
1814
+ i = stop;
1815
+ }
1816
+ }
1817
+ return out.join('\n');
1818
+ }
1819
+ /** Names the script wrapper provides — never pre-declared by the
1820
+ * compiler since they'd shadow the wrapper's bindings. */
1821
+ const HOST_PROVIDED_GLOBALS = new Set([
1822
+ // Roblox runtime singletons
1823
+ 'game',
1824
+ 'workspace',
1825
+ 'script',
1826
+ 'plugin',
1827
+ 'shared',
1828
+ // Roblox stdlib functions
1829
+ 'print',
1830
+ 'warn',
1831
+ 'error',
1832
+ 'assert',
1833
+ 'pcall',
1834
+ 'xpcall',
1835
+ 'select',
1836
+ 'tostring',
1837
+ 'tonumber',
1838
+ 'type',
1839
+ 'typeof',
1840
+ 'ipairs',
1841
+ 'pairs',
1842
+ 'next',
1843
+ 'unpack',
1844
+ 'setmetatable',
1845
+ 'getmetatable',
1846
+ 'rawget',
1847
+ 'rawset',
1848
+ 'rawequal',
1849
+ 'rawlen',
1850
+ 'wait',
1851
+ 'spawn',
1852
+ 'delay',
1853
+ 'defer',
1854
+ 'tick',
1855
+ 'time',
1856
+ // Uppercase legacy aliases (Wait, Spawn, Delay) and pre-2018 globals
1857
+ // (Game, Workspace) provided by the host preamble in cli.ts.
1858
+ 'Wait',
1859
+ 'Spawn',
1860
+ 'Delay',
1861
+ 'Game',
1862
+ 'Workspace',
1863
+ 'LoadLibrary',
1864
+ 'elapsedTime',
1865
+ 'ElapsedTime',
1866
+ 'task',
1867
+ 'require',
1868
+ 'newproxy',
1869
+ // Lua libraries
1870
+ 'math',
1871
+ 'string',
1872
+ 'table',
1873
+ 'os',
1874
+ 'io',
1875
+ 'coroutine',
1876
+ 'utf8',
1877
+ 'bit32',
1878
+ 'buffer',
1879
+ 'debug',
1880
+ // Roblox datatypes (provided by import)
1881
+ 'Instance',
1882
+ 'Vector3',
1883
+ 'Vector2',
1884
+ 'CFrame',
1885
+ 'Color3',
1886
+ 'UDim',
1887
+ 'UDim2',
1888
+ 'BrickColor',
1889
+ 'Enum',
1890
+ 'Ray',
1891
+ 'Region3',
1892
+ 'Rect',
1893
+ 'NumberRange',
1894
+ 'NumberSequence',
1895
+ 'NumberSequenceKeypoint',
1896
+ 'ColorSequence',
1897
+ 'ColorSequenceKeypoint',
1898
+ 'Faces',
1899
+ 'Axes',
1900
+ 'TweenInfo',
1901
+ 'PhysicalProperties',
1902
+ 'RaycastParams',
1903
+ 'OverlapParams',
1904
+ 'Random',
1905
+ 'DateTime',
1906
+ 'Font',
1907
+ 'Path2DControlPoint',
1908
+ // Boolean-y / nil
1909
+ 'nil',
1910
+ '_G',
1911
+ '_ENV',
1912
+ '_VERSION',
1913
+ // Stubbed legacy Lua globals provided by the cli preamble. Without these
1914
+ // in the host set the implicit-globals walker emits a `let loadstring;`
1915
+ // predecl that shadows the preamble's `const loadstring = …`, so the
1916
+ // stub is unreachable inside the script body.
1917
+ 'loadstring',
1918
+ 'collectgarbage',
1919
+ 'ypcall',
1920
+ ]);
1921
+ function collectImplicitGlobals(parsed) {
1922
+ const referenced = new Set();
1923
+ const declared = new Set();
1924
+ if (!parsed.root)
1925
+ return referenced;
1926
+ // Walk every node looking for any Global reference — read or written.
1927
+ // Luau treats an undeclared global read as `nil`; JS strict mode throws
1928
+ // ReferenceError, so we predeclare every name the script touches so reads
1929
+ // see `undefined` (≈ nil) and writes work. CompoundAssign covers `x += 1`
1930
+ // shapes; the general Global walk catches everything else (table values,
1931
+ // function args, condition expressions, …).
1932
+ //
1933
+ // Don't predeclare names that already get their own JS binding from the
1934
+ // compile output: top-level `function foo()` emits a `function` decl,
1935
+ // top-level `local foo` emits a `let foo`. A duplicate `let foo` would
1936
+ // collide with both.
1937
+ // Track function nesting: Local/LocalFunction declarations inside a
1938
+ // function body shadow only within that scope. They must not suppress
1939
+ // an implicit top-level global of the same name (e.g. `de = math.deg`
1940
+ // at file scope while a function further down has `local de = ...`).
1941
+ let fnDepth = 0;
1942
+ // Block depth excluding the root block. Locals declared inside an
1943
+ // `if`/`for`/`while`/`do` body are block-scoped in Lua — they do NOT bind
1944
+ // at file scope. Without this, `if cond then local X = … end; print(X)`
1945
+ // mistakenly marks X as declared so its outer-scope read is never
1946
+ // predeclared and the JS emits a ReferenceError.
1947
+ let blockDepth = 0;
1948
+ const walk = (node) => {
1949
+ if (!node || typeof node !== 'object')
1950
+ return;
1951
+ const n = node;
1952
+ if (n.type === 'Global') {
1953
+ const name = n.name;
1954
+ if (typeof name === 'string' && !HOST_PROVIDED_GLOBALS.has(name)) {
1955
+ referenced.add(name);
1956
+ }
1957
+ }
1958
+ else if (n.type === 'IndexExpr') {
1959
+ // `_G["X"] = …` synthesizes a mirror assignment `X = _G["X"]` (see
1960
+ // compileAssign). Predeclare X so the mirror has something to bind to;
1961
+ // otherwise the strict-mode assignment throws ReferenceError.
1962
+ const ie = n;
1963
+ if (ie.expr?.type === 'Global'
1964
+ && ie.expr.name === '_G'
1965
+ && ie.index?.type === 'ConstantString'
1966
+ && typeof ie.index.value === 'string'
1967
+ && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(ie.index.value)
1968
+ && !HOST_PROVIDED_GLOBALS.has(ie.index.value)) {
1969
+ referenced.add(ie.index.value);
1970
+ }
1971
+ }
1972
+ else if (n.type === 'CompoundAssign') {
1973
+ const v = n.var;
1974
+ if (v && v.type === 'Global' && typeof v.name === 'string') {
1975
+ if (!HOST_PROVIDED_GLOBALS.has(v.name))
1976
+ referenced.add(v.name);
1977
+ }
1978
+ }
1979
+ else if (n.type === 'Function') {
1980
+ // FunctionStat (has `name` Expr) vs FunctionExpr (has `body`). Only the
1981
+ // stat form declares a binding name we need to track.
1982
+ const stat = n;
1983
+ if (fnDepth === 0 && blockDepth === 0 && stat.func && stat.name && stat.name.type === 'Global' && typeof stat.name.name === 'string') {
1984
+ declared.add(stat.name.name);
1985
+ }
1986
+ }
1987
+ else if (n.type === 'LocalFunction') {
1988
+ const stat = n;
1989
+ if (fnDepth === 0 && blockDepth === 0 && stat.name && typeof stat.name.name === 'string')
1990
+ declared.add(stat.name.name);
1991
+ }
1992
+ else if (n.type === 'Local' && 'vars' in n && 'values' in n) {
1993
+ if (fnDepth === 0 && blockDepth === 0) {
1994
+ const vars = n.vars;
1995
+ for (const v of vars)
1996
+ declared.add(v.name);
1997
+ }
1998
+ }
1999
+ // FunctionStat and LocalFunction both type-tag as 'Function' and wrap a
2000
+ // FunctionExpr via .func — only the FunctionExpr (.body present, no
2001
+ // .func) actually opens a new lexical scope. Incrementing on the outer
2002
+ // stat would double-count and break the depth invariant.
2003
+ const enterFn = n.type === 'Function'
2004
+ && n.body !== undefined
2005
+ && n.func === undefined;
2006
+ if (enterFn)
2007
+ fnDepth += 1;
2008
+ // Inner control-flow blocks (`if`/`for`/`while`/`repeat`/`do`) open a
2009
+ // new lexical scope in Lua. Locals declared inside don't bind at the
2010
+ // outer scope, so we treat them as not-declared for the purpose of
2011
+ // implicit-global predeclaration.
2012
+ const enterBlock = n.type === 'If' || n.type === 'For' || n.type === 'ForIn'
2013
+ || n.type === 'While' || n.type === 'Repeat' || n.type === 'Do';
2014
+ if (enterBlock)
2015
+ blockDepth += 1;
2016
+ // Recurse into all object values.
2017
+ for (const v of Object.values(n)) {
2018
+ if (Array.isArray(v))
2019
+ v.forEach(walk);
2020
+ else if (v && typeof v === 'object')
2021
+ walk(v);
2022
+ }
2023
+ if (enterBlock)
2024
+ blockDepth -= 1;
2025
+ if (enterFn)
2026
+ fnDepth -= 1;
2027
+ };
2028
+ walk(parsed.root);
2029
+ for (const name of declared)
2030
+ referenced.delete(name);
2031
+ return referenced;
2032
+ }
2033
+ /** Post-process the printer output to add blank lines around top-level
2034
+ * blocks (imports, function/class declarations, big control-flow). The
2035
+ * TS factory printer doesn't do this — it just newlines between every
2036
+ * statement — so we insert separators heuristically. The rules are
2037
+ * conservative: only insert a blank line at boundaries between distinct
2038
+ * *kinds* of top-level statements, and never inside a block. */
2039
+ function beautifyOutput(source) {
2040
+ const lines = source.split('\n');
2041
+ const out = [];
2042
+ let prev = 'start';
2043
+ let depth = 0;
2044
+ function classify(line) {
2045
+ const trimmed = line.trim();
2046
+ if (trimmed === '')
2047
+ return 'blank';
2048
+ if (trimmed.startsWith('import '))
2049
+ return 'import';
2050
+ if (/^(?:export\s+)?(?:abstract\s+)?class\b/.test(trimmed))
2051
+ return 'class';
2052
+ if (/^(?:export\s+)?(?:async\s+)?function\b/.test(trimmed))
2053
+ return 'function';
2054
+ if (/^(?:if|for|while|do|switch|try)\b/.test(trimmed))
2055
+ return 'control';
2056
+ return 'simple';
2057
+ }
2058
+ for (const raw of lines) {
2059
+ // Track brace depth on the *previous* line; statements outside any
2060
+ // block are at depth 0.
2061
+ if (depth === 0 && prev !== 'start' && prev !== 'blank') {
2062
+ const kind = classify(raw);
2063
+ // Add a blank line at the boundary between distinct kinds.
2064
+ const isBoundary = (prev === 'import' && kind !== 'import')
2065
+ || (prev === 'class' && kind !== 'blank')
2066
+ || (prev === 'function' && kind !== 'blank')
2067
+ || (prev === 'control' && kind !== 'blank')
2068
+ || (kind === 'class' || kind === 'function');
2069
+ if (isBoundary && kind !== 'blank')
2070
+ out.push('');
2071
+ }
2072
+ out.push(raw);
2073
+ if (raw.trim() !== '')
2074
+ prev = classify(raw);
2075
+ // Update brace depth based on this line's contribution.
2076
+ for (const c of raw) {
2077
+ if (c === '{')
2078
+ depth++;
2079
+ else if (c === '}')
2080
+ depth = Math.max(0, depth - 1);
2081
+ }
2082
+ }
2083
+ return out.join('\n');
2084
+ }
2085
+ /** Layer A: post-emit TypeScript type check. Runs the bundled tsc over
2086
+ * the emitted source via an in-memory CompilerHost and returns the
2087
+ * diagnostics in ParseError shape so they line up with everything else
2088
+ * in CompileResult.errors. strict:false because the translation often
2089
+ * produces any-typed positions where the Luau was untyped, and we'd
2090
+ * rather flag real semantic mistakes than drown the user in noise. */
2091
+ function runPostEmitCheck(source, _sourceName) {
2092
+ // Use a fixed, POSIX-style in-memory filename. The caller's sourceName
2093
+ // (which may be a Windows path with backslashes) only matters for the
2094
+ // user-facing error report; ts compiler internals normalize paths and
2095
+ // a mismatched-backslash filter would drop every diagnostic.
2096
+ const fileName = 'luau2ts-postcheck-input.ts';
2097
+ const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
2098
+ const options = {
2099
+ strict: false,
2100
+ noEmit: true,
2101
+ target: ts.ScriptTarget.Latest,
2102
+ module: ts.ModuleKind.NodeNext,
2103
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
2104
+ skipLibCheck: true,
2105
+ allowJs: false,
2106
+ isolatedModules: true,
2107
+ };
2108
+ // CompilerHost backed by the default host so lib.es*.d.ts files resolve
2109
+ // against the bundled typescript install, but our single in-memory
2110
+ // source file shadows getSourceFile for its own path.
2111
+ const defaultHost = ts.createCompilerHost(options, true);
2112
+ const host = {
2113
+ ...defaultHost,
2114
+ getSourceFile: (name, languageVersion, onError) => {
2115
+ if (name === fileName)
2116
+ return sourceFile;
2117
+ return defaultHost.getSourceFile(name, languageVersion, onError);
2118
+ },
2119
+ writeFile: () => undefined,
2120
+ fileExists: (name) => (name === fileName ? true : defaultHost.fileExists(name)),
2121
+ readFile: (name) => (name === fileName ? source : defaultHost.readFile(name)),
2122
+ };
2123
+ const program = ts.createProgram({ rootNames: [fileName], options, host });
2124
+ const diagnostics = ts.getPreEmitDiagnostics(program);
2125
+ const out = [];
2126
+ for (const d of diagnostics) {
2127
+ // Only surface diagnostics that point into OUR source file. lib.*.d.ts
2128
+ // errors would just be noise.
2129
+ if (!d.file || d.file.fileName !== fileName)
2130
+ continue;
2131
+ const start = d.start !== undefined ? sourceFile.getLineAndCharacterOfPosition(d.start) : { line: 0, character: 0 };
2132
+ const endPos = d.start !== undefined && d.length !== undefined
2133
+ ? sourceFile.getLineAndCharacterOfPosition(d.start + d.length)
2134
+ : start;
2135
+ const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
2136
+ out.push({
2137
+ message: `[ts:${d.code}] ${message}`,
2138
+ loc: {
2139
+ start: { line: start.line + 1, col: start.character + 1 },
2140
+ end: { line: endPos.line + 1, col: endPos.character + 1 },
2141
+ },
2142
+ });
2143
+ }
2144
+ return out;
2145
+ }
2146
+ function buildRuntimeImport(names) {
2147
+ return buildNamedImport(RUNTIME_MODULE, names);
2148
+ }
2149
+ /** Generic `import { ...names } from "<module>"` builder. Used for both the
2150
+ * default runtime helper import and macro-registered extras (`@rbxts/types`,
2151
+ * `@rbxts/services`, etc.). */
2152
+ function buildNamedImport(module, names) {
2153
+ return factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(names.map((n) => factory.createImportSpecifier(false, undefined, factory.createIdentifier(n))))), factory.createStringLiteral(module));
2154
+ }
2155
+ //# sourceMappingURL=index.js.map