@tilua/compiler 5.0.1 → 6.0.1

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 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
- return new Lowerer(program2, scopes, options).run();
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 (this.lua51) return this.compoundAssignment51(node);
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: node.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 [returns(node.argument ? [this.keptToOne(this.expression(node.argument))] : [])];
1277
+ return this.jump("return", node.argument && this.keptToOne(this.expression(node.argument)), node);
1194
1278
  case "BreakStatement":
1195
- return [{ type: "BreakStatement", ...spanOf(node) }];
1279
+ return this.jump("break", void 0, node, node.label?.name);
1196
1280
  case "ContinueStatement":
1197
- return [{ type: "ContinueStatement", ...spanOf(node) }];
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.functionBody(node.func));
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 = node.operator.slice(0, -1);
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
- return new Lowerer(program2, scopes, options).run();
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 (this.lua51) return this.compoundAssignment51(node);
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: node.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 [returns(node.argument ? [this.keptToOne(this.expression(node.argument))] : [])];
1294
+ return this.jump("return", node.argument && this.keptToOne(this.expression(node.argument)), node);
1211
1295
  case "BreakStatement":
1212
- return [{ type: "BreakStatement", ...spanOf(node) }];
1296
+ return this.jump("break", void 0, node, node.label?.name);
1213
1297
  case "ContinueStatement":
1214
- return [{ type: "ContinueStatement", ...spanOf(node) }];
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.functionBody(node.func));
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 = node.operator.slice(0, -1);
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.length ? 1 : 0);
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-Y7QGXLCV.js";
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.length ? 1 : 0);
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
- return new Lowerer(program2, scopes, options).run();
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 (this.lua51) return this.compoundAssignment51(node);
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: node.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 [returns(node.argument ? [this.keptToOne(this.expression(node.argument))] : [])];
1317
+ return this.jump("return", node.argument && this.keptToOne(this.expression(node.argument)), node);
1234
1318
  case "BreakStatement":
1235
- return [{ type: "BreakStatement", ...spanOf(node) }];
1319
+ return this.jump("break", void 0, node, node.label?.name);
1236
1320
  case "ContinueStatement":
1237
- return [{ type: "ContinueStatement", ...spanOf(node) }];
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.functionBody(node.func));
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 = node.operator.slice(0, -1);
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
@@ -4,7 +4,7 @@ import {
4
4
  lower,
5
5
  program,
6
6
  resolveConfig
7
- } from "./chunk-Y7QGXLCV.js";
7
+ } from "./chunk-EQE22LX7.js";
8
8
 
9
9
  // src/compile.ts
10
10
  import { readFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tilua/compiler",
3
- "version": "5.0.1",
3
+ "version": "6.0.1",
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": "^5.0.1"
41
+ "@tilua/parser": "^6.0.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^22.20.2",
@@ -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