@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.
@@ -172,6 +172,77 @@ var plugin = {
172
172
  };
173
173
  var languageLowering = { plugin, from: "tilua" };
174
174
 
175
+ // src/standard.ts
176
+ import { readFileSync as readFileSync2 } from "fs";
177
+ import { isFunctionType, luauString as luauString2 } from "@tilua/parser";
178
+ var read2 = (name) => readFileSync2(new URL(`../runtime/${name}`, import.meta.url), "utf8");
179
+ var console = read2("console.luau");
180
+ var result = read2("result.luau");
181
+ var place = (at) => luauString2(at.file === void 0 ? `line ${at.line}` : `${at.file}:${at.line}`);
182
+ var tag = (at) => luauString2(`[${at.file === void 0 ? `line ${at.line}` : `${at.file.replace(/\.tilua$/, "")}:${at.line}`}]:`);
183
+ function consoleMeta(args) {
184
+ const fields = [];
185
+ args.forEach((argument, index2) => {
186
+ if (argument.spread || argument.typeText === void 0 || !isFunctionType(argument.type)) return;
187
+ const written = argument.declaration ?? argument.source;
188
+ fields.push(`[${index2 + 1}] = { ${luauString2(argument.typeText)}, ${luauString2(written)} }`);
189
+ });
190
+ return fields.length ? `{ ${fields.join(", ")} }` : "nil";
191
+ }
192
+ var CONSOLE_METHODS = /* @__PURE__ */ new Set(["log", "warn", "error"]);
193
+ var REPLACED = /* @__PURE__ */ new Map([
194
+ ["pcall", "pcall"],
195
+ ["xpcall", "xpcall"],
196
+ ["ypcall", "pcall"],
197
+ ["coroutine.resume", "resume"],
198
+ ["string.find", "find"],
199
+ ["string.match", "match"],
200
+ ["string.gsub", "gsub"],
201
+ ["string.byte", "byte"],
202
+ ["math.modf", "modf"],
203
+ ["math.frexp", "frexp"],
204
+ ["next", "next"],
205
+ ["select", "select"],
206
+ ["unpack", "unpack"],
207
+ ["table.unpack", "unpack"],
208
+ ["loadstring", "loadstring"],
209
+ ["pairs", "pairs"],
210
+ ["ipairs", "ipairs"],
211
+ ["string.gmatch", "gmatch"]
212
+ ]);
213
+ var plugin2 = {
214
+ runtime: { console, result },
215
+ methodCall({ method, receiverGlobal, at, arguments: args, use }) {
216
+ if (receiverGlobal === "console" && CONSOLE_METHODS.has(method)) {
217
+ return {
218
+ callee: `${use("console")}.${method}`,
219
+ prepend: [tag(at), consoleMeta(args)],
220
+ passReceiver: false
221
+ };
222
+ }
223
+ return void 0;
224
+ },
225
+ // `print(x)` and `warn(x)` are left exactly as written: the place a line
226
+ // came from is what `console:log` / `console:warn` are for. `error` has to
227
+ // put the place inside the message it raises, so it goes through the
228
+ // runtime.
229
+ globalCall({ name, at, use }) {
230
+ if (name === "error") return { callee: `${use("console")}.raise`, prepend: [place(at)] };
231
+ const replaced = REPLACED.get(name);
232
+ if (replaced) return { callee: `${use("result")}.${replaced}` };
233
+ return void 0;
234
+ },
235
+ // Read as a value there is no call site to write down; the runtime finds
236
+ // one on the stack.
237
+ globalValue({ name, use }) {
238
+ if (name === "error") return `${use("console")}.raiseValue`;
239
+ const replaced = REPLACED.get(name);
240
+ if (replaced) return `${use("result")}.${replaced}`;
241
+ return void 0;
242
+ }
243
+ };
244
+ var standardLowering = { plugin: plugin2, from: "tilua (standard library)" };
245
+
175
246
  // src/lower.ts
176
247
  function lower(program2, scopes, options = {}) {
177
248
  return new Lowerer(program2, scopes, options).run();
@@ -187,6 +258,18 @@ var Lowerer = class {
187
258
  for (const binding of scopes.bindings.values()) {
188
259
  if (binding.declarationNode) this.bindingByDeclaration.set(binding.declarationNode, binding);
189
260
  }
261
+ const visit = (value) => {
262
+ if (!value || typeof value !== "object") return;
263
+ if (Array.isArray(value)) return void value.forEach(visit);
264
+ const node = value;
265
+ if (node.type === "VariableDeclaration" && node.name?.type === "IdentifierPattern" && node.name.name) {
266
+ let init = node.init;
267
+ while (init?.type === "ParenthesizedExpression") init = init.expression;
268
+ if (init?.type === "ClassExpression") this.classExpressionNames.set(init, node.name.name);
269
+ }
270
+ for (const [key, child] of Object.entries(value)) if (key !== "line" && key !== "column") visit(child);
271
+ };
272
+ visit(source.body);
190
273
  }
191
274
  source;
192
275
  scopes;
@@ -208,6 +291,8 @@ var Lowerer = class {
208
291
  lua51;
209
292
  /** The source line each emitted statement came from. */
210
293
  origins = /* @__PURE__ */ new WeakMap();
294
+ /** The name each class written as a value is known by. */
295
+ classExpressionNames = /* @__PURE__ */ new WeakMap();
211
296
  run() {
212
297
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
213
298
  const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))])] : [];
@@ -261,6 +346,10 @@ var Lowerer = class {
261
346
  }
262
347
  return identifier(local2);
263
348
  }
