@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.
package/dist/cli.cjs CHANGED
@@ -6,13 +6,13 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
6
6
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
7
7
 
8
8
  // src/cli.ts
9
- var import_node_fs3 = require("fs");
9
+ var import_node_fs4 = require("fs");
10
10
  var import_node_path3 = require("path");
11
11
 
12
12
  // src/bundle.ts
13
- var import_node_fs2 = require("fs");
13
+ var import_node_fs3 = require("fs");
14
14
  var import_node_path2 = require("path");
15
- var import_parser6 = require("@tilua/parser");
15
+ var import_parser7 = require("@tilua/parser");
16
16
  var import_luau_parser2 = require("luau-parser");
17
17
 
18
18
  // src/config.ts
@@ -27,7 +27,7 @@ function resolveConfig(input, entry2, cwd = process.cwd()) {
27
27
  }
28
28
 
29
29
  // src/lower.ts
30
- var import_parser4 = require("@tilua/parser");
30
+ var import_parser5 = require("@tilua/parser");
31
31
  var import_luau_parser = require("luau-parser");
32
32
 
33
33
  // src/luau.ts
@@ -91,6 +91,7 @@ var functionBody = (params, statements, hasVarargs = false) => ({
91
91
  ...span()
92
92
  });
93
93
  var localFunction = (name, func) => ({ type: "LocalFunctionStatement", name: identifier(name), func, ...span() });
94
+ var program = (statements) => ({ type: "Program", body: block(statements), ...span() });
94
95
 
95
96
  // src/names.ts
96
97
  var Names = class _Names {
@@ -114,7 +115,7 @@ var Names = class _Names {
114
115
  if (key !== "line" && key !== "column") walk(child);
115
116
  }
116
117
  };
117
- for (const program of programs) walk(program);
118
+ for (const program2 of programs) walk(program2);
118
119
  return new _Names(taken);
119
120
  }
120
121
  /** A copy that goes on to take names independently — one per module, each
@@ -188,9 +189,80 @@ var plugin = {
188
189
  };
189
190
  var languageLowering = { plugin, from: "tilua" };
190
191
 
192
+ // src/standard.ts
193
+ var import_node_fs2 = require("fs");
194
+ var import_parser4 = require("@tilua/parser");
195
+ var read2 = (name) => (0, import_node_fs2.readFileSync)(new URL(`../runtime/${name}`, importMetaUrl), "utf8");
196
+ var console2 = read2("console.luau");
197
+ var result = read2("result.luau");
198
+ var place = (at) => (0, import_parser4.luauString)(at.file === void 0 ? `line ${at.line}` : `${at.file}:${at.line}`);
199
+ var tag = (at) => (0, import_parser4.luauString)(`[${at.file === void 0 ? `line ${at.line}` : `${at.file.replace(/\.tilua$/, "")}:${at.line}`}]:`);
200
+ function consoleMeta(args2) {
201
+ const fields = [];
202
+ args2.forEach((argument, index2) => {
203
+ if (argument.spread || argument.typeText === void 0 || !(0, import_parser4.isFunctionType)(argument.type)) return;
204
+ const written = argument.declaration ?? argument.source;
205
+ fields.push(`[${index2 + 1}] = { ${(0, import_parser4.luauString)(argument.typeText)}, ${(0, import_parser4.luauString)(written)} }`);
206
+ });
207
+ return fields.length ? `{ ${fields.join(", ")} }` : "nil";
208
+ }
209
+ var CONSOLE_METHODS = /* @__PURE__ */ new Set(["log", "warn", "error"]);
210
+ var REPLACED = /* @__PURE__ */ new Map([
211
+ ["pcall", "pcall"],
212
+ ["xpcall", "xpcall"],
213
+ ["ypcall", "pcall"],
214
+ ["coroutine.resume", "resume"],
215
+ ["string.find", "find"],
216
+ ["string.match", "match"],
217
+ ["string.gsub", "gsub"],
218
+ ["string.byte", "byte"],
219
+ ["math.modf", "modf"],
220
+ ["math.frexp", "frexp"],
221
+ ["next", "next"],
222
+ ["select", "select"],
223
+ ["unpack", "unpack"],
224
+ ["table.unpack", "unpack"],
225
+ ["loadstring", "loadstring"],
226
+ ["pairs", "pairs"],
227
+ ["ipairs", "ipairs"],
228
+ ["string.gmatch", "gmatch"]
229
+ ]);
230
+ var plugin2 = {
231
+ runtime: { console: console2, result },
232
+ methodCall({ method, receiverGlobal, at, arguments: args2, use }) {
233
+ if (receiverGlobal === "console" && CONSOLE_METHODS.has(method)) {
234
+ return {
235
+ callee: `${use("console")}.${method}`,
236
+ prepend: [tag(at), consoleMeta(args2)],
237
+ passReceiver: false
238
+ };
239
+ }
240
+ return void 0;
241
+ },
242
+ // `print(x)` and `warn(x)` are left exactly as written: the place a line
243
+ // came from is what `console:log` / `console:warn` are for. `error` has to
244
+ // put the place inside the message it raises, so it goes through the
245
+ // runtime.
246
+ globalCall({ name, at, use }) {
247
+ if (name === "error") return { callee: `${use("console")}.raise`, prepend: [place(at)] };
248
+ const replaced = REPLACED.get(name);
249
+ if (replaced) return { callee: `${use("result")}.${replaced}` };
250
+ return void 0;
251
+ },
252
+ // Read as a value there is no call site to write down; the runtime finds
253
+ // one on the stack.
254
+ globalValue({ name, use }) {
255
+ if (name === "error") return `${use("console")}.raiseValue`;
256
+ const replaced = REPLACED.get(name);
257
+ if (replaced) return `${use("result")}.${replaced}`;
258
+ return void 0;
259
+ }
260
+ };
261
+ var standardLowering = { plugin: plugin2, from: "tilua (standard library)" };
262
+
191
263
  // src/lower.ts
