@tilua/compiler 5.0.0 → 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 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 |
@@ -180,7 +190,9 @@ another, and one the class writes itself replaces the copy.
180
190
  Two links come with every class, and they cost an instance nothing because
181
191
  they live on the class table: `ClassObject`, which an instance reads through
182
192
  its metatable to reach its own class, and `ParentClass`, which a class reads
183
- to reach the one it extends.
193
+ to reach the one it extends. The class table holds its `name` too
194
+ (`tilua_class(base, "Dog")`), so `rex.ClassObject.name` says what built an
195
+ instance.
184
196
 
185
197
  A class written as a value (`const Counter = class { … }`) is the same code,
186
198
  inside a function that runs where the class is written and hands the table
@@ -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) {
@@ -258,6 +280,18 @@ var Lowerer = class {
258
280
  for (const binding of scopes.bindings.values()) {
259
281
  if (binding.declarationNode) this.bindingByDeclaration.set(binding.declarationNode, binding);
260
282
  }
283
+ const visit = (value) => {
284
+ if (!value || typeof value !== "object") return;
285
+ if (Array.isArray(value)) return void value.forEach(visit);
286
+ const node = value;
287
+ if (node.type === "VariableDeclaration" && node.name?.type === "IdentifierPattern" && node.name.name) {
288
+ let init = node.init;
289
+ while (init?.type === "ParenthesizedExpression") init = init.expression;
290
+ if (init?.type === "ClassExpression") this.classExpressionNames.set(init, node.name.name);
291
+ }
292
+ for (const [key, child] of Object.entries(value)) if (key !== "line" && key !== "column") visit(child);
293
+ };
294
+ visit(source.body);
261
295
  }
262
296
  source;
263
297
  scopes;
@@ -275,10 +309,17 @@ var Lowerer = class {
275
309
  exportsName;
276
310
  /** Globals generated code calls, renamed where the source shadows them. */
277
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 = [];
278
317
  /** Whether the output has to be plain Lua 5.1 rather than Luau. */
279
318
  lua51;
280
319
  /** The source line each emitted statement came from. */
281
320
  origins = /* @__PURE__ */ new WeakMap();
321
+ /** The name each class written as a value is known by. */
322
+ classExpressionNames = /* @__PURE__ */ new WeakMap();
282
323
  run() {
283
324
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
284
325
  const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))])] : [];
@@ -638,6 +679,16 @@ local function ${typeOf}(value, known)
638
679
  end`).body.statements);
639
680
  }
640
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
+ }
641
692
  const instanceOf = this.helpers.get("instanceof");
642
693
  if (instanceOf) {
643
694
  out.push(...parseLuau(`
@@ -660,6 +711,8 @@ end`).body.statements);
660
711
  out.push(...parseLuau(`
661
712
  local function ${accessors}(target, getters, setters)
662
713
  return ${this.builtin("setmetatable").name}(target, {
714
+ -- Read by a spread, which copies what the getters give.
715
+ __tiluaGetters = getters,
663
716
  __index = function(self, key)
664
717
  local get = getters[key]
665
718
  if get ~= nil then
@@ -683,17 +736,25 @@ end`).body.statements);
683
736
  }
684
737
  const assign2 = this.helpers.get("assign");
685
738
  if (assign2) {
686
- out.push(localFunction(assign2, functionBody(["target"], [
687
- numericFor("i", number(1), selectCount(this.builtin("select")), [
688
- local(["source"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
689
- ifThen(binary("~=", identifier("source"), nil()), [
690
- genericFor(["key", "value"], [call(this.builtin("pairs"), [identifier("source")])], [
691
- assign([index(identifier("target"), identifier("key"))], [identifier("value")])
692
- ])
693
- ])
694
- ]),
695
- returns([identifier("target")])
696
- ], true)));
739
+ out.push(...parseLuau(`
740
+ local function ${assign2}(target, ...)
741
+ for i = 1, ${this.builtin("select").name}("#", ...) do
742
+ local source = (${this.builtin("select").name}(i, ...))
743
+ if source ~= nil then
744
+ for key, value in ${this.builtin("pairs").name}(source) do
745
+ target[key] = value
746
+ end
747
+ local meta = ${this.builtin("type").name}(source) == "table" and ${this.builtin("getmetatable").name}(source) or nil
748
+ local getters = ${this.builtin("type").name}(meta) == "table" and ${this.builtin("rawget").name}(meta, "__tiluaGetters") or nil
749
+ if getters ~= nil then
750
+ for key, get in ${this.builtin("pairs").name}(getters) do
751
+ target[key] = get(source)
752
+ end
753
+ end
754
+ end
755
+ end
756
+ return target
757
+ end`).body.statements);
697
758
  }
698
759
  out.push(...runtimes);
699
760
  const concat = this.helpers.get("concat");
@@ -1052,7 +1113,49 @@ end`).body.statements);
1052
1113
  if (!this.origins.has(statement)) this.origins.set(statement, node.line.start);
1053
1114
  return statement;
1054
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. */
1055
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) {
1056
1159
  switch (node.type) {
1057
1160
  // Types, and what exists only for them.
1058
1161
  case "TypeAliasStatement":
@@ -1072,15 +1175,20 @@ end`).body.statements);
1072
1175
  return this.classDeclaration(node, "declare");
1073
1176
  case "AssignmentStatement":
1074
1177
  return this.assignment(node);
1075
- case "CompoundAssignmentStatement":
1076
- 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);
1077
1184
  return [{
1078
1185
  type: "CompoundAssignmentStatement",
1079
- operator: node.operator,
1186
+ operator,
1080
1187
  target: this.expression(node.target),
1081
1188
  value: this.expression(node.value),
1082
1189
  ...spanOf(node)
1083
1190
  }];
1191
+ }
1084
1192
  case "CallStatement": {
1085
1193
  const chain = optionalChain(node.expression);
1086
1194
  if (chain) return [this.optionalCallStatement(chain)];
@@ -1166,11 +1274,20 @@ end`).body.statements);
1166
1274
  )];