349
+ /** `table.move`. Lua 5.1 has none, and gets a function that does the same. */
350
+ move() {
351
+ return this.lua51 ? this.helper("move") : member(this.builtin("table"), "move");
352
+ }
264
353
  helper(kind) {
265
354
  let name = this.helpers.get(kind);
266
355
  if (!name) {
@@ -383,7 +472,11 @@ var Lowerer = class {
383
472
  /** The language's own lowering, then each library's, in load order. A
384
473
  * runtime is named by its lowering's place here (`loweringRuntime`). */
385
474
  get lowerings() {
386
- return this.allLowerings ??= [languageLowering, ...this.options.lowerings ?? []];
475
+ return this.allLowerings ??= [
476
+ languageLowering,
477
+ ...this.options.noLib ? [] : [standardLowering],
478
+ ...this.options.lowerings ?? []
479
+ ];
387
480
  }
388
481
  allLowerings;
389
482
  /** What `receiver:method(...)` becomes, when Luau alone would not run it.
@@ -397,22 +490,28 @@ var Lowerer = class {
397
490
  * (`console:log`), the last library loaded first. No answer leaves an
398
491
  * ordinary Luau method call: `text:upper()` reaches Lua's own. */
399
492
  loweredMethodCall(node) {
493
+ return this.loweredMethod(node.method.name, this.options.types?.methodSources.get(node), node.object, node.arguments, node.method, node);
494
+ }
495
+ /** `loweredMethodCall`, for a method named `method` of what `object`
496
+ * holds — called with `:` on it, or read off it with `.` and handed its
497
+ * `self` like any other argument. `args` are the arguments after the
498
+ * receiver; a read that is not called has none. */
499
+ loweredMethod(method, source, object2, args, name, node) {
400
500
  const lowerings = this.lowerings;
401
- const source = this.options.types?.methodSources.get(node);
402
501
  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();
403
502
  if (!asked.some((i) => lowerings[i].plugin.methodCall)) return void 0;
404
- const receiver = this.options.types?.typeOf.get(node.object);
405
- const receiverGlobal = this.globalName(node.object);
406
- const info = this.lazyArguments(node.arguments);
503
+ const receiver = this.options.types?.typeOf.get(object2);
504
+ const receiverGlobal = this.globalName(object2);
505
+ const info = this.lazyArguments(args);
407
506
  for (const i of asked) {
408
- const plugin2 = lowerings[i].plugin;
409
- if (!plugin2.methodCall) continue;
410
- const answer = plugin2.methodCall({
411
- method: node.method.name,
507
+ const plugin3 = lowerings[i].plugin;
508
+ if (!plugin3.methodCall) continue;
509
+ const answer = plugin3.methodCall({
510
+ method,
412
511
  receiver,
413
512
  receiverGlobal,
414
- argumentCount: node.arguments.length,
415
- at: this.callSite(node.method),
513
+ argumentCount: args.length,
514
+ at: this.callSite(name),
416
515
  get arguments() {
417
516
  return info();
418
517
  },
@@ -436,9 +535,9 @@ var Lowerer = class {
436
535
  if (name === void 0) return void 0;
437
536
  const info = this.lazyArguments(node.arguments);
438
537
  for (let i = lowerings.length - 1; i >= 0; i--) {
439
- const plugin2 = lowerings[i].plugin;
440
- if (!plugin2.globalCall) continue;
441
- const answer = plugin2.globalCall({
538
+ const plugin3 = lowerings[i].plugin;
539
+ if (!plugin3.globalCall) continue;
540
+ const answer = plugin3.globalCall({
442
541
  name,
443
542
  at: this.callSite(node.callee),
444
543
  get arguments() {
@@ -490,6 +589,31 @@ var Lowerer = class {
490
589
  const out = [];
491
590
  const runtimes = this.runtimeDefinitions();
492
591
  if (this.helpers.has("concat")) this.helper("array");
592
+ if (this.helpers.has("concat")) this.move();
593
+ const move = this.helpers.get("move");
594
+ if (move) {
595
+ out.push(...parseLuau(`
596
+ local function ${move}(from, first, last, at, to)
597
+ for i = first, last do
598
+ to[at + i - first] = from[i]
599
+ end
600
+ return to
601
+ end`).body.statements);
602
+ }
603
+ const each = this.helpers.get("each");
604
+ if (each) {
605
+ out.push(...parseLuau(`
606
+ local function ${each}(source, ...)
607
+ if ${this.builtin("type").name}(source) == "function" then
608
+ return source, ...
609
+ end
610
+ local meta = ${this.builtin("getmetatable").name}(source)
611
+ if meta ~= nil and meta.__iter ~= nil then
612
+ return meta.__iter(source)
613
+ end
614
+ return ${this.builtin("next").name}, source
615
+ end`).body.statements);
616
+ }
493
617
  const marker = this.helpers.get("array");
494
618
  const typeOf = this.helpers.get("typeof");
495
619
  if (marker || typeOf) {
@@ -528,19 +652,72 @@ local function ${typeOf}(value, known)
528
652
  end`).body.statements);
529
653
  }
530
654
  }
655
+ const instanceOf = this.helpers.get("instanceof");
656
+ if (instanceOf) {
657
+ out.push(...parseLuau(`
658
+ local function ${instanceOf}(value, class)
659
+ if ${this.builtin("type").name}(value) ~= "table" then
660
+ return false
661
+ end
662
+ local current = value.ClassObject
663
+ while current ~= nil do
664
+ if current == class then
665
+ return true
666
+ end
667
+ current = current.ParentClass
668
+ end
669
+ return false
670
+ end`).body.statements);
671
+ }
672
+ const accessors = this.helpers.get("accessors");
673
+ if (accessors) {
674
+ out.push(...parseLuau(`
675
+ local function ${accessors}(target, getters, setters)
676
+ return ${this.builtin("setmetatable").name}(target, {
677
+ -- Read by a spread, which copies what the getters give.
678
+ __tiluaGetters = getters,
679
+ __index = function(self, key)
680
+ local get = getters[key]
681
+ if get ~= nil then
682
+ return get(self)
683
+ end
684
+ return nil
685
+ end,
686
+ __newindex = function(self, key, value)
687
+ local set = setters[key]
688
+ if set ~= nil then
689
+ set(self, value)
690
+ return
691
+ end
692
+ if getters[key] ~= nil then
693
+ ${this.builtin("error").name}("Cannot assign to '" .. ${this.builtin("tostring").name}(key) .. "': it has a getter and no setter", 2)
694
+ end
695
+ ${this.builtin("rawset").name}(self, key, value)
696
+ end,
697
+ })
698
+ end`).body.statements);
699
+ }
531
700
  const assign2 = this.helpers.get("assign");
532
701
  if (assign2) {
533
- out.push(localFunction(assign2, functionBody(["target"], [
534
- numericFor("i", number(1), selectCount(this.builtin("select")), [
535
- local(["source"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
536
- ifThen(binary("~=", identifier("source"), nil()), [
537
- genericFor(["key", "value"], [call(this.builtin("pairs"), [identifier("source")])], [
538
- assign([index(identifier("target"), identifier("key"))], [identifier("value")])
539
- ])
540
- ])
541
- ]),
542
- returns([identifier("target")])
543
- ], true)));
702
+ out.push(...parseLuau(`
703
+ local function ${assign2}(target, ...)
704
+ for i = 1, ${this.builtin("select").name}("#", ...) do
705
+ local source = (${this.builtin("select").name}(i, ...))
706
+ if source ~= nil then
707
+ for key, value in ${this.builtin("pairs").name}(source) do
708
+ target[key] = value
709
+ end
710
+ local meta = ${this.builtin("type").name}(source) == "table" and ${this.builtin("getmetatable").name}(source) or nil
711
+ local getters = ${this.builtin("type").name}(meta) == "table" and ${this.builtin("rawget").name}(meta, "__tiluaGetters") or nil
712
+ if getters ~= nil then
713
+ for key, get in ${this.builtin("pairs").name}(getters) do
714
+ target[key] = get(source)
715
+ end
716
+ end
717
+ end
718
+ end
719
+ return target
720
+ end`).body.statements);
544
721
  }
545
722
  out.push(...runtimes);
546
723
  const concat = this.helpers.get("concat");
@@ -549,7 +726,7 @@ end`).body.statements);
549
726
  local(["result"], [this.array(table([]))]),
550
727
  numericFor("i", number(1), selectCount(this.builtin("select")), [
551
728
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
552
- callStatement(call(member(this.builtin("table"), "move"), [
729
+ callStatement(call(this.move(), [
553
730
  identifier("part"),
554
731
  number(1),
555
732
  unary("#", identifier("part")),
@@ -566,8 +743,8 @@ end`).body.statements);
566
743
  runtimeDefinitions() {
567
744
  const out = [];
568
745
  for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
569
- const { plugin: plugin2, from } = this.lowerings[index2];
570
- const source = plugin2.runtime?.[runtime2];
746
+ const { plugin: plugin3, from } = this.lowerings[index2];
747
+ const source = plugin3.runtime?.[runtime2];
571
748
  if (source === void 0) {
572
749
  this.diagnostics.push({
573
750
  message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
@@ -662,6 +839,7 @@ end`).body.statements);
662
839
  names.push("default");
663
840
  break;
664
841
  case "ExportNamedStatement": {
842
+ if (statement.typeOnly) break;
665
843
  if (statement.source) {
666
844
  const key = resolve3(statement.source);
667
845
  if (key === void 0) break;
@@ -745,6 +923,26 @@ end`).body.statements);
745
923
  for (let i = 0; i < locals.length; i += 100) declarations.push(local(locals.slice(i, i + 100), []));
746
924
  return [...declarations, ...hoisted, ...requires, ...body];
747
925
  }
926
+ /** `import("./x")`: the bundle's `import`, called with the module's name.
927
+ * It answers the module's exports table — what `import * as` names. */
928
+ importExpression(node) {
929
+ const context = this.options.module;
930
+ if (!context) {
931
+ this.report(node, "import() needs a bundle: build the project with @tilua/compiler");
932
+ return nil();
933
+ }
934
+ const source = node.source;
935
+ if (source.type !== "StringLiteral") {
936
+ this.report(source, "import() takes a string literal: which module it loads is decided when the project is built");
937
+ return nil();
938
+ }
939
+ const key = context.resolve(source.value, true);
940
+ if (key === void 0) {
941
+ this.report(source, `Cannot find module '${source.value}'`);
942
+ return nil();
943
+ }
944
+ return call(context.import ?? context.require, [string(key)]);
945
+ }
748
946
  isRewritten(node) {
749
947
  const binding = this.bindingByDeclaration.get(node);
750
948
  return binding !== void 0 && this.rewrites.has(binding.id);
@@ -891,7 +1089,7 @@ end`).body.statements);
891
1089
  case "VariableDeclaration":
892
1090
  return this.variableDeclaration(node, "declare");
893
1091
  case "FunctionDeclaration":
894
- return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: node.attributes }];
1092
+ return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: this.lua51 ? void 0 : node.attributes }];
895
1093
  case "FunctionDeclarationStatement":
896
1094
  return [this.functionDeclarationStatement(node)];
897
1095
  case "ClassDeclaration":
@@ -922,8 +1120,13 @@ end`).body.statements);
922
1120
  // kept — Lua has no expression statement — and it cannot run
923
1121
  // anything, since a call is a `CallStatement` instead, so dropping
924
1122
  // it changes nothing but the noise.
925
- case "ExpressionStatement":
926
- return [];
1123
+ case "ExpressionStatement": {
1124
+ let expression = node.expression;
1125
+ while (expression.type === "ParenthesizedExpression") expression = expression.expression;
1126
+ if (expression.type !== "ImportExpression") return [];
1127
+ const call2 = this.importExpression(expression);
1128
+ return call2.type === "CallExpression" ? [this.from(callStatement(call2), node)] : [];
1129
+ }
927
1130
  case "DoStatement":
928
1131
  return [{ type: "DoStatement", body: this.block(node.body), ...spanOf(node) }];
929
1132
  case "WhileStatement":
@@ -975,7 +1178,11 @@ end`).body.statements);
975
1178
  let source = this.expression(node.iterator);
976
1179
  if (form.viaIter) source = methodCall(source, "__iter", []);
977
1180
  const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
978
- const iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1181
+ let iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1182
+ if (this.lua51 && form.walks === "values") {
1183
+ const kind = form.viaIter ? void 0 : this.options.types?.typeOf.get(node.iterator)?.kind;
1184
+ iterators = [call(kind === "array" || kind === "tuple" ? this.builtin("ipairs") : this.helper("each"), [source])];
1185
+ }
979
1186
  return [genericFor(
980
1187
  variables,
981
1188
  iterators,
@@ -1055,11 +1262,12 @@ end`).body.statements);
1055
1262
  const { build, accessors } = this.classHelpers;
1056
1263
  const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
1057
1264
  return parseLuau(`
1058
- local function ${build}(base)
1265
+ local function ${build}(base, name)
1059
1266
  local class = { __getters = {}, __setters = {} }
1060
1267
  class.__index = class
1061
1268
  class.ClassObject = class
1062
1269
  class.ParentClass = base
1270
+ class.name = name
1063
1271
  if base ~= nil then
1064
1272
  setmetatable(class, { __index = base })
1065
1273
  setmetatable(class.__getters, { __index = base.__getters })
@@ -1113,7 +1321,11 @@ end
1113
1321
  * table is assigned to `self`, which a module has already declared. */
1114
1322
  classStatements(node, self, declare) {
1115
1323
  const { build, accessors } = this.classRuntime();
1116
- const create = call(identifier(build), [node.superclass ? this.reference(node.superclass) : nil()]);
1324
+ const called = node.name?.name ?? (node.type === "ClassExpression" ? this.classExpressionNames.get(node) : void 0) ?? "";
1325
+ const create = call(identifier(build), [
1326
+ node.superclass ? this.reference(node.superclass) : nil(),
1327
+ string(called)
1328
+ ]);
1117
1329
  const out = [declare !== void 0 ? local([declare], [create]) : assign([self], [create])];
1118
1330
  const previous = this.classContext;
1119
1331
  this.classContext = node;
@@ -1228,7 +1440,8 @@ end
1228
1440
  },
1229
1441
  isMethod,
1230
1442
  func,
1231
- attributes: node.attributes,
1443
+ // `@native` and the like are Luau's.
1444
+ attributes: this.lua51 ? void 0 : node.attributes,
1232
1445
  ...spanOf(node)
1233
1446
  };
1234
1447
  }
@@ -1329,7 +1542,7 @@ end
1329
1542
  if (element) bind(element.value, index(source, number(i + 1)), element.default);
1330
1543
  });
1331
1544
  if (pattern.rest) {
1332
- const elements = call(member(this.builtin("table"), "move"), [
1545
+ const elements = call(this.move(), [
1333
1546
  source,
1334
1547
  number(pattern.elements.length + 1),
1335
1548
  unary("#", source),
@@ -1440,8 +1653,15 @@ end
1440
1653
  case "ArrayExpression":
1441
1654
  return this.arrayExpression(node);
1442
1655
  case "BinaryExpression": {
1656
+ if (node.operator === "??") return this.nullish(node);
1443
1657
  const left = this.expression(node.left);
1444
1658
  const right = this.expression(node.right);
1659
+ if (node.operator === "in") {
1660
+ return parenthesized(binary("~=", index(this.keptToOne(right), this.keptToOne(left)), nil()));
1661
+ }
1662
+ if (node.operator === "instanceof") {
1663
+ return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
1664
+ }
1445
1665
  if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
1446
1666
  return binary(node.operator, left, right);
1447
1667
  }
@@ -1468,6 +1688,8 @@ end
1468
1688
  }
1469
1689
  case "ParenthesizedExpression":
1470
1690
  return parenthesized(this.expression(node.expression));
1691
+ case "ImportExpression":
1692
+ return this.importExpression(node);
1471
1693
  // Types only: the value is the expression itself.
1472
1694
  case "TypeAssertionExpression":
1473
1695
  case "SatisfiesExpression":
@@ -1521,7 +1743,7 @@ end
1521
1743
  const last2 = list.length - 1;
1522
1744
  return lowered.length && !many.has(list[last2]) ? [...lowered.slice(0, -1), this.keptToOne(lowered[last2])] : lowered;
1523
1745
  }
1524
- const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
1746
+ const unpack = (value) => call(this.lua51 ? this.builtin("unpack") : member(this.builtin("table"), "unpack"), [value]);
1525
1747
  const last = list[list.length - 1];
1526
1748
  if (spreads.length === 1 && last.type === "SpreadElement") {
1527
1749
  return [...list.slice(0, -1).map((a) => this.expression(a)), unpack(this.expression(last.argument))];
@@ -1535,11 +1757,21 @@ end
1535
1757
  /** One link of an access chain, read from `object`. */
1536
1758
  link(node, object2) {
1537
1759
  switch (node.type) {
1538
- case "MemberExpression":
1760
+ case "MemberExpression": {
1761
+ if (this.options.types?.lengths.has(node)) return unary("#", object2);
1762
+ const source = this.options.types?.methodReads.get(node);
1763
+ const lowered = source && this.loweredMethod(node.property.name, source, node.object, [], node.property, node);
1764
+ if (lowered) {
1765
+ const forwarded = call(calleePath(lowered.callee), [...lowered.prepend, vararg()]);
1766
+ return functionExpression(functionBody(lowered.passReceiver ? [] : ["_"], [returns([forwarded])], true));
1767
+ }
1539
1768
  return member(object2, node.property.name);
1769
+ }
1540
1770
  case "IndexExpression":
1541
1771
  return index(object2, this.expression(node.index));
1542
1772
  case "CallExpression": {
1773
+ const method = this.methodCalled(node);
1774
+ if (method) return method;
1543
1775
  const lowered = this.loweredGlobalCall(node);
1544
1776
  const args = this.arguments(node.arguments);
1545
1777
  if (!lowered) return call(object2, args);
@@ -1562,6 +1794,24 @@ end
1562
1794
  }
1563
1795
  }
1564
1796
  }
1797
+ /** `names.filter(names, f)`: a method of the value's metatable, read with
1798
+ * `.` and handed its `self` as the first argument. Written as a call of
1799
+ * the function the method really is — `array.filter(names, f)` — where
1800
+ * whoever declared the metatable says so; `undefined` leaves an ordinary
1801
+ * call, which is right for a string's Lua methods (`text.upper(text)`
1802
+ * reaches Lua's own `string.upper`). */
1803
+ methodCalled(node) {
1804
+ const callee = node.callee;
1805
+ if (callee.type !== "MemberExpression") return void 0;
1806
+ const source = this.options.types?.methodReads.get(callee);
1807
+ if (!source) return void 0;
1808
+ const spreads = node.arguments.map((a, i) => a.type === "SpreadElement" ? i : -1).filter((i) => i >= 0);
1809
+ if (!node.arguments.length || spreads.includes(0) || spreads.some((i) => i !== node.arguments.length - 1)) return void 0;
1810
+ const lowered = this.loweredMethod(callee.property.name, source, callee.object, node.arguments.slice(1), callee.property, node);
1811
+ if (!lowered) return void 0;
1812
+ const [self, ...rest] = this.arguments(node.arguments);
1813
+ return call(calleePath(lowered.callee), [...lowered.prepend, ...lowered.passReceiver ? [self] : [], ...rest]);
1814
+ }
1565
1815
  // --------------------------------------------------------
1566
1816
  // Optional chains
1567
1817
  // --------------------------------------------------------
@@ -1607,7 +1857,7 @@ end
1607
1857
  const segments = this.segments(chain);
1608
1858
  const base = this.expression(chain.base);
1609
1859
  const last = chain.links[chain.links.length - 1];
1610
- if (base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1860
+ if (!this.lua51 && base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1611
1861
  return {
1612
1862
  type: "IfElseExpression",
1613
1863
  clauses: [{ condition: binary("==", base, nil()), body: nil() }],
@@ -1628,6 +1878,32 @@ end
1628
1878
  const func = functionExpression(functionBody([], statements, passesVarargs));
1629
1879
  return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1630
1880
  }
1881
+ /** `a ?? b`: `a`, or `b` where `a` is nil — read only then. A `false`
1882
+ * stays, which is what sets it apart from `a or b`. A name is tested
1883
+ * in place; anything else is read once, into a local. */
1884
+ nullish(node) {
1885
+ const left = this.keptToOne(this.expression(node.left));
1886
+ const right = this.keptToOne(this.expression(node.right));
1887
+ if (!this.lua51 && left.type === "Identifier") {
1888
+ return {
1889
+ type: "IfElseExpression",
1890
+ clauses: [{ condition: binary("~=", left, nil()), body: left }],
1891
+ alternate: right,
1892
+ line: left.line,
1893
+ column: left.column
1894
+ };
1895
+ }
1896
+ const ref = this.names.fresh("value");
1897
+ const at = identifier(ref);
1898
+ const statements = [
1899
+ local([ref], [left]),
1900
+ ifThen(binary("~=", at, nil()), [returns([at])]),
1901
+ returns([right])
1902
+ ];
1903
+ const passesVarargs = statements.some(usesVararg);
1904
+ const func = functionExpression(functionBody([], statements, passesVarargs));
1905
+ return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1906
+ }
1631
1907
  /** `` `${a} any` `` -> `("%s any"):format(tostring(a))` */
1632
1908
  interpolatedString(node) {
1633
1909
  let format = "";
@@ -1709,12 +1985,12 @@ end
1709
1985
  }));
1710
1986
  const alternate = this.expression(node.alternate);
1711
1987
  if (clauses.every((c) => alwaysTruthy(c.body))) {
1712
- let result = alternate;
1988
+ let result2 = alternate;
1713
1989
  for (let i = clauses.length - 1; i >= 0; i--) {
1714
1990
  const { condition, body } = clauses[i];
1715
- result = binary("or", binary("and", condition, body), result);
1991
+ result2 = binary("or", binary("and", condition, body), result2);
1716
1992
  }
1717
- return parenthesized(result);
1993
+ return parenthesized(result2);
1718
1994
  }
1719
1995
  const statement = {
1720
1996
  type: "IfStatement",
@@ -1771,11 +2047,20 @@ end
1771
2047
  current = [];
1772
2048
  parts.push(this.expression(f.argument));
1773
2049
  break;
2050
+ case "TableFieldAccessor":
2051
+ break;
1774
2052
  }
1775
2053
  }
1776
- if (parts.length === 0) return table(current);
1777
- if (current.length) parts.push(table(current));
1778
- return call(this.helper("assign"), [table([]), ...parts]);
2054
+ let made;
2055
+ if (parts.length === 0) made = table(current);
2056
+ else {
2057
+ if (current.length) parts.push(table(current));
2058
+ made = call(this.helper("assign"), [table([]), ...parts]);
2059
+ }
2060
+ const accessors = node.fields.filter((f) => f.type === "TableFieldAccessor");
2061
+ if (!accessors.length) return made;
2062
+ 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)))));
2063
+ return call(this.helper("accessors"), [made, table2("get"), table2("set")]);
1779
2064
  }
1780
2065
  /** `[1, ...xs, 2]`. Without a spread it is a Luau sequence; with one, the
1781
2066
  * runs of plain elements and the spread arrays are joined by `concat`. */
@@ -2039,15 +2324,15 @@ async function loadLowerings(modules) {
2039
2324
  for (const module of modules) {
2040
2325
  try {
2041
2326
  const loaded = await import(pathToFileURL(module.file).href);
2042
- const plugin2 = loaded.default;
2043
- if (!plugin2 || typeof plugin2 !== "object") {
2327
+ const plugin3 = loaded.default;
2328
+ if (!plugin3 || typeof plugin3 !== "object") {
2044
2329
  problems.push({
2045
2330
  file: module.file,
2046
2331
  message: `'${module.from}' lowering module has no default export`
2047
2332
  });
2048
2333
  continue;
2049
2334
  }
2050
- lowerings.push({ plugin: plugin2, from: module.from, library: module.library });
2335
+ lowerings.push({ plugin: plugin3, from: module.from, library: module.library });
2051
2336
  } catch (error) {
2052
2337
  problems.push({
2053
2338
  file: module.file,
@@ -2059,8 +2344,8 @@ async function loadLowerings(modules) {
2059
2344
  }
2060
2345
 
2061
2346
  // src/bundle.ts
2062
- import { readFileSync as readFileSync2, statSync } from "fs";
2063
- import { dirname, relative, resolve as resolve2 } from "path";
2347
+ import { readFileSync as readFileSync3, statSync } from "fs";
2348
+ import { basename, dirname, extname, posix, relative, resolve as resolve2 } from "path";
2064
2349
  import {
2065
2350
  parse,
2066
2351
  analyzeScopes,
@@ -2068,17 +2353,18 @@ import {
2068
2353
  moduleExports,
2069
2354
  resolveModulePath,
2070
2355
  resolveTypeLibraries,
2356
+ importExpressions,
2071
2357
  directivesOf,
2072
2358
  applyDirectives,
2073
2359
  UNUSED_EXPECT_ERROR,
2074
2360
  ParseError,
2075
2361
  LexError,
2076
- luauString as luauString3
2362
+ luauString as luauString4
2077
2363
  } from "@tilua/parser";
2078
2364
  import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
2079
2365
 
2080
2366
  // src/linemap.ts
2081
- import { luauString as luauString2 } from "@tilua/parser";
2367
+ import { luauString as luauString3 } from "@tilua/parser";
2082
2368
  function statementsOf(node) {
2083
2369
  const out = [];
2084
2370
  const visit = (value) => {
@@ -2111,7 +2397,7 @@ function buildLineMap(emitted, printed, origins) {
2111
2397
  }
2112
2398
  function lineMapSource(map) {
2113
2399
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
2114
- const quoted = (s) => luauString2(s.replace(/\\/g, "/"));
2400
+ const quoted = (s) => luauString3(s.replace(/\\/g, "/"));
2115
2401
  return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
2116
2402
  }
2117
2403
 
@@ -2131,16 +2417,17 @@ async function bundle(options) {
2131
2417
  const file = queue.shift();
2132
2418
  if (sources.has(file)) continue;
2133
2419
  const parsed = parseFile(file, diagnostics, directives);
2134
- if (!parsed) return { modules: [], diagnostics };
2420
+ if (!parsed) return { files: [], modules: [], diagnostics };
2135
2421
  const { program: program3, text } = parsed;
2136
- const scopes = analyzeScopes(program3);
2422
+ const scopes = analyzeScopes(program3, { noLib: config?.noLib });
2137
2423
  for (const d of scopes.diagnostics) {
2138
2424
  diagnostics.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "scope" });
2139
2425
  }
2140
2426
  sources.set(file, { file, name: nameOf(file), text, program: program3, scopes });
2141
- for (const specifier of importedSpecifiers(program3)) {
2142
- const target = resolveModulePath(file, specifier, config);
2143
- if (target && !target.endsWith(".d.tilua")) queue.push(target);
2427
+ const dynamic = importExpressions(program3).flatMap((node) => node.source.type === "StringLiteral" ? [node.source.value] : []);
2428
+ for (const specifier of [...importedSpecifiers(program3), ...dynamic]) {
2429
+ const target2 = resolveModulePath(file, specifier, config);
2430
+ if (target2 && !target2.endsWith(".d.tilua")) queue.push(target2);
2144
2431
  }
2145
2432
  }
2146
2433
  const analysis = analyzeModules([...sources.values()], config);
@@ -2152,6 +2439,9 @@ async function bundle(options) {
2152
2439
  const names = Names.from(...[...sources.values()].map((s) => s.program));
2153
2440
  const G = names.fresh("G");
2154
2441
  const requireExpression = member(identifier(G), "require");
2442
+ const importExpression = member(identifier(G), "import");
2443
+ const mode = options.output ?? config?.output ?? "single";
2444
+ const target = options.target ?? config?.target ?? "luau";
2155
2445
  const modules = /* @__PURE__ */ new Map();
2156
2446
  const origins = /* @__PURE__ */ new WeakMap();
2157
2447
  const pending = [entry];
@@ -2160,38 +2450,48 @@ async function bundle(options) {
2160
2450
  if (modules.has(file)) continue;
2161
2451
  const source = sources.get(file);
2162
2452
  const shown = relative(root, file).replace(/\\/g, "/");
2163
- const lowered = lower(source.program, source.scopes, {
2453
+ const imports = /* @__PURE__ */ new Set();
2454
+ const dynamicImports = /* @__PURE__ */ new Set();
2455
+ const lowered2 = lower(source.program, source.scopes, {
2164
2456
  names,
2165
2457
  types: analysis.types.get(file),
2166
2458
  lowerings,
2167
- target: options.target ?? config?.target,
2459
+ target,
2460
+ noLib: config?.noLib,
2168
2461
  source: source.text,
2169
2462
  file: shown,
2170
2463
  module: {
2171
2464
  name: source.name,
2172
2465
  require: requireExpression,
2173
- lines: `${G}.lines`,
2466
+ import: importExpression,
2467
+ // One file has one line map. Several have one each, and a
2468
+ // module reads the one of the file it is in.
2469
+ lines: mode === "single" ? `${G}.lines` : `${G}.maps[${luauString4(source.name)}]`,
2174
2470
  fail: `${G}.fail`,
2175
2471
  arrays: `${G}.arrays`,
2176
- resolve: (specifier) => {
2177
- const target = resolveModulePath(file, specifier, config);
2178
- if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
2179
- pending.push(target);
2180
- return sources.get(target).name;
2472
+ resolve: (specifier, dynamic) => {
2473
+ const found = resolveModulePath(file, specifier, config);
2474
+ if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
2475
+ pending.push(found);
2476
+ const name = sources.get(found).name;
2477
+ (dynamic ? dynamicImports : imports).add(name);
2478
+ return name;
2181
2479
  }
2182
2480
  }
2183
2481
  });
2184
- for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2185
- for (const statement of allStatements(lowered.statements)) {
2186
- const line = lowered.origins.get(statement);
2482
+ for (const d of lowered2.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2483
+ for (const statement of allStatements(lowered2.statements)) {
2484
+ const line = lowered2.origins.get(statement);
2187
2485
  if (line !== void 0) origins.set(statement, { file: shown, line });
2188
2486
  }
2189
2487
  modules.set(file, {
2190
2488
  name: source.name,
2191
- statements: lowered.statements,
2192
- exportsName: lowered.exportsName,
2193
- info: lowered.module,
2194
- usesScriptArgs: lowered.usesScriptArgs
2489
+ statements: lowered2.statements,
2490
+ exportsName: lowered2.exportsName,
2491
+ info: lowered2.module,
2492
+ usesScriptArgs: lowered2.usesScriptArgs,
2493
+ imports,
2494
+ dynamicImports
2195
2495
  });
2196
2496
  }
2197
2497
  for (const [file, fileDirectives] of directives) {
@@ -2210,54 +2510,168 @@ async function bundle(options) {
2210
2510
  const reported = diagnostics.filter((d) => d.category !== "type" || !moduleProblems.has(`${d.file}:${d.line}:${d.column}:${d.message}`));
2211
2511
  diagnostics.length = 0;
2212
2512
  diagnostics.push(...reported);
2213
- const moduleNames = [...modules.values()].map((m) => m.name);
2513
+ const lowered = [...modules.values()];
2514
+ const moduleNames = lowered.map((m) => m.name);
2214
2515
  if (diagnostics.some((d) => d.category === "syntax" || d.category === "scope" || d.category === "module")) {
2215
- return { modules: moduleNames, diagnostics };
2216
- }
2217
- const program2 = parseLuau2(runtime(G));
2516
+ return { files: [], modules: moduleNames, diagnostics };
2517
+ }
2518
+ const several = mode !== "single";
2519
+ const layout = layoutOf(mode, modules.get(entry), lowered, options.outFile);
2520
+ const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
2521
+ const elsewhere = /* @__PURE__ */ new Map();
2522
+ for (const file of layout.slice(1)) {
2523
+ for (const module of file.modules) elsewhere.set(module.name, loadPath(layout[0].path, file.path, target));
2524
+ }
2525
+ const program2 = parseLuau2(runtime(G, {
2526
+ several,
2527
+ target,
2528
+ files: elsewhere,
2529
+ imports: lowered.some((m) => m.dynamicImports.size > 0)
2530
+ }));
2218
2531
  const assignment = program2.body.statements[1];
2219
- if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2532
+ if (usesScriptArgs) {
2220
2533
  program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2221
2534
  program2.body.statements.push(...parseLuau2(`${G}.arrays[scriptArgs] = true`).body.statements);
2535
+ if (several) program2.body.statements.push(...parseLuau2(`${G}.scriptArgs = scriptArgs`).body.statements);
2222
2536
  }
2223
2537
  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;
2224
2538
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
2225
- for (const module of modules.values()) {
2226
- const { names: exported, links, stars } = module.info;
2227
- const fields = [];
2228
- if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2229
- if (links.length) {
2230
- fields.push(field("links", table(links.map((link) => field(link.name, table([
2231
- { type: "TableFieldPositional", value: string(link.module) },
2232
- ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2233
- ]))))));
2234
- }
2235
- if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2236
- fields.push(field("load", functionExpression(functionBody([module.exportsName], module.statements, true))));
2237
- modulesTable.fields.push({ type: "TableFieldComputed", key: string(module.name), value: table(fields) });
2539
+ for (const module of layout[0].modules) {
2540
+ modulesTable.fields.push({ type: "TableFieldComputed", key: string(module.name), value: moduleTable(module) });
2238
2541
  }
2239
2542
  const ok = names.fresh("ok");
2240
- const result = names.fresh("result");
2543
+ const result2 = names.fresh("result");
2241
2544
  const entryStatements = parseLuau2(`
2242
- local ${ok}, ${result} = xpcall(function()
2243
- return ${G}.require(${luauString3(sources.get(entry).name)})
2545
+ local ${ok}, ${result2} = xpcall(function()
2546
+ return ${G}.require(${luauString4(sources.get(entry).name)})
2244
2547
  end, ${G}.fail)
2245
2548
  if not ${ok} then
2246
- error(${result}, 0)
2549
+ error(${result2}, 0)
2247
2550
  end
2248
- ${modules.get(entry).info.exports ? `return ${result}` : ""}
2551
+ ${modules.get(entry).info.exports ? `return ${result2}` : ""}
2249
2552
  `).body.statements;
2250
2553
  program2.body.statements.push(...entryStatements);
2251
2554
  const map = buildLineMap(program2, parseLuau2(print(program2)), origins);
2252
- if (map.size) {
2253
- program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, assign(
2555
+ if (map.size || several) {
2556
+ const mapStatements = [assign(
2254
2557
  [member(identifier(G), "lines")],
2255
2558
  [parseExpressionFromSource2(lineMapSource(map))]
2256
- ));
2559
+ )];
2560
+ if (several) mapStatements.push(...mapRegistration(G, layout[0], `${G}.lines`));
2561
+ program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
2562
+ }
2563
+ const files = [{ path: layout[0].path, code: print(program2) + "\n", modules: layout[0].modules.map((m) => m.name) }];
2564
+ for (const file of layout.slice(1)) {
2565
+ const body = [];
2566
+ if (file.modules.some((m) => m.usesScriptArgs)) body.push(...parseLuau2(`local scriptArgs = ${G}.scriptArgs`).body.statements);
2567
+ for (const module of file.modules) {
2568
+ body.push(assign(
2569
+ [index(member(identifier(G), "modules"), string(module.name))],
2570
+ [moduleTable(module)]
2571
+ ));
2572
+ }
2573
+ const chunk = program([returns([functionExpression(functionBody([G], body))])]);
2574
+ const lines = buildLineMap(chunk, parseLuau2(print(chunk)), origins);
2575
+ body.push(...mapRegistration(G, file, lineMapSource(lines)));
2576
+ files.push({ path: file.path, code: print(chunk) + "\n", modules: file.modules.map((m) => m.name) });
2577
+ }
2578
+ return { code: files[0].code, files, modules: moduleNames, diagnostics };
2579
+ }
2580
+ function moduleTable(module) {
2581
+ const { names: exported, links, stars } = module.info;
2582
+ const fields = [];
2583
+ if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2584
+ if (links.length) {
2585
+ fields.push(field("links", table(links.map((link) => field(link.name, table([
2586
+ { type: "TableFieldPositional", value: string(link.module) },
2587
+ ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2588
+ ]))))));
2589
+ }
2590
+ if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2591
+ fields.push(field("load", functionExpression(functionBody([module.exportsName], module.statements, true))));
2592
+ return table(fields);
2593
+ }
2594
+ function layoutOf(mode, entry, modules, outFile) {
2595
+ const extension = outFile !== void 0 && extname(outFile) || ".luau";
2596
+ const entryStem = outFile !== void 0 ? basename(outFile, extname(outFile)) : posix.basename(entry.name);
2597
+ const others = modules.filter((m) => m !== entry);
2598
+ if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry, ...others] }];
2599
+ if (mode === "modules") {
2600
+ const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
2601
+ return [entry, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
2602
+ }
2603
+ const byName = new Map(modules.map((m) => [m.name, m]));
2604
+ const reach = (root) => {
2605
+ const seen = /* @__PURE__ */ new Set();
2606
+ const stack = [root];
2607
+ while (stack.length) {
2608
+ const name = stack.pop();
2609
+ if (seen.has(name)) continue;
2610
+ seen.add(name);
2611
+ stack.push(...byName.get(name)?.imports ?? []);
2612
+ }
2613
+ return seen;
2614
+ };
2615
+ const inEntry = reach(entry.name);
2616
+ const roots = [...new Set(modules.flatMap((m) => [...m.dynamicImports]))].filter((root) => !inEntry.has(root));
2617
+ const reached = new Map(roots.map((root) => [root, reach(root)]));
2618
+ const groups = /* @__PURE__ */ new Map();
2619
+ for (const module of others) {
2620
+ if (inEntry.has(module.name)) continue;
2621
+ const by = roots.filter((root) => reached.get(root).has(module.name));
2622
+ const key = by.join("\n");
2623
+ if (!groups.has(key)) groups.set(key, { roots: by, modules: [] });
2624
+ groups.get(key).modules.push(module);
2625
+ }
2626
+ const files = [{ path: `${entryStem}${extension}`, modules: modules.filter((m) => inEntry.has(m.name)) }];
2627
+ const taken = /* @__PURE__ */ new Set([entryStem]);
2628
+ for (const group of groups.values()) {
2629
+ const label = group.roots.length === 1 ? group.roots[0].replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "chunk" : "shared";
2630
+ let stem = `${entryStem}_${label}`;
2631
+ for (let n = 2; taken.has(stem); n++) stem = `${entryStem}_${label}_${n}`;
2632
+ taken.add(stem);
2633
+ files.push({ path: `${stem}${extension}`, modules: group.modules });
2634
+ }
2635
+ return files;
2636
+ }
2637
+ function loadPath(entryPath, file, target) {
2638
+ const path = posix.relative(posix.dirname(entryPath), file);
2639
+ if (target === "lua51") return path;
2640
+ const bare = path.slice(0, path.length - posix.extname(path).length);
2641
+ return bare.startsWith("../") ? bare : `./${bare}`;
2642
+ }
2643
+ function mapRegistration(G, file, map) {
2644
+ const extension = posix.extname(file.path);
2645
+ const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
2646
+ const entry = `${G}.sources[#${G}.sources + 1]`;
2647
+ return parseLuau2([
2648
+ `${entry} = { { ${parts.map(luauString4).join(", ")} }, ${map}, ${luauString4(extension.slice(1))} }`,
2649
+ ...file.modules.map((module) => `${G}.maps[${luauString4(module.name)}] = ${G}.sources[#${G}.sources][2]`)
2650
+ ].join("\n")).body.statements;
2651
+ }
2652
+ function loader(G, target) {
2653
+ if (target === "lua51") {
2654
+ return `
2655
+ -- From beside this file, wherever that is: its own path says where.
2656
+ local folder = ""
2657
+ if debug ~= nil and debug.getinfo ~= nil then
2658
+ local at = debug.getinfo(${G}.load, "S").source
2659
+ if at:sub(1, 1) == "@" then
2660
+ folder = at:match("^@(.*[/\\\\])") or ""
2661
+ end
2662
+ end
2663
+ local chunk, problem = loadfile(folder .. file)
2664
+ if chunk == nil then
2665
+ error(problem, 0)
2666
+ end
2667
+ chunk()(${G})`;
2257
2668
  }
2258
- return { code: print(program2) + "\n", modules: moduleNames, diagnostics };
2669
+ return `
2670
+ require(file)(${G})`;
2259
2671
  }
2260
- function runtime(G) {
2672
+ function runtime(G, options) {
2673
+ const { several } = options;
2674
+ const files = [...options.files].map(([name, file]) => `[${luauString4(name)}] = ${luauString4(file)}`).join(", ");
2261
2675
  return `
2262
2676
  local ${G}
2263
2677
  ${G} = {
@@ -2305,7 +2719,7 @@ ${G} = {
2305
2719
  return message
2306
2720
  end
2307
2721
  local lines = ${G}.lines
2308
- if lines == nil then
2722
+ if lines == nil ${several ? `and #${G}.sources == 0 ` : ""}then
2309
2723
  return message .. (trace or "")
2310
2724
  end
2311
2725
  -- The bundle's name, as a position in a message spells it: where this
@@ -2325,6 +2739,81 @@ ${G} = {
2325
2739
  chunk = info.short_src
2326
2740
  end
2327
2741
  end
2742
+ ${several ? `
2743
+ -- A position in this file is read by this file's map. One in another
2744
+ -- of the bundle's files is read by that file's: the one whose name
2745
+ -- the position ends with, by the most folders where several do. A
2746
+ -- message says nothing about which file it starts in, so nothing is
2747
+ -- guessed from it here.
2748
+ local prefix = nil
2749
+ if chunk ~= nil then
2750
+ prefix = chunk:gsub("%p", "%%%0")
2751
+ end
2752
+ local function place(line)
2753
+ local origin = lines ~= nil and lines[tonumber(line)] or nil
2754
+ if origin == nil then
2755
+ return nil
2756
+ end
2757
+ return origin[1] .. ":" .. tostring(origin[2])
2758
+ end
2759
+ local function elsewhere(at, line)
2760
+ -- Already a place in the project.
2761
+ if at:sub(-6) == ".tilua" then
2762
+ return nil
2763
+ end
2764
+ local parts = {}
2765
+ for part in at:gmatch("[^/\\\\%.]+") do
2766
+ parts[#parts + 1] = part
2767
+ end
2768
+ local map, most, tied = nil, 0, false
2769
+ for _, source in ipairs(${G}.sources) do
2770
+ local names = source[1]
2771
+ local last = #parts
2772
+ if parts[last] == source[3] then
2773
+ last = last - 1
2774
+ end
2775
+ local same = 0
2776
+ while same < #names and same < last and names[#names - same] == parts[last - same] do
2777
+ same = same + 1
2778
+ end
2779
+ if same > most then
2780
+ map, most, tied = source[2], same, false
2781
+ elseif same == most then
2782
+ tied = true
2783
+ end
2784
+ end
2785
+ -- Two files it could as well be: a wrong place is worse than none.
2786
+ local origin = map ~= nil and not tied and map[tonumber(line)] or nil
2787
+ if origin == nil then
2788
+ return nil
2789
+ end
2790
+ return origin[1] .. ":" .. tostring(origin[2])
2791
+ end
2792
+ local mapped = message
2793
+ if prefix ~= nil then
2794
+ mapped = mapped:gsub(prefix .. ":(%d+)", function(line)
2795
+ return place(line)
2796
+ end)
2797
+ end
2798
+ mapped = mapped:gsub("(%S+):(%d+)", elsewhere)
2799
+ local frames = {}
2800
+ for frame in (trace or ""):gmatch("[^\\n]+") do
2801
+ local at = nil
2802
+ if prefix ~= nil then
2803
+ local line = frame:match(prefix .. ":(%d+)")
2804
+ if line ~= nil then
2805
+ at = place(line)
2806
+ end
2807
+ end
2808
+ if at == nil then
2809
+ for source, line in frame:gmatch("(%S+):(%d+)") do
2810
+ at = elsewhere(source, line)
2811
+ if at ~= nil then
2812
+ break
2813
+ end
2814
+ end
2815
+ end
2816
+ if at ~= nil then` : `
2328
2817
  if chunk == nil then
2329
2818
  chunk = message:match("^(.-):%d+:")
2330
2819
  end
@@ -2346,8 +2835,9 @@ ${G} = {
2346
2835
  for frame in (trace or ""):gmatch("[^\\n]+") do
2347
2836
  local line = frame:match(prefix .. ":(%d+)")
2348
2837
  local at = line ~= nil and place(line) or nil
2349
- if at ~= nil then
2350
- local name = frame:match("function ([%w_.:]+)")
2838
+ if at ~= nil then`}
2839
+ -- Luau writes \`function name\`; Lua 5.1, \`function 'name'\`.
2840
+ local name = frame:match("function ([%w_.:]+)") or frame:match("function '([^']+)'")
2351
2841
  frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2352
2842
  end
2353
2843
  end
@@ -2356,13 +2846,36 @@ ${G} = {
2356
2846
  end
2357
2847
  return mapped .. "\\n" .. table.concat(frames, "\\n")
2358
2848
  end,
2359
- records = {},
2849
+ records = {},${several ? `
2850
+ -- The file each module outside this one is in, as \`load\` is handed it.
2851
+ files = { ${files} },
2852
+ loaded = {},
2853
+ -- Every file's line map: by the module, and by the file's name.
2854
+ maps = {},
2855
+ sources = {},
2856
+ -- Another of the bundle's files: a function that adds its modules here.
2857
+ load = function(file)
2858
+ if ${G}.loaded[file] then
2859
+ return
2860
+ end
2861
+ ${G}.loaded[file] = true${loader(G, options.target)}
2862
+ end,` : ""}${options.imports ? `
2863
+ -- \`import("./x")\`: the module's exports, asked for where the code runs
2864
+ -- rather than before its module does. \`require\` does the rest \u2014 and
2865
+ -- loads the file the module is in, if it is not this one.
2866
+ import = function(name)
2867
+ return ${G}.require(name)
2868
+ end,` : ""}
2360
2869
  require = function(name)
2361
2870
  local record = ${G}.records[name]
2362
2871
  if record ~= nil then
2363
2872
  return record.exports
2364
2873
  end
2365
- local module = ${G}.modules[name]
2874
+ local module = ${G}.modules[name]${several ? `
2875
+ if module == nil then
2876
+ ${G}.load(${G}.files[name])
2877
+ module = ${G}.modules[name]
2878
+ end` : ""}
2366
2879
  local initialized = {}
2367
2880
  local exports = setmetatable({}, {
2368
2881
  __index = function(_, key)
@@ -2381,7 +2894,7 @@ ${G} = {
2381
2894
  return source[link[2]]
2382
2895
  end
2383
2896
  if module.stars ~= nil and key ~= "default" then
2384
- for _, star in module.stars do
2897
+ for _, star in ipairs(module.stars) do
2385
2898
  local value = ${G}.require(star)[key]
2386
2899
  if value ~= nil then
2387
2900
  return value
@@ -2422,7 +2935,7 @@ function parseFile(file, diagnostics, directives) {
2422
2935
  }
2423
2936
  let text;
2424
2937
  try {
2425
- text = readFileSync2(file, "utf8");
2938
+ text = readFileSync3(file, "utf8");
2426
2939
  } catch {
2427
2940
  diagnostics.push({ file, message: "Cannot read file", line: 1, column: 1, category: "module" });
2428
2941
  return void 0;
@@ -2456,7 +2969,7 @@ function parsedLibrary(file) {
2456
2969
  }
2457
2970
  const cached = libraryCache.get(file);
2458
2971
  if (cached && cached.stamp === stamp) return cached.program;
2459
- const program2 = parse(readFileSync2(file, "utf8"));
2972
+ const program2 = parse(readFileSync3(file, "utf8"));
2460
2973
  libraryCache.set(file, { stamp, program: program2 });
2461
2974
  return program2;
2462
2975
  }
@@ -2467,6 +2980,8 @@ function analyzeModules(modules, config) {
2467
2980
  const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], problems: [] };
2468
2981
  for (const p of libraries.problems) out.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
2469
2982
  const libs = libraries.files.map(parsedLibrary);
2983
+ const noLib = config?.noLib ?? false;
2984
+ const checked = libs.length > 0 || !noLib;
2470
2985
  const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
2471
2986
  const byFile = new Map(modules.map((m) => [m.file, m]));
2472
2987
  const exportsCache = /* @__PURE__ */ new Map();
@@ -2479,12 +2994,12 @@ function analyzeModules(modules, config) {
2479
2994
  const done = analyzed.get(file);
2480
2995
  if (done) return done;
2481
2996
  const remembered = moduleAnalyses.get(file);
2482
- 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)) {
2997
+ 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)) {
2483
2998
  analyzed.set(file, remembered.result);
2484
2999
  readWhile.set(file, remembered.read);
2485
3000
  return remembered.result;
2486
3001
  }
2487
- const scopes = analyzeScopes(program2, { builtinGlobals: globals, reportUndeclared: libs.length > 0 });
3002
+ const scopes = analyzeScopes(program2, { builtinGlobals: globals, reportUndeclared: checked, noLib });
2488
3003
  const said = [];
2489
3004
  for (const d of scopes.diagnostics) {
2490
3005
  if (d.kind !== "undeclared") continue;
@@ -2494,34 +3009,35 @@ function analyzeModules(modules, config) {
2494
3009
  const types = analyzeTypes(program2, scopes, {
2495
3010
  libs,
2496
3011
  resolveModule: resolverFor(file),
2497
- reportUnknownTypes: libs.length > 0
3012
+ noLib,
3013
+ reportUnknownTypes: checked
2498
3014
  });
2499
3015
  for (const d of types.diagnostics) {
2500
3016
  const at = d.node;
2501
3017
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
2502
3018
  }
2503
- const result = { types, diagnostics: said };
3019
+ const result2 = { types, diagnostics: said };
2504
3020
  if (partials === before) {
2505
- analyzed.set(file, result);
2506
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3021
+ analyzed.set(file, result2);
3022
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2507
3023
  const own = stampOf(file);
2508
- if (own !== void 0) read2.set(file, own);
2509
- readWhile.set(file, read2);
2510
- if ([...read2.values()].every((stamp) => stamp !== void 0)) {
2511
- moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read2, result });
3024
+ if (own !== void 0) read3.set(file, own);
3025
+ readWhile.set(file, read3);
3026
+ if ([...read3.values()].every((stamp) => stamp !== void 0)) {
3027
+ moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read3, result: result2 });
2512
3028
  }
2513
3029
  }
2514
- return result;
3030
+ return result2;
2515
3031
  };
2516
3032
  const resolverFor = (file) => (specifier) => {
2517
3033
  const target = resolveModulePath(file, specifier, config);
2518
3034
  if (!target) return void 0;
2519
3035
  const noteRead = () => {
2520
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2521
- readWhile.set(file, read2);
3036
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3037
+ readWhile.set(file, read3);
2522
3038
  const stamp = stampOf(target);
2523
- if (stamp !== void 0) read2.set(target, stamp);
2524
- for (const [below, belowStamp] of readWhile.get(target) ?? []) read2.set(below, belowStamp);
3039
+ if (stamp !== void 0) read3.set(target, stamp);
3040
+ for (const [below, belowStamp] of readWhile.get(target) ?? []) read3.set(below, belowStamp);
2525
3041
  };
2526
3042
  if (inProgress.has(target)) {
2527
3043
  partials++;
@@ -2535,15 +3051,15 @@ function analyzeModules(modules, config) {
2535
3051
  let program2 = byFile.get(target)?.program;
2536
3052
  if (!program2) {
2537
3053
  try {
2538
- program2 = parse(readFileSync2(target, "utf8"));
3054
+ program2 = parse(readFileSync3(target, "utf8"));
2539
3055
  } catch {
2540
3056
  return void 0;
2541
3057
  }
2542
3058
  }
2543
3059
  inProgress.add(target);
2544
3060
  try {
2545
- const scopes = analyzeScopes(program2, { builtinGlobals: globals });
2546
- const types = byFile.has(target) ? analyzeModule(target, program2).types : analyzeTypes(program2, scopes, { libs, resolveModule: resolverFor(target), diagnostics: false });
3061
+ const scopes = analyzeScopes(program2, { builtinGlobals: globals, noLib });
3062
+ const types = byFile.has(target) ? analyzeModule(target, program2).types : analyzeTypes(program2, scopes, { libs, noLib, resolveModule: resolverFor(target), diagnostics: false });
2547
3063
  const exports = moduleExports(program2, scopes, types, resolverFor(target));
2548
3064
  exportsCache.set(target, exports);
2549
3065
  noteRead();