@tilua/compiler 3.1.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();
@@ -210,11 +281,11 @@ var Lowerer = class {
210
281
  origins = /* @__PURE__ */ new WeakMap();
211
282
  run() {
212
283
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
284
+ const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))])] : [];
213
285
  const helpers = this.helperDefinitions();
214
286
  const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
215
- const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: vararg() }])])] : [];
216
287
  return {
217
- statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
288
+ statements: [...captured, ...helpers, ...scriptArgs, ...this.classRuntimeStatements(), ...body],
218
289
  exportsName: this.exportsName,
219
290
  module: this.info,
220
291
  diagnostics: this.diagnostics,
@@ -261,14 +332,43 @@ 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) {
267
- name = this.names.fresh(`tilua_${kind}`);
342
+ name = this.names.fresh(`tilua_${kind === "array" ? "asArray" : kind}`);
268
343
  this.helpers.set(kind, name);
269
344
  }
270
345
  return identifier(name);
271
346
  }
347
+ // `typeof v` tells an array from an object, which Luau cannot: both are
348
+ // tables. So every array the program makes — `[a, b]`, a rest parameter,
349
+ // what `:map` answers — is written down as one, in a table that holds its
350
+ // keys weakly, and `typeof` looks there. A table nothing wrote down (one a
351
+ // Lua library made) is told by its type where that says, and by its shape
352
+ // where it does not.
353
+ /** `value`, a new array, written down as one. */
354
+ array(value) {
355
+ const marked = call(this.helper("array"), [value]);
356
+ this.marked.add(marked);
357
+ return marked;
358
+ }
359
+ /** The calls `array` made: each answers the one table it was given. */
360
+ marked = /* @__PURE__ */ new WeakSet();
361
+ /** Luau reaching the record of arrays: the bundle's, or this file's own. */
362
+ arrays() {
363
+ return this.options.module?.arrays ?? this.helper("arrays").name;
364
+ }
365
+ typeofExpression(node) {
366
+ const known = this.options.types?.typeofTables.get(node);
367
+ let argument = node.argument;
368
+ while (argument.type === "ParenthesizedExpression") argument = argument.expression;
369
+ const value = this.expression(argument);
370
+ return call(this.helper("typeof"), known ? [value, string(known)] : [this.keptToOne(value)]);
371
+ }
272
372
  /** The local a library's runtime table gets in this file, emitting it the
273
373
  * first time something asks. Keyed by library and by the key the library
274
374
  * itself chose, so two libraries' tables never collide. */