192
- function lower(program, scopes, options = {}) {
193
- return new Lowerer(program, scopes, options).run();
264
+ function lower(program2, scopes, options = {}) {
265
+ return new Lowerer(program2, scopes, options).run();
194
266
  }
195
267
  var Lowerer = class {
196
268
  constructor(source, scopes, options) {
@@ -277,6 +349,10 @@ var Lowerer = class {
277
349
  }
278
350
  return identifier(local2);
279
351
  }
352
+ /** `table.move`. Lua 5.1 has none, and gets a function that does the same. */
353
+ move() {
354
+ return this.lua51 ? this.helper("move") : member(this.builtin("table"), "move");
355
+ }
280
356
  helper(kind) {
281
357
  let name = this.helpers.get(kind);
282
358
  if (!name) {
@@ -377,7 +453,7 @@ var Lowerer = class {
377
453
  const type = this.options.types?.typeOf.get(argument);
378
454
  return {
379
455
  type,
380
- typeText: type ? (0, import_parser4.formatType)(type) : void 0,
456
+ typeText: type ? (0, import_parser5.formatType)(type) : void 0,
381
457
  source: this.writtenSource(argument),
382
458
  declaration: declaration ? this.writtenSource(declaration) : void 0,
383
459
  spread: argument.type === "SpreadElement"
@@ -399,7 +475,11 @@ var Lowerer = class {
399
475
  /** The language's own lowering, then each library's, in load order. A
400
476
  * runtime is named by its lowering's place here (`loweringRuntime`). */
401
477
  get lowerings() {
402
- return this.allLowerings ??= [languageLowering, ...this.options.lowerings ?? []];
478
+ return this.allLowerings ??= [
479
+ languageLowering,
480
+ ...this.options.noLib ? [] : [standardLowering],
481
+ ...this.options.lowerings ?? []
482
+ ];
403
483
  }
404
484
  allLowerings;
405
485
  /** What `receiver:method(...)` becomes, when Luau alone would not run it.
@@ -413,22 +493,28 @@ var Lowerer = class {
413
493
  * (`console:log`), the last library loaded first. No answer leaves an
414
494
  * ordinary Luau method call: `text:upper()` reaches Lua's own. */
415
495
  loweredMethodCall(node) {
496
+ return this.loweredMethod(node.method.name, this.options.types?.methodSources.get(node), node.object, node.arguments, node.method, node);
497
+ }
498
+ /** `loweredMethodCall`, for a method named `method` of what `object`
499
+ * holds — called with `:` on it, or read off it with `.` and handed its
500
+ * `self` like any other argument. `args` are the arguments after the
501
+ * receiver; a read that is not called has none. */
502
+ loweredMethod(method, source, object2, args2, name, node) {
416
503
  const lowerings = this.lowerings;
417
- const source = this.options.types?.methodSources.get(node);
418
504
  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();
419
505
  if (!asked.some((i) => lowerings[i].plugin.methodCall)) return void 0;
420
- const receiver = this.options.types?.typeOf.get(node.object);
421
- const receiverGlobal = this.globalName(node.object);
422
- const info = this.lazyArguments(node.arguments);
506
+ const receiver = this.options.types?.typeOf.get(object2);
507
+ const receiverGlobal = this.globalName(object2);
508
+ const info = this.lazyArguments(args2);
423
509
  for (const i of asked) {
424
- const plugin2 = lowerings[i].plugin;
425
- if (!plugin2.methodCall) continue;
426
- const answer = plugin2.methodCall({
427
- method: node.method.name,
510
+ const plugin3 = lowerings[i].plugin;
511
+ if (!plugin3.methodCall) continue;
512
+ const answer = plugin3.methodCall({
513
+ method,
428
514
  receiver,
429
515
  receiverGlobal,
430
- argumentCount: node.arguments.length,
431
- at: this.callSite(node.method),
516
+ argumentCount: args2.length,
517
+ at: this.callSite(name),
432
518
  get arguments() {
433
519
  return info();
434
520
  },
@@ -452,9 +538,9 @@ var Lowerer = class {
452
538
  if (name === void 0) return void 0;
453
539
  const info = this.lazyArguments(node.arguments);
454
540
  for (let i = lowerings.length - 1; i >= 0; i--) {
455
- const plugin2 = lowerings[i].plugin;
456
- if (!plugin2.globalCall) continue;
457
- const answer = plugin2.globalCall({
541
+ const plugin3 = lowerings[i].plugin;
542
+ if (!plugin3.globalCall) continue;
543
+ const answer = plugin3.globalCall({
458
544
  name,
459
545
  at: this.callSite(node.callee),
460
546
  get arguments() {
@@ -506,6 +592,31 @@ var Lowerer = class {
506
592
  const out2 = [];
507
593
  const runtimes = this.runtimeDefinitions();
508
594
  if (this.helpers.has("concat")) this.helper("array");
595
+ if (this.helpers.has("concat")) this.move();
596
+ const move = this.helpers.get("move");
597
+ if (move) {
598
+ out2.push(...(0, import_luau_parser.parse)(`
599
+ local function ${move}(from, first, last, at, to)
600
+ for i = first, last do
601
+ to[at + i - first] = from[i]
602
+ end
603
+ return to
604
+ end`).body.statements);
605
+ }
606
+ const each = this.helpers.get("each");
607
+ if (each) {
608
+ out2.push(...(0, import_luau_parser.parse)(`
609
+ local function ${each}(source, ...)
610
+ if ${this.builtin("type").name}(source) == "function" then
611
+ return source, ...
612
+ end
613
+ local meta = ${this.builtin("getmetatable").name}(source)
614
+ if meta ~= nil and meta.__iter ~= nil then
615
+ return meta.__iter(source)
616
+ end
617
+ return ${this.builtin("next").name}, source
618
+ end`).body.statements);
619
+ }
509
620
  const marker = this.helpers.get("array");
510
621
  const typeOf = this.helpers.get("typeof");
511
622
  if (marker || typeOf) {
@@ -544,6 +655,49 @@ local function ${typeOf}(value, known)
544
655
  end`).body.statements);
545
656
  }
546
657
  }
658
+ const instanceOf = this.helpers.get("instanceof");
659
+ if (instanceOf) {
660
+ out2.push(...(0, import_luau_parser.parse)(`
661
+ local function ${instanceOf}(value, class)
662
+ if ${this.builtin("type").name}(value) ~= "table" then
663
+ return false
664
+ end
665
+ local current = value.ClassObject
666
+ while current ~= nil do
667
+ if current == class then
668
+ return true
669
+ end
670
+ current = current.ParentClass
671
+ end
672
+ return false
673
+ end`).body.statements);
674
+ }
675
+ const accessors = this.helpers.get("accessors");
676
+ if (accessors) {
677
+ out2.push(...(0, import_luau_parser.parse)(`
678
+ local function ${accessors}(target, getters, setters)
679
+ return ${this.builtin("setmetatable").name}(target, {
680
+ __index = function(self, key)
681
+ local get = getters[key]
682
+ if get ~= nil then
683
+ return get(self)
684
+ end
685
+ return nil
686
+ end,
687
+ __newindex = function(self, key, value)
688
+ local set = setters[key]
689
+ if set ~= nil then
690
+ set(self, value)
691
+ return
692
+ end
693
+ if getters[key] ~= nil then
694
+ ${this.builtin("error").name}("Cannot assign to '" .. ${this.builtin("tostring").name}(key) .. "': it has a getter and no setter", 2)
695
+ end
696
+ ${this.builtin("rawset").name}(self, key, value)
697
+ end,
698
+ })
699
+ end`).body.statements);
700
+ }
547
701
  const assign2 = this.helpers.get("assign");
548
702
  if (assign2) {
549
703
  out2.push(localFunction(assign2, functionBody(["target"], [
@@ -565,7 +719,7 @@ end`).body.statements);
565
719
  local(["result"], [this.array(table([]))]),
566
720
  numericFor("i", number(1), selectCount(this.builtin("select")), [
567
721
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
568
- callStatement(call(member(this.builtin("table"), "move"), [
722
+ callStatement(call(this.move(), [
569
723
  identifier("part"),
570
724
  number(1),
571
725
  unary("#", identifier("part")),
@@ -582,8 +736,8 @@ end`).body.statements);
582
736
  runtimeDefinitions() {
583
737
  const out2 = [];
584
738
  for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
585
- const { plugin: plugin2, from } = this.lowerings[index2];
586
- const source = plugin2.runtime?.[runtime2];
739
+ const { plugin: plugin3, from } = this.lowerings[index2];
740
+ const source = plugin3.runtime?.[runtime2];
587
741
  if (source === void 0) {
588
742
  this.diagnostics.push({
589
743
  message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
@@ -616,7 +770,7 @@ end`).body.statements);
616
770
  const requires = [];
617
771
  const moduleLocals = /* @__PURE__ */ new Map();
618
772
  const importedFrom = /* @__PURE__ */ new Map();
619
- const resolve3 = (source) => {
773
+ const resolve4 = (source) => {
620
774
  const key = context.resolve(source.value);
621
775
  if (key === void 0) this.report(source, `Cannot find module '${source.value}'`);
622
776
  return key;
@@ -655,7 +809,7 @@ end`).body.statements);
655
809
  ];
656
810
  const used = specifiers.filter((s) => (this.bindingByDeclaration.get(s.local)?.references.length ?? 0) > 0);
657
811
  if (!used.length) continue;
658
- const key = resolve3(statement.source);
812
+ const key = resolve4(statement.source);
659
813
  if (key === void 0) continue;
660
814
  const local2 = moduleLocal(key);
661
815
  for (const s of used) {
@@ -678,8 +832,9 @@ end`).body.statements);
678
832
  names.push("default");
679
833
  break;
680
834
  case "ExportNamedStatement": {
835
+ if (statement.typeOnly) break;
681
836
  if (statement.source) {
682
- const key = resolve3(statement.source);
837
+ const key = resolve4(statement.source);
683
838
  if (key === void 0) break;
684
839
  load(key);
685
840
  for (const s of statement.specifiers) links.push({ name: s.exported.name, module: key, imported: s.local.name });
@@ -695,7 +850,7 @@ end`).body.statements);
695
850
  break;
696
851
  }
697
852
  case "ExportAllStatement": {
698
- const key = resolve3(statement.source);
853
+ const key = resolve4(statement.source);
699
854
  if (key === void 0) break;
700
855
  load(key);
701
856
  stars.push(key);
@@ -761,6 +916,26 @@ end`).body.statements);
761
916
  for (let i = 0; i < locals.length; i += 100) declarations.push(local(locals.slice(i, i + 100), []));
762
917
  return [...declarations, ...hoisted, ...requires, ...body];
763
918
  }
919
+ /** `import("./x")`: the bundle's `import`, called with the module's name.
920
+ * It answers the module's exports table — what `import * as` names. */
921
+ importExpression(node) {
922
+ const context = this.options.module;
923
+ if (!context) {
924
+ this.report(node, "import() needs a bundle: build the project with @tilua/compiler");
925
+ return nil();
926
+ }
927
+ const source = node.source;
928
+ if (source.type !== "StringLiteral") {
929
+ this.report(source, "import() takes a string literal: which module it loads is decided when the project is built");
930
+ return nil();
931
+ }
932
+ const key = context.resolve(source.value, true);
933
+ if (key === void 0) {
934
+ this.report(source, `Cannot find module '${source.value}'`);
935
+ return nil();
936
+ }
937
+ return call(context.import ?? context.require, [string(key)]);
938
+ }
764
939
  isRewritten(node) {
765
940
  const binding = this.bindingByDeclaration.get(node);
766
941
  return binding !== void 0 && this.rewrites.has(binding.id);
@@ -907,7 +1082,7 @@ end`).body.statements);
907
1082
  case "VariableDeclaration":
908
1083
  return this.variableDeclaration(node, "declare");
909
1084
  case "FunctionDeclaration":
910
- return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: node.attributes }];
1085
+ return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: this.lua51 ? void 0 : node.attributes }];
911
1086
  case "FunctionDeclarationStatement":
912
1087
  return [this.functionDeclarationStatement(node)];
913
1088
  case "ClassDeclaration":
@@ -938,8 +1113,13 @@ end`).body.statements);
938
1113
  // kept — Lua has no expression statement — and it cannot run
939
1114
  // anything, since a call is a `CallStatement` instead, so dropping
940
1115
  // it changes nothing but the noise.
941
- case "ExpressionStatement":
942
- return [];
1116
+ case "ExpressionStatement": {
1117
+ let expression = node.expression;
1118
+ while (expression.type === "ParenthesizedExpression") expression = expression.expression;
1119
+ if (expression.type !== "ImportExpression") return [];
1120
+ const call2 = this.importExpression(expression);
1121
+ return call2.type === "CallExpression" ? [this.from(callStatement(call2), node)] : [];
1122
+ }
943
1123
  case "DoStatement":
944
1124
  return [{ type: "DoStatement", body: this.block(node.body), ...spanOf(node) }];
945
1125
  case "WhileStatement":
@@ -991,7 +1171,11 @@ end`).body.statements);
991
1171
  let source = this.expression(node.iterator);
992
1172
  if (form.viaIter) source = methodCall(source, "__iter", []);
993
1173
  const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
994
- const iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1174
+ let iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
1175
+ if (this.lua51 && form.walks === "values") {
1176
+ const kind = form.viaIter ? void 0 : this.options.types?.typeOf.get(node.iterator)?.kind;
1177
+ iterators = [call(kind === "array" || kind === "tuple" ? this.builtin("ipairs") : this.helper("each"), [source])];
1178
+ }
995
1179
  return [genericFor(
996
1180
  variables,
997
1181
  iterators,
@@ -1244,7 +1428,8 @@ end
1244
1428
  },
1245
1429
  isMethod,
1246
1430
  func,
1247
- attributes: node.attributes,
1431
+ // `@native` and the like are Luau's.
1432
+ attributes: this.lua51 ? void 0 : node.attributes,
1248
1433
  ...spanOf(node)
1249
1434
  };
1250
1435
  }
@@ -1345,7 +1530,7 @@ end
1345
1530
  if (element) bind(element.value, index(source, number(i + 1)), element.default);
1346
1531
  });
1347
1532
  if (pattern.rest) {
1348
- const elements = call(member(this.builtin("table"), "move"), [
1533
+ const elements = call(this.move(), [
1349
1534
  source,
1350
1535
  number(pattern.elements.length + 1),
1351
1536
  unary("#", source),
@@ -1456,8 +1641,15 @@ end
1456
1641
  case "ArrayExpression":
1457
1642
  return this.arrayExpression(node);
1458
1643
  case "BinaryExpression": {
1644
+ if (node.operator === "??") return this.nullish(node);
1459
1645
  const left = this.expression(node.left);
1460
1646
  const right = this.expression(node.right);
1647
+ if (node.operator === "in") {
1648
+ return parenthesized(binary("~=", index(this.keptToOne(right), this.keptToOne(left)), nil()));
1649
+ }
1650
+ if (node.operator === "instanceof") {
1651
+ return call(this.helper("instanceof"), [this.keptToOne(left), this.keptToOne(right)]);
1652
+ }
1461
1653
  if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
1462
1654
  return binary(node.operator, left, right);
1463
1655
  }
@@ -1484,6 +1676,8 @@ end
1484
1676
  }
1485
1677
  case "ParenthesizedExpression":
1486
1678
  return parenthesized(this.expression(node.expression));
1679
+ case "ImportExpression":
1680
+ return this.importExpression(node);
1487
1681
  // Types only: the value is the expression itself.
1488
1682
  case "TypeAssertionExpression":
1489
1683
  case "SatisfiesExpression":
@@ -1537,7 +1731,7 @@ end
1537
1731
  const last2 = list.length - 1;
1538
1732
  return lowered.length && !many.has(list[last2]) ? [...lowered.slice(0, -1), this.keptToOne(lowered[last2])] : lowered;
1539
1733
  }
1540
- const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
1734
+ const unpack = (value) => call(this.lua51 ? this.builtin("unpack") : member(this.builtin("table"), "unpack"), [value]);
1541
1735
  const last = list[list.length - 1];
1542
1736
  if (spreads.length === 1 && last.type === "SpreadElement") {
1543
1737
  return [...list.slice(0, -1).map((a) => this.expression(a)), unpack(this.expression(last.argument))];
@@ -1551,11 +1745,21 @@ end
1551
1745
  /** One link of an access chain, read from `object`. */
1552
1746
  link(node, object2) {
1553
1747
  switch (node.type) {
1554
- case "MemberExpression":
1748
+ case "MemberExpression": {
1749
+ if (this.options.types?.lengths.has(node)) return unary("#", object2);
1750
+ const source = this.options.types?.methodReads.get(node);
1751
+ const lowered = source && this.loweredMethod(node.property.name, source, node.object, [], node.property, node);
1752
+ if (lowered) {
1753
+ const forwarded = call(calleePath(lowered.callee), [...lowered.prepend, vararg()]);
1754
+ return functionExpression(functionBody(lowered.passReceiver ? [] : ["_"], [returns([forwarded])], true));
1755
+ }
1555
1756
  return member(object2, node.property.name);
1757
+ }
1556
1758
  case "IndexExpression":
1557
1759
  return index(object2, this.expression(node.index));
1558
1760
  case "CallExpression": {
1761
+ const method = this.methodCalled(node);
1762
+ if (method) return method;
1559
1763
  const lowered = this.loweredGlobalCall(node);
1560
1764
  const args2 = this.arguments(node.arguments);
1561
1765
  if (!lowered) return call(object2, args2);
@@ -1578,6 +1782,24 @@ end
1578
1782
  }
1579
1783
  }
1580
1784
  }
1785
+ /** `names.filter(names, f)`: a method of the value's metatable, read with
1786
+ * `.` and handed its `self` as the first argument. Written as a call of
1787
+ * the function the method really is — `array.filter(names, f)` — where
1788
+ * whoever declared the metatable says so; `undefined` leaves an ordinary
1789
+ * call, which is right for a string's Lua methods (`text.upper(text)`
1790
+ * reaches Lua's own `string.upper`). */
1791
+ methodCalled(node) {
1792
+ const callee = node.callee;
1793
+ if (callee.type !== "MemberExpression") return void 0;
1794
+ const source = this.options.types?.methodReads.get(callee);
1795
+ if (!source) return void 0;
1796
+ const spreads = node.arguments.map((a, i) => a.type === "SpreadElement" ? i : -1).filter((i) => i >= 0);
1797
+ if (!node.arguments.length || spreads.includes(0) || spreads.some((i) => i !== node.arguments.length - 1)) return void 0;
1798
+ const lowered = this.loweredMethod(callee.property.name, source, callee.object, node.arguments.slice(1), callee.property, node);
1799
+ if (!lowered) return void 0;
1800
+ const [self, ...rest] = this.arguments(node.arguments);
1801
+ return call(calleePath(lowered.callee), [...lowered.prepend, ...lowered.passReceiver ? [self] : [], ...rest]);
1802
+ }
1581
1803
  // --------------------------------------------------------
1582
1804
  // Optional chains
1583
1805
  // --------------------------------------------------------
@@ -1623,7 +1845,7 @@ end
1623
1845
  const segments = this.segments(chain);
1624
1846
  const base = this.expression(chain.base);
1625
1847
  const last = chain.links[chain.links.length - 1];
1626
- if (base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1848
+ if (!this.lua51 && base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
1627
1849
  return {
1628
1850
  type: "IfElseExpression",
1629
1851
  clauses: [{ condition: binary("==", base, nil()), body: nil() }],
@@ -1644,6 +1866,32 @@ end
1644
1866
  const func = functionExpression(functionBody([], statements, passesVarargs));
1645
1867
  return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1646
1868
  }
1869
+ /** `a ?? b`: `a`, or `b` where `a` is nil — read only then. A `false`
1870
+ * stays, which is what sets it apart from `a or b`. A name is tested
1871
+ * in place; anything else is read once, into a local. */
1872
+ nullish(node) {
1873
+ const left = this.keptToOne(this.expression(node.left));
1874
+ const right = this.keptToOne(this.expression(node.right));
1875
+ if (!this.lua51 && left.type === "Identifier") {
1876
+ return {
1877
+ type: "IfElseExpression",
1878
+ clauses: [{ condition: binary("~=", left, nil()), body: left }],
1879
+ alternate: right,
1880
+ line: left.line,
1881
+ column: left.column
1882
+ };
1883
+ }
1884
+ const ref = this.names.fresh("value");
1885
+ const at = identifier(ref);
1886
+ const statements = [
1887
+ local([ref], [left]),
1888
+ ifThen(binary("~=", at, nil()), [returns([at])]),
1889
+ returns([right])
1890
+ ];
1891
+ const passesVarargs = statements.some(usesVararg);
1892
+ const func = functionExpression(functionBody([], statements, passesVarargs));
1893
+ return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1894
+ }
1647
1895
  /** `` `${a} any` `` -> `("%s any"):format(tostring(a))` */
1648
1896
  interpolatedString(node) {
1649
1897
  let format = "";
@@ -1725,12 +1973,12 @@ end
1725
1973
  }));
1726
1974
  const alternate = this.expression(node.alternate);
1727
1975
  if (clauses.every((c) => alwaysTruthy(c.body))) {
1728
- let result = alternate;
1976
+ let result2 = alternate;
1729
1977
  for (let i = clauses.length - 1; i >= 0; i--) {
1730
1978
  const { condition, body } = clauses[i];
1731
- result = binary("or", binary("and", condition, body), result);
1979
+ result2 = binary("or", binary("and", condition, body), result2);
1732
1980
  }
1733
- return parenthesized(result);
1981
+ return parenthesized(result2);
1734
1982
  }
1735
1983
  const statement = {
1736
1984
  type: "IfStatement",
@@ -1787,11 +2035,20 @@ end
1787
2035
  current = [];
1788
2036
  parts.push(this.expression(f.argument));
1789
2037
  break;
2038
+ case "TableFieldAccessor":
2039
+ break;
1790
2040
  }
1791
2041
  }
1792
- if (parts.length === 0) return table(current);
1793
- if (current.length) parts.push(table(current));
1794
- return call(this.helper("assign"), [table([]), ...parts]);
2042
+ let made;
2043
+ if (parts.length === 0) made = table(current);
2044
+ else {
2045
+ if (current.length) parts.push(table(current));
2046
+ made = call(this.helper("assign"), [table([]), ...parts]);
2047
+ }
2048
+ const accessors = node.fields.filter((f) => f.type === "TableFieldAccessor");
2049
+ if (!accessors.length) return made;
2050
+ 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)))));
2051
+ return call(this.helper("accessors"), [made, table2("get"), table2("set")]);
1795
2052
  }
1796
2053
  /** `[1, ...xs, 2]`. Without a spread it is a Luau sequence; with one, the
1797
2054
  * runs of plain elements and the spread arrays are joined by `concat`. */
@@ -2048,7 +2305,7 @@ var CLASS_METAMETHODS = [
2048
2305
  ];
2049
2306
 
2050
2307
  // src/linemap.ts
2051
- var import_parser5 = require("@tilua/parser");
2308
+ var import_parser6 = require("@tilua/parser");
2052
2309
  function statementsOf(node) {
2053
2310
  const out2 = [];
2054
2311
  const visit = (value) => {
@@ -2081,7 +2338,7 @@ function buildLineMap(emitted, printed, origins) {
2081
2338
  }
2082
2339
  function lineMapSource(map) {
2083
2340
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
2084
- const quoted = (s) => (0, import_parser5.luauString)(s.replace(/\\/g, "/"));
2341
+ const quoted = (s) => (0, import_parser6.luauString)(s.replace(/\\/g, "/"));
2085
2342
  return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
2086
2343
  }
2087
2344
 
@@ -2093,15 +2350,15 @@ async function loadLowerings(modules) {
2093
2350
  for (const module2 of modules) {
2094
2351
  try {
2095
2352
  const loaded = await import((0, import_node_url.pathToFileURL)(module2.file).href);
2096
- const plugin2 = loaded.default;
2097
- if (!plugin2 || typeof plugin2 !== "object") {
2353
+ const plugin3 = loaded.default;
2354
+ if (!plugin3 || typeof plugin3 !== "object") {
2098
2355
  problems.push({
2099
2356
  file: module2.file,
2100
2357
  message: `'${module2.from}' lowering module has no default export`
2101
2358
  });
2102
2359
  continue;
2103
2360
  }
2104
- lowerings.push({ plugin: plugin2, from: module2.from, library: module2.library });
2361
+ lowerings.push({ plugin: plugin3, from: module2.from, library: module2.library });
2105
2362
  } catch (error) {
2106
2363
  problems.push({
2107
2364
  file: module2.file,
@@ -2128,16 +2385,17 @@ async function bundle(options) {
2128
2385
  const file = queue.shift();
2129
2386
  if (sources.has(file)) continue;
2130
2387
  const parsed = parseFile(file, diagnostics, directives);
2131
- if (!parsed) return { modules: [], diagnostics };
2132
- const { program: program2, text } = parsed;
2133
- const scopes = (0, import_parser6.analyzeScopes)(program2);
2388
+ if (!parsed) return { files: [], modules: [], diagnostics };
2389
+ const { program: program3, text } = parsed;
2390
+ const scopes = (0, import_parser7.analyzeScopes)(program3, { noLib: config2?.noLib });
2134
2391
  for (const d of scopes.diagnostics) {
2135
2392
  diagnostics.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "scope" });
2136
2393
  }
2137
- sources.set(file, { file, name: nameOf(file), text, program: program2, scopes });
2138
- for (const specifier of importedSpecifiers(program2)) {
2139
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config2);
2140
- if (target && !target.endsWith(".d.tilua")) queue.push(target);
2394
+ sources.set(file, { file, name: nameOf(file), text, program: program3, scopes });
2395
+ const dynamic = (0, import_parser7.importExpressions)(program3).flatMap((node) => node.source.type === "StringLiteral" ? [node.source.value] : []);
2396
+ for (const specifier of [...importedSpecifiers(program3), ...dynamic]) {
2397
+ const target2 = (0, import_parser7.resolveModulePath)(file, specifier, config2);
2398
+ if (target2 && !target2.endsWith(".d.tilua")) queue.push(target2);
2141
2399
  }
2142
2400
  }
2143
2401
  const analysis = analyzeModules([...sources.values()], config2);
@@ -2149,6 +2407,9 @@ async function bundle(options) {
2149
2407
  const names = Names.from(...[...sources.values()].map((s) => s.program));
2150
2408
  const G = names.fresh("G");
2151
2409
  const requireExpression = member(identifier(G), "require");
2410
+ const importExpression = member(identifier(G), "import");
2411
+ const mode = options.output ?? config2?.output ?? "single";
2412
+ const target = options.target ?? config2?.target ?? "luau";
2152
2413
  const modules = /* @__PURE__ */ new Map();
2153
2414
  const origins = /* @__PURE__ */ new WeakMap();
2154
2415
  const pending = [entry2];
@@ -2157,104 +2418,228 @@ async function bundle(options) {
2157
2418
  if (modules.has(file)) continue;
2158
2419
  const source = sources.get(file);
2159
2420
  const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
2160
- const lowered = lower(source.program, source.scopes, {
2421
+ const imports = /* @__PURE__ */ new Set();
2422
+ const dynamicImports = /* @__PURE__ */ new Set();
2423
+ const lowered2 = lower(source.program, source.scopes, {
2161
2424
  names,
2162
2425
  types: analysis.types.get(file),
2163
2426
  lowerings,
2164
- target: options.target ?? config2?.target,
2427
+ target,
2428
+ noLib: config2?.noLib,
2165
2429
  source: source.text,
2166
2430
  file: shown,
2167
2431
  module: {
2168
2432
  name: source.name,
2169
2433
  require: requireExpression,
2170
- lines: `${G}.lines`,
2434
+ import: importExpression,
2435
+ // One file has one line map. Several have one each, and a
2436
+ // module reads the one of the file it is in.
2437
+ lines: mode === "single" ? `${G}.lines` : `${G}.maps[${(0, import_parser7.luauString)(source.name)}]`,
2171
2438
  fail: `${G}.fail`,
2172
2439
  arrays: `${G}.arrays`,
2173
- resolve: (specifier) => {
2174
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config2);
2175
- if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
2176
- pending.push(target);
2177
- return sources.get(target).name;
2440
+ resolve: (specifier, dynamic) => {
2441
+ const found = (0, import_parser7.resolveModulePath)(file, specifier, config2);
2442
+ if (!found || found.endsWith(".d.tilua") || !sources.has(found)) return void 0;
2443
+ pending.push(found);
2444
+ const name = sources.get(found).name;
2445
+ (dynamic ? dynamicImports : imports).add(name);
2446
+ return name;
2178
2447
  }
2179
2448
  }
2180
2449
  });
2181
- for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2182
- for (const statement of allStatements(lowered.statements)) {
2183
- const line = lowered.origins.get(statement);
2450
+ for (const d of lowered2.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2451
+ for (const statement of allStatements(lowered2.statements)) {
2452
+ const line = lowered2.origins.get(statement);
2184
2453
  if (line !== void 0) origins.set(statement, { file: shown, line });
2185
2454
  }
2186
2455
  modules.set(file, {
2187
2456
  name: source.name,
2188
- statements: lowered.statements,
2189
- exportsName: lowered.exportsName,
2190
- info: lowered.module,
2191
- usesScriptArgs: lowered.usesScriptArgs
2457
+ statements: lowered2.statements,
2458
+ exportsName: lowered2.exportsName,
2459
+ info: lowered2.module,
2460
+ usesScriptArgs: lowered2.usesScriptArgs,
2461
+ imports,
2462
+ dynamicImports
2192
2463
  });
2193
2464
  }
2194
2465
  for (const [file, fileDirectives] of directives) {
2195
2466
  const semantic = diagnostics.filter((d) => d.file === file && (d.category === "scope" || d.category === "type"));
2196
- const { kept, unusedExpectErrors } = (0, import_parser6.applyDirectives)(fileDirectives, semantic, (d) => d.line);
2467
+ const { kept, unusedExpectErrors } = (0, import_parser7.applyDirectives)(fileDirectives, semantic, (d) => d.line);
2197
2468
  const suppressed = new Set(semantic.filter((d) => !kept.includes(d)));
2198
2469
  const remaining = diagnostics.filter((d) => !suppressed.has(d));
2199
2470
  diagnostics.length = 0;
2200
2471
  diagnostics.push(...remaining);
2201
2472
  if (options.typeCheck === false) continue;
2202
2473
  for (const d of unusedExpectErrors) {
2203
- diagnostics.push({ file, message: import_parser6.UNUSED_EXPECT_ERROR, line: d.line, column: d.column, category: "type" });
2474
+ diagnostics.push({ file, message: import_parser7.UNUSED_EXPECT_ERROR, line: d.line, column: d.column, category: "type" });
2204
2475
  }
2205
2476
  }
2206
2477
  const moduleProblems = new Set(diagnostics.filter((d) => d.category === "module").map((d) => `${d.file}:${d.line}:${d.column}:${d.message}`));
2207
2478
  const reported = diagnostics.filter((d) => d.category !== "type" || !moduleProblems.has(`${d.file}:${d.line}:${d.column}:${d.message}`));
2208
2479
  diagnostics.length = 0;
2209
2480
  diagnostics.push(...reported);
2210
- const moduleNames = [...modules.values()].map((m) => m.name);
2481
+ const lowered = [...modules.values()];
2482
+ const moduleNames = lowered.map((m) => m.name);
2211
2483
  if (diagnostics.some((d) => d.category === "syntax" || d.category === "scope" || d.category === "module")) {
2212
- return { modules: moduleNames, diagnostics };
2213
- }
2214
- const program = (0, import_luau_parser2.parse)(runtime(G));
2215
- const assignment = program.body.statements[1];
2216
- if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2217
- program.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2218
- program.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
2484
+ return { files: [], modules: moduleNames, diagnostics };
2485
+ }
2486
+ const several = mode !== "single";
2487
+ const layout = layoutOf(mode, modules.get(entry2), lowered, options.outFile);
2488
+ const usesScriptArgs = lowered.some((m) => m.usesScriptArgs);
2489
+ const elsewhere = /* @__PURE__ */ new Map();
2490
+ for (const file of layout.slice(1)) {
2491
+ for (const module2 of file.modules) elsewhere.set(module2.name, loadPath(layout[0].path, file.path, target));
2492
+ }
2493
+ const program2 = (0, import_luau_parser2.parse)(runtime(G, {
2494
+ several,
2495
+ target,
2496
+ files: elsewhere,
2497
+ imports: lowered.some((m) => m.dynamicImports.size > 0)
2498
+ }));
2499
+ const assignment = program2.body.statements[1];
2500
+ if (usesScriptArgs) {
2501
+ program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2502
+ program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
2503
+ if (several) program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.scriptArgs = scriptArgs`).body.statements);
2219
2504
  }
2220
2505
  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;
2221
2506
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
2222
- for (const module2 of modules.values()) {
2223
- const { names: exported, links, stars } = module2.info;
2224
- const fields = [];
2225
- if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2226
- if (links.length) {
2227
- fields.push(field("links", table(links.map((link) => field(link.name, table([
2228
- { type: "TableFieldPositional", value: string(link.module) },
2229
- ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2230
- ]))))));
2231
- }
2232
- if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2233
- fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
2234
- modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: table(fields) });
2507
+ for (const module2 of layout[0].modules) {
2508
+ modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: moduleTable(module2) });
2235
2509
  }
2236
2510
  const ok = names.fresh("ok");
2237
- const result = names.fresh("result");
2511
+ const result2 = names.fresh("result");
2238
2512
  const entryStatements = (0, import_luau_parser2.parse)(`
2239
- local ${ok}, ${result} = xpcall(function()
2240
- return ${G}.require(${(0, import_parser6.luauString)(sources.get(entry2).name)})
2513
+ local ${ok}, ${result2} = xpcall(function()
2514
+ return ${G}.require(${(0, import_parser7.luauString)(sources.get(entry2).name)})
2241
2515
  end, ${G}.fail)
2242
2516
  if not ${ok} then
2243
- error(${result}, 0)
2517
+ error(${result2}, 0)
2244
2518
  end
2245
- ${modules.get(entry2).info.exports ? `return ${result}` : ""}
2519
+ ${modules.get(entry2).info.exports ? `return ${result2}` : ""}
2246
2520
  `).body.statements;
2247
- program.body.statements.push(...entryStatements);
2248
- const map = buildLineMap(program, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program)), origins);
2249
- if (map.size) {
2250
- program.body.statements.splice(program.body.statements.length - entryStatements.length, 0, assign(
2521
+ program2.body.statements.push(...entryStatements);
2522
+ const map = buildLineMap(program2, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program2)), origins);
2523
+ if (map.size || several) {
2524
+ const mapStatements = [assign(
2251
2525
  [member(identifier(G), "lines")],
2252
2526
  [(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map))]
2253
- ));
2527
+ )];
2528
+ if (several) mapStatements.push(...mapRegistration(G, layout[0], `${G}.lines`));
2529
+ program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, ...mapStatements);
2254
2530
  }
2255
- return { code: (0, import_luau_parser2.print)(program) + "\n", modules: moduleNames, diagnostics };
2531
+ const files = [{ path: layout[0].path, code: (0, import_luau_parser2.print)(program2) + "\n", modules: layout[0].modules.map((m) => m.name) }];
2532
+ for (const file of layout.slice(1)) {
2533
+ const body = [];
2534
+ if (file.modules.some((m) => m.usesScriptArgs)) body.push(...(0, import_luau_parser2.parse)(`local scriptArgs = ${G}.scriptArgs`).body.statements);
2535
+ for (const module2 of file.modules) {
2536
+ body.push(assign(
2537
+ [index(member(identifier(G), "modules"), string(module2.name))],
2538
+ [moduleTable(module2)]
2539
+ ));
2540
+ }
2541
+ const chunk = program([returns([functionExpression(functionBody([G], body))])]);
2542
+ const lines = buildLineMap(chunk, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(chunk)), origins);
2543
+ body.push(...mapRegistration(G, file, lineMapSource(lines)));
2544
+ files.push({ path: file.path, code: (0, import_luau_parser2.print)(chunk) + "\n", modules: file.modules.map((m) => m.name) });
2545
+ }
2546
+ return { code: files[0].code, files, modules: moduleNames, diagnostics };
2547
+ }
2548
+ function moduleTable(module2) {
2549
+ const { names: exported, links, stars } = module2.info;
2550
+ const fields = [];
2551
+ if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2552
+ if (links.length) {
2553
+ fields.push(field("links", table(links.map((link) => field(link.name, table([
2554
+ { type: "TableFieldPositional", value: string(link.module) },
2555
+ ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2556
+ ]))))));
2557
+ }
2558
+ if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2559
+ fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
2560
+ return table(fields);
2561
+ }
2562
+ function layoutOf(mode, entry2, modules, outFile) {
2563
+ const extension = outFile !== void 0 && (0, import_node_path2.extname)(outFile) || ".luau";
2564
+ const entryStem = outFile !== void 0 ? (0, import_node_path2.basename)(outFile, (0, import_node_path2.extname)(outFile)) : import_node_path2.posix.basename(entry2.name);
2565
+ const others = modules.filter((m) => m !== entry2);
2566
+ if (mode === "single") return [{ path: `${entryStem}${extension}`, modules: [entry2, ...others] }];
2567
+ if (mode === "modules") {
2568
+ const pathOf = (m) => `${m.name.split("/").map((part) => part === ".." ? "__" : part).join("/")}${extension}`;
2569
+ return [entry2, ...others].map((m) => ({ path: pathOf(m), modules: [m] }));
2570
+ }
2571
+ const byName = new Map(modules.map((m) => [m.name, m]));
2572
+ const reach = (root) => {
2573
+ const seen = /* @__PURE__ */ new Set();
2574
+ const stack = [root];
2575
+ while (stack.length) {
2576
+ const name = stack.pop();
2577
+ if (seen.has(name)) continue;
2578
+ seen.add(name);
2579
+ stack.push(...byName.get(name)?.imports ?? []);
2580
+ }
2581
+ return seen;
2582
+ };
2583
+ const inEntry = reach(entry2.name);
2584
+ const roots = [...new Set(modules.flatMap((m) => [...m.dynamicImports]))].filter((root) => !inEntry.has(root));
2585
+ const reached = new Map(roots.map((root) => [root, reach(root)]));
2586
+ const groups = /* @__PURE__ */ new Map();
2587
+ for (const module2 of others) {
2588
+ if (inEntry.has(module2.name)) continue;
2589
+ const by = roots.filter((root) => reached.get(root).has(module2.name));
2590
+ const key = by.join("\n");
2591
+ if (!groups.has(key)) groups.set(key, { roots: by, modules: [] });
2592
+ groups.get(key).modules.push(module2);
2593
+ }
2594
+ const files = [{ path: `${entryStem}${extension}`, modules: modules.filter((m) => inEntry.has(m.name)) }];
2595
+ const taken = /* @__PURE__ */ new Set([entryStem]);
2596
+ for (const group of groups.values()) {
2597
+ const label = group.roots.length === 1 ? group.roots[0].replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "chunk" : "shared";
2598
+ let stem = `${entryStem}_${label}`;
2599
+ for (let n = 2; taken.has(stem); n++) stem = `${entryStem}_${label}_${n}`;
2600
+ taken.add(stem);
2601
+ files.push({ path: `${stem}${extension}`, modules: group.modules });
2602
+ }
2603
+ return files;
2256
2604
  }
2257
- function runtime(G) {
2605
+ function loadPath(entryPath, file, target) {
2606
+ const path = import_node_path2.posix.relative(import_node_path2.posix.dirname(entryPath), file);
2607
+ if (target === "lua51") return path;
2608
+ const bare = path.slice(0, path.length - import_node_path2.posix.extname(path).length);
2609
+ return bare.startsWith("../") ? bare : `./${bare}`;
2610
+ }
2611
+ function mapRegistration(G, file, map) {
2612
+ const extension = import_node_path2.posix.extname(file.path);
2613
+ const parts = file.path.slice(0, file.path.length - extension.length).split(/[/.]/).filter((part) => part !== "");
2614
+ const entry2 = `${G}.sources[#${G}.sources + 1]`;
2615
+ return (0, import_luau_parser2.parse)([
2616
+ `${entry2} = { { ${parts.map(import_parser7.luauString).join(", ")} }, ${map}, ${(0, import_parser7.luauString)(extension.slice(1))} }`,
2617
+ ...file.modules.map((module2) => `${G}.maps[${(0, import_parser7.luauString)(module2.name)}] = ${G}.sources[#${G}.sources][2]`)
2618
+ ].join("\n")).body.statements;
2619
+ }
2620
+ function loader(G, target) {
2621
+ if (target === "lua51") {
2622
+ return `
2623
+ -- From beside this file, wherever that is: its own path says where.
2624
+ local folder = ""
2625
+ if debug ~= nil and debug.getinfo ~= nil then
2626
+ local at = debug.getinfo(${G}.load, "S").source
2627
+ if at:sub(1, 1) == "@" then
2628
+ folder = at:match("^@(.*[/\\\\])") or ""
2629
+ end
2630
+ end
2631
+ local chunk, problem = loadfile(folder .. file)
2632
+ if chunk == nil then
2633
+ error(problem, 0)
2634
+ end
2635
+ chunk()(${G})`;
2636
+ }
2637
+ return `
2638
+ require(file)(${G})`;
2639
+ }
2640
+ function runtime(G, options) {
2641
+ const { several } = options;
2642
+ const files = [...options.files].map(([name, file]) => `[${(0, import_parser7.luauString)(name)}] = ${(0, import_parser7.luauString)(file)}`).join(", ");
2258
2643
  return `
2259
2644
  local ${G}
2260
2645
  ${G} = {
@@ -2302,7 +2687,7 @@ ${G} = {
2302
2687
  return message
2303
2688
  end
2304
2689
  local lines = ${G}.lines
2305
- if lines == nil then
2690
+ if lines == nil ${several ? `and #${G}.sources == 0 ` : ""}then
2306
2691
  return message .. (trace or "")
2307
2692
  end
2308
2693
  -- The bundle's name, as a position in a message spells it: where this
@@ -2322,6 +2707,81 @@ ${G} = {
2322
2707
  chunk = info.short_src
2323
2708
  end
2324
2709
  end
2710
+ ${several ? `
2711
+ -- A position in this file is read by this file's map. One in another
2712
+ -- of the bundle's files is read by that file's: the one whose name
2713
+ -- the position ends with, by the most folders where several do. A
2714
+ -- message says nothing about which file it starts in, so nothing is
2715
+ -- guessed from it here.
2716
+ local prefix = nil
2717
+ if chunk ~= nil then
2718
+ prefix = chunk:gsub("%p", "%%%0")
2719
+ end
2720
+ local function place(line)
2721
+ local origin = lines ~= nil and lines[tonumber(line)] or nil
2722
+ if origin == nil then
2723
+ return nil
2724
+ end
2725
+ return origin[1] .. ":" .. tostring(origin[2])
2726
+ end
2727
+ local function elsewhere(at, line)
2728
+ -- Already a place in the project.
2729
+ if at:sub(-6) == ".tilua" then
2730
+ return nil
2731
+ end
2732
+ local parts = {}
2733
+ for part in at:gmatch("[^/\\\\%.]+") do
2734
+ parts[#parts + 1] = part
2735
+ end
2736
+ local map, most, tied = nil, 0, false
2737
+ for _, source in ipairs(${G}.sources) do
2738
+ local names = source[1]
2739
+ local last = #parts
2740
+ if parts[last] == source[3] then
2741
+ last = last - 1
2742
+ end
2743
+ local same = 0
2744
+ while same < #names and same < last and names[#names - same] == parts[last - same] do
2745
+ same = same + 1
2746
+ end
2747
+ if same > most then
2748
+ map, most, tied = source[2], same, false
2749
+ elseif same == most then
2750
+ tied = true
2751
+ end
2752
+ end
2753
+ -- Two files it could as well be: a wrong place is worse than none.
2754
+ local origin = map ~= nil and not tied and map[tonumber(line)] or nil
2755
+ if origin == nil then
2756
+ return nil
2757
+ end
2758
+ return origin[1] .. ":" .. tostring(origin[2])
2759
+ end
2760
+ local mapped = message
2761
+ if prefix ~= nil then
2762
+ mapped = mapped:gsub(prefix .. ":(%d+)", function(line)
2763
+ return place(line)
2764
+ end)
2765
+ end
2766
+ mapped = mapped:gsub("(%S+):(%d+)", elsewhere)
2767
+ local frames = {}
2768
+ for frame in (trace or ""):gmatch("[^\\n]+") do
2769
+ local at = nil
2770
+ if prefix ~= nil then
2771
+ local line = frame:match(prefix .. ":(%d+)")
2772
+ if line ~= nil then
2773
+ at = place(line)
2774
+ end
2775
+ end
2776
+ if at == nil then
2777
+ for source, line in frame:gmatch("(%S+):(%d+)") do
2778
+ at = elsewhere(source, line)
2779
+ if at ~= nil then
2780
+ break
2781
+ end
2782
+ end
2783
+ end
2784
+ if at ~= nil then` : `
2325
2785
  if chunk == nil then
2326
2786
  chunk = message:match("^(.-):%d+:")
2327
2787
  end
@@ -2343,8 +2803,9 @@ ${G} = {
2343
2803
  for frame in (trace or ""):gmatch("[^\\n]+") do
2344
2804
  local line = frame:match(prefix .. ":(%d+)")
2345
2805
  local at = line ~= nil and place(line) or nil
2346
- if at ~= nil then
2347
- local name = frame:match("function ([%w_.:]+)")
2806
+ if at ~= nil then`}
2807
+ -- Luau writes \`function name\`; Lua 5.1, \`function 'name'\`.
2808
+ local name = frame:match("function ([%w_.:]+)") or frame:match("function '([^']+)'")
2348
2809
  frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2349
2810
  end
2350
2811
  end
@@ -2353,13 +2814,36 @@ ${G} = {
2353
2814
  end
2354
2815
  return mapped .. "\\n" .. table.concat(frames, "\\n")
2355
2816
  end,
2356
- records = {},
2817
+ records = {},${several ? `
2818
+ -- The file each module outside this one is in, as \`load\` is handed it.
2819
+ files = { ${files} },
2820
+ loaded = {},
2821
+ -- Every file's line map: by the module, and by the file's name.
2822
+ maps = {},
2823
+ sources = {},
2824
+ -- Another of the bundle's files: a function that adds its modules here.
2825
+ load = function(file)
2826
+ if ${G}.loaded[file] then
2827
+ return
2828
+ end
2829
+ ${G}.loaded[file] = true${loader(G, options.target)}
2830
+ end,` : ""}${options.imports ? `
2831
+ -- \`import("./x")\`: the module's exports, asked for where the code runs
2832
+ -- rather than before its module does. \`require\` does the rest \u2014 and
2833
+ -- loads the file the module is in, if it is not this one.
2834
+ import = function(name)
2835
+ return ${G}.require(name)
2836
+ end,` : ""}
2357
2837
  require = function(name)
2358
2838
  local record = ${G}.records[name]
2359
2839
  if record ~= nil then
2360
2840
  return record.exports
2361
2841
  end
2362
- local module = ${G}.modules[name]
2842
+ local module = ${G}.modules[name]${several ? `
2843
+ if module == nil then
2844
+ ${G}.load(${G}.files[name])
2845
+ module = ${G}.modules[name]
2846
+ end` : ""}
2363
2847
  local initialized = {}
2364
2848
  local exports = setmetatable({}, {
2365
2849
  __index = function(_, key)
@@ -2378,7 +2862,7 @@ ${G} = {
2378
2862
  return source[link[2]]
2379
2863
  end
2380
2864
  if module.stars ~= nil and key ~= "default" then
2381
- for _, star in module.stars do
2865
+ for _, star in ipairs(module.stars) do
2382
2866
  local value = ${G}.require(star)[key]
2383
2867
  if value ~= nil then
2384
2868
  return value
@@ -2403,7 +2887,7 @@ ${G} = {
2403
2887
  }
2404
2888
  function stampOf(file) {
2405
2889
  try {
2406
- const at = (0, import_node_fs2.statSync)(file);
2890
+ const at = (0, import_node_fs3.statSync)(file);
2407
2891
  return `${at.size}:${at.mtimeMs}`;
2408
2892
  } catch {
2409
2893
  return void 0;
@@ -2419,19 +2903,19 @@ function parseFile(file, diagnostics, directives) {
2419
2903
  }
2420
2904
  let text;
2421
2905
  try {
2422
- text = (0, import_node_fs2.readFileSync)(file, "utf8");
2906
+ text = (0, import_node_fs3.readFileSync)(file, "utf8");
2423
2907
  } catch {
2424
2908
  diagnostics.push({ file, message: "Cannot read file", line: 1, column: 1, category: "module" });
2425
2909
  return void 0;
2426
2910
  }
2427
2911
  try {
2428
- const program = (0, import_parser6.parse)(text);
2429
- const found = (0, import_parser6.directivesOf)(text);
2912
+ const program2 = (0, import_parser7.parse)(text);
2913
+ const found = (0, import_parser7.directivesOf)(text);
2430
2914
  directives.set(file, found);
2431
- if (stamp !== void 0) parsedFiles.set(file, { stamp, program, text, directives: found });
2432
- return { program, text };
2915
+ if (stamp !== void 0) parsedFiles.set(file, { stamp, program: program2, text, directives: found });
2916
+ return { program: program2, text };
2433
2917
  } catch (error) {
2434
- if (error instanceof import_parser6.ParseError || error instanceof import_parser6.LexError) {
2918
+ if (error instanceof import_parser7.ParseError || error instanceof import_parser7.LexError) {
2435
2919
  const { line, column } = error;
2436
2920
  diagnostics.push({ file, message: error.message, line, column, category: "syntax" });
2437
2921
  return void 0;
@@ -2439,31 +2923,33 @@ function parseFile(file, diagnostics, directives) {
2439
2923
  throw error;
2440
2924
  }
2441
2925
  }
2442
- function importedSpecifiers(program) {
2443
- return program.body.statements.flatMap((s) => s.type === "ImportStatement" || s.type === "ExportAllStatement" || s.type === "ExportNamedStatement" && s.source ? [s.source.value] : []);
2926
+ function importedSpecifiers(program2) {
2927
+ return program2.body.statements.flatMap((s) => s.type === "ImportStatement" || s.type === "ExportAllStatement" || s.type === "ExportNamedStatement" && s.source ? [s.source.value] : []);
2444
2928
  }
2445
2929
  var libraryCache = /* @__PURE__ */ new Map();
2446
2930
  function parsedLibrary(file) {
2447
2931
  let stamp;
2448
2932
  try {
2449
- const at = (0, import_node_fs2.statSync)(file);
2933
+ const at = (0, import_node_fs3.statSync)(file);
2450
2934
  stamp = `${at.size}:${at.mtimeMs}`;
2451
2935
  } catch {
2452
2936
  stamp = "";
2453
2937
  }
2454
2938
  const cached = libraryCache.get(file);
2455
2939
  if (cached && cached.stamp === stamp) return cached.program;
2456
- const program = (0, import_parser6.parse)((0, import_node_fs2.readFileSync)(file, "utf8"));
2457
- libraryCache.set(file, { stamp, program });
2458
- return program;
2940
+ const program2 = (0, import_parser7.parse)((0, import_node_fs3.readFileSync)(file, "utf8"));
2941
+ libraryCache.set(file, { stamp, program: program2 });
2942
+ return program2;
2459
2943
  }
2460
2944
  var moduleAnalyses = /* @__PURE__ */ new Map();
2461
2945
  function analyzeModules(modules, config2) {
2462
2946
  const out2 = [];
2463
2947
  const analyses = /* @__PURE__ */ new Map();
2464
- const libraries = config2 ? (0, import_parser6.resolveTypeLibraries)(config2) : { files: [], lowerings: [], problems: [] };
2948
+ const libraries = config2 ? (0, import_parser7.resolveTypeLibraries)(config2) : { files: [], lowerings: [], problems: [] };
2465
2949
  for (const p of libraries.problems) out2.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
2466
2950
  const libs = libraries.files.map(parsedLibrary);
2951
+ const noLib = config2?.noLib ?? false;
2952
+ const checked = libs.length > 0 || !noLib;
2467
2953
  const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
2468
2954
  const byFile = new Map(modules.map((m) => [m.file, m]));
2469
2955
  const exportsCache = /* @__PURE__ */ new Map();
@@ -2472,53 +2958,54 @@ function analyzeModules(modules, config2) {
2472
2958
  let partials = 0;
2473
2959
  const readWhile = /* @__PURE__ */ new Map();
2474
2960
  const settingFor = `${libraries.files.join("|")}#${config2?.directory ?? ""}#${JSON.stringify(config2?.paths ?? {})}`;
2475
- const analyzeModule = (file, program) => {
2961
+ const analyzeModule = (file, program2) => {
2476
2962
  const done = analyzed.get(file);
2477
2963
  if (done) return done;
2478
2964
  const remembered = moduleAnalyses.get(file);
2479
- if (remembered && remembered.setting === settingFor && libs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === libs[i]) && remembered.program === program && [...remembered.read].every(([read2, stamp]) => stampOf(read2) === stamp)) {
2965
+ 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)) {
2480
2966
  analyzed.set(file, remembered.result);
2481
2967
  readWhile.set(file, remembered.read);
2482
2968
  return remembered.result;
2483
2969
  }
2484
- const scopes = (0, import_parser6.analyzeScopes)(program, { builtinGlobals: globals, reportUndeclared: libs.length > 0 });
2970
+ const scopes = (0, import_parser7.analyzeScopes)(program2, { builtinGlobals: globals, reportUndeclared: checked, noLib });
2485
2971
  const said = [];
2486
2972
  for (const d of scopes.diagnostics) {
2487
2973
  if (d.kind !== "undeclared") continue;
2488
2974
  said.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "type" });
2489
2975
  }
2490
2976
  const before = partials;
2491
- const types = (0, import_parser6.analyzeTypes)(program, scopes, {
2977
+ const types = (0, import_parser7.analyzeTypes)(program2, scopes, {
2492
2978
  libs,
2493
2979
  resolveModule: resolverFor(file),
2494
- reportUnknownTypes: libs.length > 0
2980
+ noLib,
2981
+ reportUnknownTypes: checked
2495
2982
  });
2496
2983
  for (const d of types.diagnostics) {
2497
2984
  const at = d.node;
2498
2985
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
2499
2986
  }
2500
- const result = { types, diagnostics: said };
2987
+ const result2 = { types, diagnostics: said };
2501
2988
  if (partials === before) {
2502
- analyzed.set(file, result);
2503
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2989
+ analyzed.set(file, result2);
2990
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2504
2991
  const own = stampOf(file);
2505
- if (own !== void 0) read2.set(file, own);
2506
- readWhile.set(file, read2);
2507
- if ([...read2.values()].every((stamp) => stamp !== void 0)) {
2508
- moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program, read: read2, result });
2992
+ if (own !== void 0) read3.set(file, own);
2993
+ readWhile.set(file, read3);
2994
+ if ([...read3.values()].every((stamp) => stamp !== void 0)) {
2995
+ moduleAnalyses.set(file, { setting: settingFor, libs: [...libs], program: program2, read: read3, result: result2 });
2509
2996
  }
2510
2997
  }
2511
- return result;
2998
+ return result2;
2512
2999
  };
2513
3000
  const resolverFor = (file) => (specifier) => {
2514
- const target = (0, import_parser6.resolveModulePath)(file, specifier, config2);
3001
+ const target = (0, import_parser7.resolveModulePath)(file, specifier, config2);
2515
3002
  if (!target) return void 0;
2516
3003
  const noteRead = () => {
2517
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2518
- readWhile.set(file, read2);
3004
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3005
+ readWhile.set(file, read3);
2519
3006
  const stamp = stampOf(target);
2520
- if (stamp !== void 0) read2.set(target, stamp);
2521
- for (const [below, belowStamp] of readWhile.get(target) ?? []) read2.set(below, belowStamp);
3007
+ if (stamp !== void 0) read3.set(target, stamp);
3008
+ for (const [below, belowStamp] of readWhile.get(target) ?? []) read3.set(below, belowStamp);
2522
3009
  };
2523
3010
  if (inProgress.has(target)) {
2524
3011
  partials++;
@@ -2529,19 +3016,19 @@ function analyzeModules(modules, config2) {
2529
3016
  noteRead();
2530
3017
  return cached;
2531
3018
  }
2532
- let program = byFile.get(target)?.program;
2533
- if (!program) {
3019
+ let program2 = byFile.get(target)?.program;
3020
+ if (!program2) {
2534
3021
  try {
2535
- program = (0, import_parser6.parse)((0, import_node_fs2.readFileSync)(target, "utf8"));
3022
+ program2 = (0, import_parser7.parse)((0, import_node_fs3.readFileSync)(target, "utf8"));
2536
3023
  } catch {
2537
3024
  return void 0;
2538
3025
  }
2539
3026
  }
2540
3027
  inProgress.add(target);
2541
3028
  try {
2542
- const scopes = (0, import_parser6.analyzeScopes)(program, { builtinGlobals: globals });
2543
- const types = byFile.has(target) ? analyzeModule(target, program).types : (0, import_parser6.analyzeTypes)(program, scopes, { libs, resolveModule: resolverFor(target), diagnostics: false });
2544
- const exports2 = (0, import_parser6.moduleExports)(program, scopes, types, resolverFor(target));
3029
+ const scopes = (0, import_parser7.analyzeScopes)(program2, { builtinGlobals: globals, noLib });
3030
+ const types = byFile.has(target) ? analyzeModule(target, program2).types : (0, import_parser7.analyzeTypes)(program2, scopes, { libs, noLib, resolveModule: resolverFor(target), diagnostics: false });
3031
+ const exports2 = (0, import_parser7.moduleExports)(program2, scopes, types, resolverFor(target));
2545
3032
  exportsCache.set(target, exports2);
2546
3033
  noteRead();
2547
3034
  return exports2;
@@ -2574,13 +3061,14 @@ function allStatements(statements) {
2574
3061
  }
2575
3062
 
2576
3063
  // src/cli.ts
2577
- var usage = "usage: tilua <entry> [--out <file>] [--config <tilua.config.json>] [--noCheck] [--target luau|lua51]";
3064
+ var usage = "usage: tilua <entry> [--out <file>] [--output single|chunks|modules] [--config <tilua.config.json>] [--noCheck] [--target luau|lua51]";
2578
3065
  var args = process.argv.slice(2);
2579
3066
  var entry;
2580
3067
  var out;
2581
3068
  var config;
2582
3069
  var typeCheck = true;
2583
3070
  var buildTarget;
3071
+ var output;
2584
3072
  for (let i = 0; i < args.length; i++) {
2585
3073
  const arg = args[i];
2586
3074
  if (arg === "--out") out = args[++i];
@@ -2593,6 +3081,13 @@ for (let i = 0; i < args.length; i++) {
2593
3081
  process.exit(2);
2594
3082
  }
2595
3083
  buildTarget = value;
3084
+ } else if (arg === "--output") {
3085
+ const value = args[++i];
3086
+ if (value !== "single" && value !== "chunks" && value !== "modules") {
3087
+ console.error(`--output must be 'single', 'chunks' or 'modules', not '${value}'`);
3088
+ process.exit(2);
3089
+ }
3090
+ output = value;
2596
3091
  } else if (arg === "--help" || arg === "-h") {
2597
3092
  console.log(usage);
2598
3093
  process.exit(0);
@@ -2603,18 +3098,32 @@ if (!entry) {
2603
3098
  process.exit(2);
2604
3099
  }
2605
3100
  async function main(from) {
2606
- const result = await bundle({ entry: from, config, typeCheck, target: buildTarget });
2607
- for (const d of result.diagnostics) {
3101
+ const found = resolveConfig(config, (0, import_node_path3.resolve)(from)).config;
3102
+ const mode = output ?? found?.output ?? "single";
3103
+ const entryFile = mode === "modules" ? void 0 : out ?? from.replace(/\.tilua$/, "") + ".luau";
3104
+ const folder = entryFile !== void 0 ? (0, import_node_path3.dirname)(entryFile) : out ?? found?.directory ?? (0, import_node_path3.dirname)(from);
3105
+ const result2 = await bundle({
3106
+ entry: from,
3107
+ config,
3108
+ typeCheck,
3109
+ target: buildTarget,
3110
+ output: mode,
3111
+ outFile: entryFile !== void 0 ? (0, import_node_path3.basename)(entryFile) : void 0
3112
+ });
3113
+ for (const d of result2.diagnostics) {
2608
3114
  console.error(`${(0, import_node_path3.relative)(process.cwd(), d.file)}:${d.line}:${d.column} ${d.message}`);
2609
3115
  }
2610
- if (result.code === void 0) {
3116
+ if (result2.code === void 0) {
2611
3117
  console.error("no bundle written");
2612
3118
  process.exit(1);
2613
3119
  }
2614
- const target = out ?? from.replace(/\.tilua$/, "") + ".luau";
2615
- (0, import_node_fs3.writeFileSync)(target, result.code);
2616
- console.log(`${target}: ${result.modules.length} module${result.modules.length === 1 ? "" : "s"}`);
2617
- process.exit(result.diagnostics.length ? 1 : 0);
3120
+ for (const file of result2.files) {
3121
+ const target = (0, import_node_path3.join)(folder, file.path);
3122
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(target), { recursive: true });
3123
+ (0, import_node_fs4.writeFileSync)(target, file.code);
3124
+ console.log(`${target}: ${file.modules.length} module${file.modules.length === 1 ? "" : "s"}`);
3125
+ }
3126
+ process.exit(result2.diagnostics.length ? 1 : 0);
2618
3127
  }
2619
3128
  main(entry).catch((error) => {
2620
3129
  console.error(error instanceof Error ? error.message : error);