@tilua/compiler 4.0.0 → 5.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/dist/index.cjs CHANGED
@@ -33,9 +33,9 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
33
33
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
34
34
 
35
35
  // src/bundle.ts
36
- var import_node_fs2 = require("fs");
36
+ var import_node_fs3 = require("fs");
37
37
  var import_node_path2 = require("path");
38
- var import_parser6 = require("@tilua/parser");
38
+ var import_parser7 = require("@tilua/parser");
39
39
  var import_luau_parser2 = require("luau-parser");
40
40
 
41
41
  // src/config.ts
@@ -50,7 +50,7 @@ function resolveConfig(input, entry, cwd = process.cwd()) {
50
50
  }
51
51
 
52
52
  // src/lower.ts
53
- var import_parser4 = require("@tilua/parser");
53
+ var import_parser5 = require("@tilua/parser");
54
54
  var import_luau_parser = require("luau-parser");
55
55
 
56
56
  // src/luau.ts
@@ -212,6 +212,77 @@ var plugin = {
212
212
  };
213
213
  var languageLowering = { plugin, from: "tilua" };
214
214
 
215
+ // src/standard.ts
216
+ var import_node_fs2 = require("fs");
217
+ var import_parser4 = require("@tilua/parser");
218
+ var read2 = (name) => (0, import_node_fs2.readFileSync)(new URL(`../runtime/${name}`, importMetaUrl), "utf8");
219
+ var console = read2("console.luau");
220
+ var result = read2("result.luau");
221
+ var place = (at) => (0, import_parser4.luauString)(at.file === void 0 ? `line ${at.line}` : `${at.file}:${at.line}`);
222
+ var tag = (at) => (0, import_parser4.luauString)(`[${at.file === void 0 ? `line ${at.line}` : `${at.file.replace(/\.tilua$/, "")}:${at.line}`}]:`);
223
+ function consoleMeta(args) {
224
+ const fields = [];
225
+ args.forEach((argument, index2) => {
226
+ if (argument.spread || argument.typeText === void 0 || !(0, import_parser4.isFunctionType)(argument.type)) return;
227
+ const written = argument.declaration ?? argument.source;
228
+ fields.push(`[${index2 + 1}] = { ${(0, import_parser4.luauString)(argument.typeText)}, ${(0, import_parser4.luauString)(written)} }`);
229
+ });
230
+ return fields.length ? `{ ${fields.join(", ")} }` : "nil";
231
+ }
232
+ var CONSOLE_METHODS = /* @__PURE__ */ new Set(["log", "warn", "error"]);
233
+ var REPLACED = /* @__PURE__ */ new Map([
234
+ ["pcall", "pcall"],
235
+ ["xpcall", "xpcall"],
236
+ ["ypcall", "pcall"],
237
+ ["coroutine.resume", "resume"],
238
+ ["string.find", "find"],
239
+ ["string.match", "match"],
240
+ ["string.gsub", "gsub"],
241
+ ["string.byte", "byte"],
242
+ ["math.modf", "modf"],
243
+ ["math.frexp", "frexp"],
244
+ ["next", "next"],
245
+ ["select", "select"],
246
+ ["unpack", "unpack"],
247
+ ["table.unpack", "unpack"],
248
+ ["loadstring", "loadstring"],
249
+ ["pairs", "pairs"],
250
+ ["ipairs", "ipairs"],
251
+ ["string.gmatch", "gmatch"]
252
+ ]);
253
+ var plugin2 = {
254
+ runtime: { console, result },
255
+ methodCall({ method, receiverGlobal, at, arguments: args, use }) {
256
+ if (receiverGlobal === "console" && CONSOLE_METHODS.has(method)) {
257
+ return {
258
+ callee: `${use("console")}.${method}`,
259
+ prepend: [tag(at), consoleMeta(args)],
260
+ passReceiver: false
261
+ };
262
+ }
263
+ return void 0;
264
+ },
265
+ // `print(x)` and `warn(x)` are left exactly as written: the place a line
266
+ // came from is what `console:log` / `console:warn` are for. `error` has to
267
+ // put the place inside the message it raises, so it goes through the
268
+ // runtime.
269
+ globalCall({ name, at, use }) {
270
+ if (name === "error") return { callee: `${use("console")}.raise`, prepend: [place(at)] };
271
+ const replaced = REPLACED.get(name);
272
+ if (replaced) return { callee: `${use("result")}.${replaced}` };
273
+ return void 0;
274
+ },
275
+ // Read as a value there is no call site to write down; the runtime finds
276
+ // one on the stack.
277
+ globalValue({ name, use }) {
278
+ if (name === "error") return `${use("console")}.raiseValue`;
279
+ const replaced = REPLACED.get(name);
280
+ if (replaced) return `${use("result")}.${replaced}`;
281
+ return void 0;
282
+ }
283
+ };
284
+ var standardLowering = { plugin: plugin2, from: "tilua (standard library)" };
285
+
215
286
  // src/lower.ts
216
287
  function lower(program2, scopes, options = {}) {
217
288
  return new Lowerer(program2, scopes, options).run();
@@ -227,6 +298,18 @@ var Lowerer = class {
227
298
  for (const binding of scopes.bindings.values()) {
228
299
  if (binding.declarationNode) this.bindingByDeclaration.set(binding.declarationNode, binding);
229
300
  }
301
+ const visit = (value) => {
302
+ if (!value || typeof value !== "object") return;
303
+ if (Array.isArray(value)) return void value.forEach(visit);
304
+ const node = value;
305
+ if (node.type === "VariableDeclaration" && node.name?.type === "IdentifierPattern" && node.name.name) {
306
+ let init = node.init;
307
+ while (init?.type === "ParenthesizedExpression") init = init.expression;
308
+ if (init?.type === "ClassExpression") this.classExpressionNames.set(init, node.name.name);
309
+ }
310
+ for (const [key, child] of Object.entries(value)) if (key !== "line" && key !== "column") visit(child);
311
+ };
312
+ visit(source.body);
230
313
  }
231
314
  source;
232
315
  scopes;
@@ -248,6 +331,8 @@ var Lowerer = class {
248
331
  lua51;
249
332
  /** The source line each emitted statement came from. */
250
333
  origins = /* @__PURE__ */ new WeakMap();
334
+ /** The name each class written as a value is known by. */
335
+ classExpressionNames = /* @__PURE__ */ new WeakMap();
251
336
  run() {
252
337
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
253
338
  const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))])] : [];
@@ -301,6 +386,10 @@ var Lowerer = class {
301
386
  }
302
387
  return identifier(local2);
303
388
  }
