@tilua/compiler 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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();
@@ -261,6 +332,10 @@ var Lowerer = class {
261
332
  }
262
333
  return identifier(local2);
263
334
  }
335
+ /** `table.move`. Lua 5.1 has none, and gets a function that does the same. */
336
+ move() {
337
+ return this.lua51 ? this.helper("move") : member(this.builtin("table"), "move");
338
+ }
264
339
  helper(kind) {
265
340
  let name = this.helpers.get(kind);
266
341
  if (!name) {
@@ -383,7 +458,11 @@ var Lowerer = class {
383
458
  /** The language's own lowering, then each library's, in load order. A
384
459
  * runtime is named by its lowering's place here (`loweringRuntime`). */
385
460
  get lowerings() {
386
- return this.allLowerings ??= [languageLowering, ...this.options.lowerings ?? []];
461
+ return this.allLowerings ??= [
462
+ languageLowering,
463
+ ...this.options.noLib ? [] : [standardLowering],
464
+ ...this.options.lowerings ?? []
465
+ ];
387
466
  }
388
467
  allLowerings;
389
468
  /** What `receiver:method(...)` becomes, when Luau alone would not run it.
@@ -397,22 +476,28 @@ var Lowerer = class {
397
476
  * (`console:log`), the last library loaded first. No answer leaves an
398
477
  * ordinary Luau method call: `text:upper()` reaches Lua's own. */
399
478
  loweredMethodCall(node) {
479
+ return this.loweredMethod(node.method.name, this.options.types?.methodSources.get(node), node.object, node.arguments, node.method, node);
480
+ }
481
+ /** `loweredMethodCall`, for a method named `method` of what `object`
482
+ * holds — called with `:` on it, or read off it with `.` and handed its
483
+ * `self` like any other argument. `args` are the arguments after the
484
+ * receiver; a read that is not called has none. */
485
+ loweredMethod(method, source, object2, args, name, node) {
400
486
  const lowerings = this.lowerings;
401
- const source = this.options.types?.methodSources.get(node);
402
487
  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
488
  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);
489
+ const receiver = this.options.types?.typeOf.get(object2);
490
+ const receiverGlobal = this.globalName(object2);
491
+ const info = this.lazyArguments(args);
407
492
  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,
493
+ const plugin3 = lowerings[i].plugin;
494
+ if (!plugin3.methodCall) continue;
495
+ const answer = plugin3.methodCall({
496
+ method,
412
497
  receiver,
413
498
  receiverGlobal,
414
- argumentCount: node.arguments.length,
415
- at: this.callSite(node.method),
499
+ argumentCount: args.length,
500
+ at: this.callSite(name),
416
501
  get arguments() {
417
502
  return info();
418
503
  },
@@ -436,9 +521,9 @@ var Lowerer = class {
436
521
  if (name === void 0) return void 0;
437
522
  const info = this.lazyArguments(node.arguments);
438
523
  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({
524
+ const plugin3 = lowerings[i].plugin;
525
+ if (!plugin3.globalCall) continue;
526
+ const answer = plugin3.globalCall({
442
527
  name,
443
528
  at: this.callSite(node.callee),
444
529
  get arguments() {
@@ -490,6 +575,31 @@ var Lowerer = class {
490
575
  const out = [];
491
576
  const runtimes = this.runtimeDefinitions();
492
577
  if (this.helpers.has("concat")) this.helper("array");
578
+ if (this.helpers.has("concat")) this.move();
579
+ const move = this.helpers.get("move");
580
+ if (move) {
581
+ out.push(...parseLuau(`
582
+ local function ${move}(from, first, last, at, to)
583
+ for i = first, last do
584
+ to[at + i - first] = from[i]
585
+ end
586
+ return to
587
+ end`).body.statements);
588
+ }
589
+ const each = this.helpers.get("each");
590
+ if (each) {
591
+ out.push(...parseLuau(`
592
+ local function ${each}(source, ...)
593
+ if ${this.builtin("type").name}(source) == "function" then
594
+ return source, ...
595
+ end
596
+ local meta = ${this.builtin("getmetatable").name}(source)
597
+ if meta ~= nil and meta.__iter ~= nil then
598
+ return meta.__iter(source)
599
+ end
600
+ return ${this.builtin("next").name}, source
601
+ end`).body.statements);
602
+ }
493
603
  const marker = this.helpers.get("array");
494
604
  const typeOf = this.helpers.get("typeof");
495
605
  if (marker || typeOf) {
@@ -528,6 +638,49 @@ local function ${typeOf}(value, known)
528
638
  end`).body.statements);
529
639
  }
530
640
  }
641
+ const instanceOf = this.helpers.get("instanceof");
642
+ if (instanceOf) {
643
+ out.push(...parseLuau(`
644
+ local function ${instanceOf}(value, class)
645
+ if ${this.builtin("type").name}(value) ~= "table" then
646
+ return false
647
+ end
648
+ local current = value.ClassObject
649
+ while current ~= nil do
650
+ if current == class then
651
+ return true
652
+ end
653
+ current = current.ParentClass
654
+ end
655
+ return false
656
+ end`).body.statements);
657
+ }
658
+ const accessors = this.helpers.get("accessors");
659
+ if (accessors) {
660
+ out.push(...parseLuau(`
661
+ local function ${accessors}(target, getters, setters)
662
+ return ${this.builtin("setmetatable").name}(target, {
663
+ __index = function(self, key)
664
+ local get = getters[key]
665
+ if get ~= nil then
666
+ return get(self)
667
+ end
668
+ return nil
669
+ end,
670
+ __newindex = function(self, key, value)
671
+ local set = setters[key]
672
+ if set ~= nil then
673
+ set(self, value)
674
+ return
675
+ end
676
+ if getters[key] ~= nil then
677
+ ${this.builtin("error").name}("Cannot assign to '" .. ${this.builtin("tostring").name}(key) .. "': it has a getter and no setter", 2)
678
+ end
679
+ ${this.builtin("rawset").name}(self, key, value)
680
+ end,
681
+ })
682
+ end`).body.statements);
683
+ }
531
684
  const assign2 = this.helpers.get("assign");
532
685
  if (assign2) {
533
686
  out.push(localFunction(assign2, functionBody(["target"], [
@@ -549,7 +702,7 @@ end`).body.statements);
549
702
  local(["result"], [this.array(table([]))]),
550
703
  numericFor("i", number(1), selectCount(this.builtin("select")), [
551
704
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
552
- callStatement(call(member(this.builtin("table"), "move"), [
705
+ callStatement(call(this.move(), [
553
706
  identifier("part"),
554
707
  number(1),
555
708
  unary("#", identifier("part")),
@@ -566,8 +719,8 @@ end`).body.statements);
566
719
  runtimeDefinitions() {
567
720
  const out = [];
568
721
  for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
569
- const { plugin: plugin2, from } = this.lowerings[index2];
570
- const source = plugin2.runtime?.[runtime2];
722
+ const { plugin: plugin3, from } = this.lowerings[index2];
723
+ const source = plugin3.runtime?.[runtime2];
571
724
  if (source === void 0) {
572
725
  this.diagnostics.push({
573
726
  message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
@@ -662,6 +815,7 @@ end`).body.statements);
662
815
  names.push("default");
663
816
  break;
664
817
  case "ExportNamedStatement": {
818
+ if (statement.typeOnly) break;
665
819
  if (statement.source) {
666
820
  const key = resolve3(statement.source);
667
821
  if (key === void 0) break;
@@ -745,6 +899,26 @@ end`).body.statements);
745
899
  for (let i = 0; i < locals.length; i += 100) declarations.push(local(locals.slice(i, i + 100), []));
746
900
  return [...declarations, ...hoisted, ...requires, ...body];
747
901
  }
902
+ /** `import("./x")`: the bundle's `import`, called with the module's name.
903
+ * It answers the module's exports table — what `import * as` names. */
904
+ importExpression(node) {
905
+ const context = this.options.module;
906
+ if (!context) {
907
+ this.report(node, "import() needs a bundle: build the project with @tilua/compiler");
908
+ return nil();
909
+ }
910
+ const source = node.source;
911
+ if (source.type !== "StringLiteral") {
912
+ this.report(source, "import() takes a string literal: which module it loads is decided when the project is built");
913
+ return nil();
914
+ }
915
+ const key = context.resolve(source.value, true);
916
+ if (key === void 0) {
917
+ this.report(source, `Cannot find module '${source.value}'`);
918
+ return nil();
919
+ }
920
+ return call(context.import ?? context.require, [string(key)]);
921
+ }
748
922
  isRewritten(node) {
749
923
  const binding = this.bindingByDeclaration.get(node);
750
924
  return binding !== void 0 && this.rewrites.has(binding.id);
@@ -891,7 +1065,7 @@ end`).body.statements);
891
1065
  case "VariableDeclaration":
892
1066
  return this.variableDeclaration(node, "declare");
893
1067
  case "FunctionDeclaration":
894
- return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: node.attributes }];
1068
+ return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: this.lua51 ? void 0 : node.attributes }];
895
1069
  case "FunctionDeclarationStatement":
896
1070
  return [this.functionDeclarationStatement(node)];
897
1071
  case "ClassDeclaration":
@@ -922,8 +1096,13 @@ end`).body.statements);
922
1096
  // kept — Lua has no expression statement — and it cannot run
923
1097
  // anything, since a call is a `CallStatement` instead, so dropping
924
1098
  // it changes nothing but the noise.
925
- case "ExpressionStatement":
926
- return [];
1099
+ case "ExpressionStatement": {
1100
+ let expression = node.expression;
1101
+ while (expression.type === "ParenthesizedExpression") expression = expression.expression;
1102
+ if (expression.type !== "ImportExpression") return [];
1103
+ const call2 = this.importExpression(expression);
1104
+ return call2.type === "CallExpression" ? [this.from(callStatement(call2), node)] : [];
1105
+ }
927
1106
  case "DoStatement":
928
1107
  return [{ type: "DoStatement", body: this.block(node.body), ...spanOf(node) }];
929
1108
  case "WhileStatement":
@@ -975,7 +1154,11 @@ end`).body.statements);
975
1154
  let source = this.expression(node.iterator);
976
1155
  if (form.viaIter) source = methodCall(source, "__iter", []);
977
1156
  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];
1157
+ let iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1158
+ if (this.lua51 && form.walks === "values") {
1159
+ const kind = form.viaIter ? void 0 : this.options.types?.typeOf.get(node.iterator)?.kind;
1160
+ iterators = [call(kind === "array" || kind === "tuple" ? this.builtin("ipairs") : this.helper("each"), [source])];
1161
+ }
979
1162
  return [genericFor(
980
1163
  variables,
981
1164
  iterators,
@@ -1228,7 +1411,8 @@ end
1228
1411
  },
1229
1412
  isMethod,
1230
1413
  func,
1231
- attributes: node.attributes,
1414
+ // `@native` and the like are Luau's.
1415
+ attributes: this.lua51 ? void 0 : node.attributes,
1232
1416
  ...spanOf(node)
1233
1417
  };
1234
1418
  }
@@ -1329,7 +1513,7 @@ end
1329
1513
  if (element) bind(element.value, index(source, number(i + 1)), element.default);
1330
1514
  });
1331
1515
  if (pattern.rest) {
1332
- const elements = call(member(this.builtin("table"), "move"), [
1516
+ const elements = call(this.move(), [
1333
1517
  source,
1334
1518
  number(pattern.elements.length + 1),
1335
1519
  unary("#", source),
@@ -1440,8 +1624,15 @@ end
1440
1624
  case "ArrayExpression":
1441
1625
  return this.arrayExpression(node);
1442
1626
  case "BinaryExpression": {
1627
+ if (node.operator === "??") return this.nullish(node);
1443
1628
  const left = this.expression(node.left);
1444
1629
  const right = this.expression(node.right);
1630
+ if (node.operator === "in") {
1631
+ return parenthesized(binary("~=", index(this.keptToOne(right), this.keptToOne(left)), nil()));
1632
+ }
1633
+ if (node.operator === "instanceof") {
1634
+ return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
1635
+ }
1445
1636
  if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
1446
1637
  return binary(node.operator, left, right);
1447
1638
  }
@@ -1468,6 +1659,8 @@ end
1468
1659
  }
1469
1660
  case "ParenthesizedExpression":
1470
1661
  return parenthesized(this.expression(node.expression));
1662
+ case "ImportExpression":
1663
+ return this.importExpression(node);
1471
1664
  // Types only: the value is the expression itself.
1472
1665
  case "TypeAssertionExpression":
1473
1666
  case "SatisfiesExpression":
@@ -1521,7 +1714,7 @@ end
1521
1714
  const last2 = list.length - 1;
1522
1715
  return lowered.length && !many.has(list[last2]) ? [...lowered.slice(0, -1), this.keptToOne(lowered[last2])] : lowered;
1523
1716
  }
1524
- const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
1717
+ const unpack = (value) => call(this.lua51 ? this.builtin("unpack") : member(this.builtin("table"), "unpack"), [value]);
1525
1718
  const last = list[list.length - 1];
1526
1719
  if (spreads.length === 1 && last.type === "SpreadElement") {
1527
1720
  return [...list.slice(0, -1).map((a) => this.expression(a)), unpack(this.expression(last.argument))];
@@ -1535,11 +1728,21 @@ end
1535
1728
  /** One link of an access chain, read from `object`. */
1536
1729
  link(node, object2) {
1537
1730
  switch (node.type) {
1538
- case "MemberExpression":
1731
+ case "MemberExpression": {
1732
+ if (this.options.types?.lengths.has(node)) return unary("#", object2);
1733
+ const source = this.options.types?.methodReads.get(node);
1734
+ const lowered = source && this.loweredMethod(node.property.name, source, node.object, [], node.property, node);
1735
+ if (lowered) {
1736
+ const forwarded = call(calleePath(lowered.callee), [...lowered.prepend, vararg()]);
1737
+ return functionExpression(functionBody(lowered.passReceiver ? [] : ["_"], [returns([forwarded])], true));
1738
+ }
1539
1739
  return member(object2, node.property.name);
1740
+ }
1540
1741
  case "IndexExpression":
1541
1742
  return index(object2, this.expression(node.index));
1542
1743
  case "CallExpression": {
1744
+ const method = this.methodCalled(node);
1745
+ if (method) return method;
1543
1746
  const lowered = this.loweredGlobalCall(node);
1544
1747
  const args = this.arguments(node.arguments);
1545
1748
  if (!lowered) return call(object2, args);
@@ -1562,6 +1765,24 @@ end
1562
1765
  }
1563
1766
  }
1564
1767
  }
1768
+ /** `names.filter(names, f)`: a method of the value's metatable, read with
1769
+ * `.` and handed its `self` as the first argument. Written as a call of
1770
+ * the function the method really is — `array.filter(names, f)` — where
1771
+ * whoever declared the metatable says so; `undefined` leaves an ordinary
1772
+ * call, which is right for a string's Lua methods (`text.upper(text)`
1773
+ * reaches Lua's own `string.upper`). */
1774
+ methodCalled(node) {
1775
+ const callee = node.callee;
1776
+ if (callee.type !== "MemberExpression") return void 0;
1777
+ const source = this.options.types?.methodReads.get(callee);
1778
+ if (!source) return void 0;
1779
+ const spreads = node.arguments.map((a, i) => a.type === "SpreadElement" ? i : -1).filter((i) => i >= 0);
1780
+ if (!node.arguments.length || spreads.includes(0) || spreads.some((i) => i !== node.arguments.length - 1)) return void 0;
1781
+ const lowered = this.loweredMethod(callee.property.name, source, callee.object, node.arguments.slice(1), callee.property, node);
1782
+ if (!lowered) return void 0;
1783
+ const [self, ...rest] = this.arguments(node.arguments);
1784
+ return call(calleePath(lowered.callee), [...lowered.prepend, ...lowered.passReceiver ? [self] : [], ...rest]);
1785
+ }
1565
1786
  // --------------------------------------------------------
1566
1787
  // Optional chains
1567
1788
  // --------------------------------------------------------
@@ -1607,7 +1828,7 @@ end
1607
1828
  const segments = this.segments(chain);
1608
1829
  const base = this.expression(chain.base);
1609
1830
  const last = chain.links[chain.links.length - 1];
1610
- if (base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1831
+ if (!this.lua51 && base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1611
1832
  return {
1612
1833
  type: "IfElseExpression",
1613
1834
  clauses: [{ condition: binary("==", base, nil()), body: nil() }],
@@ -1628,6 +1849,32 @@ end
1628
1849
  const func = functionExpression(functionBody([], statements, passesVarargs));
1629
1850
  return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1630
1851
  }
1852
+ /** `a ?? b`: `a`, or `b` where `a` is nil — read only then. A `false`
1853
+ * stays, which is what sets it apart from `a or b`. A name is tested
1854
+ * in place; anything else is read once, into a local. */
1855
+ nullish(node) {
1856
+ const left = this.keptToOne(this.expression(node.left));
1857
+ const right = this.keptToOne(this.expression(node.right));
1858
+ if (!this.lua51 && left.type === "Identifier") {
1859
+ return {
1860
+ type: "IfElseExpression",
1861
+ clauses: [{ condition: binary("~=", left, nil()), body: left }],
1862
+ alternate: right,
1863
+ line: left.line,
1864
+ column: left.column
1865
+ };
1866
+ }
1867
+ const ref = this.names.fresh("value");
1868
+ const at = identifier(ref);
1869
+ const statements = [
1870
+ local([ref], [left]),
1871
+ ifThen(binary("~=", at, nil()), [returns([at])]),
1872
+ returns([right])
1873
+ ];
1874
+ const passesVarargs = statements.some(usesVararg);
1875
+ const func = functionExpression(functionBody([], statements, passesVarargs));
1876
+ return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1877
+ }
1631
1878
  /** `` `${a} any` `` -> `("%s any"):format(tostring(a))` */
1632
1879
  interpolatedString(node) {
1633
1880
  let format = "";
@@ -1709,12 +1956,12 @@ end
1709
1956
  }));
1710
1957
  const alternate = this.expression(node.alternate);
1711
1958
  if (clauses.every((c) => alwaysTruthy(c.body))) {
1712
- let result = alternate;
1959
+ let result2 = alternate;
1713
1960
  for (let i = clauses.length - 1; i >= 0; i--) {
1714
1961
  const { condition, body } = clauses[i];
1715
- result = binary("or", binary("and", condition, body), result);
1962
+ result2 = binary("or", binary("and", condition, body), result2);
1716
1963
  }
1717
- return parenthesized(result);
1964
+ return parenthesized(result2);
1718
1965
  }
1719
1966
  const statement = {
1720
1967
  type: "IfStatement",
@@ -1771,11 +2018,20 @@ end
1771
2018
  current = [];
1772
2019
  parts.push(this.expression(f.argument));
1773
2020
  break;
2021
+ case "TableFieldAccessor":
2022
+ break;
1774
2023
  }
1775
2024
  }
1776
- if (parts.length === 0) return table(current);
1777
- if (current.length) parts.push(table(current));
1778
- return call(this.helper("assign"), [table([]), ...parts]);
2025
+ let made;
2026
+ if (parts.length === 0) made = table(current);
2027
+ else {
2028
+ if (current.length) parts.push(table(current));
2029
+ made = call(this.helper("assign"), [table([]), ...parts]);
2030
+ }
2031
+ const accessors = node.fields.filter((f) => f.type === "TableFieldAccessor");
2032
+ if (!accessors.length) return made;
2033
+ 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)))));
2034
+ return call(this.helper("accessors"), [made, table2("get"), table2("set")]);
1779
2035
  }
1780
2036
  /** `[1, ...xs, 2]`. Without a spread it is a Luau sequence; with one, the
1781
2037
  * runs of plain elements and the spread arrays are joined by `concat`. */
@@ -2039,15 +2295,15 @@ async function loadLowerings(modules) {
2039
2295
  for (const module of modules) {
2040
2296
  try {
2041
2297
  const loaded = await import(pathToFileURL(module.file).href);
2042
- const plugin2 = loaded.default;
2043
- if (!plugin2 || typeof plugin2 !== "object") {
2298
+ const plugin3 = loaded.default;
2299
+ if (!plugin3 || typeof plugin3 !== "object") {
2044
2300
  problems.push({
2045
2301
  file: module.file,
2046
2302
  message: `'${module.from}' lowering module has no default export`
2047
2303
  });
2048
2304
  continue;
2049
2305
  }
2050
- lowerings.push({ plugin: plugin2, from: module.from, library: module.library });
2306
+ lowerings.push({ plugin: plugin3, from: module.from, library: module.library });
2051
2307
  } catch (error) {
2052
2308
  problems.push({
2053
2309
  file: module.file,
@@ -2059,8 +2315,8 @@ async function loadLowerings(modules) {
2059
2315
  }
2060
2316
 
2061
2317
  // src/bundle.ts
2062
- import { readFileSync as readFileSync2, statSync } from "fs";
2063
- import { dirname, relative, resolve as resolve2 } from "path";
2318
+ import { readFileSync as readFileSync3, statSync } from "fs";
2319
+ import { basename, dirname, extname, posix, relative, resolve as resolve2 } from "path";
2064
2320
  import {
2065
2321
  parse,
2066
2322
  analyzeScopes,
@@ -2068,17 +2324,18 @@ import {
2068
2324
  moduleExports,
2069
2325
  resolveModulePath,
2070
2326
  resolveTypeLibraries,
2327
+ importExpressions,
2071
2328
  directivesOf,
2072
2329
  applyDirectives,
2073
2330
  UNUSED_EXPECT_ERROR,
2074
2331
  ParseError,
2075
2332
  LexError,
2076
- luauString as luauString3
2333
+ luauString as luauString4
2077
2334
  } from "@tilua/parser";
2078
2335
  import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
2079
2336
 
2080
2337
  // src/linemap.ts
2081
- import { luauString as luauString2 } from "@tilua/parser";
2338
+ import { luauString as luauString3 } from "@tilua/parser";
2082
2339
  function statementsOf(node) {
2083
2340
  const out = [];
2084
2341
  const visit = (value) => {
@@ -2111,7 +2368,7 @@ function buildLineMap(emitted, printed, origins) {
2111
2368
  }
2112
2369
  function lineMapSource(map) {
2113
2370
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
2114
- const quoted = (s) => luauString2(s.replace(/\\/g, "/"));
2371
+ const quoted = (s) => luauString3(s.replace(/\\/g, "/"));
2115
2372
  return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
2116
2373
  }
2117
2374
 
@@ -2131,16 +2388,17 @@ async function bundle(options) {
2131
2388
  const file = queue.shift();
2132
2389
  if (sources.has(file)) continue;
2133
2390
  const parsed = parseFile(file, diagnostics, directives);
2134
- if (!parsed) return { modules: [], diagnostics };
2391
+ if (!parsed) return { files: [], modules: [], diagnostics };
2135
2392
  const { program: program3, text } = parsed;
2136
- const scopes = analyzeScopes(program3);
2393
+ const scopes = analyzeScopes(program3, { noLib: config?.noLib });
2137
2394
  for (const d of scopes.diagnostics) {
2138
2395
  diagnostics.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "scope" });
2139
2396
  }
2140
2397
  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);
2398
+ const dynamic = importExpressions(program3).flatMap((node) => node.source.type === "StringLiteral" ? [node.source.value] : []);
2399
+ for (const specifier of [...importedSpecifiers(program3), ...dynamic]) {
2400
+ const target2 = resolveModulePath(file, specifier, config);
2401
+ if (target2 && !target2.endsWith(".d.tilua")) queue.push(target2);
2144
2402
  }
2145
2403
  }
2146
2404
  const analysis = analyzeModules([...sources.values()], config);
@@ -2152,6 +2410,9 @@ async function bundle(options) {
2152
2410
  const names = Names.from(...[...sources.values()].map((s) => s.program));
2153
2411
  const G = names.fresh("G");
2154
2412
  const requireExpression = member(identifier(G), "require");
2413
+ const importExpression = member(identifier(G), "import");
2414
+ const mode = options.output ?? config?.output ?? "single";
2415
+ const target = options.target ?? config?.target ?? "luau";
2155
2416
  const modules = /* @__PURE__ */ new Map();
2156
2417
  const origins = /* @__PURE__ */ new WeakMap();
2157
2418
  const pending = [entry];
@@ -2160,38 +2421,48 @@ async function bundle(options) {
2160
2421
  if (modules.has(file)) continue;
2161
2422
  const source = sources.get(file);
2162
2423
  const shown = relative(root, file).replace(/\\/g, "/");
2163
- const lowered = lower(source.program, source.scopes, {
2424
+ const imports = /* @__PURE__ */ new Set();
2425
+ const dynamicImports = /* @__PURE__ */ new Set();
2426
+ const lowered2 = lower(source.program, source.scopes, {
2164
2427
  names,
2165
2428
  types: analysis.types.get(file),
2166
2429
  lowerings,
2167
- target: options.target ?? config?.target,
2430
+ target,
2431
+ noLib: config?.noLib,
2168
2432
  source: source.text,
2169
2433
  file: shown,
2170
2434
  module: {
2171
2435
  name: source.name,
2172
2436
  require: requireExpression,
2173
- lines: `${G}.lines`,
2437
+ import: importExpression,
2438
+ // One file has one line map. Several have one each, and a
2439
+ // module reads the one of the file it is in.
2440
+ lines: mode === "single" ? `${G}.lines` : `${G}.maps[${luauString4(source.name)}]`,
2174
2441
  fail: `${G}.fail`,
2175
2442
  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;
2443
+ resolve: (specifier, dynamic) => {
2444
+ const found = resolveModulePath(file, specifier, config);
2445
+ if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
2446
+ pending.push(found);
2447
+ const name = sources.get(found).name;
2448
+ (dynamic ? dynamicImports : imports).add(name);
2449
+ return name;
2181
2450
  }
2182
2451
  }
2183
2452
  });
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);
2453
+ for (const d of lowered2.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2454
+ for (const statement of allStatements(lowered2.statements)) {
2455
+ const line = lowered2.origins.get(statement);
2187
2456
  if (line !== void 0) origins.set(statement, { file: shown, line });
2188
2457
  }
2189
2458
  modules.set(file, {
2190
2459
  name: source.name,
2191
- statements: lowered.statements,
2192
- exportsName: lowered.exportsName,
2193
- info: lowered.module,
2194
- usesScriptArgs: lowered.usesScriptArgs
2460
+ statements: lowered2.statements,
2461
+ exportsName: lowered2.exportsName,
2462
+ info: lowered2.module,
2463
+ usesScriptArgs: lowered2.usesScriptArgs,
2464
+ imports,
2465
+ dynamicImports
2195
2466
  });
2196
2467
  }
2197
2468
  for (const [file, fileDirectives] of directives) {
@@ -2210,54 +2481,168 @@ async function bundle(options) {
2210
2481
  const reported = diagnostics.filter((d) => d.category !== "type" || !moduleProblems.has(`${d.file}:${d.line}:${d.column}:${d.message}`));
2211
2482
  diagnostics.length = 0;
2212
2483
  diagnostics.push(...reported);
2213
- const moduleNames = [...modules.values()].map((m) => m.name);
2484
+ const lowered = [...modules.values()];
2485
+ const moduleNames = lowered.map((m) => m.name);
2214
2486
  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));
2487
+ return { files: [], modules: moduleNames, diagnostics };
2488
+ }
2489
+ const several = mode !== "single";
2490
+ const layout = layoutOf(mode, modules.get(entry), lowered, options.outFile);
2491
+ const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
2492
+ const elsewhere = /* @__PURE__ */ new Map();
2493
+ for (const file of layout.slice(1)) {
2494
+ for (const module of file.modules) elsewhere.set(module.name, loadPath(layout[0].path, file.path, target));
2495
+ }
2496
+ const program2 = parseLuau2(runtime(G, {
2497
+ several,
2498
+ target,
2499
+ files: elsewhere,
2500
+ imports: lowered.some((m) => m.dynamicImports.size > 0)
2501
+ }));
2218
2502
  const assignment = program2.body.statements[1];
2219
- if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2503
+ if (usesScriptArgs) {
2220
2504
  program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2221
2505
  program2.body.statements.push(...parseLuau2(`${G}.arrays[scriptArgs] = true`).body.statements);
2506
+ if (several) program2.body.statements.push(...parseLuau2(`${G}.scriptArgs = scriptArgs`).body.statements);
2222
2507
  }
2223
2508
  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
2509
  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) });
2510
+ for (const module of layout[0].modules) {
2511
+ modulesTable.fields.push({ type: "TableFieldComputed", key: string(module.name), value: moduleTable(module) });
2238
2512
  }
2239
2513
  const ok = names.fresh("ok");
2240
- const result = names.fresh("result");
2514
+ const result2 = names.fresh("result");
2241
2515
  const entryStatements = parseLuau2(`
2242
- local ${ok}, ${result} = xpcall(function()
2243
- return ${G}.require(${luauString3(sources.get(entry).name)})
2516
+ local ${ok}, ${result2} = xpcall(function()
2517
+ return ${G}.require(${luauString4(sources.get(entry).name)})
2244
2518
  end, ${G}.fail)
2245
2519
  if not ${ok} then
2246
- error(${result}, 0)
2520
+ error(${result2}, 0)
2247
2521
  end
2248
- ${modules.get(entry).info.exports ? `return ${result}` : ""}
2522
+ ${modules.get(entry).info.exports ? `return ${result2}` : ""}
2249
2523
  `).body.statements;
2250
2524
  program2.body.statements.push(...entryStatements);
2251
2525
  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(
2526
+ if (map.size || several) {
2527
+ const mapStatements = [assign(
2254
2528
  [member(identifier(G), "lines")],
2255
2529
  [parseExpressionFromSource2(lineMapSource(map))]
2256
- ));
2530
+ )];
2531
+ if (several) mapStatements.push(...mapRegistration(G, layout[0], `${G}.lines`));
2532
+ program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
2257
2533
  }
2258
- return { code: print(program2) + "\n", modules: moduleNames, diagnostics };
2534
+ const files = [{ path: layout[0].path, code: print(program2) + "\n", modules: layout[0].modules.map((m) => m.name) }];
2535
+ for (const file of layout.slice(1)) {
2536
+ const body = [];
2537
+ if (file.modules.some((m) => m.usesScriptArgs)) body.push(...parseLuau2(`local scriptArgs = ${G}.scriptArgs`).body.statements);
2538
+ for (const module of file.modules) {
2539
+ body.push(assign(
2540
+ [index(member(identifier(G), "modules"), string(module.name))],
2541
+ [moduleTable(module)]
2542
+ ));
2543
+ }
2544
+ const chunk = program([returns([functionExpression(functionBody([G], body))])]);
2545
+ const lines = buildLineMap(chunk, parseLuau2(print(chunk)), origins);
2546
+ body.push(...mapRegistration(G, file, lineMapSource(lines)));
2547
+ files.push({ path: file.path, code: print(chunk) + "\n", modules: file.modules.map((m) => m.name) });
2548
+ }
2549
+ return { code: files[0].code, files, modules: moduleNames, diagnostics };
2550
+ }
2551
+ function moduleTable(module) {
2552
+ const { names: exported, links, stars } = module.info;
2553
+ const fields = [];
2554
+ if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2555
+ if (links.length) {
2556
+ fields.push(field("links", table(links.map((link) => field(link.name, table([
2557
+ { type: "TableFieldPositional", value: string(link.module) },
2558
+ ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2559
+ ]))))));
2560
+ }
2561
+ if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2562
+ fields.push(field("load", functionExpression(functionBody([module.exportsName], module.statements, true))));
2563
+ return table(fields);
2564
+ }
2565
+ function layoutOf(mode, entry, modules, outFile) {
2566
+ const extension = outFile !== void 0 && extname(outFile) || ".luau";
2567
+ const entryStem = outFile !== void 0 ? basename(outFile, extname(outFile)) : posix.basename(entry.name);
2568
+ const others = modules.filter((m) => m !== entry);
2569
+ if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry, ...others] }];
2570
+ if (mode === "modules") {
2571
+ const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
2572
+ return [entry, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
2573
+ }
2574
+ const byName = new Map(modules.map((m) => [m.name, m]));
2575
+ const reach = (root) => {
2576
+ const seen = /* @__PURE__ */ new Set();
2577
+ const stack = [root];
2578
+ while (stack.length) {
2579
+ const name = stack.pop();
2580
+ if (seen.has(name)) continue;
2581
+ seen.add(name);
2582
+ stack.push(...byName.get(name)?.imports ?? []);
2583
+ }
2584
+ return seen;
2585
+ };
2586
+ const inEntry = reach(entry.name);
2587
+ const roots = [...new Set(modules.flatMap((m) => [...m.dynamicImports]))].filter((root) => !inEntry.has(root));
2588
+ const reached = new Map(roots.map((root) => [root, reach(root)]));
2589
+ const groups = /* @__PURE__ */ new Map();
2590
+ for (const module of others) {
2591
+ if (inEntry.has(module.name)) continue;
2592
+ const by = roots.filter((root) => reached.get(root).has(module.name));
2593
+ const key = by.join("\n");
2594
+ if (!groups.has(key)) groups.set(key, { roots: by, modules: [] });
2595
+ groups.get(key).modules.push(module);
2596
+ }
2597
+ const files = [{ path: `${entryStem}${extension}`, modules: modules.filter((m) => inEntry.has(m.name)) }];
2598
+ const taken = /* @__PURE__ */ new Set([entryStem]);
2599
+ for (const group of groups.values()) {
2600
+ const label = group.roots.length === 1 ? group.roots[0].replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "chunk" : "shared";
2601
+ let stem = `${entryStem}_${label}`;
2602
+ for (let n = 2; taken.has(stem); n++) stem = `${entryStem}_${label}_${n}`;
2603
+ taken.add(stem);
2604
+ files.push({ path: `${stem}${extension}`, modules: group.modules });
2605
+ }
2606
+ return files;
2607
+ }
2608
+ function loadPath(entryPath, file, target) {
2609
+ const path = posix.relative(posix.dirname(entryPath), file);
2610
+ if (target === "lua51") return path;
2611
+ const bare = path.slice(0, path.length - posix.extname(path).length);
2612
+ return bare.startsWith("../") ? bare : `./${bare}`;
2259
2613
  }
2260
- function runtime(G) {
2614
+ function mapRegistration(G, file, map) {
2615
+ const extension = posix.extname(file.path);
2616
+ const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
2617
+ const entry = `${G}.sources[#${G}.sources + 1]`;
2618
+ return parseLuau2([
2619
+ `${entry} = { { ${parts.map(luauString4).join(", ")} }, ${map}, ${luauString4(extension.slice(1))} }`,
2620
+ ...file.modules.map((module) => `${G}.maps[${luauString4(module.name)}] = ${G}.sources[#${G}.sources][2]`)
2621
+ ].join("\n")).body.statements;
2622
+ }
2623
+ function loader(G, target) {
2624
+ if (target === "lua51") {
2625
+ return `
2626
+ -- From beside this file, wherever that is: its own path says where.
2627
+ local folder = ""
2628
+ if debug ~= nil and debug.getinfo ~= nil then
2629
+ local at = debug.getinfo(${G}.load, "S").source
2630
+ if at:sub(1, 1) == "@" then
2631
+ folder = at:match("^@(.*[/\\\\])") or ""
2632
+ end
2633
+ end
2634
+ local chunk, problem = loadfile(folder .. file)
2635
+ if chunk == nil then
2636
+ error(problem, 0)
2637
+ end
2638
+ chunk()(${G})`;
2639
+ }
2640
+ return `
2641
+ require(file)(${G})`;
2642
+ }
2643
+ function runtime(G, options) {
2644
+ const { several } = options;
2645
+ const files = [...options.files].map(([name, file]) => `[${luauString4(name)}] = ${luauString4(file)}`).join(", ");
2261
2646
  return `
2262
2647
  local ${G}
2263
2648
  ${G} = {
@@ -2305,7 +2690,7 @@ ${G} = {
2305
2690
  return message
2306
2691
  end
2307
2692
  local lines = ${G}.lines
2308
- if lines == nil then
2693
+ if lines == nil ${several ? `and #${G}.sources == 0 ` : ""}then
2309
2694
  return message .. (trace or "")
2310
2695
  end
2311
2696
  -- The bundle's name, as a position in a message spells it: where this
@@ -2325,6 +2710,81 @@ ${G} = {
2325
2710
  chunk = info.short_src
2326
2711
  end
2327
2712
  end
2713
+ ${several ? `
2714
+ -- A position in this file is read by this file's map. One in another
2715
+ -- of the bundle's files is read by that file's: the one whose name
2716
+ -- the position ends with, by the most folders where several do. A
2717
+ -- message says nothing about which file it starts in, so nothing is
2718
+ -- guessed from it here.
2719
+ local prefix = nil
2720
+ if chunk ~= nil then
2721
+ prefix = chunk:gsub("%p", "%%%0")
2722
+ end
2723
+ local function place(line)
2724
+ local origin = lines ~= nil and lines[tonumber(line)] or nil
2725
+ if origin == nil then
2726
+ return nil
2727
+ end
2728
+ return origin[1] .. ":" .. tostring(origin[2])
2729
+ end
2730
+ local function elsewhere(at, line)
2731
+ -- Already a place in the project.
2732
+ if at:sub(-6) == ".tilua" then
2733
+ return nil
2734
+ end
2735
+ local parts = {}
2736
+ for part in at:gmatch("[^/\\\\%.]+") do
2737
+ parts[#parts + 1] = part
2738
+ end
2739
+ local map, most, tied = nil, 0, false
2740
+ for _, source in ipairs(${G}.sources) do
2741
+ local names = source[1]
2742
+ local last = #parts
2743
+ if parts[last] == source[3] then
2744
+ last = last - 1
2745
+ end
2746
+ local same = 0
2747
+ while same < #names and same < last and names[#names - same] == parts[last - same] do
2748
+ same = same + 1
2749
+ end
2750
+ if same > most then
2751
+ map, most, tied = source[2], same, false
2752
+ elseif same == most then
2753
+ tied = true
2754
+ end
2755
+ end
2756
+ -- Two files it could as well be: a wrong place is worse than none.
2757
+ local origin = map ~= nil and not tied and map[tonumber(line)] or nil
2758
+ if origin == nil then
2759
+ return nil
2760
+ end
2761
+ return origin[1] .. ":" .. tostring(origin[2])
2762
+ end
2763
+ local mapped = message
2764
+ if prefix ~= nil then
2765
+ mapped = mapped:gsub(prefix .. ":(%d+)", function(line)
2766
+ return place(line)
2767
+ end)
2768
+ end
2769
+ mapped = mapped:gsub("(%S+):(%d+)", elsewhere)
2770
+ local frames = {}
2771
+ for frame in (trace or ""):gmatch("[^\\n]+") do
2772
+ local at = nil
2773
+ if prefix ~= nil then
2774
+ local line = frame:match(prefix .. ":(%d+)")
2775
+ if line ~= nil then
2776
+ at = place(line)
2777
+ end
2778
+ end
2779
+ if at == nil then
2780
+ for source, line in frame:gmatch("(%S+):(%d+)") do
2781
+ at = elsewhere(source, line)
2782
+ if at ~= nil then
2783
+ break
2784
+ end
2785
+ end
2786
+ end
2787
+ if at ~= nil then` : `
2328
2788
  if chunk == nil then
2329
2789
  chunk = message:match("^(.-):%d+:")
2330
2790
  end
@@ -2346,8 +2806,9 @@ ${G} = {
2346
2806
  for frame in (trace or ""):gmatch("[^\\n]+") do
2347
2807
  local line = frame:match(prefix .. ":(%d+)")
2348
2808
  local at = line ~= nil and place(line) or nil
2349
- if at ~= nil then
2350
- local name = frame:match("function ([%w_.:]+)")
2809
+ if at ~= nil then`}
2810
+ -- Luau writes \`function name\`; Lua 5.1, \`function 'name'\`.
2811
+ local name = frame:match("function ([%w_.:]+)") or frame:match("function '([^']+)'")
2351
2812
  frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2352
2813
  end
2353
2814
  end
@@ -2356,13 +2817,36 @@ ${G} = {
2356
2817
  end
2357
2818
  return mapped .. "\\n" .. table.concat(frames, "\\n")
2358
2819
  end,
2359
- records = {},
2820
+ records = {},${several ? `
2821
+ -- The file each module outside this one is in, as \`load\` is handed it.
2822
+ files = { ${files} },
2823
+ loaded = {},
2824
+ -- Every file's line map: by the module, and by the file's name.
2825
+ maps = {},
2826
+ sources = {},
2827
+ -- Another of the bundle's files: a function that adds its modules here.
2828
+ load = function(file)
2829
+ if ${G}.loaded[file] then
2830
+ return
2831
+ end
2832
+ ${G}.loaded[file] = true${loader(G, options.target)}
2833
+ end,` : ""}${options.imports ? `
2834
+ -- \`import("./x")\`: the module's exports, asked for where the code runs
2835
+ -- rather than before its module does. \`require\` does the rest \u2014 and
2836
+ -- loads the file the module is in, if it is not this one.
2837
+ import = function(name)
2838
+ return ${G}.require(name)
2839
+ end,` : ""}
2360
2840
  require = function(name)
2361
2841
  local record = ${G}.records[name]
2362
2842
  if record ~= nil then
2363
2843
  return record.exports
2364
2844
  end
2365
- local module = ${G}.modules[name]
2845
+ local module = ${G}.modules[name]${several ? `
2846
+ if module == nil then
2847
+ ${G}.load(${G}.files[name])
2848
+ module = ${G}.modules[name]
2849
+ end` : ""}
2366
2850
  local initialized = {}
2367
2851
  local exports = setmetatable({}, {
2368
2852
  __index = function(_, key)
@@ -2381,7 +2865,7 @@ ${G} = {
2381
2865
  return source[link[2]]
2382
2866
  end
2383
2867
  if module.stars ~= nil and key ~= "default" then
2384
- for _, star in module.stars do
2868
+ for _, star in ipairs(module.stars) do
2385
2869
  local value = ${G}.require(star)[key]
2386
2870
  if value ~= nil then
2387
2871
  return value
@@ -2422,7 +2906,7 @@ function parseFile(file, diagnostics, directives) {
2422
2906
  }
2423
2907
  let text;
2424
2908
  try {
2425
- text = readFileSync2(file, "utf8");
2909
+ text = readFileSync3(file, "utf8");
2426
2910
  } catch {
2427
2911
  diagnostics.push({ file, message: "Cannot read file", line: 1, column: 1, category: "module" });
2428
2912
  return void 0;
@@ -2456,7 +2940,7 @@ function parsedLibrary(file) {
2456
2940
  }
2457
2941
  const cached = libraryCache.get(file);
2458
2942
  if (cached && cached.stamp === stamp) return cached.program;
2459
- const program2 = parse(readFileSync2(file, "utf8"));
2943
+ const program2 = parse(readFileSync3(file, "utf8"));
2460
2944
  libraryCache.set(file, { stamp, program: program2 });
2461
2945
  return program2;
2462
2946
  }
@@ -2467,6 +2951,8 @@ function analyzeModules(modules, config) {
2467
2951
  const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], problems: [] };
2468
2952
  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
2953
  const libs = libraries.files.map(parsedLibrary);
2954
+ const noLib = config?.noLib ?? false;
2955
+ const checked = libs.length > 0 || !noLib;
2470
2956
  const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
2471
2957
  const byFile = new Map(modules.map((m) => [m.file, m]));
2472
2958
  const exportsCache = /* @__PURE__ */ new Map();
@@ -2479,12 +2965,12 @@ function analyzeModules(modules, config) {
2479
2965
  const done = analyzed.get(file);
2480
2966
  if (done) return done;
2481
2967
  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)) {
2968
+ 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
2969
  analyzed.set(file, remembered.result);
2484
2970
  readWhile.set(file, remembered.read);
2485
2971
  return remembered.result;
2486
2972
  }
2487
- const scopes = analyzeScopes(program2, { builtinGlobals: globals, reportUndeclared: libs.length > 0 });
2973
+ const scopes = analyzeScopes(program2, { builtinGlobals: globals, reportUndeclared: checked, noLib });
2488
2974
  const said = [];
2489
2975
  for (const d of scopes.diagnostics) {
2490
2976
  if (d.kind !== "undeclared") continue;
@@ -2494,34 +2980,35 @@ function analyzeModules(modules, config) {
2494
2980
  const types = analyzeTypes(program2, scopes, {
2495
2981
  libs,
2496
2982
  resolveModule: resolverFor(file),
2497
- reportUnknownTypes: libs.length > 0
2983
+ noLib,
2984
+ reportUnknownTypes: checked
2498
2985
  });
2499
2986
  for (const d of types.diagnostics) {
2500
2987
  const at = d.node;
2501
2988
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
2502
2989
  }
2503
- const result = { types, diagnostics: said };
2990
+ const result2 = { types, diagnostics: said };
2504
2991
  if (partials === before) {
2505
- analyzed.set(file, result);
2506
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2992
+ analyzed.set(file, result2);
2993
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2507
2994
  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 });
2995
+ if (own !== void 0) read3.set(file, own);
2996
+ readWhile.set(file, read3);
2997
+ if ([...read3.values()].every((stamp) => stamp !== void 0)) {
2998
+ moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read3, result: result2 });
2512
2999
  }
2513
3000
  }
2514
- return result;
3001
+ return result2;
2515
3002
  };
2516
3003
  const resolverFor = (file) => (specifier) => {
2517
3004
  const target = resolveModulePath(file, specifier, config);
2518
3005
  if (!target) return void 0;
2519
3006
  const noteRead = () => {
2520
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2521
- readWhile.set(file, read2);
3007
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3008
+ readWhile.set(file, read3);
2522
3009
  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);
3010
+ if (stamp !== void 0) read3.set(target, stamp);
3011
+ for (const [below, belowStamp] of readWhile.get(target) ?? []) read3.set(below, belowStamp);
2525
3012
  };
2526
3013
  if (inProgress.has(target)) {
2527
3014
  partials++;
@@ -2535,15 +3022,15 @@ function analyzeModules(modules, config) {
2535
3022
  let program2 = byFile.get(target)?.program;
2536
3023
  if (!program2) {
2537
3024
  try {
2538
- program2 = parse(readFileSync2(target, "utf8"));
3025
+ program2 = parse(readFileSync3(target, "utf8"));
2539
3026
  } catch {
2540
3027
  return void 0;
2541
3028
  }
2542
3029
  }
2543
3030
  inProgress.add(target);
2544
3031
  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 });
3032
+ const scopes = analyzeScopes(program2, { builtinGlobals: globals, noLib });
3033
+ const types = byFile.has(target) ? analyzeModule(target, program2).types : analyzeTypes(program2, scopes, { libs, noLib, resolveModule: resolverFor(target), diagnostics: false });
2547
3034
  const exports = moduleExports(program2, scopes, types, resolverFor(target));
2548
3035
  exportsCache.set(target, exports);
2549
3036
  noteRead();