1167
1275
  }
1168
1276
  case "ReturnStatement":
1169
- return [returns(node.argument ? [this.keptToOne(this.expression(node.argument))] : [])];
1277
+ return this.jump("return", node.argument && this.keptToOne(this.expression(node.argument)), node);
1170
1278
  case "BreakStatement":
1171
- return [{ type: "BreakStatement", ...spanOf(node) }];
1279
+ return this.jump("break", void 0, node, node.label?.name);
1172
1280
  case "ContinueStatement":
1173
- 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);
1174
1291
  case "ImportStatement":
1175
1292
  if (node.isTypeOnly) return [];
1176
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");
@@ -1183,6 +1300,90 @@ end`).body.statements);
1183
1300
  return [];
1184
1301
  }
1185
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
+ }
1186
1387
  functionDeclarationStatement(node) {
1187
1388
  const statement = this.functionStatement(this.reference(node.target.base), node, this.functionBody(node.func), node.isMethod, node.target.path.map((p) => p.name));
1188
1389
  if (!node.target.method) return statement;
@@ -1238,11 +1439,12 @@ end`).body.statements);
1238
1439
  const { build, accessors } = this.classHelpers;
1239
1440
  const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
1240
1441
  return parseLuau(`
1241
- local function ${build}(base)
1442
+ local function ${build}(base, name)
1242
1443
  local class = { __getters = {}, __setters = {} }
1243
1444
  class.__index = class
1244
1445
  class.ClassObject = class
1245
1446
  class.ParentClass = base
1447
+ class.name = name
1246
1448
  if base ~= nil then
1247
1449
  setmetatable(class, { __index = base })
1248
1450
  setmetatable(class.__getters, { __index = base.__getters })
@@ -1296,7 +1498,11 @@ end
1296
1498
  * table is assigned to `self`, which a module has already declared. */
1297
1499
  classStatements(node, self, declare) {
1298
1500
  const { build, accessors } = this.classRuntime();
1299
- const create = call(identifier(build), [node.superclass ? this.reference(node.superclass) : nil()]);
1501
+ const called = node.name?.name ?? (node.type === "ClassExpression" ? this.classExpressionNames.get(node) : void 0) ?? "";
1502
+ const create = call(identifier(build), [
1503
+ node.superclass ? this.reference(node.superclass) : nil(),
1504
+ string(called)
1505
+ ]);
1300
1506
  const out = [declare !== void 0 ? local([declare], [create]) : assign([self], [create])];
1301
1507
  const previous = this.classContext;
1302
1508
  this.classContext = node;
@@ -1567,6 +1773,18 @@ end
1567
1773
  // Functions
1568
1774
  // --------------------------------------------------------
1569
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) {
1570
1788
  const params = func.isMethod ? func.params.slice(1) : func.params;
1571
1789
  const prelude = [];
1572
1790
  const names = [];
@@ -1617,8 +1835,10 @@ end
1617
1835
  return nil();
1618
1836
  case "InterpolatedStringExpression":
1619
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 `:`.
1620
1840
  case "FunctionExpression":
1621
- return functionExpression(this.functionBody(node.func));
1841
+ return functionExpression(this.classFunctionBody(node.func));
1622
1842
  case "TableExpression":
1623
1843
  return this.tableExpression(node);
1624
1844
  case "ArrayExpression":
@@ -1633,6 +1853,11 @@ end
1633
1853
  if (node.operator === "instanceof") {
1634
1854
  return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
1635
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
+ }
1636
1861
  if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
1637
1862
  return binary(node.operator, left, right);
1638
1863
  }
@@ -1892,14 +2117,52 @@ end
1892
2117
  if (!args.length) return string(text);
1893
2118
  return methodCall(parenthesized(string(format)), "format", args);
1894
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
+ }
1895
2158
  /** `a += b` for Lua 5.1, which has no compound assignment: `a = a + b`.
1896
2159
  *
1897
2160
  * The target has to be read and written, and writing it out twice would
1898
2161
  * evaluate whatever addresses it twice — `t[next()] += 1` would advance
1899
2162
  * twice and add to the wrong slot. So anything but a plain name has its
1900
2163
  * object and key taken into locals first, and both halves use those. */
1901
- compoundAssignment51(node) {
1902
- const operator = node.operator.slice(0, -1);
2164
+ compoundAssignment51(node, written = node.operator) {
2165
+ const operator = written.slice(0, -1);
1903
2166
  const before = [];
1904
2167
  let target;
1905
2168
  if (node.target.type === "MemberExpression" || node.target.type === "IndexExpression") {
@@ -2098,6 +2361,7 @@ function optionalChain(node) {
2098
2361
  if (first < 0) return void 0;
2099
2362
  return { base: linkObject(spine[first]), links: spine.slice(first) };
2100
2363
  }
2364
+ var LOOPS = /* @__PURE__ */ new Set(["WhileStatement", "RepeatStatement", "NumericForStatement", "GenericForStatement"]);
2101
2365
  function usesVararg(node) {
2102
2366
  if (Array.isArray(node)) return node.some(usesVararg);
2103
2367
  if (!node || typeof node !== "object") return false;
@@ -2985,7 +3249,7 @@ function analyzeModules(modules, config) {
2985
3249
  });
2986
3250
  for (const d of types.diagnostics) {
2987
3251
  const at = d.node;
2988
- 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 });
2989
3253
  }
2990
3254
  const result2 = { types, diagnostics: said };
2991
3255
  if (partials === before) {