@tilua/compiler 5.0.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/{chunk-Y7QGXLCV.js → chunk-EQE22LX7.js} +246 -11
- package/dist/cli.cjs +248 -13
- package/dist/cli.js +3 -3
- package/dist/index.cjs +246 -11
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/runtime/array.luau +11 -0
package/README.md
CHANGED
|
@@ -75,6 +75,14 @@ JavaScript module the build loads.
|
|
|
75
75
|
| `for (const item in xs)` | `for _, item in xs do … end` — an array or a table walked for its values |
|
|
76
76
|
| `for (const item in step)` | `for item in step do … end` — an iterator function |
|
|
77
77
|
| `for (const [k, v] in pairs(t))` | `for item in unpack(pairs(t), 1, 3) do local k, v = item[1], item[2] … end` — an iteration is `[step, state, first]` |
|
|
78
|
+
| `t ??= v` | `if t == nil then t = v end` — what addresses `t` read once, into locals |
|
|
79
|
+
| `{ m() { … } }` | `{ m = function(this) … end }` — called with `:` |
|
|
80
|
+
| `break outer` from a loop inside `outer` | a flag `outer` declares, raised, and a `break`; after each loop on the way out, a check that breaks again — or, at `outer`, does the `break` or `continue` |
|
|
81
|
+
| `throw value` | `error(value, 0)` — level 0, so a `catch` is handed exactly what was thrown |
|
|
82
|
+
| `try { a } catch (e) { b }` | `local ok, result = pcall(function() a end) if not ok then local e = result b end` |
|
|
83
|
+
| `try { a } catch (e) { b } finally { c }` | `a` and then `b` each in a `pcall`, `c` after them, and what neither caught thrown again |
|
|
84
|
+
| a `return`, `break` or `continue` that leaves a `try` | its function returns a code — and the value — and an `if` after the `pcall` does the jump there |
|
|
85
|
+
| code after a `return`, `break` or `continue` | left out: Luau allows nothing after one in its block |
|
|
78
86
|
| `pcall(f)`, and other globals Luau answers several values from | the type library's own function, answering one — see below |
|
|
79
87
|
| `function f(a, ...rest)` | `function f(a, ...) local rest = {...}` |
|
|
80
88
|
| `scriptArgs` | the chunk's own `...`, as `local scriptArgs = { ... }` |
|
|
@@ -83,6 +91,8 @@ JavaScript module the build loads.
|
|
|
83
91
|
| `import("./shop")` | `G.import("src/shop")` — the bundle's `require`, wrapped |
|
|
84
92
|
| `a ?? b` | `if a ~= nil then a else b` for a name; otherwise a function that reads `a` once |
|
|
85
93
|
| `n++` / `n--` | `n += 1` / `n -= 1` |
|
|
94
|
+
| `s + t`, `s += t` with a string on a side | `s .. t`, `s ..= t` |
|
|
95
|
+
| `a + b` where only the values can say — `any`, `string \| number` | `tilua_add(a, b)`: joins if either is a string, else adds |
|
|
86
96
|
| `"key" in t` | `(t["key"] ~= nil)` |
|
|
87
97
|
| `v instanceof Dog` | `tilua_instanceof(v, Dog)` — walks `v.ClassObject`, then each `.ParentClass` |
|
|
88
98
|
| `{ a: 1, get b() { … }, set b(v) { … } }` | `tilua_accessors({ a = 1 }, { b = … }, { b = … })` — a metatable runs the accessors |
|
|
@@ -245,7 +245,29 @@ var standardLowering = { plugin: plugin2, from: "tilua (standard library)" };
|
|
|
245
245
|
|
|
246
246
|
// src/lower.ts
|
|
247
247
|
function lower(program2, scopes, options = {}) {
|
|
248
|
-
|
|
248
|
+
const result2 = new Lowerer(program2, scopes, options).run();
|
|
249
|
+
dropUnreachable(result2.statements);
|
|
250
|
+
return result2;
|
|
251
|
+
}
|
|
252
|
+
function dropUnreachable(statements) {
|
|
253
|
+
const exit = statements.findIndex((s) => s.type === "ReturnStatement" || s.type === "BreakStatement" || s.type === "ContinueStatement");
|
|
254
|
+
if (exit >= 0) statements.length = exit + 1;
|
|
255
|
+
for (const statement of statements) visitBlocks(statement);
|
|
256
|
+
}
|
|
257
|
+
function visitBlocks(node) {
|
|
258
|
+
if (!node || typeof node !== "object") return;
|
|
259
|
+
if (Array.isArray(node)) {
|
|
260
|
+
for (const item of node) visitBlocks(item);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const record = node;
|
|
264
|
+
if (record.type === "Block" && Array.isArray(record.statements)) {
|
|
265
|
+
dropUnreachable(record.statements);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
for (const key in record) {
|
|
269
|
+
if (key !== "line" && key !== "column") visitBlocks(record[key]);
|
|
270
|
+
}
|
|
249
271
|
}
|
|
250
272
|
var Lowerer = class {
|
|
251
273
|
constructor(source, scopes, options) {
|
|
@@ -287,6 +309,11 @@ var Lowerer = class {
|
|
|
287
309
|
exportsName;
|
|
288
310
|
/** Globals generated code calls, renamed where the source shadows them. */
|
|
289
311
|
builtins = /* @__PURE__ */ new Map();
|
|
312
|
+
/** The `try` blocks being lowered in the current function, innermost
|
|
313
|
+
* last; see `tryStatement`. A function written inside one starts afresh. */
|
|
314
|
+
tryFrames = [];
|
|
315
|
+
/** The loops being lowered in the current function, innermost last. */
|
|
316
|
+
loops = [];
|
|
290
317
|
/** Whether the output has to be plain Lua 5.1 rather than Luau. */
|
|
291
318
|
lua51;
|
|
292
319
|
/** The source line each emitted statement came from. */
|
|
@@ -652,6 +679,16 @@ local function ${typeOf}(value, known)
|
|
|
652
679
|
end`).body.statements);
|
|
653
680
|
}
|
|
654
681
|
}
|
|
682
|
+
const add = this.helpers.get("add");
|
|
683
|
+
if (add) {
|
|
684
|
+
out.push(...parseLuau(`
|
|
685
|
+
local function ${add}(a, b)
|
|
686
|
+
if ${this.builtin("type").name}(a) == "string" or ${this.builtin("type").name}(b) == "string" then
|
|
687
|
+
return ${this.builtin("tostring").name}(a) .. ${this.builtin("tostring").name}(b)
|
|
688
|
+
end
|
|
689
|
+
return a + b
|
|
690
|
+
end`).body.statements);
|
|
691
|
+
}
|
|
655
692
|
const instanceOf = this.helpers.get("instanceof");
|
|
656
693
|
if (instanceOf) {
|
|
657
694
|
out.push(...parseLuau(`
|
|
@@ -1076,7 +1113,49 @@ end`).body.statements);
|
|
|
1076
1113
|
if (!this.origins.has(statement)) this.origins.set(statement, node.line.start);
|
|
1077
1114
|
return statement;
|
|
1078
1115
|
}
|
|
1116
|
+
/** A statement; a loop with what its labels need around it.
|
|
1117
|
+
*
|
|
1118
|
+
* Luau has no labels, and no `goto` to stand in for them. `break outer`
|
|
1119
|
+
* in a loop inside `outer` sets a flag `outer` declares and breaks the
|
|
1120
|
+
* loop it is in; right after that loop, the flag is checked, and the
|
|
1121
|
+
* check breaks again — or, once it is `outer` that would be left, does
|
|
1122
|
+
* what was asked of `outer` itself. `continue outer` is the same, with
|
|
1123
|
+
* `continue` done there and the flag lowered for the next time round. */
|
|
1079
1124
|
statementOf(node) {
|
|
1125
|
+
if (!LOOPS.has(node.type)) return this.statementKind(node);
|
|
1126
|
+
const loop = {
|
|
1127
|
+
label: node.label?.name,
|
|
1128
|
+
tryDepth: this.tryFrames.length,
|
|
1129
|
+
leaving: /* @__PURE__ */ new Set()
|
|
1130
|
+
};
|
|
1131
|
+
this.loops.push(loop);
|
|
1132
|
+
let lowered;
|
|
1133
|
+
try {
|
|
1134
|
+
lowered = this.statementKind(node);
|
|
1135
|
+
} finally {
|
|
1136
|
+
this.loops.pop();
|
|
1137
|
+
}
|
|
1138
|
+
const flags = [loop.breakFlag, loop.continueFlag].filter((f) => f !== void 0);
|
|
1139
|
+
const out = [...flags.length ? [local(flags, flags.map(() => boolean(false)))] : [], ...lowered];
|
|
1140
|
+
const enclosing = this.loops[this.loops.length - 1];
|
|
1141
|
+
for (const target of loop.leaving) {
|
|
1142
|
+
if (target === enclosing) {
|
|
1143
|
+
if (target.breakFlag) out.push(ifThen(identifier(target.breakFlag), this.jump("break", void 0)));
|
|
1144
|
+
if (target.continueFlag) {
|
|
1145
|
+
out.push(ifThen(identifier(target.continueFlag), [
|
|
1146
|
+
assign([identifier(target.continueFlag)], [boolean(false)]),
|
|
1147
|
+
...this.jump("continue", void 0)
|
|
1148
|
+
]));
|
|
1149
|
+
}
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
enclosing?.leaving.add(target);
|
|
1153
|
+
const raised = [target.breakFlag, target.continueFlag].filter((f) => f !== void 0).map((f) => identifier(f)).reduce((a, b) => binary("or", a, b));
|
|
1154
|
+
out.push(ifThen(raised, this.jump("break", void 0)));
|
|
1155
|
+
}
|
|
1156
|
+
return out;
|
|
1157
|
+
}
|
|
1158
|
+
statementKind(node) {
|
|
1080
1159
|
switch (node.type) {
|
|
1081
1160
|
// Types, and what exists only for them.
|
|
1082
1161
|
case "TypeAliasStatement":
|
|
@@ -1096,15 +1175,20 @@ end`).body.statements);
|
|
|
1096
1175
|
return this.classDeclaration(node, "declare");
|
|
1097
1176
|
case "AssignmentStatement":
|
|
1098
1177
|
return this.assignment(node);
|
|
1099
|
-
case "CompoundAssignmentStatement":
|
|
1100
|
-
if (
|
|
1178
|
+
case "CompoundAssignmentStatement": {
|
|
1179
|
+
if (node.operator === "??=") return this.nullishAssignment(node);
|
|
1180
|
+
const join = node.operator === "+=" ? this.options.types?.joins.get(node) : void 0;
|
|
1181
|
+
if (join === "dynamic") return this.dynamicAddition(node);
|
|
1182
|
+
const operator = join === "concat" ? "..=" : node.operator;
|
|
1183
|
+
if (this.lua51) return this.compoundAssignment51(node, operator);
|
|
1101
1184
|
return [{
|
|
1102
1185
|
type: "CompoundAssignmentStatement",
|
|
1103
|
-
operator
|
|
1186
|
+
operator,
|
|
1104
1187
|
target: this.expression(node.target),
|
|
1105
1188
|
value: this.expression(node.value),
|
|
1106
1189
|
...spanOf(node)
|
|
1107
1190
|
}];
|
|
1191
|
+
}
|
|
1108
1192
|
case "CallStatement": {
|
|
1109
1193
|
const chain = optionalChain(node.expression);
|
|
1110
1194
|
if (chain) return [this.optionalCallStatement(chain)];
|
|
@@ -1190,11 +1274,20 @@ end`).body.statements);
|
|
|
1190
1274
|
)];
|
|
1191
1275
|
}
|
|
1192
1276
|
case "ReturnStatement":
|
|
1193
|
-
return
|
|
1277
|
+
return this.jump("return", node.argument && this.keptToOne(this.expression(node.argument)), node);
|
|
1194
1278
|
case "BreakStatement":
|
|
1195
|
-
return
|
|
1279
|
+
return this.jump("break", void 0, node, node.label?.name);
|
|
1196
1280
|
case "ContinueStatement":
|
|
1197
|
-
return
|
|
1281
|
+
return this.jump("continue", void 0, node, node.label?.name);
|
|
1282
|
+
// Level 0: what is thrown is what a `catch` is handed, with no
|
|
1283
|
+
// position put in front of a string.
|
|
1284
|
+
case "ThrowStatement":
|
|
1285
|
+
return [callStatement(call(
|
|
1286
|
+
this.builtin("error"),
|
|
1287
|
+
[this.keptToOne(this.expression(node.argument)), number(0)]
|
|
1288
|
+
))];
|
|
1289
|
+
case "TryStatement":
|
|
1290
|
+
return this.tryStatement(node);
|
|
1198
1291
|
case "ImportStatement":
|
|
1199
1292
|
if (node.isTypeOnly) return [];
|
|
1200
1293
|
this.report(node, this.options.module ? "Imports and exports belong at the top level of a module" : "Imports and exports need a bundle: build the project with @tilua/compiler");
|
|
@@ -1207,6 +1300,90 @@ end`).body.statements);
|
|
|
1207
1300
|
return [];
|
|
1208
1301
|
}
|
|
1209
1302
|
}
|
|
1303
|
+
/** A `return`, `break` or `continue`. Inside a `try` that it leaves, it
|
|
1304
|
+
* is the `try`'s function returning a code saying which — and the value
|
|
1305
|
+
* — for the code after the `pcall` to do it there. */
|
|
1306
|
+
jump(kind, value, node, label) {
|
|
1307
|
+
const at = node ? spanOf(node) : {};
|
|
1308
|
+
const frame = this.tryFrames[this.tryFrames.length - 1];
|
|
1309
|
+
const target = kind === "return" ? void 0 : label === void 0 ? this.loops[this.loops.length - 1] : this.loops.findLast((loop) => loop.label === label);
|
|
1310
|
+
if (frame && (kind === "return" || target && target.tryDepth < this.tryFrames.length)) {
|
|
1311
|
+
const key = `${kind}:${label ?? ""}`;
|
|
1312
|
+
let exit = frame.exits.get(key);
|
|
1313
|
+
if (!exit) {
|
|
1314
|
+
exit = { kind, label, code: kind === "return" ? 1 : frame.exits.size + 2 };
|
|
1315
|
+
frame.exits.set(key, exit);
|
|
1316
|
+
}
|
|
1317
|
+
if (value) frame.returnsValue = true;
|
|
1318
|
+
return [returns([number(exit.code), ...value ? [value] : []])];
|
|
1319
|
+
}
|
|
1320
|
+
if (kind === "return") return [{ ...returns(value ? [value] : []), ...at }];
|
|
1321
|
+
const plain = () => kind === "break" ? { ...breakStatement(), ...at } : { ...breakStatement(), type: "ContinueStatement", ...at };
|
|
1322
|
+
const innermost = this.loops[this.loops.length - 1];
|
|
1323
|
+
if (!target || target === innermost) return [plain()];
|
|
1324
|
+
const flag = kind === "break" ? target.breakFlag ??= this.names.fresh(`${target.label}Broke`) : target.continueFlag ??= this.names.fresh(`${target.label}Continued`);
|
|
1325
|
+
innermost.leaving.add(target);
|
|
1326
|
+
return [assign([identifier(flag)], [boolean(true)]), { ...breakStatement(), ...at }];
|
|
1327
|
+
}
|
|
1328
|
+
/** `try { a } catch (e) { b } finally { c }`:
|
|
1329
|
+
*
|
|
1330
|
+
* local ok, result, value = pcall(function() a end)
|
|
1331
|
+
* if not ok then
|
|
1332
|
+
* ok, result, value = pcall(function(e) b end, result)
|
|
1333
|
+
* end
|
|
1334
|
+
* do c end
|
|
1335
|
+
* if not ok then error(result, 0) end
|
|
1336
|
+
* if result == 1 then return value elseif ... end
|
|
1337
|
+
*
|
|
1338
|
+
* Without a `finally`, the `catch` runs in place of the second `pcall`:
|
|
1339
|
+
* nothing has to run after it. A `return`, `break` or `continue` that
|
|
1340
|
+
* leaves the `try` cannot be written inside its function; it returns a
|
|
1341
|
+
* code instead (see `jump`), and the last `if` does what it says. */
|
|
1342
|
+
tryStatement(node) {
|
|
1343
|
+
const ok = this.names.fresh("ok");
|
|
1344
|
+
const result2 = this.names.fresh("result");
|
|
1345
|
+
const value = this.names.fresh("value");
|
|
1346
|
+
const frame = { exits: /* @__PURE__ */ new Map(), returnsValue: false };
|
|
1347
|
+
const guarded = (block2, params, args) => {
|
|
1348
|
+
this.tryFrames.push(frame);
|
|
1349
|
+
let statements;
|
|
1350
|
+
try {
|
|
1351
|
+
statements = this.block(block2).statements;
|
|
1352
|
+
} finally {
|
|
1353
|
+
this.tryFrames.pop();
|
|
1354
|
+
}
|
|
1355
|
+
const passesVarargs = statements.some(usesVararg);
|
|
1356
|
+
const func = functionExpression(functionBody(params, statements, passesVarargs));
|
|
1357
|
+
return call(this.builtin("pcall"), [func, ...args, ...passesVarargs ? [vararg()] : []]);
|
|
1358
|
+
};
|
|
1359
|
+
const attempt = guarded(node.block, [], []);
|
|
1360
|
+
const param = node.handler?.param && this.name(node.handler.param.name);
|
|
1361
|
+
const inPlace = node.handler && !node.finalizer ? this.block(node.handler.body, param ? [local([param], [identifier(result2)])] : []) : void 0;
|
|
1362
|
+
const caught = node.handler && node.finalizer ? guarded(node.handler.body, [param ?? "_"], [identifier(result2)]) : void 0;
|
|
1363
|
+
const finalizer = node.finalizer && this.block(node.finalizer);
|
|
1364
|
+
const returnsValue = frame.returnsValue;
|
|
1365
|
+
const usesResult = returnsValue || frame.exits.size > 0 || !!param || !!node.finalizer;
|
|
1366
|
+
const names = [ok, ...usesResult ? [result2] : [], ...returnsValue ? [value] : []];
|
|
1367
|
+
const failed = () => unary("not", identifier(ok));
|
|
1368
|
+
const out = [this.from(local(names, [attempt]), node)];
|
|
1369
|
+
const clauses = [];
|
|
1370
|
+
if (inPlace) clauses.push({ type: "IfClause", condition: failed(), body: inPlace, ...spanOf(node.handler) });
|
|
1371
|
+
if (caught) out.push(ifThen(failed(), [assign(names.map(identifier), [caught])]));
|
|
1372
|
+
if (finalizer) {
|
|
1373
|
+
out.push(doBlock(finalizer.statements));
|
|
1374
|
+
out.push(ifThen(failed(), [callStatement(call(this.builtin("error"), [identifier(result2), number(0)]))]));
|
|
1375
|
+
}
|
|
1376
|
+
for (const exit of [...frame.exits.values()].sort((a, b) => a.code - b.code)) {
|
|
1377
|
+
clauses.push({
|
|
1378
|
+
type: "IfClause",
|
|
1379
|
+
condition: binary("==", identifier(result2), number(exit.code)),
|
|
1380
|
+
body: block(this.jump(exit.kind, exit.kind === "return" && returnsValue ? identifier(value) : void 0, void 0, exit.label)),
|
|
1381
|
+
...spanOf(node)
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
if (clauses.length) out.push({ type: "IfStatement", clauses, ...spanOf(node) });
|
|
1385
|
+
return out;
|
|
1386
|
+
}
|
|
1210
1387
|
functionDeclarationStatement(node) {
|
|
1211
1388
|
const statement = this.functionStatement(this.reference(node.target.base), node, this.functionBody(node.func), node.isMethod, node.target.path.map((p) => p.name));
|
|
1212
1389
|
if (!node.target.method) return statement;
|
|
@@ -1596,6 +1773,18 @@ end
|
|
|
1596
1773
|
// Functions
|
|
1597
1774
|
// --------------------------------------------------------
|
|
1598
1775
|
functionBody(func) {
|
|
1776
|
+
const frames = this.tryFrames;
|
|
1777
|
+
const loops = this.loops;
|
|
1778
|
+
this.tryFrames = [];
|
|
1779
|
+
this.loops = [];
|
|
1780
|
+
try {
|
|
1781
|
+
return this.functionBodyIn(func);
|
|
1782
|
+
} finally {
|
|
1783
|
+
this.tryFrames = frames;
|
|
1784
|
+
this.loops = loops;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
functionBodyIn(func) {
|
|
1599
1788
|
const params = func.isMethod ? func.params.slice(1) : func.params;
|
|
1600
1789
|
const prelude = [];
|
|
1601
1790
|
const names = [];
|
|
@@ -1646,8 +1835,10 @@ end
|
|
|
1646
1835
|
return nil();
|
|
1647
1836
|
case "InterpolatedStringExpression":
|
|
1648
1837
|
return this.interpolatedString(node);
|
|
1838
|
+
// An object's method, `{ m() { ... } }`, keeps its `this` as the
|
|
1839
|
+
// first parameter written out: a function value has no `:`.
|
|
1649
1840
|
case "FunctionExpression":
|
|
1650
|
-
return functionExpression(this.
|
|
1841
|
+
return functionExpression(this.classFunctionBody(node.func));
|
|
1651
1842
|
case "TableExpression":
|
|
1652
1843
|
return this.tableExpression(node);
|
|
1653
1844
|
case "ArrayExpression":
|
|
@@ -1662,6 +1853,11 @@ end
|
|
|
1662
1853
|
if (node.operator === "instanceof") {
|
|
1663
1854
|
return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
|
|
1664
1855
|
}
|
|
1856
|
+
if (node.operator === "+") {
|
|
1857
|
+
const join = this.options.types?.joins.get(node);
|
|
1858
|
+
if (join === "concat") return binary("..", left, right);
|
|
1859
|
+
if (join === "dynamic") return call(this.helper("add"), [this.keptToOne(left), this.keptToOne(right)]);
|
|
1860
|
+
}
|
|
1665
1861
|
if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
|
|
1666
1862
|
return binary(node.operator, left, right);
|
|
1667
1863
|
}
|
|
@@ -1921,14 +2117,52 @@ end
|
|
|
1921
2117
|
if (!args.length) return string(text);
|
|
1922
2118
|
return methodCall(parenthesized(string(format)), "format", args);
|
|
1923
2119
|
}
|
|
2120
|
+
/** `t ??= v`: `if t == nil then t = v end`, which no Luau has an
|
|
2121
|
+
* operator for. `v` is read only where `t` is nil. As in `a += b` for
|
|
2122
|
+
* Lua 5.1, what addresses the target is read once: an object or key
|
|
2123
|
+
* that is not a plain name goes into a local first. */
|
|
2124
|
+
nullishAssignment(node) {
|
|
2125
|
+
const { before, target } = this.targetOnce(node.target);
|
|
2126
|
+
const statement = ifThen(binary("==", target, nil()), [assign([target], [this.expression(node.value)])]);
|
|
2127
|
+
return before.length ? [doBlock([...before, statement])] : [statement];
|
|
2128
|
+
}
|
|
2129
|
+
/** `t += v` where only the values can say whether it joins or adds:
|
|
2130
|
+
* `t = tilua_add(t, v)`, with what addresses `t` read once. */
|
|
2131
|
+
dynamicAddition(node) {
|
|
2132
|
+
const { before, target } = this.targetOnce(node.target);
|
|
2133
|
+
const statement = assign([target], [call(this.helper("add"), [target, this.keptToOne(this.expression(node.value))])]);
|
|
2134
|
+
return before.length ? [doBlock([...before, statement])] : [statement];
|
|
2135
|
+
}
|
|
2136
|
+
/** An assignment's target, as an expression that may be read and written
|
|
2137
|
+
* freely: an object or key that is not a plain name goes into a local
|
|
2138
|
+
* first, so that `t[next()]` advances once. */
|
|
2139
|
+
targetOnce(node) {
|
|
2140
|
+
const before = [];
|
|
2141
|
+
const once = (expression, name) => {
|
|
2142
|
+
if (expression.type === "Identifier" || expression.type === "StringLiteral" || expression.type === "NumberLiteral") return expression;
|
|
2143
|
+
const local2 = this.names.fresh(name);
|
|
2144
|
+
before.push(local([local2], [expression]));
|
|
2145
|
+
return identifier(local2);
|
|
2146
|
+
};
|
|
2147
|
+
let target;
|
|
2148
|
+
if (node.type === "MemberExpression") {
|
|
2149
|
+
target = member(once(this.expression(node.object), "target"), node.property.name);
|
|
2150
|
+
} else if (node.type === "IndexExpression") {
|
|
2151
|
+
const object2 = once(this.expression(node.object), "target");
|
|
2152
|
+
target = index(object2, once(this.expression(node.index), "key"));
|
|
2153
|
+
} else {
|
|
2154
|
+
target = this.expression(node);
|
|
2155
|
+
}
|
|
2156
|
+
return { before, target };
|
|
2157
|
+
}
|
|
1924
2158
|
/** `a += b` for Lua 5.1, which has no compound assignment: `a = a + b`.
|
|
1925
2159
|
*
|
|
1926
2160
|
* The target has to be read and written, and writing it out twice would
|
|
1927
2161
|
* evaluate whatever addresses it twice — `t[next()] += 1` would advance
|
|
1928
2162
|
* twice and add to the wrong slot. So anything but a plain name has its
|
|
1929
2163
|
* object and key taken into locals first, and both halves use those. */
|
|
1930
|
-
compoundAssignment51(node) {
|
|
1931
|
-
const operator =
|
|
2164
|
+
compoundAssignment51(node, written = node.operator) {
|
|
2165
|
+
const operator = written.slice(0, -1);
|
|
1932
2166
|
const before = [];
|
|
1933
2167
|
let target;
|
|
1934
2168
|
if (node.target.type === "MemberExpression" || node.target.type === "IndexExpression") {
|
|
@@ -2127,6 +2361,7 @@ function optionalChain(node) {
|
|
|
2127
2361
|
if (first < 0) return void 0;
|
|
2128
2362
|
return { base: linkObject(spine[first]), links: spine.slice(first) };
|
|
2129
2363
|
}
|
|
2364
|
+
var LOOPS = /* @__PURE__ */ new Set(["WhileStatement", "RepeatStatement", "NumericForStatement", "GenericForStatement"]);
|
|
2130
2365
|
function usesVararg(node) {
|
|
2131
2366
|
if (Array.isArray(node)) return node.some(usesVararg);
|
|
2132
2367
|
if (!node || typeof node !== "object") return false;
|
|
@@ -3014,7 +3249,7 @@ function analyzeModules(modules, config) {
|
|
|
3014
3249
|
});
|
|
3015
3250
|
for (const d of types.diagnostics) {
|
|
3016
3251
|
const at = d.node;
|
|
3017
|
-
said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
|
|
3252
|
+
said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type", severity: d.severity });
|
|
3018
3253
|
}
|
|
3019
3254
|
const result2 = { types, diagnostics: said };
|
|
3020
3255
|
if (partials === before) {
|
package/dist/cli.cjs
CHANGED
|
@@ -262,7 +262,29 @@ var standardLowering = { plugin: plugin2, from: "tilua (standard library)" };
|
|
|
262
262
|
|
|
263
263
|
// src/lower.ts
|
|
264
264
|
function lower(program2, scopes, options = {}) {
|
|
265
|
-
|
|
265
|
+
const result2 = new Lowerer(program2, scopes, options).run();
|
|
266
|
+
dropUnreachable(result2.statements);
|
|
267
|
+
return result2;
|
|
268
|
+
}
|
|
269
|
+
function dropUnreachable(statements) {
|
|
270
|
+
const exit = statements.findIndex((s) => s.type === "ReturnStatement" || s.type === "BreakStatement" || s.type === "ContinueStatement");
|
|
271
|
+
if (exit >= 0) statements.length = exit + 1;
|
|
272
|
+
for (const statement of statements) visitBlocks(statement);
|
|
273
|
+
}
|
|
274
|
+
function visitBlocks(node) {
|
|
275
|
+
if (!node || typeof node !== "object") return;
|
|
276
|
+
if (Array.isArray(node)) {
|
|
277
|
+
for (const item of node) visitBlocks(item);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const record = node;
|
|
281
|
+
if (record.type === "Block" && Array.isArray(record.statements)) {
|
|
282
|
+
dropUnreachable(record.statements);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const key in record) {
|
|
286
|
+
if (key !== "line" && key !== "column") visitBlocks(record[key]);
|
|
287
|
+
}
|
|
266
288
|
}
|
|
267
289
|
var Lowerer = class {
|
|
268
290
|
constructor(source, scopes, options) {
|
|
@@ -304,6 +326,11 @@ var Lowerer = class {
|
|
|
304
326
|
exportsName;
|
|
305
327
|
/** Globals generated code calls, renamed where the source shadows them. */
|
|
306
328
|
builtins = /* @__PURE__ */ new Map();
|
|
329
|
+
/** The `try` blocks being lowered in the current function, innermost
|
|
330
|
+
* last; see `tryStatement`. A function written inside one starts afresh. */
|
|
331
|
+
tryFrames = [];
|
|
332
|
+
/** The loops being lowered in the current function, innermost last. */
|
|
333
|
+
loops = [];
|
|
307
334
|
/** Whether the output has to be plain Lua 5.1 rather than Luau. */
|
|
308
335
|
lua51;
|
|
309
336
|
/** The source line each emitted statement came from. */
|
|
@@ -669,6 +696,16 @@ local function ${typeOf}(value, known)
|
|
|
669
696
|
end`).body.statements);
|
|
670
697
|
}
|
|
671
698
|
}
|
|
699
|
+
const add = this.helpers.get("add");
|
|
700
|
+
if (add) {
|
|
701
|
+
out2.push(...(0, import_luau_parser.parse)(`
|
|
702
|
+
local function ${add}(a, b)
|
|
703
|
+
if ${this.builtin("type").name}(a) == "string" or ${this.builtin("type").name}(b) == "string" then
|
|
704
|
+
return ${this.builtin("tostring").name}(a) .. ${this.builtin("tostring").name}(b)
|
|
705
|
+
end
|
|
706
|
+
return a + b
|
|
707
|
+
end`).body.statements);
|
|
708
|
+
}
|
|
672
709
|
const instanceOf = this.helpers.get("instanceof");
|
|
673
710
|
if (instanceOf) {
|
|
674
711
|
out2.push(...(0, import_luau_parser.parse)(`
|
|
@@ -1093,7 +1130,49 @@ end`).body.statements);
|
|
|
1093
1130
|
if (!this.origins.has(statement)) this.origins.set(statement, node.line.start);
|
|
1094
1131
|
return statement;
|
|
1095
1132
|
}
|
|
1133
|
+
/** A statement; a loop with what its labels need around it.
|
|
1134
|
+
*
|
|
1135
|
+
* Luau has no labels, and no `goto` to stand in for them. `break outer`
|
|
1136
|
+
* in a loop inside `outer` sets a flag `outer` declares and breaks the
|
|
1137
|
+
* loop it is in; right after that loop, the flag is checked, and the
|
|
1138
|
+
* check breaks again — or, once it is `outer` that would be left, does
|
|
1139
|
+
* what was asked of `outer` itself. `continue outer` is the same, with
|
|
1140
|
+
* `continue` done there and the flag lowered for the next time round. */
|
|
1096
1141
|
statementOf(node) {
|
|
1142
|
+
if (!LOOPS.has(node.type)) return this.statementKind(node);
|
|
1143
|
+
const loop = {
|
|
1144
|
+
label: node.label?.name,
|
|
1145
|
+
tryDepth: this.tryFrames.length,
|
|
1146
|
+
leaving: /* @__PURE__ */ new Set()
|
|
1147
|
+
};
|
|
1148
|
+
this.loops.push(loop);
|
|
1149
|
+
let lowered;
|
|
1150
|
+
try {
|
|
1151
|
+
lowered = this.statementKind(node);
|
|
1152
|
+
} finally {
|
|
1153
|
+
this.loops.pop();
|
|
1154
|
+
}
|
|
1155
|
+
const flags = [loop.breakFlag, loop.continueFlag].filter((f) => f !== void 0);
|
|
1156
|
+
const out2 = [...flags.length ? [local(flags, flags.map(() => boolean(false)))] : [], ...lowered];
|
|
1157
|
+
const enclosing = this.loops[this.loops.length - 1];
|
|
1158
|
+
for (const target of loop.leaving) {
|
|
1159
|
+
if (target === enclosing) {
|
|
1160
|
+
if (target.breakFlag) out2.push(ifThen(identifier(target.breakFlag), this.jump("break", void 0)));
|
|
1161
|
+
if (target.continueFlag) {
|
|
1162
|
+
out2.push(ifThen(identifier(target.continueFlag), [
|
|
1163
|
+
assign([identifier(target.continueFlag)], [boolean(false)]),
|
|
1164
|
+
...this.jump("continue", void 0)
|
|
1165
|
+
]));
|
|
1166
|
+
}
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
enclosing?.leaving.add(target);
|
|
1170
|
+
const raised = [target.breakFlag, target.continueFlag].filter((f) => f !== void 0).map((f) => identifier(f)).reduce((a, b) => binary("or", a, b));
|
|
1171
|
+
out2.push(ifThen(raised, this.jump("break", void 0)));
|
|
1172
|
+
}
|
|
1173
|
+
return out2;
|
|
1174
|
+
}
|
|
1175
|
+
statementKind(node) {
|
|
1097
1176
|
switch (node.type) {
|
|
1098
1177
|
// Types, and what exists only for them.
|
|
1099
1178
|
case "TypeAliasStatement":
|
|
@@ -1113,15 +1192,20 @@ end`).body.statements);
|
|
|
1113
1192
|
return this.classDeclaration(node, "declare");
|
|
1114
1193
|
case "AssignmentStatement":
|
|
1115
1194
|
return this.assignment(node);
|
|
1116
|
-
case "CompoundAssignmentStatement":
|
|
1117
|
-
if (
|
|
1195
|
+
case "CompoundAssignmentStatement": {
|
|
1196
|
+
if (node.operator === "??=") return this.nullishAssignment(node);
|
|
1197
|
+
const join2 = node.operator === "+=" ? this.options.types?.joins.get(node) : void 0;
|
|
1198
|
+
if (join2 === "dynamic") return this.dynamicAddition(node);
|
|
1199
|
+
const operator = join2 === "concat" ? "..=" : node.operator;
|
|
1200
|
+
if (this.lua51) return this.compoundAssignment51(node, operator);
|
|
1118
1201
|
return [{
|
|
1119
1202
|
type: "CompoundAssignmentStatement",
|
|
1120
|
-
operator
|
|
1203
|
+
operator,
|
|
1121
1204
|
target: this.expression(node.target),
|
|
1122
1205
|
value: this.expression(node.value),
|
|
1123
1206
|
...spanOf(node)
|
|
1124
1207
|
}];
|
|
1208
|
+
}
|
|
1125
1209
|
case "CallStatement": {
|
|
1126
1210
|
const chain = optionalChain(node.expression);
|
|
1127
1211
|
if (chain) return [this.optionalCallStatement(chain)];
|
|
@@ -1207,11 +1291,20 @@ end`).body.statements);
|
|
|
1207
1291
|
)];
|
|
1208
1292
|
}
|
|
1209
1293
|
case "ReturnStatement":
|
|
1210
|
-
return
|
|
1294
|
+
return this.jump("return", node.argument && this.keptToOne(this.expression(node.argument)), node);
|
|
1211
1295
|
case "BreakStatement":
|
|
1212
|
-
return
|
|
1296
|
+
return this.jump("break", void 0, node, node.label?.name);
|
|
1213
1297
|
case "ContinueStatement":
|
|
1214
|
-
return
|
|
1298
|
+
return this.jump("continue", void 0, node, node.label?.name);
|
|
1299
|
+
// Level 0: what is thrown is what a `catch` is handed, with no
|
|
1300
|
+
// position put in front of a string.
|
|
1301
|
+
case "ThrowStatement":
|
|
1302
|
+
return [callStatement(call(
|
|
1303
|
+
this.builtin("error"),
|
|
1304
|
+
[this.keptToOne(this.expression(node.argument)), number(0)]
|
|
1305
|
+
))];
|
|
1306
|
+
case "TryStatement":
|
|
1307
|
+
return this.tryStatement(node);
|
|
1215
1308
|
case "ImportStatement":
|
|
1216
1309
|
if (node.isTypeOnly) return [];
|
|
1217
1310
|
this.report(node, this.options.module ? "Imports and exports belong at the top level of a module" : "Imports and exports need a bundle: build the project with @tilua/compiler");
|
|
@@ -1224,6 +1317,90 @@ end`).body.statements);
|
|
|
1224
1317
|
return [];
|
|
1225
1318
|
}
|
|
1226
1319
|
}
|
|
1320
|
+
/** A `return`, `break` or `continue`. Inside a `try` that it leaves, it
|
|
1321
|
+
* is the `try`'s function returning a code saying which — and the value
|
|
1322
|
+
* — for the code after the `pcall` to do it there. */
|
|
1323
|
+
jump(kind, value, node, label) {
|
|
1324
|
+
const at = node ? spanOf(node) : {};
|
|
1325
|
+
const frame = this.tryFrames[this.tryFrames.length - 1];
|
|
1326
|
+
const target = kind === "return" ? void 0 : label === void 0 ? this.loops[this.loops.length - 1] : this.loops.findLast((loop) => loop.label === label);
|
|
1327
|
+
if (frame && (kind === "return" || target && target.tryDepth < this.tryFrames.length)) {
|
|
1328
|
+
const key = `${kind}:${label ?? ""}`;
|
|
1329
|
+
let exit = frame.exits.get(key);
|
|
1330
|
+
if (!exit) {
|
|
1331
|
+
exit = { kind, label, code: kind === "return" ? 1 : frame.exits.size + 2 };
|
|
1332
|
+
frame.exits.set(key, exit);
|
|
1333
|
+
}
|
|
1334
|
+
if (value) frame.returnsValue = true;
|
|
1335
|
+
return [returns([number(exit.code), ...value ? [value] : []])];
|
|
1336
|
+
}
|
|
1337
|
+
if (kind === "return") return [{ ...returns(value ? [value] : []), ...at }];
|
|
1338
|
+
const plain = () => kind === "break" ? { ...breakStatement(), ...at } : { ...breakStatement(), type: "ContinueStatement", ...at };
|
|
1339
|
+
const innermost = this.loops[this.loops.length - 1];
|
|
1340
|
+
if (!target || target === innermost) return [plain()];
|
|
1341
|
+
const flag = kind === "break" ? target.breakFlag ??= this.names.fresh(`${target.label}Broke`) : target.continueFlag ??= this.names.fresh(`${target.label}Continued`);
|
|
1342
|
+
innermost.leaving.add(target);
|
|
1343
|
+
return [assign([identifier(flag)], [boolean(true)]), { ...breakStatement(), ...at }];
|
|
1344
|
+
}
|
|
1345
|
+
/** `try { a } catch (e) { b } finally { c }`:
|
|
1346
|
+
*
|
|
1347
|
+
* local ok, result, value = pcall(function() a end)
|
|
1348
|
+
* if not ok then
|
|
1349
|
+
* ok, result, value = pcall(function(e) b end, result)
|
|
1350
|
+
* end
|
|
1351
|
+
* do c end
|
|
1352
|
+
* if not ok then error(result, 0) end
|
|
1353
|
+
* if result == 1 then return value elseif ... end
|
|
1354
|
+
*
|
|
1355
|
+
* Without a `finally`, the `catch` runs in place of the second `pcall`:
|
|
1356
|
+
* nothing has to run after it. A `return`, `break` or `continue` that
|
|
1357
|
+
* leaves the `try` cannot be written inside its function; it returns a
|
|
1358
|
+
* code instead (see `jump`), and the last `if` does what it says. */
|
|
1359
|
+
tryStatement(node) {
|
|
1360
|
+
const ok = this.names.fresh("ok");
|
|
1361
|
+
const result2 = this.names.fresh("result");
|
|
1362
|
+
const value = this.names.fresh("value");
|
|
1363
|
+
const frame = { exits: /* @__PURE__ */ new Map(), returnsValue: false };
|
|
1364
|
+
const guarded = (block2, params, args2) => {
|
|
1365
|
+
this.tryFrames.push(frame);
|
|
1366
|
+
let statements;
|
|
1367
|
+
try {
|
|
1368
|
+
statements = this.block(block2).statements;
|
|
1369
|
+
} finally {
|
|
1370
|
+
this.tryFrames.pop();
|
|
1371
|
+
}
|
|
1372
|
+
const passesVarargs = statements.some(usesVararg);
|
|
1373
|
+
const func = functionExpression(functionBody(params, statements, passesVarargs));
|
|
1374
|
+
return call(this.builtin("pcall"), [func, ...args2, ...passesVarargs ? [vararg()] : []]);
|
|
1375
|
+
};
|
|
1376
|
+
const attempt = guarded(node.block, [], []);
|
|
1377
|
+
const param = node.handler?.param && this.name(node.handler.param.name);
|
|
1378
|
+
const inPlace = node.handler && !node.finalizer ? this.block(node.handler.body, param ? [local([param], [identifier(result2)])] : []) : void 0;
|
|
1379
|
+
const caught = node.handler && node.finalizer ? guarded(node.handler.body, [param ?? "_"], [identifier(result2)]) : void 0;
|
|
1380
|
+
const finalizer = node.finalizer && this.block(node.finalizer);
|
|
1381
|
+
const returnsValue = frame.returnsValue;
|
|
1382
|
+
const usesResult = returnsValue || frame.exits.size > 0 || !!param || !!node.finalizer;
|
|
1383
|
+
const names = [ok, ...usesResult ? [result2] : [], ...returnsValue ? [value] : []];
|
|
1384
|
+
const failed = () => unary("not", identifier(ok));
|
|
1385
|
+
const out2 = [this.from(local(names, [attempt]), node)];
|
|
1386
|
+
const clauses = [];
|
|
1387
|
+
if (inPlace) clauses.push({ type: "IfClause", condition: failed(), body: inPlace, ...spanOf(node.handler) });
|
|
1388
|
+
if (caught) out2.push(ifThen(failed(), [assign(names.map(identifier), [caught])]));
|
|
1389
|
+
if (finalizer) {
|
|
1390
|
+
out2.push(doBlock(finalizer.statements));
|
|
1391
|
+
out2.push(ifThen(failed(), [callStatement(call(this.builtin("error"), [identifier(result2), number(0)]))]));
|
|
1392
|
+
}
|
|
1393
|
+
for (const exit of [...frame.exits.values()].sort((a, b) => a.code - b.code)) {
|
|
1394
|
+
clauses.push({
|
|
1395
|
+
type: "IfClause",
|
|
1396
|
+
condition: binary("==", identifier(result2), number(exit.code)),
|
|
1397
|
+
body: block(this.jump(exit.kind, exit.kind === "return" && returnsValue ? identifier(value) : void 0, void 0, exit.label)),
|
|
1398
|
+
...spanOf(node)
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
if (clauses.length) out2.push({ type: "IfStatement", clauses, ...spanOf(node) });
|
|
1402
|
+
return out2;
|
|
1403
|
+
}
|
|
1227
1404
|
functionDeclarationStatement(node) {
|
|
1228
1405
|
const statement = this.functionStatement(this.reference(node.target.base), node, this.functionBody(node.func), node.isMethod, node.target.path.map((p) => p.name));
|
|
1229
1406
|
if (!node.target.method) return statement;
|
|
@@ -1613,6 +1790,18 @@ end
|
|
|
1613
1790
|
// Functions
|
|
1614
1791
|
// --------------------------------------------------------
|
|
1615
1792
|
functionBody(func) {
|
|
1793
|
+
const frames = this.tryFrames;
|
|
1794
|
+
const loops = this.loops;
|
|
1795
|
+
this.tryFrames = [];
|
|
1796
|
+
this.loops = [];
|
|
1797
|
+
try {
|
|
1798
|
+
return this.functionBodyIn(func);
|
|
1799
|
+
} finally {
|
|
1800
|
+
this.tryFrames = frames;
|
|
1801
|
+
this.loops = loops;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
functionBodyIn(func) {
|
|
1616
1805
|
const params = func.isMethod ? func.params.slice(1) : func.params;
|
|
1617
1806
|
const prelude = [];
|
|
1618
1807
|
const names = [];
|
|
@@ -1663,8 +1852,10 @@ end
|
|
|
1663
1852
|
return nil();
|
|
1664
1853
|
case "InterpolatedStringExpression":
|
|
1665
1854
|
return this.interpolatedString(node);
|
|
1855
|
+
// An object's method, `{ m() { ... } }`, keeps its `this` as the
|
|
1856
|
+
// first parameter written out: a function value has no `:`.
|
|
1666
1857
|
case "FunctionExpression":
|
|
1667
|
-
return functionExpression(this.
|
|
1858
|
+
return functionExpression(this.classFunctionBody(node.func));
|
|
1668
1859
|
case "TableExpression":
|
|
1669
1860
|
return this.tableExpression(node);
|
|
1670
1861
|
case "ArrayExpression":
|
|
@@ -1679,6 +1870,11 @@ end
|
|
|
1679
1870
|
if (node.operator === "instanceof") {
|
|
1680
1871
|
return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
|
|
1681
1872
|
}
|
|
1873
|
+
if (node.operator === "+") {
|
|
1874
|
+
const join2 = this.options.types?.joins.get(node);
|
|
1875
|
+
if (join2 === "concat") return binary("..", left, right);
|
|
1876
|
+
if (join2 === "dynamic") return call(this.helper("add"), [this.keptToOne(left), this.keptToOne(right)]);
|
|
1877
|
+
}
|
|
1682
1878
|
if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
|
|
1683
1879
|
return binary(node.operator, left, right);
|
|
1684
1880
|
}
|
|
@@ -1938,14 +2134,52 @@ end
|
|
|
1938
2134
|
if (!args2.length) return string(text);
|
|
1939
2135
|
return methodCall(parenthesized(string(format)), "format", args2);
|
|
1940
2136
|
}
|
|
2137
|
+
/** `t ??= v`: `if t == nil then t = v end`, which no Luau has an
|
|
2138
|
+
* operator for. `v` is read only where `t` is nil. As in `a += b` for
|
|
2139
|
+
* Lua 5.1, what addresses the target is read once: an object or key
|
|
2140
|
+
* that is not a plain name goes into a local first. */
|
|
2141
|
+
nullishAssignment(node) {
|
|
2142
|
+
const { before, target } = this.targetOnce(node.target);
|
|
2143
|
+
const statement = ifThen(binary("==", target, nil()), [assign([target], [this.expression(node.value)])]);
|
|
2144
|
+
return before.length ? [doBlock([...before, statement])] : [statement];
|
|
2145
|
+
}
|
|
2146
|
+
/** `t += v` where only the values can say whether it joins or adds:
|
|
2147
|
+
* `t = tilua_add(t, v)`, with what addresses `t` read once. */
|
|
2148
|
+
dynamicAddition(node) {
|
|
2149
|
+
const { before, target } = this.targetOnce(node.target);
|
|
2150
|
+
const statement = assign([target], [call(this.helper("add"), [target, this.keptToOne(this.expression(node.value))])]);
|
|
2151
|
+
return before.length ? [doBlock([...before, statement])] : [statement];
|
|
2152
|
+
}
|
|
2153
|
+
/** An assignment's target, as an expression that may be read and written
|
|
2154
|
+
* freely: an object or key that is not a plain name goes into a local
|
|
2155
|
+
* first, so that `t[next()]` advances once. */
|
|
2156
|
+
targetOnce(node) {
|
|
2157
|
+
const before = [];
|
|
2158
|
+
const once = (expression, name) => {
|
|
2159
|
+
if (expression.type === "Identifier" || expression.type === "StringLiteral" || expression.type === "NumberLiteral") return expression;
|
|
2160
|
+
const local2 = this.names.fresh(name);
|
|
2161
|
+
before.push(local([local2], [expression]));
|
|
2162
|
+
return identifier(local2);
|
|
2163
|
+
};
|
|
2164
|
+
let target;
|
|
2165
|
+
if (node.type === "MemberExpression") {
|
|
2166
|
+
target = member(once(this.expression(node.object), "target"), node.property.name);
|
|
2167
|
+
} else if (node.type === "IndexExpression") {
|
|
2168
|
+
const object2 = once(this.expression(node.object), "target");
|
|
2169
|
+
target = index(object2, once(this.expression(node.index), "key"));
|
|
2170
|
+
} else {
|
|
2171
|
+
target = this.expression(node);
|
|
2172
|
+
}
|
|
2173
|
+
return { before, target };
|
|
2174
|
+
}
|
|
1941
2175
|
/** `a += b` for Lua 5.1, which has no compound assignment: `a = a + b`.
|
|
1942
2176
|
*
|
|
1943
2177
|
* The target has to be read and written, and writing it out twice would
|
|
1944
2178
|
* evaluate whatever addresses it twice — `t[next()] += 1` would advance
|
|
1945
2179
|
* twice and add to the wrong slot. So anything but a plain name has its
|
|
1946
2180
|
* object and key taken into locals first, and both halves use those. */
|
|
1947
|
-
compoundAssignment51(node) {
|
|
1948
|
-
const operator =
|
|
2181
|
+
compoundAssignment51(node, written = node.operator) {
|
|
2182
|
+
const operator = written.slice(0, -1);
|
|
1949
2183
|
const before = [];
|
|
1950
2184
|
let target;
|
|
1951
2185
|
if (node.target.type === "MemberExpression" || node.target.type === "IndexExpression") {
|
|
@@ -2144,6 +2378,7 @@ function optionalChain(node) {
|
|
|
2144
2378
|
if (first < 0) return void 0;
|
|
2145
2379
|
return { base: linkObject(spine[first]), links: spine.slice(first) };
|
|
2146
2380
|
}
|
|
2381
|
+
var LOOPS = /* @__PURE__ */ new Set(["WhileStatement", "RepeatStatement", "NumericForStatement", "GenericForStatement"]);
|
|
2147
2382
|
function usesVararg(node) {
|
|
2148
2383
|
if (Array.isArray(node)) return node.some(usesVararg);
|
|
2149
2384
|
if (!node || typeof node !== "object") return false;
|
|
@@ -3011,7 +3246,7 @@ function analyzeModules(modules, config2) {
|
|
|
3011
3246
|
});
|
|
3012
3247
|
for (const d of types.diagnostics) {
|
|
3013
3248
|
const at = d.node;
|
|
3014
|
-
said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
|
|
3249
|
+
said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type", severity: d.severity });
|
|
3015
3250
|
}
|
|
3016
3251
|
const result2 = { types, diagnostics: said };
|
|
3017
3252
|
if (partials === before) {
|
|
@@ -3140,7 +3375,7 @@ async function main(from) {
|
|
|
3140
3375
|
outFile: entryFile !== void 0 ? (0, import_node_path3.basename)(entryFile) : void 0
|
|
3141
3376
|
});
|
|
3142
3377
|
for (const d of result2.diagnostics) {
|
|
3143
|
-
console.error(`${(0, import_node_path3.relative)(process.cwd(), d.file)}:${d.line}:${d.column} ${d.message}`);
|
|
3378
|
+
console.error(`${(0, import_node_path3.relative)(process.cwd(), d.file)}:${d.line}:${d.column} ${d.severity === "warning" ? "warning: " : ""}${d.message}`);
|
|
3144
3379
|
}
|
|
3145
3380
|
if (result2.code === void 0) {
|
|
3146
3381
|
console.error("no bundle written");
|
|
@@ -3152,7 +3387,7 @@ async function main(from) {
|
|
|
3152
3387
|
(0, import_node_fs4.writeFileSync)(target, file.code);
|
|
3153
3388
|
console.log(`${target}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
|
|
3154
3389
|
}
|
|
3155
|
-
process.exit(result2.diagnostics.
|
|
3390
|
+
process.exit(result2.diagnostics.some((d) => d.severity !== "warning") ? 1 : 0);
|
|
3156
3391
|
}
|
|
3157
3392
|
main(entry).catch((error) => {
|
|
3158
3393
|
console.error(error instanceof Error ? error.message : error);
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
bundle,
|
|
4
4
|
resolveConfig
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EQE22LX7.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { mkdirSync, writeFileSync } from "fs";
|
|
@@ -57,7 +57,7 @@ async function main(from) {
|
|
|
57
57
|
outFile: entryFile !== void 0 ? basename(entryFile) : void 0
|
|
58
58
|
});
|
|
59
59
|
for (const d of result.diagnostics) {
|
|
60
|
-
console.error(`${relative(process.cwd(), d.file)}:${d.line}:${d.column} ${d.message}`);
|
|
60
|
+
console.error(`${relative(process.cwd(), d.file)}:${d.line}:${d.column} ${d.severity === "warning" ? "warning: " : ""}${d.message}`);
|
|
61
61
|
}
|
|
62
62
|
if (result.code === void 0) {
|
|
63
63
|
console.error("no bundle written");
|
|
@@ -69,7 +69,7 @@ async function main(from) {
|
|
|
69
69
|
writeFileSync(target, file.code);
|
|
70
70
|
console.log(`${target}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
|
|
71
71
|
}
|
|
72
|
-
process.exit(result.diagnostics.
|
|
72
|
+
process.exit(result.diagnostics.some((d) => d.severity !== "warning") ? 1 : 0);
|
|
73
73
|
}
|
|
74
74
|
main(entry).catch((error) => {
|
|
75
75
|
console.error(error instanceof Error ? error.message : error);
|
package/dist/index.cjs
CHANGED
|
@@ -285,7 +285,29 @@ var standardLowering = { plugin: plugin2, from: "tilua (standard library)" };
|
|
|
285
285
|
|
|
286
286
|
// src/lower.ts
|
|
287
287
|
function lower(program2, scopes, options = {}) {
|
|
288
|
-
|
|
288
|
+
const result2 = new Lowerer(program2, scopes, options).run();
|
|
289
|
+
dropUnreachable(result2.statements);
|
|
290
|
+
return result2;
|
|
291
|
+
}
|
|
292
|
+
function dropUnreachable(statements) {
|
|
293
|
+
const exit = statements.findIndex((s) => s.type === "ReturnStatement" || s.type === "BreakStatement" || s.type === "ContinueStatement");
|
|
294
|
+
if (exit >= 0) statements.length = exit + 1;
|
|
295
|
+
for (const statement of statements) visitBlocks(statement);
|
|
296
|
+
}
|
|
297
|
+
function visitBlocks(node) {
|
|
298
|
+
if (!node || typeof node !== "object") return;
|
|
299
|
+
if (Array.isArray(node)) {
|
|
300
|
+
for (const item of node) visitBlocks(item);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const record = node;
|
|
304
|
+
if (record.type === "Block" && Array.isArray(record.statements)) {
|
|
305
|
+
dropUnreachable(record.statements);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
for (const key in record) {
|
|
309
|
+
if (key !== "line" && key !== "column") visitBlocks(record[key]);
|
|
310
|
+
}
|
|
289
311
|
}
|
|
290
312
|
var Lowerer = class {
|
|
291
313
|
constructor(source, scopes, options) {
|
|
@@ -327,6 +349,11 @@ var Lowerer = class {
|
|
|
327
349
|
exportsName;
|
|
328
350
|
/** Globals generated code calls, renamed where the source shadows them. */
|
|
329
351
|
builtins = /* @__PURE__ */ new Map();
|
|
352
|
+
/** The `try` blocks being lowered in the current function, innermost
|
|
353
|
+
* last; see `tryStatement`. A function written inside one starts afresh. */
|
|
354
|
+
tryFrames = [];
|
|
355
|
+
/** The loops being lowered in the current function, innermost last. */
|
|
356
|
+
loops = [];
|
|
330
357
|
/** Whether the output has to be plain Lua 5.1 rather than Luau. */
|
|
331
358
|
lua51;
|
|
332
359
|
/** The source line each emitted statement came from. */
|
|
@@ -692,6 +719,16 @@ local function ${typeOf}(value, known)
|
|
|
692
719
|
end`).body.statements);
|
|
693
720
|
}
|
|
694
721
|
}
|
|
722
|
+
const add = this.helpers.get("add");
|
|
723
|
+
if (add) {
|
|
724
|
+
out.push(...(0, import_luau_parser.parse)(`
|
|
725
|
+
local function ${add}(a, b)
|
|
726
|
+
if ${this.builtin("type").name}(a) == "string" or ${this.builtin("type").name}(b) == "string" then
|
|
727
|
+
return ${this.builtin("tostring").name}(a) .. ${this.builtin("tostring").name}(b)
|
|
728
|
+
end
|
|
729
|
+
return a + b
|
|
730
|
+
end`).body.statements);
|
|
731
|
+
}
|
|
695
732
|
const instanceOf = this.helpers.get("instanceof");
|
|
696
733
|
if (instanceOf) {
|
|
697
734
|
out.push(...(0, import_luau_parser.parse)(`
|
|
@@ -1116,7 +1153,49 @@ end`).body.statements);
|
|
|
1116
1153
|
if (!this.origins.has(statement)) this.origins.set(statement, node.line.start);
|
|
1117
1154
|
return statement;
|
|
1118
1155
|
}
|
|
1156
|
+
/** A statement; a loop with what its labels need around it.
|
|
1157
|
+
*
|
|
1158
|
+
* Luau has no labels, and no `goto` to stand in for them. `break outer`
|
|
1159
|
+
* in a loop inside `outer` sets a flag `outer` declares and breaks the
|
|
1160
|
+
* loop it is in; right after that loop, the flag is checked, and the
|
|
1161
|
+
* check breaks again — or, once it is `outer` that would be left, does
|
|
1162
|
+
* what was asked of `outer` itself. `continue outer` is the same, with
|
|
1163
|
+
* `continue` done there and the flag lowered for the next time round. */
|
|
1119
1164
|
statementOf(node) {
|
|
1165
|
+
if (!LOOPS.has(node.type)) return this.statementKind(node);
|
|
1166
|
+
const loop = {
|
|
1167
|
+
label: node.label?.name,
|
|
1168
|
+
tryDepth: this.tryFrames.length,
|
|
1169
|
+
leaving: /* @__PURE__ */ new Set()
|
|
1170
|
+
};
|
|
1171
|
+
this.loops.push(loop);
|
|
1172
|
+
let lowered;
|
|
1173
|
+
try {
|
|
1174
|
+
lowered = this.statementKind(node);
|
|
1175
|
+
} finally {
|
|
1176
|
+
this.loops.pop();
|
|
1177
|
+
}
|
|
1178
|
+
const flags = [loop.breakFlag, loop.continueFlag].filter((f) => f !== void 0);
|
|
1179
|
+
const out = [...flags.length ? [local(flags, flags.map(() => boolean(false)))] : [], ...lowered];
|
|
1180
|
+
const enclosing = this.loops[this.loops.length - 1];
|
|
1181
|
+
for (const target of loop.leaving) {
|
|
1182
|
+
if (target === enclosing) {
|
|
1183
|
+
if (target.breakFlag) out.push(ifThen(identifier(target.breakFlag), this.jump("break", void 0)));
|
|
1184
|
+
if (target.continueFlag) {
|
|
1185
|
+
out.push(ifThen(identifier(target.continueFlag), [
|
|
1186
|
+
assign([identifier(target.continueFlag)], [boolean(false)]),
|
|
1187
|
+
...this.jump("continue", void 0)
|
|
1188
|
+
]));
|
|
1189
|
+
}
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
enclosing?.leaving.add(target);
|
|
1193
|
+
const raised = [target.breakFlag, target.continueFlag].filter((f) => f !== void 0).map((f) => identifier(f)).reduce((a, b) => binary("or", a, b));
|
|
1194
|
+
out.push(ifThen(raised, this.jump("break", void 0)));
|
|
1195
|
+
}
|
|
1196
|
+
return out;
|
|
1197
|
+
}
|
|
1198
|
+
statementKind(node) {
|
|
1120
1199
|
switch (node.type) {
|
|
1121
1200
|
// Types, and what exists only for them.
|
|
1122
1201
|
case "TypeAliasStatement":
|
|
@@ -1136,15 +1215,20 @@ end`).body.statements);
|
|
|
1136
1215
|
return this.classDeclaration(node, "declare");
|
|
1137
1216
|
case "AssignmentStatement":
|
|
1138
1217
|
return this.assignment(node);
|
|
1139
|
-
case "CompoundAssignmentStatement":
|
|
1140
|
-
if (
|
|
1218
|
+
case "CompoundAssignmentStatement": {
|
|
1219
|
+
if (node.operator === "??=") return this.nullishAssignment(node);
|
|
1220
|
+
const join = node.operator === "+=" ? this.options.types?.joins.get(node) : void 0;
|
|
1221
|
+
if (join === "dynamic") return this.dynamicAddition(node);
|
|
1222
|
+
const operator = join === "concat" ? "..=" : node.operator;
|
|
1223
|
+
if (this.lua51) return this.compoundAssignment51(node, operator);
|
|
1141
1224
|
return [{
|
|
1142
1225
|
type: "CompoundAssignmentStatement",
|
|
1143
|
-
operator
|
|
1226
|
+
operator,
|
|
1144
1227
|
target: this.expression(node.target),
|
|
1145
1228
|
value: this.expression(node.value),
|
|
1146
1229
|
...spanOf(node)
|
|
1147
1230
|
}];
|
|
1231
|
+
}
|
|
1148
1232
|
case "CallStatement": {
|
|
1149
1233
|
const chain = optionalChain(node.expression);
|
|
1150
1234
|
if (chain) return [this.optionalCallStatement(chain)];
|
|
@@ -1230,11 +1314,20 @@ end`).body.statements);
|
|
|
1230
1314
|
)];
|
|
1231
1315
|
}
|
|
1232
1316
|
case "ReturnStatement":
|
|
1233
|
-
return
|
|
1317
|
+
return this.jump("return", node.argument && this.keptToOne(this.expression(node.argument)), node);
|
|
1234
1318
|
case "BreakStatement":
|
|
1235
|
-
return
|
|
1319
|
+
return this.jump("break", void 0, node, node.label?.name);
|
|
1236
1320
|
case "ContinueStatement":
|
|
1237
|
-
return
|
|
1321
|
+
return this.jump("continue", void 0, node, node.label?.name);
|
|
1322
|
+
// Level 0: what is thrown is what a `catch` is handed, with no
|
|
1323
|
+
// position put in front of a string.
|
|
1324
|
+
case "ThrowStatement":
|
|
1325
|
+
return [callStatement(call(
|
|
1326
|
+
this.builtin("error"),
|
|
1327
|
+
[this.keptToOne(this.expression(node.argument)), number(0)]
|
|
1328
|
+
))];
|
|
1329
|
+
case "TryStatement":
|
|
1330
|
+
return this.tryStatement(node);
|
|
1238
1331
|
case "ImportStatement":
|
|
1239
1332
|
if (node.isTypeOnly) return [];
|
|
1240
1333
|
this.report(node, this.options.module ? "Imports and exports belong at the top level of a module" : "Imports and exports need a bundle: build the project with @tilua/compiler");
|
|
@@ -1247,6 +1340,90 @@ end`).body.statements);
|
|
|
1247
1340
|
return [];
|
|
1248
1341
|
}
|
|
1249
1342
|
}
|
|
1343
|
+
/** A `return`, `break` or `continue`. Inside a `try` that it leaves, it
|
|
1344
|
+
* is the `try`'s function returning a code saying which — and the value
|
|
1345
|
+
* — for the code after the `pcall` to do it there. */
|
|
1346
|
+
jump(kind, value, node, label) {
|
|
1347
|
+
const at = node ? spanOf(node) : {};
|
|
1348
|
+
const frame = this.tryFrames[this.tryFrames.length - 1];
|
|
1349
|
+
const target = kind === "return" ? void 0 : label === void 0 ? this.loops[this.loops.length - 1] : this.loops.findLast((loop) => loop.label === label);
|
|
1350
|
+
if (frame && (kind === "return" || target && target.tryDepth < this.tryFrames.length)) {
|
|
1351
|
+
const key = `${kind}:${label ?? ""}`;
|
|
1352
|
+
let exit = frame.exits.get(key);
|
|
1353
|
+
if (!exit) {
|
|
1354
|
+
exit = { kind, label, code: kind === "return" ? 1 : frame.exits.size + 2 };
|
|
1355
|
+
frame.exits.set(key, exit);
|
|
1356
|
+
}
|
|
1357
|
+
if (value) frame.returnsValue = true;
|
|
1358
|
+
return [returns([number(exit.code), ...value ? [value] : []])];
|
|
1359
|
+
}
|
|
1360
|
+
if (kind === "return") return [{ ...returns(value ? [value] : []), ...at }];
|
|
1361
|
+
const plain = () => kind === "break" ? { ...breakStatement(), ...at } : { ...breakStatement(), type: "ContinueStatement", ...at };
|
|
1362
|
+
const innermost = this.loops[this.loops.length - 1];
|
|
1363
|
+
if (!target || target === innermost) return [plain()];
|
|
1364
|
+
const flag = kind === "break" ? target.breakFlag ??= this.names.fresh(`${target.label}Broke`) : target.continueFlag ??= this.names.fresh(`${target.label}Continued`);
|
|
1365
|
+
innermost.leaving.add(target);
|
|
1366
|
+
return [assign([identifier(flag)], [boolean(true)]), { ...breakStatement(), ...at }];
|
|
1367
|
+
}
|
|
1368
|
+
/** `try { a } catch (e) { b } finally { c }`:
|
|
1369
|
+
*
|
|
1370
|
+
* local ok, result, value = pcall(function() a end)
|
|
1371
|
+
* if not ok then
|
|
1372
|
+
* ok, result, value = pcall(function(e) b end, result)
|
|
1373
|
+
* end
|
|
1374
|
+
* do c end
|
|
1375
|
+
* if not ok then error(result, 0) end
|
|
1376
|
+
* if result == 1 then return value elseif ... end
|
|
1377
|
+
*
|
|
1378
|
+
* Without a `finally`, the `catch` runs in place of the second `pcall`:
|
|
1379
|
+
* nothing has to run after it. A `return`, `break` or `continue` that
|
|
1380
|
+
* leaves the `try` cannot be written inside its function; it returns a
|
|
1381
|
+
* code instead (see `jump`), and the last `if` does what it says. */
|
|
1382
|
+
tryStatement(node) {
|
|
1383
|
+
const ok = this.names.fresh("ok");
|
|
1384
|
+
const result2 = this.names.fresh("result");
|
|
1385
|
+
const value = this.names.fresh("value");
|
|
1386
|
+
const frame = { exits: /* @__PURE__ */ new Map(), returnsValue: false };
|
|
1387
|
+
const guarded = (block2, params, args) => {
|
|
1388
|
+
this.tryFrames.push(frame);
|
|
1389
|
+
let statements;
|
|
1390
|
+
try {
|
|
1391
|
+
statements = this.block(block2).statements;
|
|
1392
|
+
} finally {
|
|
1393
|
+
this.tryFrames.pop();
|
|
1394
|
+
}
|
|
1395
|
+
const passesVarargs = statements.some(usesVararg);
|
|
1396
|
+
const func = functionExpression(functionBody(params, statements, passesVarargs));
|
|
1397
|
+
return call(this.builtin("pcall"), [func, ...args, ...passesVarargs ? [vararg()] : []]);
|
|
1398
|
+
};
|
|
1399
|
+
const attempt = guarded(node.block, [], []);
|
|
1400
|
+
const param = node.handler?.param && this.name(node.handler.param.name);
|
|
1401
|
+
const inPlace = node.handler && !node.finalizer ? this.block(node.handler.body, param ? [local([param], [identifier(result2)])] : []) : void 0;
|
|
1402
|
+
const caught = node.handler && node.finalizer ? guarded(node.handler.body, [param ?? "_"], [identifier(result2)]) : void 0;
|
|
1403
|
+
const finalizer = node.finalizer && this.block(node.finalizer);
|
|
1404
|
+
const returnsValue = frame.returnsValue;
|
|
1405
|
+
const usesResult = returnsValue || frame.exits.size > 0 || !!param || !!node.finalizer;
|
|
1406
|
+
const names = [ok, ...usesResult ? [result2] : [], ...returnsValue ? [value] : []];
|
|
1407
|
+
const failed = () => unary("not", identifier(ok));
|
|
1408
|
+
const out = [this.from(local(names, [attempt]), node)];
|
|
1409
|
+
const clauses = [];
|
|
1410
|
+
if (inPlace) clauses.push({ type: "IfClause", condition: failed(), body: inPlace, ...spanOf(node.handler) });
|
|
1411
|
+
if (caught) out.push(ifThen(failed(), [assign(names.map(identifier), [caught])]));
|
|
1412
|
+
if (finalizer) {
|
|
1413
|
+
out.push(doBlock(finalizer.statements));
|
|
1414
|
+
out.push(ifThen(failed(), [callStatement(call(this.builtin("error"), [identifier(result2), number(0)]))]));
|
|
1415
|
+
}
|
|
1416
|
+
for (const exit of [...frame.exits.values()].sort((a, b) => a.code - b.code)) {
|
|
1417
|
+
clauses.push({
|
|
1418
|
+
type: "IfClause",
|
|
1419
|
+
condition: binary("==", identifier(result2), number(exit.code)),
|
|
1420
|
+
body: block(this.jump(exit.kind, exit.kind === "return" && returnsValue ? identifier(value) : void 0, void 0, exit.label)),
|
|
1421
|
+
...spanOf(node)
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
if (clauses.length) out.push({ type: "IfStatement", clauses, ...spanOf(node) });
|
|
1425
|
+
return out;
|
|
1426
|
+
}
|
|
1250
1427
|
functionDeclarationStatement(node) {
|
|
1251
1428
|
const statement = this.functionStatement(this.reference(node.target.base), node, this.functionBody(node.func), node.isMethod, node.target.path.map((p) => p.name));
|
|
1252
1429
|
if (!node.target.method) return statement;
|
|
@@ -1636,6 +1813,18 @@ end
|
|
|
1636
1813
|
// Functions
|
|
1637
1814
|
// --------------------------------------------------------
|
|
1638
1815
|
functionBody(func) {
|
|
1816
|
+
const frames = this.tryFrames;
|
|
1817
|
+
const loops = this.loops;
|
|
1818
|
+
this.tryFrames = [];
|
|
1819
|
+
this.loops = [];
|
|
1820
|
+
try {
|
|
1821
|
+
return this.functionBodyIn(func);
|
|
1822
|
+
} finally {
|
|
1823
|
+
this.tryFrames = frames;
|
|
1824
|
+
this.loops = loops;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
functionBodyIn(func) {
|
|
1639
1828
|
const params = func.isMethod ? func.params.slice(1) : func.params;
|
|
1640
1829
|
const prelude = [];
|
|
1641
1830
|
const names = [];
|
|
@@ -1686,8 +1875,10 @@ end
|
|
|
1686
1875
|
return nil();
|
|
1687
1876
|
case "InterpolatedStringExpression":
|
|
1688
1877
|
return this.interpolatedString(node);
|
|
1878
|
+
// An object's method, `{ m() { ... } }`, keeps its `this` as the
|
|
1879
|
+
// first parameter written out: a function value has no `:`.
|
|
1689
1880
|
case "FunctionExpression":
|
|
1690
|
-
return functionExpression(this.
|
|
1881
|
+
return functionExpression(this.classFunctionBody(node.func));
|
|
1691
1882
|
case "TableExpression":
|
|
1692
1883
|
return this.tableExpression(node);
|
|
1693
1884
|
case "ArrayExpression":
|
|
@@ -1702,6 +1893,11 @@ end
|
|
|
1702
1893
|
if (node.operator === "instanceof") {
|
|
1703
1894
|
return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
|
|
1704
1895
|
}
|
|
1896
|
+
if (node.operator === "+") {
|
|
1897
|
+
const join = this.options.types?.joins.get(node);
|
|
1898
|
+
if (join === "concat") return binary("..", left, right);
|
|
1899
|
+
if (join === "dynamic") return call(this.helper("add"), [this.keptToOne(left), this.keptToOne(right)]);
|
|
1900
|
+
}
|
|
1705
1901
|
if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
|
|
1706
1902
|
return binary(node.operator, left, right);
|
|
1707
1903
|
}
|
|
@@ -1961,14 +2157,52 @@ end
|
|
|
1961
2157
|
if (!args.length) return string(text);
|
|
1962
2158
|
return methodCall(parenthesized(string(format)), "format", args);
|
|
1963
2159
|
}
|
|
2160
|
+
/** `t ??= v`: `if t == nil then t = v end`, which no Luau has an
|
|
2161
|
+
* operator for. `v` is read only where `t` is nil. As in `a += b` for
|
|
2162
|
+
* Lua 5.1, what addresses the target is read once: an object or key
|
|
2163
|
+
* that is not a plain name goes into a local first. */
|
|
2164
|
+
nullishAssignment(node) {
|
|
2165
|
+
const { before, target } = this.targetOnce(node.target);
|
|
2166
|
+
const statement = ifThen(binary("==", target, nil()), [assign([target], [this.expression(node.value)])]);
|
|
2167
|
+
return before.length ? [doBlock([...before, statement])] : [statement];
|
|
2168
|
+
}
|
|
2169
|
+
/** `t += v` where only the values can say whether it joins or adds:
|
|
2170
|
+
* `t = tilua_add(t, v)`, with what addresses `t` read once. */
|
|
2171
|
+
dynamicAddition(node) {
|
|
2172
|
+
const { before, target } = this.targetOnce(node.target);
|
|
2173
|
+
const statement = assign([target], [call(this.helper("add"), [target, this.keptToOne(this.expression(node.value))])]);
|
|
2174
|
+
return before.length ? [doBlock([...before, statement])] : [statement];
|
|
2175
|
+
}
|
|
2176
|
+
/** An assignment's target, as an expression that may be read and written
|
|
2177
|
+
* freely: an object or key that is not a plain name goes into a local
|
|
2178
|
+
* first, so that `t[next()]` advances once. */
|
|
2179
|
+
targetOnce(node) {
|
|
2180
|
+
const before = [];
|
|
2181
|
+
const once = (expression, name) => {
|
|
2182
|
+
if (expression.type === "Identifier" || expression.type === "StringLiteral" || expression.type === "NumberLiteral") return expression;
|
|
2183
|
+
const local2 = this.names.fresh(name);
|
|
2184
|
+
before.push(local([local2], [expression]));
|
|
2185
|
+
return identifier(local2);
|
|
2186
|
+
};
|
|
2187
|
+
let target;
|
|
2188
|
+
if (node.type === "MemberExpression") {
|
|
2189
|
+
target = member(once(this.expression(node.object), "target"), node.property.name);
|
|
2190
|
+
} else if (node.type === "IndexExpression") {
|
|
2191
|
+
const object2 = once(this.expression(node.object), "target");
|
|
2192
|
+
target = index(object2, once(this.expression(node.index), "key"));
|
|
2193
|
+
} else {
|
|
2194
|
+
target = this.expression(node);
|
|
2195
|
+
}
|
|
2196
|
+
return { before, target };
|
|
2197
|
+
}
|
|
1964
2198
|
/** `a += b` for Lua 5.1, which has no compound assignment: `a = a + b`.
|
|
1965
2199
|
*
|
|
1966
2200
|
* The target has to be read and written, and writing it out twice would
|
|
1967
2201
|
* evaluate whatever addresses it twice — `t[next()] += 1` would advance
|
|
1968
2202
|
* twice and add to the wrong slot. So anything but a plain name has its
|
|
1969
2203
|
* object and key taken into locals first, and both halves use those. */
|
|
1970
|
-
compoundAssignment51(node) {
|
|
1971
|
-
const operator =
|
|
2204
|
+
compoundAssignment51(node, written = node.operator) {
|
|
2205
|
+
const operator = written.slice(0, -1);
|
|
1972
2206
|
const before = [];
|
|
1973
2207
|
let target;
|
|
1974
2208
|
if (node.target.type === "MemberExpression" || node.target.type === "IndexExpression") {
|
|
@@ -2167,6 +2401,7 @@ function optionalChain(node) {
|
|
|
2167
2401
|
if (first < 0) return void 0;
|
|
2168
2402
|
return { base: linkObject(spine[first]), links: spine.slice(first) };
|
|
2169
2403
|
}
|
|
2404
|
+
var LOOPS = /* @__PURE__ */ new Set(["WhileStatement", "RepeatStatement", "NumericForStatement", "GenericForStatement"]);
|
|
2170
2405
|
function usesVararg(node) {
|
|
2171
2406
|
if (Array.isArray(node)) return node.some(usesVararg);
|
|
2172
2407
|
if (!node || typeof node !== "object") return false;
|
|
@@ -3034,7 +3269,7 @@ function analyzeModules(modules, config) {
|
|
|
3034
3269
|
});
|
|
3035
3270
|
for (const d of types.diagnostics) {
|
|
3036
3271
|
const at = d.node;
|
|
3037
|
-
said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
|
|
3272
|
+
said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type", severity: d.severity });
|
|
3038
3273
|
}
|
|
3039
3274
|
const result2 = { types, diagnostics: said };
|
|
3040
3275
|
if (partials === before) {
|
package/dist/index.d.cts
CHANGED
|
@@ -256,6 +256,9 @@ interface BundleDiagnostic {
|
|
|
256
256
|
* `syntax`, `scope` (such as assigning to an import) and `module`
|
|
257
257
|
* problems leave no bundle. */
|
|
258
258
|
readonly category: "syntax" | "scope" | "module" | "type" | "config";
|
|
259
|
+
/** A test that always comes out the same, and the like: said, and no
|
|
260
|
+
* reason for the build to fail. */
|
|
261
|
+
readonly severity?: "warning";
|
|
259
262
|
}
|
|
260
263
|
interface BundleResult {
|
|
261
264
|
/** The bundle — the entry's file, when there are several — or `undefined`
|
package/dist/index.d.ts
CHANGED
|
@@ -256,6 +256,9 @@ interface BundleDiagnostic {
|
|
|
256
256
|
* `syntax`, `scope` (such as assigning to an import) and `module`
|
|
257
257
|
* problems leave no bundle. */
|
|
258
258
|
readonly category: "syntax" | "scope" | "module" | "type" | "config";
|
|
259
|
+
/** A test that always comes out the same, and the like: said, and no
|
|
260
|
+
* reason for the build to fail. */
|
|
261
|
+
readonly severity?: "warning";
|
|
259
262
|
}
|
|
260
263
|
interface BundleResult {
|
|
261
264
|
/** The bundle — the entry's file, when there are several — or `undefined`
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tilua/compiler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Compiles tilua to Luau: lowers the @tilua/parser AST to a luau-parser AST and prints it",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"type": "module",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"luau-parser": "^1.0.3",
|
|
41
|
-
"@tilua/parser": "^
|
|
41
|
+
"@tilua/parser": "^6.0.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^22.20.2",
|
package/runtime/array.luau
CHANGED
|
@@ -154,6 +154,17 @@ function __NAME__.entries(t)
|
|
|
154
154
|
return out
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
+
-- `Object.fromEntries`: each `[key, value]` pair a member of a new table,
|
|
158
|
+
-- a later pair over an earlier one of the same key.
|
|
159
|
+
function __NAME__.fromEntries(t)
|
|
160
|
+
local out = {}
|
|
161
|
+
for i = 1, #t do
|
|
162
|
+
local entry = t[i]
|
|
163
|
+
out[entry[1]] = entry[2]
|
|
164
|
+
end
|
|
165
|
+
return out
|
|
166
|
+
end
|
|
167
|
+
|
|
157
168
|
function __NAME__.flat(t)
|
|
158
169
|
local out = __ARRAY__({})
|
|
159
170
|
for i = 1, #t do
|