@gwigz/slua-tstl-plugin 0.2.0 → 0.3.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 +34 -0
- package/dist/index.js +208 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
7
|
- Translates TypeScript patterns to native Luau/LSL equivalents (see below)
|
|
8
|
+
- Automatically adjusts `ll.*` index arguments and return values from 0-based to 1-based
|
|
9
|
+
- Optimizes self-reassignment array concat/spread to in-place `table.extend`
|
|
8
10
|
- Handles adjusting `Vector`, `Quaternion`, and `UUID` casing
|
|
9
11
|
- Validates `luaTarget` is set to `Luau`
|
|
10
12
|
|
|
@@ -46,6 +48,7 @@ String methods are translated to LSL `ll.*` functions or Luau `string.*` stdlib
|
|
|
46
48
|
| `str.repeat(n)` | `string.rep(str, n)` |
|
|
47
49
|
| `str.substring(start)` | `string.sub(str, start + 1)` |
|
|
48
50
|
| `str.substring(s, e)` | `string.sub(str, s + 1, e)` |
|
|
51
|
+
| `str.replaceAll(a, b)` | `ll.ReplaceSubString(str, a, b, 0)` |
|
|
49
52
|
|
|
50
53
|
> [!NOTE]
|
|
51
54
|
> `str.indexOf(x, fromIndex)` and `str.startsWith(x, position)` with a second argument fall through to TSTL's default handling. Similarly, `str.split()` with no separator is not transformed.
|
|
@@ -87,6 +90,37 @@ Comparisons of a bitwise AND against zero are automatically optimized to `bit32.
|
|
|
87
90
|
|
|
88
91
|
This works with `!=`, `==`, and with the zero on either side (`0 !== (a & b)`).
|
|
89
92
|
|
|
93
|
+
### `ll.*` index adjustment
|
|
94
|
+
|
|
95
|
+
SLua's `ll.*` functions use 1-based indexing (Lua convention), but TypeScript uses 0-based. The plugin automatically adjusts index arguments and return values based on `@indexArg` and `@indexReturn` JSDoc tags in the type definitions:
|
|
96
|
+
|
|
97
|
+
| TypeScript | Lua output |
|
|
98
|
+
| -------------------------------- | ------------------------------------------------------------ |
|
|
99
|
+
| `ll.GetSubString("hello", 0, 2)` | `ll.GetSubString("hello", 1, 3)` |
|
|
100
|
+
| `ll.GetSubString("hello", i, j)` | `ll.GetSubString("hello", i + 1, j + 1)` |
|
|
101
|
+
| `ll.ListFindList(a, b)` | `____tmp = ll.ListFindList(a, b); ____tmp and (____tmp - 1)` |
|
|
102
|
+
|
|
103
|
+
- **`@indexArg`** parameters get `+ 1` (constant-folded for literals)
|
|
104
|
+
- **`@indexReturn`** wraps the result in a nil-safe `____tmp and (____tmp - 1)` expression
|
|
105
|
+
- Functions without these tags (e.g. `ll.Say`) are left unchanged
|
|
106
|
+
|
|
107
|
+
### Array concat self-assignment
|
|
108
|
+
|
|
109
|
+
When an array is reassigned to itself with additional elements appended, the plugin emits `table.extend` (SLua's in-place append) instead of TSTL's `__TS__ArrayConcat` which allocates a new table:
|
|
110
|
+
|
|
111
|
+
| TypeScript | Lua output |
|
|
112
|
+
| ---------------------------- | --------------------------------------- |
|
|
113
|
+
| `arr = arr.concat(b)` | `table.extend(arr, b)` |
|
|
114
|
+
| `arr = arr.concat(b, c)` | `table.extend(table.extend(arr, b), c)` |
|
|
115
|
+
| `arr = [...arr, ...b]` | `table.extend(arr, b)` |
|
|
116
|
+
| `arr = [...arr, ...b, ...c]` | `table.extend(table.extend(arr, b), c)` |
|
|
117
|
+
|
|
118
|
+
This optimization only applies when:
|
|
119
|
+
|
|
120
|
+
- The expression is a statement (not `const result = arr.concat(b)`)
|
|
121
|
+
- The LHS is a simple identifier matching the receiver/first spread
|
|
122
|
+
- All concat arguments / spread expressions are array-typed
|
|
123
|
+
|
|
90
124
|
### Floor division
|
|
91
125
|
|
|
92
126
|
`Math.floor(a / b)` is translated to the native Luau floor division operator `//`:
|
package/dist/index.js
CHANGED
|
@@ -113,6 +113,17 @@ function isArrayType(expr, checker) {
|
|
|
113
113
|
const type = checker.getTypeAtLocation(expr);
|
|
114
114
|
return checker.isArrayLikeType(type);
|
|
115
115
|
}
|
|
116
|
+
function isDetectedEventType(expr, checker) {
|
|
117
|
+
const type = checker.getTypeAtLocation(expr);
|
|
118
|
+
return type.symbol?.name === "DetectedEvent";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Returns true when `node` is `detectedEvent.index` — a property access
|
|
122
|
+
* on a `DetectedEvent` reading the `.index` field.
|
|
123
|
+
*/
|
|
124
|
+
function isDetectedEventIndex(node, checker) {
|
|
125
|
+
return node.name.text === "index" && isDetectedEventType(node.expression, checker);
|
|
126
|
+
}
|
|
116
127
|
/**
|
|
117
128
|
* Checks whether `node` is `obj.method(args)` where `obj` matches the
|
|
118
129
|
* given type predicate and the method name matches.
|
|
@@ -149,6 +160,175 @@ function isGlobalCall(node, name) {
|
|
|
149
160
|
function createNamespacedCall(ns, fn, args, node) {
|
|
150
161
|
return tstl.createCallExpression(tstl.createTableIndexExpression(tstl.createIdentifier(ns), tstl.createStringLiteral(fn)), args, node);
|
|
151
162
|
}
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Self-reassignment array concat -> table.extend optimization
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
/**
|
|
167
|
+
* Detects `arr = arr.concat(b, c, ...)` where LHS is a simple identifier,
|
|
168
|
+
* the receiver matches LHS, and all concat arguments are array-typed.
|
|
169
|
+
*/
|
|
170
|
+
function extractConcatSelfAssignment(expr, checker) {
|
|
171
|
+
if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
172
|
+
return null;
|
|
173
|
+
if (!ts.isIdentifier(expr.left))
|
|
174
|
+
return null;
|
|
175
|
+
if (!ts.isCallExpression(expr.right))
|
|
176
|
+
return null;
|
|
177
|
+
if (!ts.isPropertyAccessExpression(expr.right.expression))
|
|
178
|
+
return null;
|
|
179
|
+
if (expr.right.expression.name.text !== "concat")
|
|
180
|
+
return null;
|
|
181
|
+
if (!ts.isIdentifier(expr.right.expression.expression))
|
|
182
|
+
return null;
|
|
183
|
+
if (expr.right.expression.expression.text !== expr.left.text)
|
|
184
|
+
return null;
|
|
185
|
+
if (expr.right.arguments.length === 0)
|
|
186
|
+
return null;
|
|
187
|
+
for (const arg of expr.right.arguments) {
|
|
188
|
+
if (!isArrayType(arg, checker))
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return { name: expr.left, args: expr.right.arguments };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Detects `arr = [...arr, ...b, ...c]` where LHS is a simple identifier,
|
|
195
|
+
* all elements are spreads, the first spread matches LHS, and all tail
|
|
196
|
+
* spread expressions are array-typed.
|
|
197
|
+
*/
|
|
198
|
+
function extractSpreadSelfAssignment(expr, checker) {
|
|
199
|
+
if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
200
|
+
return null;
|
|
201
|
+
if (!ts.isIdentifier(expr.left))
|
|
202
|
+
return null;
|
|
203
|
+
if (!ts.isArrayLiteralExpression(expr.right))
|
|
204
|
+
return null;
|
|
205
|
+
const elements = expr.right.elements;
|
|
206
|
+
if (elements.length < 2)
|
|
207
|
+
return null;
|
|
208
|
+
for (const el of elements) {
|
|
209
|
+
if (!ts.isSpreadElement(el))
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const first = elements[0];
|
|
213
|
+
if (!ts.isIdentifier(first.expression))
|
|
214
|
+
return null;
|
|
215
|
+
if (first.expression.text !== expr.left.text)
|
|
216
|
+
return null;
|
|
217
|
+
const tailArgs = [];
|
|
218
|
+
for (let i = 1; i < elements.length; i++) {
|
|
219
|
+
const spread = elements[i];
|
|
220
|
+
if (!isArrayType(spread.expression, checker))
|
|
221
|
+
return null;
|
|
222
|
+
tailArgs.push(spread.expression);
|
|
223
|
+
}
|
|
224
|
+
return { name: expr.left, args: tailArgs };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Builds nested `table.extend` calls:
|
|
228
|
+
* - Single arg: `table.extend(arr, b)`
|
|
229
|
+
* - Multiple: `table.extend(table.extend(arr, b), c)`
|
|
230
|
+
*/
|
|
231
|
+
function emitChainedExtend(target, args, node) {
|
|
232
|
+
let result = createNamespacedCall("table", "extend", [target, args[0]], node);
|
|
233
|
+
for (let i = 1; i < args.length; i++) {
|
|
234
|
+
result = createNamespacedCall("table", "extend", [result, args[i]], node);
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* For an `ll.Foo(...)` call, inspect the resolved signature's JSDoc tags
|
|
240
|
+
* to determine which arguments have `@indexArg` semantics and whether
|
|
241
|
+
* the return value has `@indexReturn` semantics.
|
|
242
|
+
*/
|
|
243
|
+
function getLLIndexSemantics(node, checker) {
|
|
244
|
+
// Must be ll.Something(...)
|
|
245
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
246
|
+
return null;
|
|
247
|
+
if (!ts.isIdentifier(node.expression.expression))
|
|
248
|
+
return null;
|
|
249
|
+
if (node.expression.expression.text !== "ll")
|
|
250
|
+
return null;
|
|
251
|
+
const sig = checker.getResolvedSignature(node);
|
|
252
|
+
const decl = sig?.declaration;
|
|
253
|
+
if (!decl || !ts.isFunctionDeclaration(decl))
|
|
254
|
+
return null;
|
|
255
|
+
const tags = ts.getJSDocTags(decl);
|
|
256
|
+
const indexArgs = new Set();
|
|
257
|
+
let indexReturn = false;
|
|
258
|
+
for (const tag of tags) {
|
|
259
|
+
const tagName = tag.tagName.text;
|
|
260
|
+
if (tagName === "indexArg") {
|
|
261
|
+
const text = typeof tag.comment === "string"
|
|
262
|
+
? tag.comment
|
|
263
|
+
: Array.isArray(tag.comment)
|
|
264
|
+
? tag.comment.map((c) => c.text).join("")
|
|
265
|
+
: undefined;
|
|
266
|
+
if (text) {
|
|
267
|
+
indexArgs.add(text.trim());
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else if (tagName === "indexReturn") {
|
|
271
|
+
indexReturn = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (indexArgs.size === 0 && !indexReturn)
|
|
275
|
+
return null;
|
|
276
|
+
return { indexArgs, indexReturn };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Adjusts a 0-based index argument to 1-based for Lua.
|
|
280
|
+
* Constant-folds numeric literals; otherwise emits `expr + 1`.
|
|
281
|
+
*/
|
|
282
|
+
function adjustIndexArg(arg, context) {
|
|
283
|
+
if (ts.isNumericLiteral(arg)) {
|
|
284
|
+
return tstl.createNumericLiteral(Number(arg.text) + 1);
|
|
285
|
+
}
|
|
286
|
+
// DetectedEvent.index is already 1-based at runtime. The PropertyAccess
|
|
287
|
+
// visitor emits `- 1` to make it 0-based for TS, and @indexArg adds `+ 1`.
|
|
288
|
+
// These cancel out, so emit the raw property access directly.
|
|
289
|
+
if (ts.isPropertyAccessExpression(arg) && isDetectedEventIndex(arg, context.checker)) {
|
|
290
|
+
const obj = context.transformExpression(arg.expression);
|
|
291
|
+
return tstl.createTableIndexExpression(obj, tstl.createStringLiteral("index"));
|
|
292
|
+
}
|
|
293
|
+
return tstl.createBinaryExpression(context.transformExpression(arg), tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator, arg);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Emit an `ll.Foo(...)` call with automatic index adjustments:
|
|
297
|
+
* - `@indexArg` parameters get `+1`
|
|
298
|
+
* - `@indexReturn` wraps the result in a nil-safe `__tmp and (__tmp - 1)`
|
|
299
|
+
*/
|
|
300
|
+
function emitLLIndexCall(node, context, semantics) {
|
|
301
|
+
const expr = node.expression;
|
|
302
|
+
const fnName = expr.name.text;
|
|
303
|
+
// Resolve parameter names from the declaration
|
|
304
|
+
const sig = context.checker.getResolvedSignature(node);
|
|
305
|
+
const params = sig?.declaration && ts.isFunctionDeclaration(sig.declaration)
|
|
306
|
+
? sig.declaration.parameters
|
|
307
|
+
: undefined;
|
|
308
|
+
const args = node.arguments.map((arg, i) => {
|
|
309
|
+
const paramName = params?.[i]?.name;
|
|
310
|
+
const name = paramName && ts.isIdentifier(paramName) ? paramName.text : undefined;
|
|
311
|
+
if (name && semantics.indexArgs.has(name)) {
|
|
312
|
+
return adjustIndexArg(arg, context);
|
|
313
|
+
}
|
|
314
|
+
return context.transformExpression(arg);
|
|
315
|
+
});
|
|
316
|
+
const call = createNamespacedCall("ll", fnName, args, node);
|
|
317
|
+
if (!semantics.indexReturn) {
|
|
318
|
+
return call;
|
|
319
|
+
}
|
|
320
|
+
// Check if return type is number-like (skip for list/string returns like llFindNotecardTextSync)
|
|
321
|
+
const retType = context.checker.getTypeAtLocation(node);
|
|
322
|
+
const isNumberReturn = !!(retType.flags & ts.TypeFlags.NumberLike) ||
|
|
323
|
+
(retType.isUnion() && retType.types.some((t) => !!(t.flags & ts.TypeFlags.NumberLike)));
|
|
324
|
+
if (!isNumberReturn) {
|
|
325
|
+
return call;
|
|
326
|
+
}
|
|
327
|
+
// Nil-safe return adjustment: `local __tmp = ll.Fn(...); __tmp and (__tmp - 1)`
|
|
328
|
+
const tempId = tstl.createIdentifier("____tmp");
|
|
329
|
+
context.addPrecedingStatements(tstl.createVariableDeclarationStatement(tempId, call, node));
|
|
330
|
+
return tstl.createBinaryExpression(tstl.cloneIdentifier(tempId), tstl.createParenthesizedExpression(tstl.createBinaryExpression(tstl.cloneIdentifier(tempId), tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator)), tstl.SyntaxKind.AndOperator, node);
|
|
331
|
+
}
|
|
152
332
|
const CALL_TRANSFORMS = [
|
|
153
333
|
// JSON.stringify(val) -> lljson.encode(val)
|
|
154
334
|
{
|
|
@@ -351,6 +531,12 @@ const plugin = {
|
|
|
351
531
|
result.table.text = replacement;
|
|
352
532
|
}
|
|
353
533
|
}
|
|
534
|
+
// DetectedEvent.index is 1-based at runtime in SLua; emit `obj.index - 1`
|
|
535
|
+
// so TypeScript sees a 0-based value. This composes correctly with
|
|
536
|
+
// @indexArg functions (which add +1, cancelling out the -1).
|
|
537
|
+
if (isDetectedEventIndex(node, context.checker)) {
|
|
538
|
+
return tstl.createBinaryExpression(result, tstl.createNumericLiteral(1), tstl.SyntaxKind.SubtractionOperator, node);
|
|
539
|
+
}
|
|
354
540
|
return result;
|
|
355
541
|
},
|
|
356
542
|
[ts.SyntaxKind.BinaryExpression]: (node, context) => {
|
|
@@ -400,6 +586,15 @@ const plugin = {
|
|
|
400
586
|
const call = createBit32Call(compoundFn, [left, right], node);
|
|
401
587
|
return [tstl.createAssignmentStatement(left, call, node)];
|
|
402
588
|
}
|
|
589
|
+
// Self-reassignment concat/spread -> table.extend (in-place, no assignment needed)
|
|
590
|
+
const concatMatch = extractConcatSelfAssignment(node.expression, context.checker) ??
|
|
591
|
+
extractSpreadSelfAssignment(node.expression, context.checker);
|
|
592
|
+
if (concatMatch) {
|
|
593
|
+
const target = context.transformExpression(concatMatch.name);
|
|
594
|
+
const args = concatMatch.args.map((a) => context.transformExpression(a));
|
|
595
|
+
const call = emitChainedExtend(target, args, node);
|
|
596
|
+
return [tstl.createExpressionStatement(call, node)];
|
|
597
|
+
}
|
|
403
598
|
}
|
|
404
599
|
return context.superTransformStatements(node);
|
|
405
600
|
},
|
|
@@ -410,6 +605,11 @@ const plugin = {
|
|
|
410
605
|
return transform.emit(node, context);
|
|
411
606
|
}
|
|
412
607
|
}
|
|
608
|
+
// ll.* index-semantics: automatic 0->1 index adjustment
|
|
609
|
+
const indexSemantics = getLLIndexSemantics(node, context.checker);
|
|
610
|
+
if (indexSemantics) {
|
|
611
|
+
return emitLLIndexCall(node, context, indexSemantics);
|
|
612
|
+
}
|
|
413
613
|
// `Math.floor(a / b)` -> `a // b` (native Luau floor division operator)
|
|
414
614
|
if (isMathFloor(node)) {
|
|
415
615
|
const arg = node.arguments[0];
|
|
@@ -445,6 +645,14 @@ const plugin = {
|
|
|
445
645
|
return diagnostics;
|
|
446
646
|
},
|
|
447
647
|
beforeEmit(program, _options, _emitHost, result) {
|
|
648
|
+
// Strip internal @indexArg / @indexReturn JSDoc tags from Lua comments.
|
|
649
|
+
// These are only consumed by the plugin at transpile time; they should
|
|
650
|
+
// not leak into the output as Lua comments.
|
|
651
|
+
for (const file of result) {
|
|
652
|
+
file.code = file.code
|
|
653
|
+
.replace(/^--\s*@index(?:Arg|Return)\b.*\n/gm, "")
|
|
654
|
+
.replace(/^(---.*)\n(?:-- *\n)+(?=local |ll\.)/gm, "$1\n");
|
|
655
|
+
}
|
|
448
656
|
// Strip empty module boilerplate from files without explicit exports.
|
|
449
657
|
// `moduleDetection: "force"` causes TSTL to wrap every file as a module;
|
|
450
658
|
// standalone SLua scripts don't need the ____exports wrapper.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gwigz/slua-tstl-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "TypeScriptToLua plugin for targeting Second Life's SLua runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"typescript-to-lua": "^1.33.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"typescript": "~5.
|
|
31
|
+
"typescript": "~5.9.3"
|
|
32
32
|
}
|
|
33
33
|
}
|