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.
- package/dist/cli/args.d.ts +23 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +177 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +71 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/modes.d.ts +20 -0
- package/dist/cli/modes.d.ts.map +1 -0
- package/dist/cli/modes.js +145 -0
- package/dist/cli/modes.js.map +1 -0
- package/dist/compile/class-shape.d.ts +31 -0
- package/dist/compile/class-shape.d.ts.map +1 -0
- package/dist/compile/class-shape.js +291 -0
- package/dist/compile/class-shape.js.map +1 -0
- package/dist/compile/context.d.ts +86 -0
- package/dist/compile/context.d.ts.map +1 -0
- package/dist/compile/context.js +144 -0
- package/dist/compile/context.js.map +1 -0
- package/dist/compile/index.d.ts +58 -0
- package/dist/compile/index.d.ts.map +1 -0
- package/dist/compile/index.js +2155 -0
- package/dist/compile/index.js.map +1 -0
- package/dist/compile/macros/datatypes.d.ts +2 -0
- package/dist/compile/macros/datatypes.d.ts.map +1 -0
- package/dist/compile/macros/datatypes.js +76 -0
- package/dist/compile/macros/datatypes.js.map +1 -0
- package/dist/compile/macros/index.d.ts +33 -0
- package/dist/compile/macros/index.d.ts.map +1 -0
- package/dist/compile/macros/index.js +71 -0
- package/dist/compile/macros/index.js.map +1 -0
- package/dist/compile/macros/instance.d.ts +2 -0
- package/dist/compile/macros/instance.d.ts.map +1 -0
- package/dist/compile/macros/instance.js +58 -0
- package/dist/compile/macros/instance.js.map +1 -0
- package/dist/compile/macros/stdlib.d.ts +2 -0
- package/dist/compile/macros/stdlib.d.ts.map +1 -0
- package/dist/compile/macros/stdlib.js +140 -0
- package/dist/compile/macros/stdlib.js.map +1 -0
- package/dist/compile/rbxts-runtime.d.ts +2 -0
- package/dist/compile/rbxts-runtime.d.ts.map +1 -0
- package/dist/compile/rbxts-runtime.js +163 -0
- package/dist/compile/rbxts-runtime.js.map +1 -0
- package/dist/compile/sourcemap.d.ts +25 -0
- package/dist/compile/sourcemap.d.ts.map +1 -0
- package/dist/compile/sourcemap.js +71 -0
- package/dist/compile/sourcemap.js.map +1 -0
- package/dist/compile/type.d.ts +6 -0
- package/dist/compile/type.d.ts.map +1 -0
- package/dist/compile/type.js +122 -0
- package/dist/compile/type.js.map +1 -0
- package/dist/compile/util.d.ts +38 -0
- package/dist/compile/util.d.ts.map +1 -0
- package/dist/compile/util.js +153 -0
- package/dist/compile/util.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +227 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/types.d.ts +430 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/dist/parser/types.js +14 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/parser/wasm/luau-parser.d.mts +21 -0
- package/dist/parser/wasm/luau-parser.mjs +2 -0
- package/dist/parser/wasm/luau-parser.wasm +0 -0
- package/dist/rojo/index.d.ts +4 -0
- package/dist/rojo/index.d.ts.map +1 -0
- package/dist/rojo/index.js +3 -0
- package/dist/rojo/index.js.map +1 -0
- package/dist/rojo/load-project.d.ts +12 -0
- package/dist/rojo/load-project.d.ts.map +1 -0
- package/dist/rojo/load-project.js +35 -0
- package/dist/rojo/load-project.js.map +1 -0
- package/dist/rojo/types.d.ts +39 -0
- package/dist/rojo/types.d.ts.map +1 -0
- package/dist/rojo/types.js +2 -0
- package/dist/rojo/types.js.map +1 -0
- package/dist/rojo/walk-tree.d.ts +40 -0
- package/dist/rojo/walk-tree.d.ts.map +1 -0
- package/dist/rojo/walk-tree.js +164 -0
- package/dist/rojo/walk-tree.js.map +1 -0
- package/dist/runtime/arith.d.ts +13 -0
- package/dist/runtime/arith.d.ts.map +1 -0
- package/dist/runtime/arith.js +151 -0
- package/dist/runtime/arith.js.map +1 -0
- package/dist/runtime/index-helper.d.ts +3 -0
- package/dist/runtime/index-helper.d.ts.map +1 -0
- package/dist/runtime/index-helper.js +40 -0
- package/dist/runtime/index-helper.js.map +1 -0
- package/dist/runtime/index.d.ts +13 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +13 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/iterator.d.ts +58 -0
- package/dist/runtime/iterator.d.ts.map +1 -0
- package/dist/runtime/iterator.js +181 -0
- package/dist/runtime/iterator.js.map +1 -0
- package/dist/runtime/length.d.ts +2 -0
- package/dist/runtime/length.d.ts.map +1 -0
- package/dist/runtime/length.js +15 -0
- package/dist/runtime/length.js.map +1 -0
- package/dist/runtime/lua-stdlib.d.ts +186 -0
- package/dist/runtime/lua-stdlib.d.ts.map +1 -0
- package/dist/runtime/lua-stdlib.js +502 -0
- package/dist/runtime/lua-stdlib.js.map +1 -0
- package/dist/runtime/metatable.d.ts +16 -0
- package/dist/runtime/metatable.d.ts.map +1 -0
- package/dist/runtime/metatable.js +129 -0
- package/dist/runtime/metatable.js.map +1 -0
- package/dist/runtime/pattern.d.ts +21 -0
- package/dist/runtime/pattern.d.ts.map +1 -0
- package/dist/runtime/pattern.js +375 -0
- package/dist/runtime/pattern.js.map +1 -0
- package/dist/runtime/pcall.d.ts +12 -0
- package/dist/runtime/pcall.d.ts.map +1 -0
- package/dist/runtime/pcall.js +54 -0
- package/dist/runtime/pcall.js.map +1 -0
- package/dist/runtime/string-lib.d.ts +31 -0
- package/dist/runtime/string-lib.d.ts.map +1 -0
- package/dist/runtime/string-lib.js +296 -0
- package/dist/runtime/string-lib.js.map +1 -0
- package/dist/runtime/table-lib.d.ts +18 -0
- package/dist/runtime/table-lib.d.ts.map +1 -0
- package/dist/runtime/table-lib.js +133 -0
- package/dist/runtime/table-lib.js.map +1 -0
- package/dist/runtime/tostring.d.ts +3 -0
- package/dist/runtime/tostring.d.ts.map +1 -0
- package/dist/runtime/tostring.js +82 -0
- package/dist/runtime/tostring.js.map +1 -0
- package/dist/runtime/truthy.d.ts +13 -0
- package/dist/runtime/truthy.d.ts.map +1 -0
- package/dist/runtime/truthy.js +26 -0
- package/dist/runtime/truthy.js.map +1 -0
- 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
|