389
+ /** `table.move`. Lua 5.1 has none, and gets a function that does the same. */
390
+ move() {
391
+ return this.lua51 ? this.helper("move") : member(this.builtin("table"), "move");
392
+ }
304
393
  helper(kind) {
305
394
  let name = this.helpers.get(kind);
306
395
  if (!name) {
@@ -401,7 +490,7 @@ var Lowerer = class {
401
490
  const type = this.options.types?.typeOf.get(argument);
402
491
  return {
403
492
  type,
404
- typeText: type ? (0, import_parser4.formatType)(type) : void 0,
493
+ typeText: type ? (0, import_parser5.formatType)(type) : void 0,
405
494
  source: this.writtenSource(argument),
406
495
  declaration: declaration ? this.writtenSource(declaration) : void 0,
407
496
  spread: argument.type === "SpreadElement"
@@ -423,7 +512,11 @@ var Lowerer = class {
423
512
  /** The language's own lowering, then each library's, in load order. A
424
513
  * runtime is named by its lowering's place here (`loweringRuntime`). */
425
514
  get lowerings() {
426
- return this.allLowerings ??= [languageLowering, ...this.options.lowerings ?? []];
515
+ return this.allLowerings ??= [
516
+ languageLowering,
517
+ ...this.options.noLib ? [] : [standardLowering],
518
+ ...this.options.lowerings ?? []
519
+ ];
427
520
  }
428
521
  allLowerings;
429
522
  /** What `receiver:method(...)` becomes, when Luau alone would not run it.
@@ -437,22 +530,28 @@ var Lowerer = class {
437
530
  * (`console:log`), the last library loaded first. No answer leaves an
438
531
  * ordinary Luau method call: `text:upper()` reaches Lua's own. */
439
532
  loweredMethodCall(node) {
533
+ return this.loweredMethod(node.method.name, this.options.types?.methodSources.get(node), node.object, node.arguments, node.method, node);
534
+ }
535
+ /** `loweredMethodCall`, for a method named `method` of what `object`
536
+ * holds — called with `:` on it, or read off it with `.` and handed its
537
+ * `self` like any other argument. `args` are the arguments after the
538
+ * receiver; a read that is not called has none. */
539
+ loweredMethod(method, source, object2, args, name, node) {
440
540
  const lowerings = this.lowerings;
441
- const source = this.options.types?.methodSources.get(node);
442
541
  const asked = source?.origin === "language" ? [0] : source?.origin === "library" ? lowerings.flatMap((l, i) => l.library === source.library ? [i] : []) : source?.origin === "program" ? [] : lowerings.map((_, i) => i).slice(1).reverse();
443
542
  if (!asked.some((i) => lowerings[i].plugin.methodCall)) return void 0;
444
- const receiver = this.options.types?.typeOf.get(node.object);
445
- const receiverGlobal = this.globalName(node.object);
446
- const info = this.lazyArguments(node.arguments);
543
+ const receiver = this.options.types?.typeOf.get(object2);
544
+ const receiverGlobal = this.globalName(object2);
545
+ const info = this.lazyArguments(args);
447
546
  for (const i of asked) {
448
- const plugin2 = lowerings[i].plugin;
449
- if (!plugin2.methodCall) continue;
450
- const answer = plugin2.methodCall({
451
- method: node.method.name,
547
+ const plugin3 = lowerings[i].plugin;
548
+ if (!plugin3.methodCall) continue;
549
+ const answer = plugin3.methodCall({
550
+ method,
452
551
  receiver,
453
552
  receiverGlobal,
454
- argumentCount: node.arguments.length,
455
- at: this.callSite(node.method),
553
+ argumentCount: args.length,
554
+ at: this.callSite(name),
456
555
  get arguments() {
457
556
  return info();
458
557
  },
@@ -476,9 +575,9 @@ var Lowerer = class {
476
575
  if (name === void 0) return void 0;
477
576
  const info = this.lazyArguments(node.arguments);
478
577
  for (let i = lowerings.length - 1; i >= 0; i--) {
479
- const plugin2 = lowerings[i].plugin;
480
- if (!plugin2.globalCall) continue;
481
- const answer = plugin2.globalCall({
578
+ const plugin3 = lowerings[i].plugin;
579
+ if (!plugin3.globalCall) continue;
580
+ const answer = plugin3.globalCall({
482
581
  name,
483
582
  at: this.callSite(node.callee),
484
583
  get arguments() {
@@ -530,6 +629,31 @@ var Lowerer = class {
530
629
  const out = [];
531
630
  const runtimes = this.runtimeDefinitions();
532
631
  if (this.helpers.has("concat")) this.helper("array");
632
+ if (this.helpers.has("concat")) this.move();
633
+ const move = this.helpers.get("move");
634
+ if (move) {
635
+ out.push(...(0, import_luau_parser.parse)(`
636
+ local function ${move}(from, first, last, at, to)
637
+ for i = first, last do
638
+ to[at + i - first] = from[i]
639
+ end
640
+ return to
641
+ end`).body.statements);
642
+ }
643
+ const each = this.helpers.get("each");
644
+ if (each) {
645
+ out.push(...(0, import_luau_parser.parse)(`
646
+ local function ${each}(source, ...)
647
+ if ${this.builtin("type").name}(source) == "function" then
648
+ return source, ...
649
+ end
650
+ local meta = ${this.builtin("getmetatable").name}(source)
651
+ if meta ~= nil and meta.__iter ~= nil then
652
+ return meta.__iter(source)
653
+ end
654
+ return ${this.builtin("next").name}, source
655
+ end`).body.statements);
656
+ }
533
657
  const marker = this.helpers.get("array");
534
658
  const typeOf = this.helpers.get("typeof");
535
659
  if (marker || typeOf) {
@@ -568,19 +692,72 @@ local function ${typeOf}(value, known)
568
692
  end`).body.statements);
569
693
  }
570
694
  }
695
+ const instanceOf = this.helpers.get("instanceof");
696
+ if (instanceOf) {
697
+ out.push(...(0, import_luau_parser.parse)(`
698
+ local function ${instanceOf}(value, class)
699
+ if ${this.builtin("type").name}(value) ~= "table" then
700
+ return false
701
+ end
702
+ local current = value.ClassObject
703
+ while current ~= nil do
704
+ if current == class then
705
+ return true
706
+ end
707
+ current = current.ParentClass
708
+ end
709
+ return false
710
+ end`).body.statements);
711
+ }
712
+ const accessors = this.helpers.get("accessors");
713
+ if (accessors) {
714
+ out.push(...(0, import_luau_parser.parse)(`
715
+ local function ${accessors}(target, getters, setters)
716
+ return ${this.builtin("setmetatable").name}(target, {
717
+ -- Read by a spread, which copies what the getters give.
718
+ __tiluaGetters = getters,
719
+ __index = function(self, key)
720
+ local get = getters[key]
721
+ if get ~= nil then
722
+ return get(self)
723
+ end
724
+ return nil
725
+ end,
726
+ __newindex = function(self, key, value)
727
+ local set = setters[key]
728
+ if set ~= nil then
729
+ set(self, value)
730
+ return
731
+ end
732
+ if getters[key] ~= nil then
733
+ ${this.builtin("error").name}("Cannot assign to '" .. ${this.builtin("tostring").name}(key) .. "': it has a getter and no setter", 2)
734
+ end
735
+ ${this.builtin("rawset").name}(self, key, value)
736
+ end,
737
+ })
738
+ end`).body.statements);
739
+ }
571
740
  const assign2 = this.helpers.get("assign");
572
741
  if (assign2) {
573
- out.push(localFunction(assign2, functionBody(["target"], [
574
- numericFor("i", number(1), selectCount(this.builtin("select")), [
575
- local(["source"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
576
- ifThen(binary("~=", identifier("source"), nil()), [
577
- genericFor(["key", "value"], [call(this.builtin("pairs"), [identifier("source")])], [
578
- assign([index(identifier("target"), identifier("key"))], [identifier("value")])
579
- ])
580
- ])
581
- ]),
582
- returns([identifier("target")])
583
- ], true)));
742
+ out.push(...(0, import_luau_parser.parse)(`
743
+ local function ${assign2}(target, ...)
744
+ for i = 1, ${this.builtin("select").name}("#", ...) do
745
+ local source = (${this.builtin("select").name}(i, ...))
746
+ if source ~= nil then
747
+ for key, value in ${this.builtin("pairs").name}(source) do
748
+ target[key] = value
749
+ end
750
+ local meta = ${this.builtin("type").name}(source) == "table" and ${this.builtin("getmetatable").name}(source) or nil
751
+ local getters = ${this.builtin("type").name}(meta) == "table" and ${this.builtin("rawget").name}(meta, "__tiluaGetters") or nil
752
+ if getters ~= nil then
753
+ for key, get in ${this.builtin("pairs").name}(getters) do
754
+ target[key] = get(source)
755
+ end
756
+ end
757
+ end
758
+ end
759
+ return target
760
+ end`).body.statements);
584
761
  }
585
762
  out.push(...runtimes);
586
763
  const concat = this.helpers.get("concat");
@@ -589,7 +766,7 @@ end`).body.statements);
589
766
  local(["result"], [this.array(table([]))]),
590
767
  numericFor("i", number(1), selectCount(this.builtin("select")), [
591
768
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
592
- callStatement(call(member(this.builtin("table"), "move"), [
769
+ callStatement(call(this.move(), [
593
770
  identifier("part"),
594
771
  number(1),
595
772
  unary("#", identifier("part")),
@@ -606,8 +783,8 @@ end`).body.statements);
606
783
  runtimeDefinitions() {
607
784
  const out = [];
608
785
  for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
609
- const { plugin: plugin2, from } = this.lowerings[index2];
610
- const source = plugin2.runtime?.[runtime2];
786
+ const { plugin: plugin3, from } = this.lowerings[index2];
787
+ const source = plugin3.runtime?.[runtime2];
611
788
  if (source === void 0) {
612
789
  this.diagnostics.push({
613
790
  message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
@@ -702,6 +879,7 @@ end`).body.statements);
702
879
  names.push("default");
703
880
  break;
704
881
  case "ExportNamedStatement": {
882
+ if (statement.typeOnly) break;
705
883
  if (statement.source) {
706
884
  const key = resolve4(statement.source);
707
885
  if (key === void 0) break;
@@ -785,6 +963,26 @@ end`).body.statements);
785
963
  for (let i = 0; i < locals.length; i += 100) declarations.push(local(locals.slice(i, i + 100), []));
786
964
  return [...declarations, ...hoisted, ...requires, ...body];
787
965
  }
966
+ /** `import("./x")`: the bundle's `import`, called with the module's name.
967
+ * It answers the module's exports table — what `import * as` names. */
968
+ importExpression(node) {
969
+ const context = this.options.module;
970
+ if (!context) {
971
+ this.report(node, "import() needs a bundle: build the project with @tilua/compiler");
972
+ return nil();
973
+ }
974
+ const source = node.source;
975
+ if (source.type !== "StringLiteral") {
976
+ this.report(source, "import() takes a string literal: which module it loads is decided when the project is built");
977
+ return nil();
978
+ }
979
+ const key = context.resolve(source.value, true);
980
+ if (key === void 0) {
981
+ this.report(source, `Cannot find module '${source.value}'`);
982
+ return nil();
983
+ }
984
+ return call(context.import ?? context.require, [string(key)]);
985
+ }
788
986
  isRewritten(node) {
789
987
  const binding = this.bindingByDeclaration.get(node);
790
988
  return binding !== void 0 && this.rewrites.has(binding.id);
@@ -931,7 +1129,7 @@ end`).body.statements);
931
1129
  case "VariableDeclaration":
932
1130
  return this.variableDeclaration(node, "declare");
933
1131
  case "FunctionDeclaration":
934
- return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: node.attributes }];
1132
+ return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: this.lua51 ? void 0 : node.attributes }];
935
1133
  case "FunctionDeclarationStatement":
936
1134
  return [this.functionDeclarationStatement(node)];
937
1135
  case "ClassDeclaration":
@@ -962,8 +1160,13 @@ end`).body.statements);
962
1160
  // kept — Lua has no expression statement — and it cannot run
963
1161
  // anything, since a call is a `CallStatement` instead, so dropping
964
1162
  // it changes nothing but the noise.
965
- case "ExpressionStatement":
966
- return [];
1163
+ case "ExpressionStatement": {
1164
+ let expression = node.expression;
1165
+ while (expression.type === "ParenthesizedExpression") expression = expression.expression;
1166
+ if (expression.type !== "ImportExpression") return [];
1167
+ const call2 = this.importExpression(expression);
1168
+ return call2.type === "CallExpression" ? [this.from(callStatement(call2), node)] : [];
1169
+ }
967
1170
  case "DoStatement":
968
1171
  return [{ type: "DoStatement", body: this.block(node.body), ...spanOf(node) }];
969
1172
  case "WhileStatement":
@@ -1015,7 +1218,11 @@ end`).body.statements);
1015
1218
  let source = this.expression(node.iterator);
1016
1219
  if (form.viaIter) source = methodCall(source, "__iter", []);
1017
1220
  const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
1018
- const iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1221
+ let iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1222
+ if (this.lua51 && form.walks === "values") {
1223
+ const kind = form.viaIter ? void 0 : this.options.types?.typeOf.get(node.iterator)?.kind;
1224
+ iterators = [call(kind === "array" || kind === "tuple" ? this.builtin("ipairs") : this.helper("each"), [source])];
1225
+ }
1019
1226
  return [genericFor(
1020
1227
  variables,
1021
1228
  iterators,
@@ -1095,11 +1302,12 @@ end`).body.statements);
1095
1302
  const { build, accessors } = this.classHelpers;
1096
1303
  const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
1097
1304
  return (0, import_luau_parser.parse)(`
1098
- local function ${build}(base)
1305
+ local function ${build}(base, name)
1099
1306
  local class = { __getters = {}, __setters = {} }
1100
1307
  class.__index = class
1101
1308
  class.ClassObject = class
1102
1309
  class.ParentClass = base
1310
+ class.name = name
1103
1311
  if base ~= nil then
1104
1312
  setmetatable(class, { __index = base })
1105
1313
  setmetatable(class.__getters, { __index = base.__getters })
@@ -1153,7 +1361,11 @@ end
1153
1361
  * table is assigned to `self`, which a module has already declared. */
1154
1362
  classStatements(node, self, declare) {
1155
1363
  const { build, accessors } = this.classRuntime();
1156
- const create = call(identifier(build), [node.superclass ? this.reference(node.superclass) : nil()]);
1364
+ const called = node.name?.name ?? (node.type === "ClassExpression" ? this.classExpressionNames.get(node) : void 0) ?? "";
1365
+ const create = call(identifier(build), [
1366
+ node.superclass ? this.reference(node.superclass) : nil(),
1367
+ string(called)
1368
+ ]);
1157
1369
  const out = [declare !== void 0 ? local([declare], [create]) : assign([self], [create])];
1158
1370
  const previous = this.classContext;
1159
1371
  this.classContext = node;
@@ -1268,7 +1480,8 @@ end
1268
1480
  },
1269
1481
  isMethod,
1270
1482
  func,
1271
- attributes: node.attributes,
1483
+ // `@native` and the like are Luau's.
1484
+ attributes: this.lua51 ? void 0 : node.attributes,
1272
1485
  ...spanOf(node)
1273
1486
  };
1274
1487
  }
@@ -1369,7 +1582,7 @@ end
1369
1582
  if (element) bind(element.value, index(source, number(i + 1)), element.default);
1370
1583
  });
1371
1584
  if (pattern.rest) {
1372
- const elements = call(member(this.builtin("table"), "move"), [
1585
+ const elements = call(this.move(), [
1373
1586
  source,
1374
1587
  number(pattern.elements.length + 1),
1375
1588
  unary("#", source),
@@ -1480,8 +1693,15 @@ end
1480
1693
  case "ArrayExpression":
1481
1694
  return this.arrayExpression(node);
1482
1695
  case "BinaryExpression": {
1696
+ if (node.operator === "??") return this.nullish(node);
1483
1697
  const left = this.expression(node.left);
1484
1698
  const right = this.expression(node.right);
1699
+ if (node.operator === "in") {
1700
+ return parenthesized(binary("~=", index(this.keptToOne(right), this.keptToOne(left)), nil()));
1701
+ }
1702
+ if (node.operator === "instanceof") {
1703
+ return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
1704
+ }
1485
1705
  if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
1486
1706
  return binary(node.operator, left, right);
1487
1707
  }
@@ -1508,6 +1728,8 @@ end
1508
1728
  }
1509
1729
  case "ParenthesizedExpression":
1510
1730
  return parenthesized(this.expression(node.expression));
1731
+ case "ImportExpression":
1732
+ return this.importExpression(node);
1511
1733
  // Types only: the value is the expression itself.
1512
1734
  case "TypeAssertionExpression":
1513
1735
  case "SatisfiesExpression":
@@ -1561,7 +1783,7 @@ end
1561
1783
  const last2 = list.length - 1;
1562
1784
  return lowered.length && !many.has(list[last2]) ? [...lowered.slice(0, -1), this.keptToOne(lowered[last2])] : lowered;
1563
1785
  }
1564
- const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
1786
+ const unpack = (value) => call(this.lua51 ? this.builtin("unpack") : member(this.builtin("table"), "unpack"), [value]);
1565
1787
  const last = list[list.length - 1];
1566
1788
  if (spreads.length === 1 && last.type === "SpreadElement") {
1567
1789
  return [...list.slice(0, -1).map((a) => this.expression(a)), unpack(this.expression(last.argument))];
@@ -1575,11 +1797,21 @@ end
1575
1797
  /** One link of an access chain, read from `object`. */
1576
1798
  link(node, object2) {
1577
1799
  switch (node.type) {
1578
- case "MemberExpression":
1800
+ case "MemberExpression": {
1801
+ if (this.options.types?.lengths.has(node)) return unary("#", object2);
1802
+ const source = this.options.types?.methodReads.get(node);
1803
+ const lowered = source && this.loweredMethod(node.property.name, source, node.object, [], node.property, node);
1804
+ if (lowered) {
1805
+ const forwarded = call(calleePath(lowered.callee), [...lowered.prepend, vararg()]);
1806
+ return functionExpression(functionBody(lowered.passReceiver ? [] : ["_"], [returns([forwarded])], true));
1807
+ }
1579
1808
  return member(object2, node.property.name);
1809
+ }
1580
1810
  case "IndexExpression":
1581
1811
  return index(object2, this.expression(node.index));
1582
1812
  case "CallExpression": {
1813
+ const method = this.methodCalled(node);
1814
+ if (method) return method;
1583
1815
  const lowered = this.loweredGlobalCall(node);
1584
1816
  const args = this.arguments(node.arguments);
1585
1817
  if (!lowered) return call(object2, args);
@@ -1602,6 +1834,24 @@ end
1602
1834
  }
1603
1835
  }
1604
1836
  }
1837
+ /** `names.filter(names, f)`: a method of the value's metatable, read with
1838
+ * `.` and handed its `self` as the first argument. Written as a call of
1839
+ * the function the method really is — `array.filter(names, f)` — where
1840
+ * whoever declared the metatable says so; `undefined` leaves an ordinary
1841
+ * call, which is right for a string's Lua methods (`text.upper(text)`
1842
+ * reaches Lua's own `string.upper`). */
1843
+ methodCalled(node) {
1844
+ const callee = node.callee;
1845
+ if (callee.type !== "MemberExpression") return void 0;
1846
+ const source = this.options.types?.methodReads.get(callee);
1847
+ if (!source) return void 0;
1848
+ const spreads = node.arguments.map((a, i) => a.type === "SpreadElement" ? i : -1).filter((i) => i >= 0);
1849
+ if (!node.arguments.length || spreads.includes(0) || spreads.some((i) => i !== node.arguments.length - 1)) return void 0;
1850
+ const lowered = this.loweredMethod(callee.property.name, source, callee.object, node.arguments.slice(1), callee.property, node);
1851
+ if (!lowered) return void 0;
1852
+ const [self, ...rest] = this.arguments(node.arguments);
1853
+ return call(calleePath(lowered.callee), [...lowered.prepend, ...lowered.passReceiver ? [self] : [], ...rest]);
1854
+ }
1605
1855
  // --------------------------------------------------------
1606
1856
  // Optional chains
1607
1857
  // --------------------------------------------------------
@@ -1647,7 +1897,7 @@ end
1647
1897
  const segments = this.segments(chain);
1648
1898
  const base = this.expression(chain.base);
1649
1899
  const last = chain.links[chain.links.length - 1];
1650
- if (base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1900
+ if (!this.lua51 && base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1651
1901
  return {
1652
1902
  type: "IfElseExpression",
1653
1903
  clauses: [{ condition: binary("==", base, nil()), body: nil() }],
@@ -1668,6 +1918,32 @@ end
1668
1918
  const func = functionExpression(functionBody([], statements, passesVarargs));
1669
1919
  return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1670
1920
  }
1921
+ /** `a ?? b`: `a`, or `b` where `a` is nil — read only then. A `false`
1922
+ * stays, which is what sets it apart from `a or b`. A name is tested
1923
+ * in place; anything else is read once, into a local. */
1924
+ nullish(node) {
1925
+ const left = this.keptToOne(this.expression(node.left));
1926
+ const right = this.keptToOne(this.expression(node.right));
1927
+ if (!this.lua51 && left.type === "Identifier") {
1928
+ return {
1929
+ type: "IfElseExpression",
1930
+ clauses: [{ condition: binary("~=", left, nil()), body: left }],
1931
+ alternate: right,
1932
+ line: left.line,
1933
+ column: left.column
1934
+ };
1935
+ }
1936
+ const ref = this.names.fresh("value");
1937
+ const at = identifier(ref);
1938
+ const statements = [
1939
+ local([ref], [left]),
1940
+ ifThen(binary("~=", at, nil()), [returns([at])]),
1941
+ returns([right])
1942
+ ];
1943
+ const passesVarargs = statements.some(usesVararg);
1944
+ const func = functionExpression(functionBody([], statements, passesVarargs));
1945
+ return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1946
+ }
1671
1947
  /** `` `${a} any` `` -> `("%s any"):format(tostring(a))` */
1672
1948
  interpolatedString(node) {
1673
1949
  let format = "";
@@ -1749,12 +2025,12 @@ end
1749
2025
  }));
1750
2026
  const alternate = this.expression(node.alternate);
1751
2027
  if (clauses.every((c) => alwaysTruthy(c.body))) {
1752
- let result = alternate;
2028
+ let result2 = alternate;
1753
2029
  for (let i = clauses.length - 1; i >= 0; i--) {
1754
2030
  const { condition, body } = clauses[i];
1755
- result = binary("or", binary("and", condition, body), result);
2031
+ result2 = binary("or", binary("and", condition, body), result2);
1756
2032
  }
1757
- return parenthesized(result);
2033
+ return parenthesized(result2);
1758
2034
  }
1759
2035
  const statement = {
1760
2036
  type: "IfStatement",
@@ -1811,11 +2087,20 @@ end
1811
2087
  current = [];
1812
2088
  parts.push(this.expression(f.argument));
1813
2089
  break;
2090
+ case "TableFieldAccessor":
2091
+ break;
1814
2092
  }
1815
2093
  }
1816
- if (parts.length === 0) return table(current);
1817
- if (current.length) parts.push(table(current));
1818
- return call(this.helper("assign"), [table([]), ...parts]);
2094
+ let made;
2095
+ if (parts.length === 0) made = table(current);
2096
+ else {
2097
+ if (current.length) parts.push(table(current));
2098
+ made = call(this.helper("assign"), [table([]), ...parts]);
2099
+ }
2100
+ const accessors = node.fields.filter((f) => f.type === "TableFieldAccessor");
2101
+ if (!accessors.length) return made;
2102
+ const table2 = (kind) => table(accessors.filter((f) => f.kind === kind).map((f) => field(f.key.type === "Identifier" ? f.key.name : f.key.value, functionExpression(this.classFunctionBody(f.func)))));
2103
+ return call(this.helper("accessors"), [made, table2("get"), table2("set")]);
1819
2104
  }
1820
2105
  /** `[1, ...xs, 2]`. Without a spread it is a Luau sequence; with one, the
1821
2106
  * runs of plain elements and the spread arrays are joined by `concat`. */
@@ -2072,7 +2357,7 @@ var CLASS_METAMETHODS = [
2072
2357
  ];
2073
2358
 
2074
2359
  // src/linemap.ts
2075
- var import_parser5 = require("@tilua/parser");
2360
+ var import_parser6 = require("@tilua/parser");
2076
2361
  function statementsOf(node) {
2077
2362
  const out = [];
2078
2363
  const visit = (value) => {
@@ -2105,7 +2390,7 @@ function buildLineMap(emitted, printed, origins) {
2105
2390
  }
2106
2391
  function lineMapSource(map) {
2107
2392
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
2108
- const quoted = (s) => (0, import_parser5.luauString)(s.replace(/\\/g, "/"));
2393
+ const quoted = (s) => (0, import_parser6.luauString)(s.replace(/\\/g, "/"));
2109
2394
  return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
2110
2395
  }
2111
2396
 
@@ -2117,15 +2402,15 @@ async function loadLowerings(modules) {
2117
2402
  for (const module2 of modules) {
2118
2403
  try {
2119
2404
  const loaded = await import((0, import_node_url.pathToFileURL)(module2.file).href);
2120
- const plugin2 = loaded.default;
2121
- if (!plugin2 || typeof plugin2 !== "object") {
2405
+ const plugin3 = loaded.default;
2406
+ if (!plugin3 || typeof plugin3 !== "object") {
2122
2407
  problems.push({
2123
2408
  file: module2.file,
2124
2409
  message: `'${module2.from}' lowering module has no default export`
2125
2410
  });
2126
2411
  continue;
2127
2412
  }
2128
- lowerings.push({ plugin: plugin2, from: module2.from, library: module2.library });
2413
+ lowerings.push({ plugin: plugin3, from: module2.from, library: module2.library });
2129
2414
  } catch (error) {
2130
2415
  problems.push({
2131
2416
  file: module2.file,
@@ -2152,16 +2437,17 @@ async function bundle(options) {
2152
2437
  const file = queue.shift();
2153
2438
  if (sources.has(file)) continue;
2154
2439
  const parsed = parseFile(file, diagnostics, directives);
2155
- if (!parsed) return { modules: [], diagnostics };
2440
+ if (!parsed) return { files: [], modules: [], diagnostics };
2156
2441
  const { program: program3, text } = parsed;
2157
- const scopes = (0, import_parser6.analyzeScopes)(program3);
2442
+ const scopes = (0, import_parser7.analyzeScopes)(program3, { noLib: config?.noLib });
2158
2443
  for (const d of scopes.diagnostics) {
2159
2444
  diagnostics.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "scope" });
2160
2445
  }
2161
2446
  sources.set(file, { file, name: nameOf(file), text, program: program3, scopes });
2162
- for (const specifier of importedSpecifiers(program3)) {
2163
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config);
2164
- if (target && !target.endsWith(".d.tilua")) queue.push(target);
2447
+ const dynamic = (0, import_parser7.importExpressions)(program3).flatMap((node) => node.source.type === "StringLiteral" ? [node.source.value] : []);
2448
+ for (const specifier of [...importedSpecifiers(program3), ...dynamic]) {
2449
+ const target2 = (0, import_parser7.resolveModulePath)(file, specifier, config);
2450
+ if (target2 && !target2.endsWith(".d.tilua")) queue.push(target2);
2165
2451
  }
2166
2452
  }
2167
2453
  const analysis = analyzeModules([...sources.values()], config);
@@ -2173,6 +2459,9 @@ async function bundle(options) {
2173
2459
  const names = Names.from(...[...sources.values()].map((s) => s.program));
2174
2460
  const G = names.fresh("G");
2175
2461
  const requireExpression = member(identifier(G), "require");
2462
+ const importExpression = member(identifier(G), "import");
2463
+ const mode = options.output ?? config?.output ?? "single";
2464
+ const target = options.target ?? config?.target ?? "luau";
2176
2465
  const modules = /* @__PURE__ */ new Map();
2177
2466
  const origins = /* @__PURE__ */ new WeakMap();
2178
2467
  const pending = [entry];
@@ -2181,104 +2470,228 @@ async function bundle(options) {
2181
2470
  if (modules.has(file)) continue;
2182
2471
  const source = sources.get(file);
2183
2472
  const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
2184
- const lowered = lower(source.program, source.scopes, {
2473
+ const imports = /* @__PURE__ */ new Set();
2474
+ const dynamicImports = /* @__PURE__ */ new Set();
2475
+ const lowered2 = lower(source.program, source.scopes, {
2185
2476
  names,
2186
2477
  types: analysis.types.get(file),
2187
2478
  lowerings,
2188
- target: options.target ?? config?.target,
2479
+ target,
2480
+ noLib: config?.noLib,
2189
2481
  source: source.text,
2190
2482
  file: shown,
2191
2483
  module: {
2192
2484
  name: source.name,
2193
2485
  require: requireExpression,
2194
- lines: `${G}.lines`,
2486
+ import: importExpression,
2487
+ // One file has one line map. Several have one each, and a
2488
+ // module reads the one of the file it is in.
2489
+ lines: mode === "single" ? `${G}.lines` : `${G}.maps[${(0, import_parser7.luauString)(source.name)}]`,
2195
2490
  fail: `${G}.fail`,
2196
2491
  arrays: `${G}.arrays`,
2197
- resolve: (specifier) => {
2198
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config);
2199
- if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
2200
- pending.push(target);
2201
- return sources.get(target).name;
2492
+ resolve: (specifier, dynamic) => {
2493
+ const found = (0, import_parser7.resolveModulePath)(file, specifier, config);
2494
+ if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
2495
+ pending.push(found);
2496
+ const name = sources.get(found).name;
2497
+ (dynamic ? dynamicImports : imports).add(name);
2498
+ return name;
2202
2499
  }
2203
2500
  }
2204
2501
  });
2205
- for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2206
- for (const statement of allStatements(lowered.statements)) {
2207
- const line = lowered.origins.get(statement);
2502
+ for (const d of lowered2.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2503
+ for (const statement of allStatements(lowered2.statements)) {
2504
+ const line = lowered2.origins.get(statement);
2208
2505
  if (line !== void 0) origins.set(statement, { file: shown, line });
2209
2506
  }
2210
2507
  modules.set(file, {
2211
2508
  name: source.name,
2212
- statements: lowered.statements,
2213
- exportsName: lowered.exportsName,
2214
- info: lowered.module,
2215
- usesScriptArgs: lowered.usesScriptArgs
2509
+ statements: lowered2.statements,
2510
+ exportsName: lowered2.exportsName,
2511
+ info: lowered2.module,
2512
+ usesScriptArgs: lowered2.usesScriptArgs,
2513
+ imports,
2514
+ dynamicImports
2216
2515
  });
2217
2516
  }
2218
2517
  for (const [file, fileDirectives] of directives) {
2219
2518
  const semantic = diagnostics.filter((d) => d.file === file && (d.category === "scope" || d.category === "type"));
2220
- const { kept, unusedExpectErrors } = (0, import_parser6.applyDirectives)(fileDirectives, semantic, (d) => d.line);
2519
+ const { kept, unusedExpectErrors } = (0, import_parser7.applyDirectives)(fileDirectives, semantic, (d) => d.line);
2221
2520
  const suppressed = new Set(semantic.filter((d) => !kept.includes(d)));
2222
2521
  const remaining = diagnostics.filter((d) => !suppressed.has(d));
2223
2522
  diagnostics.length = 0;
2224
2523
  diagnostics.push(...remaining);
2225
2524
  if (options.typeCheck === false) continue;
2226
2525
  for (const d of unusedExpectErrors) {
2227
- diagnostics.push({ file, message: import_parser6.UNUSED_EXPECT_ERROR, line: d.line, column: d.column, category: "type" });
2526
+ diagnostics.push({ file, message: import_parser7.UNUSED_EXPECT_ERROR, line: d.line, column: d.column, category: "type" });
2228
2527
  }
2229
2528
  }
2230
2529
  const moduleProblems = new Set(diagnostics.filter((d) => d.category === "module").map((d) => `${d.file}:${d.line}:${d.column}:${d.message}`));
2231
2530
  const reported = diagnostics.filter((d) => d.category !== "type" || !moduleProblems.has(`${d.file}:${d.line}:${d.column}:${d.message}`));
2232
2531
  diagnostics.length = 0;
2233
2532
  diagnostics.push(...reported);
2234
- const moduleNames = [...modules.values()].map((m) => m.name);
2533
+ const lowered = [...modules.values()];
2534
+ const moduleNames = lowered.map((m) => m.name);
2235
2535
  if (diagnostics.some((d) => d.category === "syntax" || d.category === "scope" || d.category === "module")) {
2236
- return { modules: moduleNames, diagnostics };
2237
- }
2238
- const program2 = (0, import_luau_parser2.parse)(runtime(G));
2536
+ return { files: [], modules: moduleNames, diagnostics };
2537
+ }
2538
+ const several = mode !== "single";
2539
+ const layout = layoutOf(mode, modules.get(entry), lowered, options.outFile);
2540
+ const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
2541
+ const elsewhere = /* @__PURE__ */ new Map();
2542
+ for (const file of layout.slice(1)) {
2543
+ for (const module2 of file.modules) elsewhere.set(module2.name, loadPath(layout[0].path, file.path, target));
2544
+ }
2545
+ const program2 = (0, import_luau_parser2.parse)(runtime(G, {
2546
+ several,
2547
+ target,
2548
+ files: elsewhere,
2549
+ imports: lowered.some((m) => m.dynamicImports.size > 0)
2550
+ }));
2239
2551
  const assignment = program2.body.statements[1];
2240
- if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2552
+ if (usesScriptArgs) {
2241
2553
  program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2242
2554
  program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
2555
+ if (several) program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.scriptArgs = scriptArgs`).body.statements);
2243
2556
  }
2244
2557
  const modulesTable = assignment.type === "AssignmentStatement" && assignment.values[0].type === "TableExpression" ? assignment.values[0].fields.find((f) => f.type === "TableFieldNamed" && f.name.name === "modules")?.value : void 0;
2245
2558
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
2246
- for (const module2 of modules.values()) {
2247
- const { names: exported, links, stars } = module2.info;
2248
- const fields = [];
2249
- if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2250
- if (links.length) {
2251
- fields.push(field("links", table(links.map((link) => field(link.name, table([
2252
- { type: "TableFieldPositional", value: string(link.module) },
2253
- ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2254
- ]))))));
2255
- }
2256
- if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2257
- fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
2258
- modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: table(fields) });
2559
+ for (const module2 of layout[0].modules) {
2560
+ modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: moduleTable(module2) });
2259
2561
  }
2260
2562
  const ok = names.fresh("ok");
2261
- const result = names.fresh("result");
2563
+ const result2 = names.fresh("result");
2262
2564
  const entryStatements = (0, import_luau_parser2.parse)(`
2263
- local ${ok}, ${result} = xpcall(function()
2264
- return ${G}.require(${(0, import_parser6.luauString)(sources.get(entry).name)})
2565
+ local ${ok}, ${result2} = xpcall(function()
2566
+ return ${G}.require(${(0, import_parser7.luauString)(sources.get(entry).name)})
2265
2567
  end, ${G}.fail)
2266
2568
  if not ${ok} then
2267
- error(${result}, 0)
2569
+ error(${result2}, 0)
2268
2570
  end
2269
- ${modules.get(entry).info.exports ? `return ${result}` : ""}
2571
+ ${modules.get(entry).info.exports ? `return ${result2}` : ""}
2270
2572
  `).body.statements;
2271
2573
  program2.body.statements.push(...entryStatements);
2272
2574
  const map = buildLineMap(program2, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program2)), origins);
2273
- if (map.size) {
2274
- program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, assign(
2575
+ if (map.size || several) {
2576
+ const mapStatements = [assign(
2275
2577
  [member(identifier(G), "lines")],
2276
2578
  [(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map))]
2277
- ));
2579
+ )];
2580
+ if (several) mapStatements.push(...mapRegistration(G, layout[0], `${G}.lines`));
2581
+ program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
2278
2582
  }
2279
- return { code: (0, import_luau_parser2.print)(program2) + "\n", modules: moduleNames, diagnostics };
2583
+ const files = [{ path: layout[0].path, code: (0, import_luau_parser2.print)(program2) + "\n", modules: layout[0].modules.map((m) => m.name) }];
2584
+ for (const file of layout.slice(1)) {
2585
+ const body = [];
2586
+ if (file.modules.some((m) => m.usesScriptArgs)) body.push(...(0, import_luau_parser2.parse)(`local scriptArgs = ${G}.scriptArgs`).body.statements);
2587
+ for (const module2 of file.modules) {
2588
+ body.push(assign(
2589
+ [index(member(identifier(G), "modules"), string(module2.name))],
2590
+ [moduleTable(module2)]
2591
+ ));
2592
+ }
2593
+ const chunk = program([returns([functionExpression(functionBody([G], body))])]);
2594
+ const lines = buildLineMap(chunk, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(chunk)), origins);
2595
+ body.push(...mapRegistration(G, file, lineMapSource(lines)));
2596
+ files.push({ path: file.path, code: (0, import_luau_parser2.print)(chunk) + "\n", modules: file.modules.map((m) => m.name) });
2597
+ }
2598
+ return { code: files[0].code, files, modules: moduleNames, diagnostics };
2599
+ }
2600
+ function moduleTable(module2) {
2601
+ const { names: exported, links, stars } = module2.info;
2602
+ const fields = [];
2603
+ if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2604
+ if (links.length) {
2605
+ fields.push(field("links", table(links.map((link) => field(link.name, table([
2606
+ { type: "TableFieldPositional", value: string(link.module) },
2607
+ ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2608
+ ]))))));
2609
+ }
2610
+ if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2611
+ fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
2612
+ return table(fields);
2613
+ }
2614
+ function layoutOf(mode, entry, modules, outFile) {
2615
+ const extension = outFile !== void 0 && (0, import_node_path2.extname)(outFile) || ".luau";
2616
+ const entryStem = outFile !== void 0 ? (0, import_node_path2.basename)(outFile, (0, import_node_path2.extname)(outFile)) : import_node_path2.posix.basename(entry.name);
2617
+ const others = modules.filter((m) => m !== entry);
2618
+ if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry, ...others] }];
2619
+ if (mode === "modules") {
2620
+ const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
2621
+ return [entry, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
2622
+ }
2623
+ const byName = new Map(modules.map((m) => [m.name, m]));
2624
+ const reach = (root) => {
2625
+ const seen = /* @__PURE__ */ new Set();
2626
+ const stack = [root];
2627
+ while (stack.length) {
2628
+ const name = stack.pop();
2629
+ if (seen.has(name)) continue;
2630
+ seen.add(name);
2631
+ stack.push(...byName.get(name)?.imports ?? []);
2632
+ }
2633
+ return seen;
2634
+ };
2635
+ const inEntry = reach(entry.name);
2636
+ const roots = [...new Set(modules.flatMap((m) => [...m.dynamicImports]))].filter((root) => !inEntry.has(root));
2637
+ const reached = new Map(roots.map((root) => [root, reach(root)]));
2638
+ const groups = /* @__PURE__ */ new Map();
2639
+ for (const module2 of others) {
2640
+ if (inEntry.has(module2.name)) continue;
2641
+ const by = roots.filter((root) => reached.get(root).has(module2.name));
2642
+ const key = by.join("\n");
2643
+ if (!groups.has(key)) groups.set(key, { roots: by, modules: [] });
2644
+ groups.get(key).modules.push(module2);
2645
+ }
2646
+ const files = [{ path: `${entryStem}${extension}`, modules: modules.filter((m) => inEntry.has(m.name)) }];
2647
+ const taken = /* @__PURE__ */ new Set([entryStem]);
2648
+ for (const group of groups.values()) {
2649
+ const label = group.roots.length === 1 ? group.roots[0].replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "chunk" : "shared";
2650
+ let stem = `${entryStem}_${label}`;
2651
+ for (let n = 2; taken.has(stem); n++) stem = `${entryStem}_${label}_${n}`;
2652
+ taken.add(stem);
2653
+ files.push({ path: `${stem}${extension}`, modules: group.modules });
2654
+ }
2655
+ return files;
2656
+ }
2657
+ function loadPath(entryPath, file, target) {
2658
+ const path = import_node_path2.posix.relative(import_node_path2.posix.dirname(entryPath), file);
2659
+ if (target === "lua51") return path;
2660
+ const bare = path.slice(0, path.length - import_node_path2.posix.extname(path).length);
2661
+ return bare.startsWith("../") ? bare : `./${bare}`;
2662
+ }
2663
+ function mapRegistration(G, file, map) {
2664
+ const extension = import_node_path2.posix.extname(file.path);
2665
+ const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
2666
+ const entry = `${G}.sources[#${G}.sources + 1]`;
2667
+ return (0, import_luau_parser2.parse)([
2668
+ `${entry} = { { ${parts.map(import_parser7.luauString).join(", ")} }, ${map}, ${(0, import_parser7.luauString)(extension.slice(1))} }`,
2669
+ ...file.modules.map((module2) => `${G}.maps[${(0, import_parser7.luauString)(module2.name)}] = ${G}.sources[#${G}.sources][2]`)
2670
+ ].join("\n")).body.statements;
2671
+ }
2672
+ function loader(G, target) {
2673
+ if (target === "lua51") {
2674
+ return `
2675
+ -- From beside this file, wherever that is: its own path says where.
2676
+ local folder = ""
2677
+ if debug ~= nil and debug.getinfo ~= nil then
2678
+ local at = debug.getinfo(${G}.load, "S").source
2679
+ if at:sub(1, 1) == "@" then
2680
+ folder = at:match("^@(.*[/\\\\])") or ""
2681
+ end
2682
+ end
2683
+ local chunk, problem = loadfile(folder .. file)
2684
+ if chunk == nil then
2685
+ error(problem, 0)
2686
+ end
2687
+ chunk()(${G})`;
2688
+ }
2689
+ return `
2690
+ require(file)(${G})`;
2280
2691
  }
2281
- function runtime(G) {
2692
+ function runtime(G, options) {
2693
+ const { several } = options;
2694
+ const files = [...options.files].map(([name, file]) => `[${(0, import_parser7.luauString)(name)}] = ${(0, import_parser7.luauString)(file)}`).join(", ");
2282
2695
  return `
2283
2696
  local ${G}
2284
2697
  ${G} = {
@@ -2326,7 +2739,7 @@ ${G} = {
2326
2739
  return message
2327
2740
  end
2328
2741
  local lines = ${G}.lines
2329
- if lines == nil then
2742
+ if lines == nil ${several ? `and #${G}.sources == 0 ` : ""}then
2330
2743
  return message .. (trace or "")
2331
2744
  end
2332
2745
  -- The bundle's name, as a position in a message spells it: where this
@@ -2346,6 +2759,81 @@ ${G} = {
2346
2759
  chunk = info.short_src
2347
2760
  end
2348
2761
  end
2762
+ ${several ? `
2763
+ -- A position in this file is read by this file's map. One in another
2764
+ -- of the bundle's files is read by that file's: the one whose name
2765
+ -- the position ends with, by the most folders where several do. A
2766
+ -- message says nothing about which file it starts in, so nothing is
2767
+ -- guessed from it here.
2768
+ local prefix = nil
2769
+ if chunk ~= nil then
2770
+ prefix = chunk:gsub("%p", "%%%0")
2771
+ end
2772
+ local function place(line)
2773
+ local origin = lines ~= nil and lines[tonumber(line)] or nil
2774
+ if origin == nil then
2775
+ return nil
2776
+ end
2777
+ return origin[1] .. ":" .. tostring(origin[2])
2778
+ end
2779
+ local function elsewhere(at, line)
2780
+ -- Already a place in the project.
2781
+ if at:sub(-6) == ".tilua" then
2782
+ return nil
2783
+ end
2784
+ local parts = {}
2785
+ for part in at:gmatch("[^/\\\\%.]+") do
2786
+ parts[#parts + 1] = part
2787
+ end
2788
+ local map, most, tied = nil, 0, false
2789
+ for _, source in ipairs(${G}.sources) do
2790
+ local names = source[1]
2791
+ local last = #parts
2792
+ if parts[last] == source[3] then
2793
+ last = last - 1
2794
+ end
2795
+ local same = 0
2796
+ while same < #names and same < last and names[#names - same] == parts[last - same] do
2797
+ same = same + 1
2798
+ end
2799
+ if same > most then
2800
+ map, most, tied = source[2], same, false
2801
+ elseif same == most then
2802
+ tied = true
2803
+ end
2804
+ end
2805
+ -- Two files it could as well be: a wrong place is worse than none.
2806
+ local origin = map ~= nil and not tied and map[tonumber(line)] or nil
2807
+ if origin == nil then
2808
+ return nil
2809
+ end
2810
+ return origin[1] .. ":" .. tostring(origin[2])
2811
+ end
2812
+ local mapped = message
2813
+ if prefix ~= nil then
2814
+ mapped = mapped:gsub(prefix .. ":(%d+)", function(line)
2815
+ return place(line)
2816
+ end)
2817
+ end
2818
+ mapped = mapped:gsub("(%S+):(%d+)", elsewhere)
2819
+ local frames = {}
2820
+ for frame in (trace or ""):gmatch("[^\\n]+") do
2821
+ local at = nil
2822
+ if prefix ~= nil then
2823
+ local line = frame:match(prefix .. ":(%d+)")
2824
+ if line ~= nil then
2825
+ at = place(line)
2826
+ end
2827
+ end
2828
+ if at == nil then
2829
+ for source, line in frame:gmatch("(%S+):(%d+)") do
2830
+ at = elsewhere(source, line)
2831
+ if at ~= nil then
2832
+ break
2833
+ end
2834
+ end
2835
+ end
2836
+ if at ~= nil then` : `
2349
2837
  if chunk == nil then
2350
2838
  chunk = message:match("^(.-):%d+:")
2351
2839
  end
@@ -2367,8 +2855,9 @@ ${G} = {
2367
2855
  for frame in (trace or ""):gmatch("[^\\n]+") do
2368
2856
  local line = frame:match(prefix .. ":(%d+)")
2369
2857
  local at = line ~= nil and place(line) or nil
2370
- if at ~= nil then
2371
- local name = frame:match("function ([%w_.:]+)")
2858
+ if at ~= nil then`}
2859
+ -- Luau writes \`function name\`; Lua 5.1, \`function 'name'\`.
2860
+ local name = frame:match("function ([%w_.:]+)") or frame:match("function '([^']+)'")
2372
2861
  frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2373
2862
  end
2374
2863
  end
@@ -2377,13 +2866,36 @@ ${G} = {
2377
2866
  end
2378
2867
  return mapped .. "\\n" .. table.concat(frames, "\\n")
2379
2868
  end,
2380
- records = {},
2869
+ records = {},${several ? `
2870
+ -- The file each module outside this one is in, as \`load\` is handed it.
2871
+ files = { ${files} },
2872
+ loaded = {},
2873
+ -- Every file's line map: by the module, and by the file's name.
2874
+ maps = {},
2875
+ sources = {},
2876
+ -- Another of the bundle's files: a function that adds its modules here.
2877
+ load = function(file)
2878
+ if ${G}.loaded[file] then
2879
+ return
2880
+ end
2881
+ ${G}.loaded[file] = true${loader(G, options.target)}
2882
+ end,` : ""}${options.imports ? `
2883
+ -- \`import("./x")\`: the module's exports, asked for where the code runs
2884
+ -- rather than before its module does. \`require\` does the rest \u2014 and
2885
+ -- loads the file the module is in, if it is not this one.
2886
+ import = function(name)
2887
+ return ${G}.require(name)
2888
+ end,` : ""}
2381
2889
  require = function(name)
2382
2890
  local record = ${G}.records[name]
2383
2891
  if record ~= nil then
2384
2892
  return record.exports
2385
2893
  end
2386
- local module = ${G}.modules[name]
2894
+ local module = ${G}.modules[name]${several ? `
2895
+ if module == nil then
2896
+ ${G}.load(${G}.files[name])
2897
+ module = ${G}.modules[name]
2898
+ end` : ""}
2387
2899
  local initialized = {}
2388
2900
  local exports = setmetatable({}, {
2389
2901
  __index = function(_, key)
@@ -2402,7 +2914,7 @@ ${G} = {
2402
2914
  return source[link[2]]
2403
2915
  end
2404
2916
  if module.stars ~= nil and key ~= "default" then
2405
- for _, star in module.stars do
2917
+ for _, star in ipairs(module.stars) do
2406
2918
  local value = ${G}.require(star)[key]
2407
2919
  if value ~= nil then
2408
2920
  return value
@@ -2427,7 +2939,7 @@ ${G} = {
2427
2939
  }
2428
2940
  function stampOf(file) {
2429
2941
  try {
2430
- const at = (0, import_node_fs2.statSync)(file);
2942
+ const at = (0, import_node_fs3.statSync)(file);
2431
2943
  return `${at.size}:${at.mtimeMs}`;
2432
2944
  } catch {
2433
2945
  return void 0;
@@ -2443,19 +2955,19 @@ function parseFile(file, diagnostics, directives) {
2443
2955
  }
2444
2956
  let text;
2445
2957
  try {
2446
- text = (0, import_node_fs2.readFileSync)(file, "utf8");
2958
+ text = (0, import_node_fs3.readFileSync)(file, "utf8");
2447
2959
  } catch {
2448
2960
  diagnostics.push({ file, message: "Cannot read file", line: 1, column: 1, category: "module" });
2449
2961
  return void 0;
2450
2962
  }
2451
2963
  try {
2452
- const program2 = (0, import_parser6.parse)(text);
2453
- const found = (0, import_parser6.directivesOf)(text);
2964
+ const program2 = (0, import_parser7.parse)(text);
2965
+ const found = (0, import_parser7.directivesOf)(text);
2454
2966
  directives.set(file, found);
2455
2967
  if (stamp !== void 0) parsedFiles.set(file, { stamp, program: program2, text, directives: found });
2456
2968
  return { program: program2, text };
2457
2969
  } catch (error) {
2458
- if (error instanceof import_parser6.ParseError || error instanceof import_parser6.LexError) {
2970
+ if (error instanceof import_parser7.ParseError || error instanceof import_parser7.LexError) {
2459
2971
  const { line, column } = error;
2460
2972
  diagnostics.push({ file, message: error.message, line, column, category: "syntax" });
2461
2973
  return void 0;
@@ -2470,14 +2982,14 @@ var libraryCache = /* @__PURE__ */ new Map();
2470
2982
  function parsedLibrary(file) {
2471
2983
  let stamp;
2472
2984
  try {
2473
- const at = (0, import_node_fs2.statSync)(file);
2985
+ const at = (0, import_node_fs3.statSync)(file);
2474
2986
  stamp = `${at.size}:${at.mtimeMs}`;
2475
2987
  } catch {
2476
2988
  stamp = "";
2477
2989
  }
2478
2990
  const cached = libraryCache.get(file);
2479
2991
  if (cached && cached.stamp === stamp) return cached.program;
2480
- const program2 = (0, import_parser6.parse)((0, import_node_fs2.readFileSync)(file, "utf8"));
2992
+ const program2 = (0, import_parser7.parse)((0, import_node_fs3.readFileSync)(file, "utf8"));
2481
2993
  libraryCache.set(file, { stamp, program: program2 });
2482
2994
  return program2;
2483
2995
  }
@@ -2485,9 +2997,11 @@ var moduleAnalyses = /* @__PURE__ */ new Map();
2485
2997
  function analyzeModules(modules, config) {
2486
2998
  const out = [];
2487
2999
  const analyses = /* @__PURE__ */ new Map();
2488
- const libraries = config ? (0, import_parser6.resolveTypeLibraries)(config) : { files: [], lowerings: [], problems: [] };
3000
+ const libraries = config ? (0, import_parser7.resolveTypeLibraries)(config) : { files: [], lowerings: [], problems: [] };
2489
3001
  for (const p of libraries.problems) out.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
2490
3002
  const libs = libraries.files.map(parsedLibrary);
3003
+ const noLib = config?.noLib ?? false;
3004
+ const checked = libs.length > 0 || !noLib;
2491
3005
  const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
2492
3006
  const byFile = new Map(modules.map((m) => [m.file, m]));
2493
3007
  const exportsCache = /* @__PURE__ */ new Map();
@@ -2500,49 +3014,50 @@ function analyzeModules(modules, config) {
2500
3014
  const done = analyzed.get(file);
2501
3015
  if (done) return done;
2502
3016
  const remembered = moduleAnalyses.get(file);
2503
- if (remembered && remembered.setting === settingFor && libs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === libs[i]) && remembered.program === program2 && [...remembered.read].every(([read2, stamp]) => stampOf(read2) === stamp)) {
3017
+ if (remembered && remembered.setting === settingFor && libs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === libs[i]) && remembered.program === program2 && [...remembered.read].every(([read3, stamp]) => stampOf(read3) === stamp)) {
2504
3018
  analyzed.set(file, remembered.result);
2505
3019
  readWhile.set(file, remembered.read);
2506
3020
  return remembered.result;
2507
3021
  }
2508
- const scopes = (0, import_parser6.analyzeScopes)(program2, { builtinGlobals: globals, reportUndeclared: libs.length > 0 });
3022
+ const scopes = (0, import_parser7.analyzeScopes)(program2, { builtinGlobals: globals, reportUndeclared: checked, noLib });
2509
3023
  const said = [];
2510
3024
  for (const d of scopes.diagnostics) {
2511
3025
  if (d.kind !== "undeclared") continue;
2512
3026
  said.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "type" });
2513
3027
  }
2514
3028
  const before = partials;
2515
- const types = (0, import_parser6.analyzeTypes)(program2, scopes, {
3029
+ const types = (0, import_parser7.analyzeTypes)(program2, scopes, {
2516
3030
  libs,
2517
3031
  resolveModule: resolverFor(file),
2518
- reportUnknownTypes: libs.length > 0
3032
+ noLib,
3033
+ reportUnknownTypes: checked
2519
3034
  });
2520
3035
  for (const d of types.diagnostics) {
2521
3036
  const at = d.node;
2522
3037
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
2523
3038
  }
2524
- const result = { types, diagnostics: said };
3039
+ const result2 = { types, diagnostics: said };
2525
3040
  if (partials === before) {
2526
- analyzed.set(file, result);
2527
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3041
+ analyzed.set(file, result2);
3042
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2528
3043
  const own = stampOf(file);
2529
- if (own !== void 0) read2.set(file, own);
2530
- readWhile.set(file, read2);
2531
- if ([...read2.values()].every((stamp) => stamp !== void 0)) {
2532
- moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read2, result });
3044
+ if (own !== void 0) read3.set(file, own);
3045
+ readWhile.set(file, read3);
3046
+ if ([...read3.values()].every((stamp) => stamp !== void 0)) {
3047
+ moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read3, result: result2 });
2533
3048
  }
2534
3049
  }
2535
- return result;
3050
+ return result2;
2536
3051
  };
2537
3052
  const resolverFor = (file) => (specifier) => {
2538
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config);
3053
+ const target = (0, import_parser7.resolveModulePath)(file, specifier, config);
2539
3054
  if (!target) return void 0;
2540
3055
  const noteRead = () => {
2541
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2542
- readWhile.set(file, read2);
3056
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3057
+ readWhile.set(file, read3);
2543
3058
  const stamp = stampOf(target);
2544
- if (stamp !== void 0) read2.set(target, stamp);
2545
- for (const [below, belowStamp] of readWhile.get(target) ?? []) read2.set(below, belowStamp);
3059
+ if (stamp !== void 0) read3.set(target, stamp);
3060
+ for (const [below, belowStamp] of readWhile.get(target) ?? []) read3.set(below, belowStamp);
2546
3061
  };
2547
3062
  if (inProgress.has(target)) {
2548
3063
  partials++;
@@ -2556,16 +3071,16 @@ function analyzeModules(modules, config) {
2556
3071
  let program2 = byFile.get(target)?.program;
2557
3072
  if (!program2) {
2558
3073
  try {
2559
- program2 = (0, import_parser6.parse)((0, import_node_fs2.readFileSync)(target, "utf8"));
3074
+ program2 = (0, import_parser7.parse)((0, import_node_fs3.readFileSync)(target, "utf8"));
2560
3075
  } catch {
2561
3076
  return void 0;
2562
3077
  }
2563
3078
  }
2564
3079
  inProgress.add(target);
2565
3080
  try {
2566
- const scopes = (0, import_parser6.analyzeScopes)(program2, { builtinGlobals: globals });
2567
- const types = byFile.has(target) ? analyzeModule(target, program2).types : (0, import_parser6.analyzeTypes)(program2, scopes, { libs, resolveModule: resolverFor(target), diagnostics: false });
2568
- const exports2 = (0, import_parser6.moduleExports)(program2, scopes, types, resolverFor(target));
3081
+ const scopes = (0, import_parser7.analyzeScopes)(program2, { builtinGlobals: globals, noLib });
3082
+ const types = byFile.has(target) ? analyzeModule(target, program2).types : (0, import_parser7.analyzeTypes)(program2, scopes, { libs, noLib, resolveModule: resolverFor(target), diagnostics: false });
3083
+ const exports2 = (0, import_parser7.moduleExports)(program2, scopes, types, resolverFor(target));
2569
3084
  exportsCache.set(target, exports2);
2570
3085
  noteRead();
2571
3086
  return exports2;
@@ -2598,37 +3113,39 @@ function allStatements(statements) {
2598
3113
  }
2599
3114
 
2600
3115
  // src/compile.ts
2601
- var import_node_fs3 = require("fs");
3116
+ var import_node_fs4 = require("fs");
2602
3117
  var import_node_path3 = require("path");
2603
- var import_parser7 = require("@tilua/parser");
3118
+ var import_parser8 = require("@tilua/parser");
2604
3119
  var import_luau_parser3 = require("luau-parser");
2605
3120
  async function compile(source, config) {
2606
3121
  let program2;
2607
3122
  try {
2608
- program2 = (0, import_parser7.parse)(source);
3123
+ program2 = (0, import_parser8.parse)(source);
2609
3124
  } catch (error) {
2610
- if (error instanceof import_parser7.ParseError || error instanceof import_parser7.LexError) {
3125
+ if (error instanceof import_parser8.ParseError || error instanceof import_parser8.LexError) {
2611
3126
  const { line, column } = error;
2612
3127
  return { diagnostics: [{ message: error.message, line, column }] };
2613
3128
  }
2614
3129
  throw error;
2615
3130
  }
2616
- const scopes = (0, import_parser7.analyzeScopes)(program2);
3131
+ const { config: resolved, problems } = resolveConfig(config, (0, import_node_path3.resolve)("main.tilua"));
3132
+ const noLib = resolved?.noLib ?? false;
3133
+ const scopes = (0, import_parser8.analyzeScopes)(program2, { noLib });
2617
3134
  const diagnostics = scopes.diagnostics.map((d) => ({
2618
3135
  message: d.message,
2619
3136
  line: d.node.line.start,
2620
3137
  column: d.node.column.start
2621
3138
  }));
2622
- const { config: resolved, problems } = resolveConfig(config, (0, import_node_path3.resolve)("main.tilua"));
2623
- const libraries = resolved ? (0, import_parser7.resolveTypeLibraries)(resolved) : { files: [], lowerings: [], problems: [] };
3139
+ const libraries = resolved ? (0, import_parser8.resolveTypeLibraries)(resolved) : { files: [], lowerings: [], problems: [] };
2624
3140
  const { lowerings, problems: loweringProblems } = await loadLowerings(libraries.lowerings);
2625
3141
  for (const p of [...problems, ...libraries.problems]) {
2626
3142
  diagnostics.push({ message: p.message, line: p.line ?? 1, column: p.column ?? 1 });
2627
3143
  }
2628
3144
  for (const p of loweringProblems) diagnostics.push({ message: p.message, line: 1, column: 1 });
2629
- const libs = libraries.files.map((file) => (0, import_parser7.parse)((0, import_node_fs3.readFileSync)(file, "utf8")));
3145
+ const libs = libraries.files.map((file) => (0, import_parser8.parse)((0, import_node_fs4.readFileSync)(file, "utf8")));
2630
3146
  const lowered = lower(program2, scopes, {
2631
- types: (0, import_parser7.analyzeTypes)(program2, scopes, { libs, diagnostics: false }),
3147
+ types: (0, import_parser8.analyzeTypes)(program2, scopes, { libs, noLib, diagnostics: false }),
3148
+ noLib,
2632
3149
  lowerings,
2633
3150
  target: resolved?.target,
2634
3151
  source