@@ -358,7 +458,11 @@ var Lowerer = class {
358
458
  /** The language's own lowering, then each library's, in load order. A
359
459
  * runtime is named by its lowering's place here (`loweringRuntime`). */
360
460
  get lowerings() {
361
- return this.allLowerings ??= [languageLowering, ...this.options.lowerings ?? []];
461
+ return this.allLowerings ??= [
462
+ languageLowering,
463
+ ...this.options.noLib ? [] : [standardLowering],
464
+ ...this.options.lowerings ?? []
465
+ ];
362
466
  }
363
467
  allLowerings;
364
468
  /** What `receiver:method(...)` becomes, when Luau alone would not run it.
@@ -372,22 +476,28 @@ var Lowerer = class {
372
476
  * (`console:log`), the last library loaded first. No answer leaves an
373
477
  * ordinary Luau method call: `text:upper()` reaches Lua's own. */
374
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) {
375
486
  const lowerings = this.lowerings;
376
- const source = this.options.types?.methodSources.get(node);
377
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();
378
488
  if (!asked.some((i) => lowerings[i].plugin.methodCall)) return void 0;
379
- const receiver = this.options.types?.typeOf.get(node.object);
380
- const receiverGlobal = this.globalName(node.object);
381
- 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);
382
492
  for (const i of asked) {
383
- const plugin2 = lowerings[i].plugin;
384
- if (!plugin2.methodCall) continue;
385
- const answer = plugin2.methodCall({
386
- method: node.method.name,
493
+ const plugin3 = lowerings[i].plugin;
494
+ if (!plugin3.methodCall) continue;
495
+ const answer = plugin3.methodCall({
496
+ method,
387
497
  receiver,
388
498
  receiverGlobal,
389
- argumentCount: node.arguments.length,
390
- at: this.callSite(node.method),
499
+ argumentCount: args.length,
500
+ at: this.callSite(name),
391
501
  get arguments() {
392
502
  return info();
393
503
  },
@@ -411,9 +521,9 @@ var Lowerer = class {
411
521
  if (name === void 0) return void 0;
412
522
  const info = this.lazyArguments(node.arguments);
413
523
  for (let i = lowerings.length - 1; i >= 0; i--) {
414
- const plugin2 = lowerings[i].plugin;
415
- if (!plugin2.globalCall) continue;
416
- const answer = plugin2.globalCall({
524
+ const plugin3 = lowerings[i].plugin;
525
+ if (!plugin3.globalCall) continue;
526
+ const answer = plugin3.globalCall({
417
527
  name,
418
528
  at: this.callSite(node.callee),
419
529
  get arguments() {
@@ -463,6 +573,114 @@ var Lowerer = class {
463
573
  }
464
574
  helperDefinitions() {
465
575
  const out = [];
576
+ const runtimes = this.runtimeDefinitions();
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
+ }
603
+ const marker = this.helpers.get("array");
604
+ const typeOf = this.helpers.get("typeof");
605
+ if (marker || typeOf) {
606
+ const arrays = this.arrays();
607
+ const own = this.helpers.get("arrays");
608
+ if (own) {
609
+ out.push(...parseLuau(`local ${own} = ${this.builtin("setmetatable").name}({}, { __mode = "k" })`).body.statements);
610
+ }
611
+ if (marker) {
612
+ out.push(...parseLuau(`
613
+ local function ${marker}(t)
614
+ ${arrays}[t] = true
615
+ return t
616
+ end`).body.statements);
617
+ }
618
+ if (typeOf) {
619
+ out.push(...parseLuau(`
620
+ local function ${typeOf}(value, known)
621
+ local name = ${this.builtin(this.lua51 ? "type" : "typeof").name}(value)
622
+ if name ~= "table" then
623
+ return name
624
+ end
625
+ if known ~= nil then
626
+ return known
627
+ end
628
+ if ${arrays}[value] then
629
+ return "array"
630
+ end
631
+ if ${this.builtin("getmetatable").name}(value) ~= nil then
632
+ return "object"
633
+ end
634
+ if #value > 0 then
635
+ return "array"
636
+ end
637
+ return "object"
638
+ end`).body.statements);
639
+ }
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
+ }
466
684
  const assign2 = this.helpers.get("assign");
467
685
  if (assign2) {
468
686
  out.push(localFunction(assign2, functionBody(["target"], [
@@ -477,28 +695,14 @@ var Lowerer = class {
477
695
  returns([identifier("target")])
478
696
  ], true)));
479
697
  }
480
- for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
481
- const { plugin: plugin2, from } = this.lowerings[index2];
482
- const source = plugin2.runtime?.[runtime2];
483
- if (source === void 0) {
484
- this.diagnostics.push({
485
- message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
486
- line: 1,
487
- column: 1
488
- });
489
- continue;
490
- }
491
- const lines = this.options.module?.lines ?? "nil";
492
- const fail = this.options.module?.fail ?? "nil";
493
- out.push(...parseLuau(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
494
- }
698
+ out.push(...runtimes);
495
699
  const concat = this.helpers.get("concat");
496
700
  if (concat) {
497
701
  out.push(localFunction(concat, functionBody([], [
498
- local(["result"], [table([])]),
702
+ local(["result"], [this.array(table([]))]),
499
703
  numericFor("i", number(1), selectCount(this.builtin("select")), [
500
704
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
501
- callStatement(call(member(this.builtin("table"), "move"), [
705
+ callStatement(call(this.move(), [
502
706
  identifier("part"),
503
707
  number(1),
504
708
  unary("#", identifier("part")),
@@ -511,6 +715,26 @@ var Lowerer = class {
511
715
  }
512
716
  return out;
513
717
  }
718
+ /** The runtime tables the file's libraries were asked for. */
719
+ runtimeDefinitions() {
720
+ const out = [];
721
+ for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
722
+ const { plugin: plugin3, from } = this.lowerings[index2];
723
+ const source = plugin3.runtime?.[runtime2];
724
+ if (source === void 0) {
725
+ this.diagnostics.push({
726
+ message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
727
+ line: 1,
728
+ column: 1
729
+ });
730
+ continue;
731
+ }
732
+ const lines = this.options.module?.lines ?? "nil";
733
+ const fail = this.options.module?.fail ?? "nil";
734
+ out.push(...parseLuau(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail).replace(/__ARRAY__/g, () => this.helper("array").name)).body.statements);
735
+ }
736
+ return out;
737
+ }
514
738
  // --------------------------------------------------------
515
739
  // Modules
516
740
  // --------------------------------------------------------
@@ -591,6 +815,7 @@ var Lowerer = class {
591
815
  names.push("default");
592
816
  break;
593
817
  case "ExportNamedStatement": {
818
+ if (statement.typeOnly) break;
594
819
  if (statement.source) {
595
820
  const key = resolve3(statement.source);
596
821
  if (key === void 0) break;
@@ -674,6 +899,26 @@ var Lowerer = class {
674
899
  for (let i = 0; i < locals.length; i += 100) declarations.push(local(locals.slice(i, i + 100), []));
675
900
  return [...declarations, ...hoisted, ...requires, ...body];
676
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
+ }
677
922
  isRewritten(node) {
678
923
  const binding = this.bindingByDeclaration.get(node);
679
924
  return binding !== void 0 && this.rewrites.has(binding.id);
@@ -820,7 +1065,7 @@ var Lowerer = class {
820
1065
  case "VariableDeclaration":
821
1066
  return this.variableDeclaration(node, "declare");
822
1067
  case "FunctionDeclaration":
823
- 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 }];
824
1069
  case "FunctionDeclarationStatement":
825
1070
  return [this.functionDeclarationStatement(node)];
826
1071
  case "ClassDeclaration":
@@ -851,8 +1096,13 @@ var Lowerer = class {
851
1096
  // kept — Lua has no expression statement — and it cannot run
852
1097
  // anything, since a call is a `CallStatement` instead, so dropping
853
1098
  // it changes nothing but the noise.
854
- case "ExpressionStatement":
855
- 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
+ }
856
1106
  case "DoStatement":
857
1107
  return [{ type: "DoStatement", body: this.block(node.body), ...spanOf(node) }];
858
1108
  case "WhileStatement":
@@ -904,7 +1154,11 @@ var Lowerer = class {
904
1154
  let source = this.expression(node.iterator);
905
1155
  if (form.viaIter) source = methodCall(source, "__iter", []);
906
1156
  const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
907
- 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
+ }
908
1162
  return [genericFor(
909
1163
  variables,
910
1164
  iterators,
@@ -1157,7 +1411,8 @@ end
1157
1411
  },
1158
1412
  isMethod,
1159
1413
  func,
1160
- attributes: node.attributes,
1414
+ // `@native` and the like are Luau's.
1415
+ attributes: this.lua51 ? void 0 : node.attributes,
1161
1416
  ...spanOf(node)
1162
1417
  };
1163
1418
  }
@@ -1258,12 +1513,12 @@ end
1258
1513
  if (element) bind(element.value, index(source, number(i + 1)), element.default);
1259
1514
  });
1260
1515
  if (pattern.rest) {
1261
- const elements = call(member(this.builtin("table"), "move"), [
1516
+ const elements = call(this.move(), [
1262
1517
  source,
1263
1518
  number(pattern.elements.length + 1),
1264
1519
  unary("#", source),
1265
1520
  number(1),
1266
- table([])
1521
+ this.array(table([]))
1267
1522
  ]);
1268
1523
  const rest = this.restTarget(pattern.rest, elements, mode, after);
1269
1524
  after.push(...rest.then);
@@ -1317,7 +1572,7 @@ end
1317
1572
  const names = [];
1318
1573
  for (const p of params) {
1319
1574
  if (p.rest) {
1320
- prelude.push(local([this.name(p.name)], [table([{ type: "TableFieldPositional", value: vararg() }])]));
1575
+ prelude.push(local([this.name(p.name)], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))]));
1321
1576
  continue;
1322
1577
  }
1323
1578
  const name = p.pattern ? this.names.fresh("arg") : this.name(p.name);
@@ -1369,12 +1624,20 @@ end
1369
1624
  case "ArrayExpression":
1370
1625
  return this.arrayExpression(node);
1371
1626
  case "BinaryExpression": {
1627
+ if (node.operator === "??") return this.nullish(node);
1372
1628
  const left = this.expression(node.left);
1373
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
+ }
1374
1636
  if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
1375
1637
  return binary(node.operator, left, right);
1376
1638
  }
1377
1639
  case "UnaryExpression":
1640
+ if (node.operator === "typeof") return this.typeofExpression(node);
1378
1641
  return unary(node.operator, this.expression(node.argument));
1379
1642
  case "SuperExpression":
1380
1643
  return this.superClassReference(node);
@@ -1396,6 +1659,8 @@ end
1396
1659
  }
1397
1660
  case "ParenthesizedExpression":
1398
1661
  return parenthesized(this.expression(node.expression));
1662
+ case "ImportExpression":
1663
+ return this.importExpression(node);
1399
1664
  // Types only: the value is the expression itself.
1400
1665
  case "TypeAssertionExpression":
1401
1666
  case "SatisfiesExpression":
@@ -1429,7 +1694,7 @@ end
1429
1694
  * kept to one there, `f((g()))`. Every value is taken only where the
1430
1695
  * program asks for them, as an array's last element: `[g()]`. */
1431
1696
  keptToOne(value) {
1432
- return expandsToMany(value) ? parenthesized(value) : value;
1697
+ return expandsToMany(value) && !this.marked.has(value) ? parenthesized(value) : value;
1433
1698
  }
1434
1699
  /** Is this `scriptArgs` the language's own, not a name the file declared? */
1435
1700
  isLanguageGlobal(node) {
@@ -1449,7 +1714,7 @@ end
1449
1714
  const last2 = list.length - 1;
1450
1715
  return lowered.length && !many.has(list[last2]) ? [...lowered.slice(0, -1), this.keptToOne(lowered[last2])] : lowered;
1451
1716
  }
1452
- 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]);
1453
1718
  const last = list[list.length - 1];
1454
1719
  if (spreads.length === 1 && last.type === "SpreadElement") {
1455
1720
  return [...list.slice(0, -1).map((a) => this.expression(a)), unpack(this.expression(last.argument))];
@@ -1463,11 +1728,21 @@ end
1463
1728
  /** One link of an access chain, read from `object`. */
1464
1729
  link(node, object2) {
1465
1730
  switch (node.type) {
1466
- 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
+ }
1467
1739
  return member(object2, node.property.name);
1740
+ }
1468
1741
  case "IndexExpression":
1469
1742
  return index(object2, this.expression(node.index));
1470
1743
  case "CallExpression": {
1744
+ const method = this.methodCalled(node);
1745
+ if (method) return method;
1471
1746
  const lowered = this.loweredGlobalCall(node);
1472
1747
  const args = this.arguments(node.arguments);
1473
1748
  if (!lowered) return call(object2, args);
@@ -1490,6 +1765,24 @@ end
1490
1765
  }
1491
1766
  }
1492
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
+ }
1493
1786
  // --------------------------------------------------------
1494
1787
  // Optional chains
1495
1788
  // --------------------------------------------------------
@@ -1535,7 +1828,7 @@ end
1535
1828
  const segments = this.segments(chain);
1536
1829
  const base = this.expression(chain.base);
1537
1830
  const last = chain.links[chain.links.length - 1];
1538
- 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") {
1539
1832
  return {
1540
1833
  type: "IfElseExpression",
1541
1834
  clauses: [{ condition: binary("==", base, nil()), body: nil() }],
@@ -1556,6 +1849,32 @@ end
1556
1849
  const func = functionExpression(functionBody([], statements, passesVarargs));
1557
1850
  return call(parenthesized(func), passesVarargs ? [vararg()] : []);
1558
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
+ }
1559
1878
  /** `` `${a} any` `` -> `("%s any"):format(tostring(a))` */
1560
1879
  interpolatedString(node) {
1561
1880
  let format = "";
@@ -1637,12 +1956,12 @@ end
1637
1956
  }));
1638
1957
  const alternate = this.expression(node.alternate);
1639
1958
  if (clauses.every((c) => alwaysTruthy(c.body))) {
1640
- let result = alternate;
1959
+ let result2 = alternate;
1641
1960
  for (let i = clauses.length - 1; i >= 0; i--) {
1642
1961
  const { condition, body } = clauses[i];
1643
- result = binary("or", binary("and", condition, body), result);
1962
+ result2 = binary("or", binary("and", condition, body), result2);
1644
1963
  }
1645
- return parenthesized(result);
1964
+ return parenthesized(result2);
1646
1965
  }
1647
1966
  const statement = {
1648
1967
  type: "IfStatement",
@@ -1699,18 +2018,27 @@ end
1699
2018
  current = [];
1700
2019
  parts.push(this.expression(f.argument));
1701
2020
  break;
2021
+ case "TableFieldAccessor":
2022
+ break;
1702
2023
  }
1703
2024
  }
1704
- if (parts.length === 0) return table(current);
1705
- if (current.length) parts.push(table(current));
1706
- 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")]);
1707
2035
  }
1708
2036
  /** `[1, ...xs, 2]`. Without a spread it is a Luau sequence; with one, the
1709
2037
  * runs of plain elements and the spread arrays are joined by `concat`. */
1710
2038
  arrayExpression(node) {
1711
2039
  const elements = packs(node.elements).items;
1712
2040
  if (!elements.some((e) => e.type === "SpreadElement")) {
1713
- return table(elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) })));
2041
+ return this.array(table(elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) }))));
1714
2042
  }
1715
2043
  const parts = [];
1716
2044
  let current = [];
@@ -1967,15 +2295,15 @@ async function loadLowerings(modules) {
1967
2295
  for (const module of modules) {
1968
2296
  try {
1969
2297
  const loaded = await import(pathToFileURL(module.file).href);
1970
- const plugin2 = loaded.default;
1971
- if (!plugin2 || typeof plugin2 !== "object") {
2298
+ const plugin3 = loaded.default;
2299
+ if (!plugin3 || typeof plugin3 !== "object") {
1972
2300
  problems.push({
1973
2301
  file: module.file,
1974
2302
  message: `'${module.from}' lowering module has no default export`
1975
2303
  });
1976
2304
  continue;
1977
2305
  }
1978
- lowerings.push({ plugin: plugin2, from: module.from, library: module.library });
2306
+ lowerings.push({ plugin: plugin3, from: module.from, library: module.library });
1979
2307
  } catch (error) {
1980
2308
  problems.push({
1981
2309
  file: module.file,
@@ -1987,8 +2315,8 @@ async function loadLowerings(modules) {
1987
2315
  }
1988
2316
 
1989
2317
  // src/bundle.ts
1990
- import { readFileSync as readFileSync2, statSync } from "fs";
1991
- 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";
1992
2320
  import {
1993
2321
  parse,
1994
2322
  analyzeScopes,
@@ -1996,17 +2324,18 @@ import {
1996
2324
  moduleExports,
1997
2325
  resolveModulePath,
1998
2326
  resolveTypeLibraries,
2327
+ importExpressions,
1999
2328
  directivesOf,
2000
2329
  applyDirectives,
2001
2330
  UNUSED_EXPECT_ERROR,
2002
2331
  ParseError,
2003
2332
  LexError,
2004
- luauString as luauString3
2333
+ luauString as luauString4
2005
2334
  } from "@tilua/parser";
2006
2335
  import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
2007
2336
 
2008
2337
  // src/linemap.ts
2009
- import { luauString as luauString2 } from "@tilua/parser";
2338
+ import { luauString as luauString3 } from "@tilua/parser";
2010
2339
  function statementsOf(node) {
2011
2340
  const out = [];
2012
2341
  const visit = (value) => {
@@ -2039,7 +2368,7 @@ function buildLineMap(emitted, printed, origins) {
2039
2368
  }
2040
2369
  function lineMapSource(map) {
2041
2370
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
2042
- const quoted = (s) => luauString2(s.replace(/\\/g, "/"));
2371
+ const quoted = (s) => luauString3(s.replace(/\\/g, "/"));
2043
2372
  return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
2044
2373
  }
2045
2374
 
@@ -2059,16 +2388,17 @@ async function bundle(options) {
2059
2388
  const file = queue.shift();
2060
2389
  if (sources.has(file)) continue;
2061
2390
  const parsed = parseFile(file, diagnostics, directives);
2062
- if (!parsed) return { modules: [], diagnostics };
2391
+ if (!parsed) return { files: [], modules: [], diagnostics };
2063
2392
  const { program: program3, text } = parsed;
2064
- const scopes = analyzeScopes(program3);
2393
+ const scopes = analyzeScopes(program3, { noLib: config?.noLib });
2065
2394
  for (const d of scopes.diagnostics) {
2066
2395
  diagnostics.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "scope" });
2067
2396
  }
2068
2397
  sources.set(file, { file, name: nameOf(file), text, program: program3, scopes });
2069
- for (const specifier of importedSpecifiers(program3)) {
2070
- const target = resolveModulePath(file, specifier, config);
2071
- 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);
2072
2402
  }
2073
2403
  }
2074
2404
  const analysis = analyzeModules([...sources.values()], config);
@@ -2080,6 +2410,9 @@ async function bundle(options) {
2080
2410
  const names = Names.from(...[...sources.values()].map((s) => s.program));
2081
2411
  const G = names.fresh("G");
2082
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";
2083
2416
  const modules = /* @__PURE__ */ new Map();
2084
2417
  const origins = /* @__PURE__ */ new WeakMap();
2085
2418
  const pending = [entry];
@@ -2088,37 +2421,48 @@ async function bundle(options) {
2088
2421
  if (modules.has(file)) continue;
2089
2422
  const source = sources.get(file);
2090
2423
  const shown = relative(root, file).replace(/\\/g, "/");
2091
- 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, {
2092
2427
  names,
2093
2428
  types: analysis.types.get(file),
2094
2429
  lowerings,
2095
- target: options.target ?? config?.target,
2430
+ target,
2431
+ noLib: config?.noLib,
2096
2432
  source: source.text,
2097
2433
  file: shown,
2098
2434
  module: {
2099
2435
  name: source.name,
2100
2436
  require: requireExpression,
2101
- 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)}]`,
2102
2441
  fail: `${G}.fail`,
2103
- resolve: (specifier) => {
2104
- const target = resolveModulePath(file, specifier, config);
2105
- if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
2106
- pending.push(target);
2107
- return sources.get(target).name;
2442
+ arrays: `${G}.arrays`,
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;
2108
2450
  }
2109
2451
  }
2110
2452
  });
2111
- for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2112
- for (const statement of allStatements(lowered.statements)) {
2113
- 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);
2114
2456
  if (line !== void 0) origins.set(statement, { file: shown, line });
2115
2457
  }
2116
2458
  modules.set(file, {
2117
2459
  name: source.name,
2118
- statements: lowered.statements,
2119
- exportsName: lowered.exportsName,
2120
- info: lowered.module,
2121
- usesScriptArgs: lowered.usesScriptArgs
2460
+ statements: lowered2.statements,
2461
+ exportsName: lowered2.exportsName,
2462
+ info: lowered2.module,
2463
+ usesScriptArgs: lowered2.usesScriptArgs,
2464
+ imports,
2465
+ dynamicImports
2122
2466
  });
2123
2467
  }
2124
2468
  for (const [file, fileDirectives] of directives) {
@@ -2137,57 +2481,175 @@ async function bundle(options) {
2137
2481
  const reported = diagnostics.filter((d) => d.category !== "type" || !moduleProblems.has(`${d.file}:${d.line}:${d.column}:${d.message}`));
2138
2482
  diagnostics.length = 0;
2139
2483
  diagnostics.push(...reported);
2140
- const moduleNames = [...modules.values()].map((m) => m.name);
2484
+ const lowered = [...modules.values()];
2485
+ const moduleNames = lowered.map((m) => m.name);
2141
2486
  if (diagnostics.some((d) => d.category === "syntax" || d.category === "scope" || d.category === "module")) {
2142
- return { modules: moduleNames, diagnostics };
2143
- }
2144
- 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
+ }));
2145
2502
  const assignment = program2.body.statements[1];
2146
- if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2503
+ if (usesScriptArgs) {
2147
2504
  program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
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);
2148
2507
  }
2149
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;
2150
2509
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
2151
- for (const module of modules.values()) {
2152
- const { names: exported, links, stars } = module.info;
2153
- const fields = [];
2154
- if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
2155
- if (links.length) {
2156
- fields.push(field("links", table(links.map((link) => field(link.name, table([
2157
- { type: "TableFieldPositional", value: string(link.module) },
2158
- ...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
2159
- ]))))));
2160
- }
2161
- if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
2162
- fields.push(field("load", functionExpression(functionBody([module.exportsName], module.statements, true))));
2163
- 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) });
2164
2512
  }
2165
2513
  const ok = names.fresh("ok");
2166
- const result = names.fresh("result");
2514
+ const result2 = names.fresh("result");
2167
2515
  const entryStatements = parseLuau2(`
2168
- local ${ok}, ${result} = xpcall(function()
2169
- return ${G}.require(${luauString3(sources.get(entry).name)})
2516
+ local ${ok}, ${result2} = xpcall(function()
2517
+ return ${G}.require(${luauString4(sources.get(entry).name)})
2170
2518
  end, ${G}.fail)
2171
2519
  if not ${ok} then
2172
- error(${result}, 0)
2520
+ error(${result2}, 0)
2173
2521
  end
2174
- ${modules.get(entry).info.exports ? `return ${result}` : ""}
2522
+ ${modules.get(entry).info.exports ? `return ${result2}` : ""}
2175
2523
  `).body.statements;
2176
2524
  program2.body.statements.push(...entryStatements);
2177
2525
  const map = buildLineMap(program2, parseLuau2(print(program2)), origins);
2178
- if (map.size) {
2179
- program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, assign(
2526
+ if (map.size || several) {
2527
+ const mapStatements = [assign(
2180
2528
  [member(identifier(G), "lines")],
2181
2529
  [parseExpressionFromSource2(lineMapSource(map))]
2182
- ));
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);
2533
+ }
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}`;
2613
+ }
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})`;
2183
2639
  }
2184
- return { code: print(program2) + "\n", modules: moduleNames, diagnostics };
2640
+ return `
2641
+ require(file)(${G})`;
2185
2642
  }
2186
- function runtime(G) {
2643
+ function runtime(G, options) {
2644
+ const { several } = options;
2645
+ const files = [...options.files].map(([name, file]) => `[${luauString4(name)}] = ${luauString4(file)}`).join(", ");
2187
2646
  return `
2188
2647
  local ${G}
2189
2648
  ${G} = {
2190
2649
  modules = {},
2650
+ -- Every array the program made, so \`typeof\` can tell one from an object.
2651
+ -- The keys are held weakly: being written down keeps no array alive.
2652
+ arrays = setmetatable({}, { __mode = "k" }),
2191
2653
  -- Filled in just before the entry runs: bundle line -> { file, line }.
2192
2654
  lines = nil,
2193
2655
  -- What an error nothing caught says, in the project's files: the bundle's
@@ -2228,7 +2690,7 @@ ${G} = {
2228
2690
  return message
2229
2691
  end
2230
2692
  local lines = ${G}.lines
2231
- if lines == nil then
2693
+ if lines == nil ${several ? `and #${G}.sources == 0 ` : ""}then
2232
2694
  return message .. (trace or "")
2233
2695
  end
2234
2696
  -- The bundle's name, as a position in a message spells it: where this
@@ -2248,6 +2710,81 @@ ${G} = {
2248
2710
  chunk = info.short_src
2249
2711
  end
2250
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` : `
2251
2788
  if chunk == nil then
2252
2789
  chunk = message:match("^(.-):%d+:")
2253
2790
  end
@@ -2269,8 +2806,9 @@ ${G} = {
2269
2806
  for frame in (trace or ""):gmatch("[^\\n]+") do
2270
2807
  local line = frame:match(prefix .. ":(%d+)")
2271
2808
  local at = line ~= nil and place(line) or nil
2272
- if at ~= nil then
2273
- 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 '([^']+)'")
2274
2812
  frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2275
2813
  end
2276
2814
  end
@@ -2279,13 +2817,36 @@ ${G} = {
2279
2817
  end
2280
2818
  return mapped .. "\\n" .. table.concat(frames, "\\n")
2281
2819
  end,
2282
- 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,` : ""}
2283
2840
  require = function(name)
2284
2841
  local record = ${G}.records[name]
2285
2842
  if record ~= nil then
2286
2843
  return record.exports
2287
2844
  end
2288
- 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` : ""}
2289
2850
  local initialized = {}
2290
2851
  local exports = setmetatable({}, {
2291
2852
  __index = function(_, key)
@@ -2304,7 +2865,7 @@ ${G} = {
2304
2865
  return source[link[2]]
2305
2866
  end
2306
2867
  if module.stars ~= nil and key ~= "default" then
2307
- for _, star in module.stars do
2868
+ for _, star in ipairs(module.stars) do
2308
2869
  local value = ${G}.require(star)[key]
2309
2870
  if value ~= nil then
2310
2871
  return value
@@ -2345,7 +2906,7 @@ function parseFile(file, diagnostics, directives) {
2345
2906
  }
2346
2907
  let text;
2347
2908
  try {
2348
- text = readFileSync2(file, "utf8");
2909
+ text = readFileSync3(file, "utf8");
2349
2910
  } catch {
2350
2911
  diagnostics.push({ file, message: "Cannot read file", line: 1, column: 1, category: "module" });
2351
2912
  return void 0;
@@ -2379,7 +2940,7 @@ function parsedLibrary(file) {
2379
2940
  }
2380
2941
  const cached = libraryCache.get(file);
2381
2942
  if (cached && cached.stamp === stamp) return cached.program;
2382
- const program2 = parse(readFileSync2(file, "utf8"));
2943
+ const program2 = parse(readFileSync3(file, "utf8"));
2383
2944
  libraryCache.set(file, { stamp, program: program2 });
2384
2945
  return program2;
2385
2946
  }
@@ -2390,6 +2951,8 @@ function analyzeModules(modules, config) {
2390
2951
  const libraries = config ? resolveTypeLibraries(config) : { files: [], lowerings: [], problems: [] };
2391
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" });
2392
2953
  const libs = libraries.files.map(parsedLibrary);
2954
+ const noLib = config?.noLib ?? false;
2955
+ const checked = libs.length > 0 || !noLib;
2393
2956
  const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
2394
2957
  const byFile = new Map(modules.map((m) => [m.file, m]));
2395
2958
  const exportsCache = /* @__PURE__ */ new Map();
@@ -2402,12 +2965,12 @@ function analyzeModules(modules, config) {
2402
2965
  const done = analyzed.get(file);
2403
2966
  if (done) return done;
2404
2967
  const remembered = moduleAnalyses.get(file);
2405
- 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)) {
2406
2969
  analyzed.set(file, remembered.result);
2407
2970
  readWhile.set(file, remembered.read);
2408
2971
  return remembered.result;
2409
2972
  }
2410
- const scopes = analyzeScopes(program2, { builtinGlobals: globals, reportUndeclared: libs.length > 0 });
2973
+ const scopes = analyzeScopes(program2, { builtinGlobals: globals, reportUndeclared: checked, noLib });
2411
2974
  const said = [];
2412
2975
  for (const d of scopes.diagnostics) {
2413
2976
  if (d.kind !== "undeclared") continue;
@@ -2417,34 +2980,35 @@ function analyzeModules(modules, config) {
2417
2980
  const types = analyzeTypes(program2, scopes, {
2418
2981
  libs,
2419
2982
  resolveModule: resolverFor(file),
2420
- reportUnknownTypes: libs.length > 0
2983
+ noLib,
2984
+ reportUnknownTypes: checked
2421
2985
  });
2422
2986
  for (const d of types.diagnostics) {
2423
2987
  const at = d.node;
2424
2988
  said.push({ file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
2425
2989
  }
2426
- const result = { types, diagnostics: said };
2990
+ const result2 = { types, diagnostics: said };
2427
2991
  if (partials === before) {
2428
- analyzed.set(file, result);
2429
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2992
+ analyzed.set(file, result2);
2993
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2430
2994
  const own = stampOf(file);
2431
- if (own !== void 0) read2.set(file, own);
2432
- readWhile.set(file, read2);
2433
- if ([...read2.values()].every((stamp) => stamp !== void 0)) {
2434
- 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 });
2435
2999
  }
2436
3000
  }
2437
- return result;
3001
+ return result2;
2438
3002
  };
2439
3003
  const resolverFor = (file) => (specifier) => {
2440
3004
  const target = resolveModulePath(file, specifier, config);
2441
3005
  if (!target) return void 0;
2442
3006
  const noteRead = () => {
2443
- const read2 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
2444
- readWhile.set(file, read2);
3007
+ const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
3008
+ readWhile.set(file, read3);
2445
3009
  const stamp = stampOf(target);
2446
- if (stamp !== void 0) read2.set(target, stamp);
2447
- 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);
2448
3012
  };
2449
3013
  if (inProgress.has(target)) {
2450
3014
  partials++;
@@ -2458,15 +3022,15 @@ function analyzeModules(modules, config) {
2458
3022
  let program2 = byFile.get(target)?.program;
2459
3023
  if (!program2) {
2460
3024
  try {
2461
- program2 = parse(readFileSync2(target, "utf8"));
3025
+ program2 = parse(readFileSync3(target, "utf8"));
2462
3026
  } catch {
2463
3027
  return void 0;
2464
3028
  }
2465
3029
  }
2466
3030
  inProgress.add(target);
2467
3031
  try {
2468
- const scopes = analyzeScopes(program2, { builtinGlobals: globals });
2469
- 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 });
2470
3034
  const exports = moduleExports(program2, scopes, types, resolverFor(target));
2471
3035
  exportsCache.set(target, exports);
2472
3036
  noteRead();