@gwigz/slua-tstl-plugin 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -16
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +50 -0
- package/dist/define.d.ts +19 -0
- package/dist/define.js +83 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +341 -662
- package/dist/lua-ast-walk.d.ts +10 -0
- package/dist/lua-ast-walk.js +422 -0
- package/dist/lua-transforms.d.ts +41 -0
- package/dist/lua-transforms.js +302 -0
- package/dist/optimize.d.ts +31 -0
- package/dist/optimize.js +106 -0
- package/dist/transforms.d.ts +7 -0
- package/dist/transforms.js +195 -0
- package/dist/utils.d.ts +130 -0
- package/dist/utils.js +412 -0
- package/package.json +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as lua from "typescript-to-lua";
|
|
3
|
+
import { walkBlocks, walkIdentifiers, containsIdentifier } from "./lua-ast-walk.js";
|
|
4
|
+
const JSDOC_TAG_RE = /@(?:index(?:Arg|Return)|define)\b/;
|
|
5
|
+
/**
|
|
6
|
+
* Strip internal JSDoc tags from Lua comments (@indexArg, @indexReturn, @define).
|
|
7
|
+
* These are only consumed by the plugin at transpile time.
|
|
8
|
+
*
|
|
9
|
+
* TSTL generates JSDoc as flat string arrays in leadingComments:
|
|
10
|
+
* ["-", " @define FLAG", " continuation line", " @indexArg 0"]
|
|
11
|
+
* Each string becomes a `-- <text>` line. When we find a tag line, we also
|
|
12
|
+
* remove its continuation lines (those starting with ` ` but not ` @`).
|
|
13
|
+
*/
|
|
14
|
+
export function stripInternalJSDocTags(file) {
|
|
15
|
+
let changed = false;
|
|
16
|
+
walkBlocks(file, (statements) => {
|
|
17
|
+
for (const stmt of statements) {
|
|
18
|
+
if (!stmt.leadingComments || stmt.leadingComments.length === 0)
|
|
19
|
+
continue;
|
|
20
|
+
const filtered = [];
|
|
21
|
+
let skip = false;
|
|
22
|
+
for (const comment of stmt.leadingComments) {
|
|
23
|
+
if (typeof comment === "string") {
|
|
24
|
+
if (JSDOC_TAG_RE.test(comment)) {
|
|
25
|
+
// Tag line, skip it and subsequent continuation lines
|
|
26
|
+
skip = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (skip) {
|
|
30
|
+
// Continuation line: starts with space but not with ` @tag`
|
|
31
|
+
if (comment.startsWith(" ") && !comment.match(/^ @\w/)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Not a continuation, stop skipping
|
|
35
|
+
skip = false;
|
|
36
|
+
}
|
|
37
|
+
filtered.push(comment);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Block comment (string[]), strip if any line contains the tag
|
|
41
|
+
skip = false;
|
|
42
|
+
if (comment.some((line) => JSDOC_TAG_RE.test(line))) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
filtered.push(comment);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// If only the JSDoc opener "-" remains, remove it too
|
|
49
|
+
if (filtered.length === 1 && filtered[0] === "-") {
|
|
50
|
+
filtered.length = 0;
|
|
51
|
+
}
|
|
52
|
+
if (filtered.length !== stmt.leadingComments.length) {
|
|
53
|
+
stmt.leadingComments = filtered.length > 0 ? filtered : undefined;
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Strip empty module boilerplate from files without explicit exports.
|
|
62
|
+
* `moduleDetection: "force"` causes TSTL to wrap every file as a module;
|
|
63
|
+
* standalone SLua scripts don't need the ____exports wrapper.
|
|
64
|
+
*/
|
|
65
|
+
export function stripEmptyModuleBoilerplate(file, sourceFiles) {
|
|
66
|
+
const stmts = file.statements;
|
|
67
|
+
if (stmts.length < 2)
|
|
68
|
+
return false;
|
|
69
|
+
// Find `local ____exports = {}` at top level
|
|
70
|
+
const declIdx = stmts.findIndex((s) => lua.isVariableDeclarationStatement(s) &&
|
|
71
|
+
s.left.length === 1 &&
|
|
72
|
+
s.left[0].text === "____exports" &&
|
|
73
|
+
s.right &&
|
|
74
|
+
s.right.length === 1 &&
|
|
75
|
+
lua.isTableExpression(s.right[0]) &&
|
|
76
|
+
s.right[0].fields.length === 0);
|
|
77
|
+
if (declIdx === -1)
|
|
78
|
+
return false;
|
|
79
|
+
// Find `return ____exports` at end
|
|
80
|
+
const last = stmts[stmts.length - 1];
|
|
81
|
+
if (!lua.isReturnStatement(last) ||
|
|
82
|
+
last.expressions.length !== 1 ||
|
|
83
|
+
!lua.isIdentifier(last.expressions[0]) ||
|
|
84
|
+
last.expressions[0].text !== "____exports") {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// Check TS source files for explicit exports
|
|
88
|
+
const hasExplicitExports = sourceFiles?.some((sf) => sf.statements.some((s) => ts.isExportDeclaration(s) ||
|
|
89
|
+
ts.isExportAssignment(s) ||
|
|
90
|
+
(ts.canHaveModifiers(s) &&
|
|
91
|
+
ts.getModifiers(s)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))));
|
|
92
|
+
if (hasExplicitExports)
|
|
93
|
+
return false;
|
|
94
|
+
// Remove both: the decl and the return
|
|
95
|
+
stmts.splice(stmts.length - 1, 1); // remove return first (higher index)
|
|
96
|
+
stmts.splice(declIdx, 1); // then remove decl
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Collapse default-parameter nil-checks into `x = x or <literal>`.
|
|
101
|
+
* Matches: if x == nil then x = <literal> end
|
|
102
|
+
* Safe for strings and numbers (both truthy in Lua); the TS source only
|
|
103
|
+
* generates these for string/number defaults.
|
|
104
|
+
*/
|
|
105
|
+
export function collapseDefaultParamNilChecks(file) {
|
|
106
|
+
let changed = false;
|
|
107
|
+
walkBlocks(file, (statements) => {
|
|
108
|
+
for (let i = 0; i < statements.length; i++) {
|
|
109
|
+
const stmt = statements[i];
|
|
110
|
+
if (!lua.isIfStatement(stmt))
|
|
111
|
+
continue;
|
|
112
|
+
if (stmt.elseBlock)
|
|
113
|
+
continue;
|
|
114
|
+
// condition: x == nil
|
|
115
|
+
if (!lua.isBinaryExpression(stmt.condition))
|
|
116
|
+
continue;
|
|
117
|
+
if (stmt.condition.operator !== lua.SyntaxKind.EqualityOperator)
|
|
118
|
+
continue;
|
|
119
|
+
if (!lua.isIdentifier(stmt.condition.left))
|
|
120
|
+
continue;
|
|
121
|
+
if (!lua.isNilLiteral(stmt.condition.right))
|
|
122
|
+
continue;
|
|
123
|
+
const paramName = stmt.condition.left;
|
|
124
|
+
// ifBlock has exactly 1 statement: x = <literal>
|
|
125
|
+
if (stmt.ifBlock.statements.length !== 1)
|
|
126
|
+
continue;
|
|
127
|
+
const inner = stmt.ifBlock.statements[0];
|
|
128
|
+
if (!lua.isAssignmentStatement(inner))
|
|
129
|
+
continue;
|
|
130
|
+
if (inner.left.length !== 1 || inner.right.length !== 1)
|
|
131
|
+
continue;
|
|
132
|
+
const assignTarget = inner.left[0];
|
|
133
|
+
if (!lua.isIdentifier(assignTarget))
|
|
134
|
+
continue;
|
|
135
|
+
if (assignTarget.text !== paramName.text)
|
|
136
|
+
continue;
|
|
137
|
+
const literal = inner.right[0];
|
|
138
|
+
if (!lua.isStringLiteral(literal) && !lua.isNumericLiteral(literal)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Replace with: x = x or <literal>
|
|
142
|
+
const orExpr = lua.createBinaryExpression(lua.cloneIdentifier(paramName), literal, lua.SyntaxKind.OrOperator);
|
|
143
|
+
const assignment = lua.createAssignmentStatement(lua.cloneIdentifier(paramName), orExpr);
|
|
144
|
+
// Preserve leading comments from the if statement
|
|
145
|
+
if (stmt.leadingComments) {
|
|
146
|
+
assignment.leadingComments = stmt.leadingComments;
|
|
147
|
+
}
|
|
148
|
+
statements[i] = assignment;
|
|
149
|
+
changed = true;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return changed;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Shorten TSTL destructuring temp names: ____*_result_* -> _rN.
|
|
156
|
+
* Two-pass: collect unique names in order, then rename all identifiers.
|
|
157
|
+
*/
|
|
158
|
+
export function shortenTempNames(file) {
|
|
159
|
+
const seen = new Map();
|
|
160
|
+
let counter = 0;
|
|
161
|
+
const tempRe = /^____\w+_result_\d+$/;
|
|
162
|
+
// Pass 1: collect unique temp names in order of first occurrence
|
|
163
|
+
walkIdentifiers(file, (id) => {
|
|
164
|
+
if (tempRe.test(id.text) && !seen.has(id.text)) {
|
|
165
|
+
seen.set(id.text, `_r${counter++}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
if (seen.size === 0)
|
|
169
|
+
return false;
|
|
170
|
+
// Pass 2: rename all matching identifiers
|
|
171
|
+
walkIdentifiers(file, (id) => {
|
|
172
|
+
const short = seen.get(id.text);
|
|
173
|
+
if (short)
|
|
174
|
+
id.text = short;
|
|
175
|
+
});
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Collapse consecutive field accesses from the same shortened temp into
|
|
180
|
+
* multi-assignment: `local a = _r0.x; local b = _r0.y` -> `local a, b = _r0.x, _r0.y`.
|
|
181
|
+
* Only collapses when the base matches `_r\d+` (shortened temps).
|
|
182
|
+
*/
|
|
183
|
+
export function collapseFieldAccesses(file) {
|
|
184
|
+
let changed = false;
|
|
185
|
+
const tempBaseRe = /^_r\d+$/;
|
|
186
|
+
walkBlocks(file, (statements) => {
|
|
187
|
+
let i = 0;
|
|
188
|
+
while (i < statements.length) {
|
|
189
|
+
const stmt = statements[i];
|
|
190
|
+
// Match: local <name> = <temp>.<field>
|
|
191
|
+
if (!lua.isVariableDeclarationStatement(stmt) ||
|
|
192
|
+
stmt.left.length !== 1 ||
|
|
193
|
+
!stmt.right ||
|
|
194
|
+
stmt.right.length !== 1 ||
|
|
195
|
+
!lua.isTableIndexExpression(stmt.right[0]) ||
|
|
196
|
+
!lua.isIdentifier(stmt.right[0].table) ||
|
|
197
|
+
!tempBaseRe.test(stmt.right[0].table.text)) {
|
|
198
|
+
i++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const baseName = stmt.right[0].table.text;
|
|
202
|
+
// Look ahead for consecutive field accesses on the same base
|
|
203
|
+
const left = [...stmt.left];
|
|
204
|
+
const right = [...stmt.right];
|
|
205
|
+
let end = i + 1;
|
|
206
|
+
while (end < statements.length) {
|
|
207
|
+
const next = statements[end];
|
|
208
|
+
if (lua.isVariableDeclarationStatement(next) &&
|
|
209
|
+
next.left.length === 1 &&
|
|
210
|
+
next.right &&
|
|
211
|
+
next.right.length === 1 &&
|
|
212
|
+
lua.isTableIndexExpression(next.right[0]) &&
|
|
213
|
+
lua.isIdentifier(next.right[0].table) &&
|
|
214
|
+
next.right[0].table.text === baseName) {
|
|
215
|
+
left.push(...next.left);
|
|
216
|
+
right.push(...next.right);
|
|
217
|
+
end++;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (end - i > 1) {
|
|
224
|
+
// Create merged multi-assignment
|
|
225
|
+
const merged = lua.createVariableDeclarationStatement(left, right);
|
|
226
|
+
if (stmt.leadingComments) {
|
|
227
|
+
merged.leadingComments = stmt.leadingComments;
|
|
228
|
+
}
|
|
229
|
+
statements.splice(i, end - i, merged);
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return changed;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Merge forward-declared `local x` with its first `x = value` assignment.
|
|
239
|
+
* Only inlines when there are no references to x between declaration and assignment.
|
|
240
|
+
*/
|
|
241
|
+
export function inlineForwardDeclarations(file) {
|
|
242
|
+
let changed = false;
|
|
243
|
+
walkBlocks(file, (statements) => {
|
|
244
|
+
for (let i = 0; i < statements.length; i++) {
|
|
245
|
+
const stmt = statements[i];
|
|
246
|
+
// Match forward declaration: `local var1, var2, ...` with no initializer
|
|
247
|
+
if (!lua.isVariableDeclarationStatement(stmt) || (stmt.right && stmt.right.length > 0)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const vars = stmt.left;
|
|
251
|
+
const inlined = new Set();
|
|
252
|
+
for (const varId of vars) {
|
|
253
|
+
const varName = varId.text;
|
|
254
|
+
// Scan forward for assignment to same identifier
|
|
255
|
+
for (let j = i + 1; j < statements.length; j++) {
|
|
256
|
+
const candidate = statements[j];
|
|
257
|
+
if (lua.isAssignmentStatement(candidate) &&
|
|
258
|
+
candidate.left.length === 1 &&
|
|
259
|
+
lua.isIdentifier(candidate.left[0]) &&
|
|
260
|
+
candidate.left[0].text === varName &&
|
|
261
|
+
candidate.right.length === 1) {
|
|
262
|
+
// Don't inline if RHS references the variable (self-referencing)
|
|
263
|
+
if (containsIdentifier(candidate.right[0], varName))
|
|
264
|
+
break;
|
|
265
|
+
// Don't inline multi-variable assignments like `a, b = fn()`
|
|
266
|
+
// (already handled by the length check above)
|
|
267
|
+
// Replace assignment with variable declaration
|
|
268
|
+
const newDecl = lua.createVariableDeclarationStatement(lua.cloneIdentifier(candidate.left[0]), candidate.right[0]);
|
|
269
|
+
if (candidate.leadingComments) {
|
|
270
|
+
newDecl.leadingComments = candidate.leadingComments;
|
|
271
|
+
}
|
|
272
|
+
statements[j] = newDecl;
|
|
273
|
+
inlined.add(varName);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
// Reference to varName prevents inlining
|
|
277
|
+
if (containsIdentifier(candidate, varName))
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (inlined.size === 0)
|
|
282
|
+
continue;
|
|
283
|
+
changed = true;
|
|
284
|
+
const remaining = vars.filter((v) => !inlined.has(v.text));
|
|
285
|
+
if (remaining.length === 0) {
|
|
286
|
+
// Remove the forward declaration entirely
|
|
287
|
+
statements.splice(i, 1);
|
|
288
|
+
i--; // re-check this index
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Keep remaining vars in the forward declaration
|
|
292
|
+
statements[i] = lua.createVariableDeclarationStatement(remaining);
|
|
293
|
+
if (stmt.leadingComments) {
|
|
294
|
+
;
|
|
295
|
+
statements[i].leadingComments =
|
|
296
|
+
stmt.leadingComments;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
return changed;
|
|
302
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { CallTransform } from "./transforms.js";
|
|
3
|
+
export interface OptimizeFlags {
|
|
4
|
+
/** Inline `.filter()` calls as `for` loops with `ipairs`. Default: false */
|
|
5
|
+
filter?: boolean;
|
|
6
|
+
/** Rewrite `x = x + n` to `x += n` (Luau compound assignment). Default: false */
|
|
7
|
+
compoundAssignment?: boolean;
|
|
8
|
+
/** Reorder `Math.floor((a / b) * c)` to `a * c // b`. */
|
|
9
|
+
floorMultiply?: boolean;
|
|
10
|
+
/** Emit bare `string.find`/`table.find` for indexOf presence checks. */
|
|
11
|
+
indexOf?: boolean;
|
|
12
|
+
/** Shorten TSTL destructuring temp names (`____fn_result_N` -> `_rN`). */
|
|
13
|
+
shortenTemps?: boolean;
|
|
14
|
+
/** Merge forward-declared `local x` with its first `x = value` assignment. */
|
|
15
|
+
inlineLocals?: boolean;
|
|
16
|
+
/** Strip `tostring()` from number-typed template literal interpolations. */
|
|
17
|
+
numericConcat?: boolean;
|
|
18
|
+
/** Collapse `if x == nil then x = <literal> end` to `x = x or <literal>`. */
|
|
19
|
+
defaultParams?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const ALL_OPTIMIZE: Required<OptimizeFlags>;
|
|
22
|
+
/**
|
|
23
|
+
* Count `arr.filter(cb)` calls and return a set of file names where inlining
|
|
24
|
+
* should be skipped (the shared `__TS__ArrayFilter` helper is smaller).
|
|
25
|
+
*
|
|
26
|
+
* When `bundle` is true (luaBundle mode), all source files end up in a single
|
|
27
|
+
* output, so the total across the program is what matters. Otherwise each
|
|
28
|
+
* file is counted independently.
|
|
29
|
+
*/
|
|
30
|
+
export declare function countFilterCalls(program: ts.Program, bundle: boolean): Set<string>;
|
|
31
|
+
export declare function createOptimizeTransforms(filterSkipFiles: Set<string>): CallTransform[];
|
package/dist/optimize.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as tstl from "typescript-to-lua";
|
|
3
|
+
import { isMethodCall, isArrayType } from "./utils.js";
|
|
4
|
+
export const ALL_OPTIMIZE = {
|
|
5
|
+
filter: true,
|
|
6
|
+
compoundAssignment: true,
|
|
7
|
+
floorMultiply: true,
|
|
8
|
+
indexOf: true,
|
|
9
|
+
shortenTemps: true,
|
|
10
|
+
inlineLocals: true,
|
|
11
|
+
numericConcat: true,
|
|
12
|
+
defaultParams: true,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Count `arr.filter(cb)` calls and return a set of file names where inlining
|
|
16
|
+
* should be skipped (the shared `__TS__ArrayFilter` helper is smaller).
|
|
17
|
+
*
|
|
18
|
+
* When `bundle` is true (luaBundle mode), all source files end up in a single
|
|
19
|
+
* output, so the total across the program is what matters. Otherwise each
|
|
20
|
+
* file is counted independently.
|
|
21
|
+
*/
|
|
22
|
+
export function countFilterCalls(program, bundle) {
|
|
23
|
+
const skip = new Set();
|
|
24
|
+
const checker = program.getTypeChecker();
|
|
25
|
+
const sourceFiles = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile);
|
|
26
|
+
if (bundle) {
|
|
27
|
+
let total = 0;
|
|
28
|
+
for (const sf of sourceFiles) {
|
|
29
|
+
ts.forEachChild(sf, function visit(node) {
|
|
30
|
+
if (isArrayFilterCall(node, checker))
|
|
31
|
+
total++;
|
|
32
|
+
ts.forEachChild(node, visit);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (total > 1) {
|
|
36
|
+
for (const sf of sourceFiles) {
|
|
37
|
+
skip.add(sf.fileName);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (const sf of sourceFiles) {
|
|
43
|
+
let count = 0;
|
|
44
|
+
ts.forEachChild(sf, function visit(node) {
|
|
45
|
+
if (isArrayFilterCall(node, checker))
|
|
46
|
+
count++;
|
|
47
|
+
ts.forEachChild(node, visit);
|
|
48
|
+
});
|
|
49
|
+
if (count > 1) {
|
|
50
|
+
skip.add(sf.fileName);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return skip;
|
|
55
|
+
}
|
|
56
|
+
function isArrayFilterCall(node, checker) {
|
|
57
|
+
return (ts.isCallExpression(node) &&
|
|
58
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
59
|
+
node.expression.name.text === "filter" &&
|
|
60
|
+
node.arguments.length === 1 &&
|
|
61
|
+
isArrayType(node.expression.expression, checker));
|
|
62
|
+
}
|
|
63
|
+
export function createOptimizeTransforms(filterSkipFiles) {
|
|
64
|
+
let counter = 0;
|
|
65
|
+
return [
|
|
66
|
+
// arr.filter(cb) -> inline for loop with ipairs
|
|
67
|
+
{
|
|
68
|
+
match: (node, checker) => {
|
|
69
|
+
if (filterSkipFiles.has(node.getSourceFile().fileName))
|
|
70
|
+
return false;
|
|
71
|
+
return isMethodCall(node, checker, isArrayType, "filter", 1);
|
|
72
|
+
},
|
|
73
|
+
emit: (node, context) => {
|
|
74
|
+
const n = counter++;
|
|
75
|
+
const resultId = tstl.createIdentifier(`____opt_${n}`);
|
|
76
|
+
const valueId = tstl.createIdentifier(`____opt_v_${n}`);
|
|
77
|
+
const cbId = tstl.createIdentifier(`____opt_fn_${n}`);
|
|
78
|
+
const arr = context.transformExpression(node.expression.expression);
|
|
79
|
+
const cb = context.transformExpression(node.arguments[0]);
|
|
80
|
+
// Strip TSTL's context parameter (____) from the callback if present.
|
|
81
|
+
// Array callbacks are always called positionally; the context param is dead.
|
|
82
|
+
if (tstl.isFunctionExpression(cb) &&
|
|
83
|
+
cb.params &&
|
|
84
|
+
cb.params.length > 0 &&
|
|
85
|
+
cb.params[0].text === "____") {
|
|
86
|
+
cb.params = cb.params.slice(1);
|
|
87
|
+
}
|
|
88
|
+
// local ____opt_fn_N = <callback>
|
|
89
|
+
context.addPrecedingStatements(tstl.createVariableDeclarationStatement(cbId, cb, node));
|
|
90
|
+
// local ____opt_N = {}
|
|
91
|
+
context.addPrecedingStatements(tstl.createVariableDeclarationStatement(tstl.cloneIdentifier(resultId), tstl.createTableExpression(), node));
|
|
92
|
+
// ____opt_fn_N(____opt_v_N)
|
|
93
|
+
const filterCall = tstl.createCallExpression(tstl.cloneIdentifier(cbId), [
|
|
94
|
+
tstl.cloneIdentifier(valueId),
|
|
95
|
+
]);
|
|
96
|
+
// ____opt_N[#____opt_N + 1] = ____opt_v_N
|
|
97
|
+
const appendStmt = tstl.createAssignmentStatement(tstl.createTableIndexExpression(tstl.cloneIdentifier(resultId), tstl.createBinaryExpression(tstl.createUnaryExpression(tstl.cloneIdentifier(resultId), tstl.SyntaxKind.LengthOperator), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator)), tstl.cloneIdentifier(valueId));
|
|
98
|
+
// if ____opt_fn_N(____opt_v_N) then ... end
|
|
99
|
+
const ifStmt = tstl.createIfStatement(filterCall, tstl.createBlock([appendStmt]));
|
|
100
|
+
// for _, ____opt_v_N in ipairs(arr) do ... end
|
|
101
|
+
context.addPrecedingStatements(tstl.createForInStatement(tstl.createBlock([ifStmt]), [tstl.createIdentifier("_"), valueId], [tstl.createCallExpression(tstl.createIdentifier("ipairs"), [arr])], node));
|
|
102
|
+
return tstl.cloneIdentifier(resultId);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as tstl from "typescript-to-lua";
|
|
3
|
+
export type CallTransform = {
|
|
4
|
+
match: (node: ts.CallExpression, checker: ts.TypeChecker) => boolean;
|
|
5
|
+
emit: (node: ts.CallExpression, context: tstl.TransformationContext) => tstl.Expression;
|
|
6
|
+
};
|
|
7
|
+
export declare const CALL_TRANSFORMS: CallTransform[];
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as tstl from "typescript-to-lua";
|
|
3
|
+
import { isMethodCall, isNamespaceCall, isGlobalCall, isStringType, isArrayType, createNamespacedCall, createStringFindCall, } from "./utils.js";
|
|
4
|
+
export const CALL_TRANSFORMS = [
|
|
5
|
+
// JSON.stringify(val) -> lljson.encode(val)
|
|
6
|
+
{
|
|
7
|
+
match: (node) => isNamespaceCall(node, "JSON", "stringify"),
|
|
8
|
+
emit: (node, context) => {
|
|
9
|
+
const args = node.arguments.map((a) => context.transformExpression(a));
|
|
10
|
+
return createNamespacedCall("lljson", "encode", args, node);
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
// JSON.parse(str) -> lljson.decode(str)
|
|
14
|
+
{
|
|
15
|
+
match: (node) => isNamespaceCall(node, "JSON", "parse"),
|
|
16
|
+
emit: (node, context) => {
|
|
17
|
+
const args = node.arguments.map((a) => context.transformExpression(a));
|
|
18
|
+
return createNamespacedCall("lljson", "decode", args, node);
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
// btoa(str) -> llbase64.encode(str)
|
|
22
|
+
{
|
|
23
|
+
match: (node) => isGlobalCall(node, "btoa") && node.arguments.length === 1,
|
|
24
|
+
emit: (node, context) => {
|
|
25
|
+
const args = node.arguments.map((a) => context.transformExpression(a));
|
|
26
|
+
return createNamespacedCall("llbase64", "encode", args, node);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
// atob(str) -> llbase64.decode(str)
|
|
30
|
+
{
|
|
31
|
+
match: (node) => isGlobalCall(node, "atob") && node.arguments.length === 1,
|
|
32
|
+
emit: (node, context) => {
|
|
33
|
+
const args = node.arguments.map((a) => context.transformExpression(a));
|
|
34
|
+
return createNamespacedCall("llbase64", "decode", args, node);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
// str.toUpperCase() -> ll.ToUpper(str)
|
|
38
|
+
{
|
|
39
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "toUpperCase", 0),
|
|
40
|
+
emit: (node, context) => {
|
|
41
|
+
const str = context.transformExpression(node.expression.expression);
|
|
42
|
+
return createNamespacedCall("ll", "ToUpper", [str], node);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
// str.toLowerCase() -> ll.ToLower(str)
|
|
46
|
+
{
|
|
47
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "toLowerCase", 0),
|
|
48
|
+
emit: (node, context) => {
|
|
49
|
+
const str = context.transformExpression(node.expression.expression);
|
|
50
|
+
return createNamespacedCall("ll", "ToLower", [str], node);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
// str.trim() -> ll.StringTrim(str, STRING_TRIM)
|
|
54
|
+
{
|
|
55
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "trim", 0),
|
|
56
|
+
emit: (node, context) => {
|
|
57
|
+
const str = context.transformExpression(node.expression.expression);
|
|
58
|
+
return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM")], node);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
// str.trimStart() -> ll.StringTrim(str, STRING_TRIM_HEAD)
|
|
62
|
+
{
|
|
63
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "trimStart", 0),
|
|
64
|
+
emit: (node, context) => {
|
|
65
|
+
const str = context.transformExpression(node.expression.expression);
|
|
66
|
+
return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM_HEAD")], node);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
// str.trimEnd() -> ll.StringTrim(str, STRING_TRIM_TAIL)
|
|
70
|
+
{
|
|
71
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "trimEnd", 0),
|
|
72
|
+
emit: (node, context) => {
|
|
73
|
+
const str = context.transformExpression(node.expression.expression);
|
|
74
|
+
return createNamespacedCall("ll", "StringTrim", [str, tstl.createIdentifier("STRING_TRIM_TAIL")], node);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
// str.indexOf(x) -> (string.find(str, x, 1, true) or 0) - 1
|
|
78
|
+
{
|
|
79
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "indexOf", 1),
|
|
80
|
+
emit: (node, context) => {
|
|
81
|
+
const str = context.transformExpression(node.expression.expression);
|
|
82
|
+
const search = context.transformExpression(node.arguments[0]);
|
|
83
|
+
const findOrZero = tstl.createBinaryExpression(createStringFindCall(str, search, node), tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
|
|
84
|
+
return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
// str.indexOf(x, fromIndex) -> (string.find(str, x, fromIndex + 1, true) or 0) - 1
|
|
88
|
+
{
|
|
89
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "indexOf", 2),
|
|
90
|
+
emit: (node, context) => {
|
|
91
|
+
const str = context.transformExpression(node.expression.expression);
|
|
92
|
+
const search = context.transformExpression(node.arguments[0]);
|
|
93
|
+
const fromArg = node.arguments[1];
|
|
94
|
+
const init = ts.isNumericLiteral(fromArg)
|
|
95
|
+
? tstl.createNumericLiteral(Number(fromArg.text) + 1)
|
|
96
|
+
: tstl.createBinaryExpression(context.transformExpression(fromArg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, node);
|
|
97
|
+
const findCall = createNamespacedCall("string", "find", [str, search, init, tstl.createBooleanLiteral(true)], node);
|
|
98
|
+
const findOrZero = tstl.createBinaryExpression(findCall, tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
|
|
99
|
+
return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
// str.includes(x) -> string.find(str, x, 1, true) ~= nil
|
|
103
|
+
{
|
|
104
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "includes", 1),
|
|
105
|
+
emit: (node, context) => {
|
|
106
|
+
const str = context.transformExpression(node.expression.expression);
|
|
107
|
+
const search = context.transformExpression(node.arguments[0]);
|
|
108
|
+
return tstl.createBinaryExpression(createStringFindCall(str, search, node), tstl.createNilLiteral(), tstl.SyntaxKind.InequalityOperator, node);
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
// str.split(sep) -> string.split(str, sep) (1-arg only)
|
|
112
|
+
{
|
|
113
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "split", 1),
|
|
114
|
+
emit: (node, context) => {
|
|
115
|
+
const str = context.transformExpression(node.expression.expression);
|
|
116
|
+
const sep = context.transformExpression(node.arguments[0]);
|
|
117
|
+
return createNamespacedCall("string", "split", [str, sep], node);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
// str.repeat(n) -> string.rep(str, n)
|
|
121
|
+
{
|
|
122
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "repeat", 1),
|
|
123
|
+
emit: (node, context) => {
|
|
124
|
+
const str = context.transformExpression(node.expression.expression);
|
|
125
|
+
const n = context.transformExpression(node.arguments[0]);
|
|
126
|
+
return createNamespacedCall("string", "rep", [str, n], node);
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
// str.startsWith(search) -> string.find(str, search, 1, true) == 1 (1-arg only)
|
|
130
|
+
{
|
|
131
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, "startsWith", 1),
|
|
132
|
+
emit: (node, context) => {
|
|
133
|
+
const str = context.transformExpression(node.expression.expression);
|
|
134
|
+
const search = context.transformExpression(node.arguments[0]);
|
|
135
|
+
return tstl.createBinaryExpression(createStringFindCall(str, search, node), tstl.createNumericLiteral(1), tstl.SyntaxKind.EqualityOperator, node);
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
// str.substring(start) -> string.sub(str, start + 1)
|
|
139
|
+
// str.substring(start, end) -> string.sub(str, start + 1, end)
|
|
140
|
+
{
|
|
141
|
+
match: (node, checker) => {
|
|
142
|
+
if (!isMethodCall(node, checker, isStringType, "substring")) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return node.arguments.length === 1 || node.arguments.length === 2;
|
|
146
|
+
},
|
|
147
|
+
emit: (node, context) => {
|
|
148
|
+
const str = context.transformExpression(node.expression.expression);
|
|
149
|
+
const startArg = node.arguments[0];
|
|
150
|
+
const start = ts.isNumericLiteral(startArg)
|
|
151
|
+
? tstl.createNumericLiteral(Number(startArg.text) + 1)
|
|
152
|
+
: tstl.createBinaryExpression(context.transformExpression(startArg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, node);
|
|
153
|
+
const args = [str, start];
|
|
154
|
+
if (node.arguments.length === 2) {
|
|
155
|
+
args.push(context.transformExpression(node.arguments[1]));
|
|
156
|
+
}
|
|
157
|
+
return createNamespacedCall("string", "sub", args, node);
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
// str.replace / str.replaceAll -> ll.ReplaceSubString(str, search, replacement, count)
|
|
161
|
+
// count=1 for replace (first match only), count=0 for replaceAll
|
|
162
|
+
...[
|
|
163
|
+
["replace", 1],
|
|
164
|
+
["replaceAll", 0],
|
|
165
|
+
].map(([method, count]) => ({
|
|
166
|
+
match: (node, checker) => isMethodCall(node, checker, isStringType, method, 2),
|
|
167
|
+
emit: (node, context) => {
|
|
168
|
+
const str = context.transformExpression(node.expression.expression);
|
|
169
|
+
const search = context.transformExpression(node.arguments[0]);
|
|
170
|
+
const replacement = context.transformExpression(node.arguments[1]);
|
|
171
|
+
return createNamespacedCall("ll", "ReplaceSubString", [str, search, replacement, tstl.createNumericLiteral(count)], node);
|
|
172
|
+
},
|
|
173
|
+
})),
|
|
174
|
+
// arr.includes(val) -> table.find(arr, val) ~= nil
|
|
175
|
+
{
|
|
176
|
+
match: (node, checker) => isMethodCall(node, checker, isArrayType, "includes", 1),
|
|
177
|
+
emit: (node, context) => {
|
|
178
|
+
const arr = context.transformExpression(node.expression.expression);
|
|
179
|
+
const val = context.transformExpression(node.arguments[0]);
|
|
180
|
+
const findCall = createNamespacedCall("table", "find", [arr, val], node);
|
|
181
|
+
return tstl.createBinaryExpression(findCall, tstl.createNilLiteral(), tstl.SyntaxKind.InequalityOperator, node);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
// arr.indexOf(val) -> (table.find(arr, val) or 0) - 1 (1-arg only)
|
|
185
|
+
{
|
|
186
|
+
match: (node, checker) => isMethodCall(node, checker, isArrayType, "indexOf", 1),
|
|
187
|
+
emit: (node, context) => {
|
|
188
|
+
const arr = context.transformExpression(node.expression.expression);
|
|
189
|
+
const val = context.transformExpression(node.arguments[0]);
|
|
190
|
+
const findCall = createNamespacedCall("table", "find", [arr, val], node);
|
|
191
|
+
const findOrZero = tstl.createBinaryExpression(findCall, tstl.createNumericLiteral(0), tstl.SyntaxKind.OrOperator, node);
|
|
192
|
+
return tstl.createBinaryExpression(tstl.createParenthesizedExpression(findOrZero), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
];
|