@tilua/compiler 1.0.2 → 2.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.
@@ -11,7 +11,7 @@ function resolveConfig(input, entry, cwd = process.cwd()) {
11
11
 
12
12
  // src/lower.ts
13
13
  import { formatType } from "@tilua/parser";
14
- import { parse as parseLuau } from "luau-parser";
14
+ import { parse as parseLuau, parseExpressionFromSource } from "luau-parser";
15
15
 
16
16
  // src/luau.ts
17
17
  var NO_SPAN = { line: { start: 0, end: 0 }, column: { start: 0, end: 0 } };
@@ -101,144 +101,6 @@ function isLuauName(name) {
101
101
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !LUAU_KEYWORDS.has(name);
102
102
  }
103
103
 
104
- // src/console.ts
105
- var CONSOLE_RUNTIME = `
106
- local __NAME__ = (function()
107
- local console = {}
108
- local MAX_DEPTH = 4
109
- local ESCAPES = { ["\\n"] = "\\\\n", ["\\r"] = "\\\\r", ["\\t"] = "\\\\t", ["\\""] = "\\\\\\"", ["\\\\"] = "\\\\\\\\" }
110
-
111
- local function quote(text)
112
- return '"' .. text:gsub('[%c"\\\\]', function(c) return ESCAPES[c] or ("\\\\%d"):format(c:byte()) end) .. '"'
113
- end
114
-
115
- -- A table with 1..n and nothing else prints as a list, as it was written.
116
- local function isArray(value)
117
- local count = 0
118
- for key in pairs(value) do
119
- if type(key) ~= "number" then return false end
120
- count = count + 1
121
- end
122
- return count == #value
123
- end
124
-
125
- local function isIdentifier(key)
126
- return type(key) == "string" and key:match("^[A-Za-z_][A-Za-z0-9_]*$") ~= nil
127
- end
128
-
129
- local render
130
-
131
- local function renderTable(value, meta, seen, depth, indent)
132
- if seen[value] then return "<cycle>" end
133
- if depth > MAX_DEPTH then return "..." end
134
- seen[value] = true
135
- local inner = indent .. " "
136
- local parts = {}
137
- if isArray(value) then
138
- for i = 1, #value do
139
- parts[#parts + 1] = inner .. render(value[i], nil, seen, depth + 1, inner)
140
- end
141
- else
142
- local keys = {}
143
- for key in pairs(value) do keys[#keys + 1] = key end
144
- table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
145
- for _, key in ipairs(keys) do
146
- local name = isIdentifier(key) and key or ("[" .. render(key, nil, seen, depth + 1, inner) .. "]")
147
- parts[#parts + 1] = inner .. name .. " = " .. render(value[key], nil, seen, depth + 1, inner)
148
- end
149
- end
150
- seen[value] = nil
151
- if #parts == 0 then return "{}" end
152
- local open = isArray(value) and "[" or "{"
153
- local close = isArray(value) and "]" or "}"
154
- return open .. "\\n" .. table.concat(parts, ",\\n") .. "\\n" .. indent .. close
155
- end
156
-
157
- -- \`meta\` is what the compiler knew about this value and the runtime cannot:
158
- -- a function's type, and the code it was written as.
159
- render = function(value, meta, seen, depth, indent)
160
- local kind = type(value)
161
- if kind == "string" then return quote(value) end
162
- if kind == "number" or kind == "boolean" or kind == "nil" then return tostring(value) end
163
- if kind == "function" then
164
- if meta == nil then return "function" end
165
- if meta[2] == nil or meta[2] == "" then return meta[1] end
166
- return meta[1] .. " " .. meta[2]
167
- end
168
- if kind == "table" then
169
- local shown = rawget(value, "__tostring") == nil and getmetatable(value)
170
- -- A value with its own \`__tostring\` (a Roblox Vector3, a class
171
- -- instance that says how it reads) is trusted to describe itself.
172
- if shown ~= nil and rawget(shown, "__tostring") ~= nil then return tostring(value) end
173
- return renderTable(value, meta, seen, depth, indent)
174
- end
175
- return tostring(value)
176
- end
177
-
178
- local function describe(meta, index, value)
179
- return render(value, meta ~= nil and meta[index] or nil, {}, 1, "")
180
- end
181
-
182
- local function join(meta, count, ...)
183
- local parts = {}
184
- for i = 1, count do
185
- parts[#parts + 1] = describe(meta, i, (select(i, ...)))
186
- end
187
- return table.concat(parts, " ")
188
- end
189
-
190
- -- A line of the bundle, as the place in the project that wrote it.
191
- local lines = __LINES__
192
- local function place(line)
193
- local origin = lines ~= nil and lines[tonumber(line)]
194
- if origin == nil then return nil end
195
- return origin[1] .. ":" .. tostring(origin[2])
196
- end
197
-
198
- -- Luau names the bundle in every frame; the project's files are what the
199
- -- reader knows. Frames the map has nothing for are dropped: they are this
200
- -- runtime's own, and the bundle's.
201
- local function trace(level)
202
- local raw = debug.traceback(nil, level)
203
- local out = {}
204
- for frame in raw:gmatch("[^\\n]+") do
205
- local line = frame:match(":(%d+)")
206
- local at = line ~= nil and place(line) or nil
207
- if at ~= nil then
208
- local name = frame:match("function ([%w_.:]+)")
209
- out[#out + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
210
- end
211
- end
212
- return table.concat(out, "\\n")
213
- end
214
-
215
- -- Never on the caller's thread: reporting a problem must not become one.
216
- local detach = (task ~= nil and task.spawn) or function(f) return f() end
217
-
218
- local function raise(prefix, meta, count, ...)
219
- local message = prefix .. join(meta, count, ...)
220
- local where = trace(3)
221
- detach(function()
222
- error(where ~= "" and (message .. "\\n" .. where) or message, 0)
223
- end)
224
- end
225
-
226
- function console.log(meta, ...)
227
- print(join(meta, select("#", ...), ...))
228
- end
229
-
230
- function console.warn(meta, ...)
231
- raise("[warn] ", meta, select("#", ...), ...)
232
- end
233
-
234
- function console.error(meta, ...)
235
- raise("", meta, select("#", ...), ...)
236
- end
237
-
238
- return console
239
- end)()
240
- `;
241
-
242
104
  // src/names.ts
243
105
  var Names = class _Names {
244
106
  constructor(taken) {
@@ -318,14 +180,18 @@ var Lowerer = class {
318
180
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
319
181
  const helpers = this.helperDefinitions();
320
182
  const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
183
+ const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: vararg() }])])] : [];
321
184
  return {
322
- statements: [...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
185
+ statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
323
186
  exportsName: this.exportsName,
324
187
  module: this.info,
325
188
  diagnostics: this.diagnostics,
326
- origins: this.origins
189
+ origins: this.origins,
190
+ usesScriptArgs: this.usesScriptArgs
327
191
  };
328
192
  }
193
+ /** Whether the file reads `scriptArgs`, the language's own global. */
194
+ usesScriptArgs = false;
329
195
  report(node, message) {
330
196
  this.diagnostics.push({ message, line: node.line.start, column: node.column.start });
331
197
  }
@@ -386,60 +252,9 @@ var Lowerer = class {
386
252
  }
387
253
  loweringNames = /* @__PURE__ */ new Map();
388
254
  loweringUsed = [];
389
- /** What a library says `receiver:method(...)` is. The last library
390
- * loaded is asked first, so a project's own library can answer for a
391
- * method an earlier one also claims; no answer leaves an ordinary Luau
392
- * method call, which is what a value that answers to the method itself
393
- * wants (`text:upper()`). */
394
- /** `console:log(a, f)` and its two siblings.
395
- *
396
- * Handled here rather than through a library's lowering because what they
397
- * need is not a different callee but *more than was written*: a function
398
- * prints as its type and its code, and neither survives to runtime. The
399
- * compiler is the only thing that has both, so it passes them along:
400
- *
401
- * console:log(a, double)
402
- * -- becomes
403
- * tilua_console.log({ [2] = { "(x: number) => number", "(x: number) => x * 2" } }, a, double)
404
- *
405
- * The meta table is `nil` when nothing was worth saying, which is most
406
- * calls. `console` itself is never passed: it holds nothing. */
407
- consoleCall(node) {
408
- if (node.object.type !== "Identifier" || node.object.name !== "console") return void 0;
409
- const method = node.method.name;
410
- if (method !== "log" && method !== "warn" && method !== "error") return void 0;
411
- const binding = this.scopes.bindingOf.get(node.object);
412
- if (binding !== void 0 && this.scopes.bindings.get(binding)?.kind !== "global") return void 0;
413
- const meta = this.consoleMeta(node.arguments);
414
- return call(
415
- member(this.helper("console"), method),
416
- [meta ?? nil(), ...this.values(node.arguments)]
417
- );
418
- }
419
- /** What the compiler knows about the arguments and the runtime cannot:
420
- * for each function argument, the type it was inferred as and the code it
421
- * was written as. Keyed by position, so an argument that needs nothing
422
- * costs nothing. */
423
- consoleMeta(args) {
424
- const fields = [];
425
- args.forEach((argument, index2) => {
426
- const type = this.options.types?.typeOf.get(argument);
427
- if (!type || !isFunctionType(type)) return;
428
- const written = this.writtenSource(this.declarationOf(argument) ?? argument);
429
- fields.push({
430
- type: "TableFieldComputed",
431
- key: number(index2 + 1),
432
- value: table([
433
- { type: "TableFieldPositional", value: string(formatType(type)) },
434
- { type: "TableFieldPositional", value: string(written) }
435
- ])
436
- });
437
- });
438
- return fields.length ? table(fields) : void 0;
439
- }
440
255
  /** What a name was declared as: the value of `const f = ...`, or the whole
441
- * `function f() { ... }`. Built once, on the first `console` call that
442
- * needs it — most files have none. */
256
+ * `function f() { ... }`. Built once, on the first call a library asks
257
+ * about — most files have none. */
443
258
  declarationOf(argument) {
444
259
  if (argument.type !== "Identifier") return void 0;
445
260
  if (!this.declarations) {
@@ -478,21 +293,131 @@ var Lowerer = class {
478
293
  const trim = (line) => line.startsWith(indent) ? line.slice(indent.length) : line;
479
294
  return [first, ...middle.map(trim), trim(last)].join("\n");
480
295
  }
296
+ /** Where `node` was written, as a library is told it. */
297
+ callSite(node) {
298
+ return { file: this.options.file, line: node.line.start, column: node.column.start };
299
+ }
300
+ /** What the compiler knows about each argument and the runtime cannot:
301
+ * its type, and the code it — or what it names — was written as. Built
302
+ * only if a library reads it. */
303
+ argumentInfo(args) {
304
+ return args.map((argument) => {
305
+ const declaration = this.declarationOf(argument);
306
+ const type = this.options.types?.typeOf.get(argument);
307
+ return {
308
+ type,
309
+ typeText: type ? formatType(type) : void 0,
310
+ source: this.writtenSource(argument),
311
+ declaration: declaration ? this.writtenSource(declaration) : void 0,
312
+ spread: argument.type === "SpreadElement"
313
+ };
314
+ });
315
+ }
316
+ /** The name a global is read by at `node`, or `undefined` when `node` is
317
+ * not a global — a local of the same name is the author's. */
318
+ globalName(node) {
319
+ if (node.type === "MemberExpression" && !node.optional) {
320
+ const base = this.globalName(node.object);
321
+ return base === void 0 ? void 0 : `${base}.${node.property.name}`;
322
+ }
323
+ if (node.type !== "Identifier") return void 0;
324
+ const id = this.scopes.bindingOf.get(node);
325
+ if (id === void 0) return node.name;
326
+ return this.scopes.bindings.get(id)?.kind === "global" ? node.name : void 0;
327
+ }
328
+ /** What a library says `receiver:method(...)` is. The last library
329
+ * loaded is asked first, so a project's own library can answer for a
330
+ * method an earlier one also claims; no answer leaves an ordinary Luau
331
+ * method call, which is what a value that answers to the method itself
332
+ * wants (`text:upper()`). */
481
333
  loweredMethodCall(node) {
482
334
  const lowerings = this.options.lowerings;
483
- if (!lowerings?.length) return void 0;
335
+ if (!lowerings?.some((l) => l.plugin.methodCall)) return void 0;
484
336
  const receiver = this.options.types?.typeOf.get(node.object);
337
+ const receiverGlobal = this.globalName(node.object);
338
+ const info = this.lazyArguments(node.arguments);
485
339
  for (let i = lowerings.length - 1; i >= 0; i--) {
486
- const answer = lowerings[i].plugin.methodCall?.({
340
+ const plugin = lowerings[i].plugin;
341
+ if (!plugin.methodCall) continue;
342
+ const answer = plugin.methodCall({
487
343
  method: node.method.name,
488
344
  receiver,
345
+ receiverGlobal,
489
346
  argumentCount: node.arguments.length,
347
+ at: this.callSite(node.method),
348
+ get arguments() {
349
+ return info();
350
+ },
351
+ use: (runtime2) => this.loweringRuntime(i, runtime2)
352
+ });
353
+ if (answer) {
354
+ return {
355
+ callee: answer.callee,
356
+ prepend: this.prepended(answer.prepend, i, node),
357
+ passReceiver: answer.passReceiver !== false
358
+ };
359
+ }
360
+ }
361
+ return void 0;
362
+ }
363
+ /** What a library says a call to a global — `print(x)` — is. */
364
+ loweredGlobalCall(node) {
365
+ const lowerings = this.options.lowerings;
366
+ if (!lowerings?.some((l) => l.plugin.globalCall)) return void 0;
367
+ const name = this.globalName(node.callee);
368
+ if (name === void 0) return void 0;
369
+ const info = this.lazyArguments(node.arguments);
370
+ for (let i = lowerings.length - 1; i >= 0; i--) {
371
+ const plugin = lowerings[i].plugin;
372
+ if (!plugin.globalCall) continue;
373
+ const answer = plugin.globalCall({
374
+ name,
375
+ at: this.callSite(node.callee),
376
+ get arguments() {
377
+ return info();
378
+ },
379
+ use: (runtime2) => this.loweringRuntime(i, runtime2)
380
+ });
381
+ if (answer) return { callee: answer.callee, prepend: this.prepended(answer.prepend, i, node) };
382
+ }
383
+ return void 0;
384
+ }
385
+ /** What a library says a global read as a value — `local p = print` — is. */
386
+ loweredGlobalValue(node) {
387
+ const lowerings = this.options.lowerings;
388
+ if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
389
+ const name = this.globalName(node);
390
+ if (name === void 0) return void 0;
391
+ for (let i = lowerings.length - 1; i >= 0; i--) {
392
+ const answer = lowerings[i].plugin.globalValue?.({
393
+ name,
394
+ at: this.callSite(node),
490
395
  use: (runtime2) => this.loweringRuntime(i, runtime2)
491
396
  });
492
- if (answer) return { callee: answer.callee, passReceiver: answer.passReceiver !== false };
397
+ if (answer !== void 0) return this.libraryExpression(answer, i, node);
493
398
  }
494
399
  return void 0;
495
400
  }
401
+ lazyArguments(args) {
402
+ let info;
403
+ return () => info ??= this.argumentInfo(args);
404
+ }
405
+ prepended(sources, index2, node) {
406
+ return (sources ?? []).flatMap((source) => {
407
+ const expression = this.libraryExpression(source, index2, node);
408
+ return expression ? [expression] : [];
409
+ });
410
+ }
411
+ /** Luau a library wrote as an expression. One that does not parse is the
412
+ * library's mistake, reported at the call it was written for. */
413
+ libraryExpression(source, index2, node) {
414
+ try {
415
+ return parseExpressionFromSource(source);
416
+ } catch (error) {
417
+ this.report(node, `'${this.options.lowerings[index2].from}' lowered this to Luau that does not parse: ${JSON.stringify(source)} (${error.message})`);
418
+ return void 0;
419
+ }
420
+ }
496
421
  helperDefinitions() {
497
422
  const out = [];
498
423
  const assign2 = this.helpers.get("assign");
@@ -520,12 +445,9 @@ var Lowerer = class {
520
445
  });
521
446
  continue;
522
447
  }
523
- out.push(...parseLuau(source.replace(/__NAME__/g, name)).body.statements);
524
- }
525
- const console = this.helpers.get("console");
526
- if (console) {
527
448
  const lines = this.options.module?.lines ?? "nil";
528
- out.push(...parseLuau(CONSOLE_RUNTIME.replace(/__NAME__/g, console).replace(/__LINES__/g, lines)).body.statements);
449
+ const fail = this.options.module?.fail ?? "nil";
450
+ out.push(...parseLuau(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
529
451
  }
530
452
  const concat = this.helpers.get("concat");
531
453
  if (concat) {
@@ -694,7 +616,7 @@ var Lowerer = class {
694
616
  break;
695
617
  }
696
618
  case "ReturnStatement":
697
- if (declaration.arguments.length) this.report(declaration, "A module cannot return a value; export it instead");
619
+ if (declaration.argument) this.report(declaration, "A module cannot return a value; export it instead");
698
620
  body.push(returns([]));
699
621
  break;
700
622
  case "ImportStatement":
@@ -873,7 +795,7 @@ var Lowerer = class {
873
795
  case "CallStatement": {
874
796
  const chain = optionalChain(node.expression);
875
797
  if (chain) return [this.optionalCallStatement(chain)];
876
- const expression = this.expression(node.expression);
798
+ const expression = this.callValues(node.expression);
877
799
  if (expression.type !== "CallExpression" && expression.type !== "MethodCallExpression") {
878
800
  this.report(node, "A statement must be a call");
879
801
  return [];
@@ -926,23 +848,26 @@ var Lowerer = class {
926
848
  }];
927
849
  case "GenericForStatement": {
928
850
  const prelude = [];
929
- const variables = node.variables.map((target) => {
930
- if (target.type === "IdentifierPattern") return this.name(target.name);
931
- const temp = this.names.fresh("item");
932
- prelude.push(...this.destructure(target, identifier(temp), "declare"));
933
- return temp;
934
- });
935
- if (variables.length === 1 && this.options.types?.iteratesValues.has(node)) {
936
- variables.unshift(this.names.fresh("_"));
851
+ let item;
852
+ if (node.variable.type === "IdentifierPattern") {
853
+ item = this.name(node.variable.name);
854
+ } else {
855
+ item = this.names.fresh("item");
856
+ prelude.push(...this.destructure(node.variable, identifier(item), "declare"));
937
857
  }
858
+ const form = this.options.types?.loops.get(node) ?? { walks: "values", viaIter: false };
859
+ let source = this.expression(node.iterator);
860
+ if (form.viaIter) source = methodCall(source, "__iter", []);
861
+ const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
862
+ const iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
938
863
  return [genericFor(
939
864
  variables,
940
- node.iterators.map((e) => this.expression(e)),
865
+ iterators,
941
866
  this.loopBody51(this.block(node.body, prelude)).statements
942
867
  )];
943
868
  }
944
869
  case "ReturnStatement":
945
- return [returns(this.values(node.arguments))];
870
+ return [returns(node.argument ? [this.expression(node.argument)] : [])];
946
871
  case "BreakStatement":
947
872
  return [{ type: "BreakStatement", ...spanOf(node) }];
948
873
  case "ContinueStatement":
@@ -986,7 +911,7 @@ var Lowerer = class {
986
911
  // end
987
912
  // function Dog.speak(this) ... end
988
913
  //
989
- // `new Dog(x)` is `Dog.new(x)`, `dog:speak()` passes the instance
914
+ // `Dog.new(x)` builds one, `dog:speak()` passes the instance
990
915
  // as `this`, and `super.speak()` is `Animal.speak(this)` — the
991
916
  // base's own function, run on this instance.
992
917
  // ============================================================
@@ -1012,6 +937,7 @@ var Lowerer = class {
1012
937
  classRuntimeStatements() {
1013
938
  if (!this.classHelpers) return [];
1014
939
  const { build, accessors } = this.classHelpers;
940
+ const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
1015
941
  return parseLuau(`
1016
942
  local function ${build}(base)
1017
943
  local class = { __getters = {}, __setters = {} }
@@ -1022,6 +948,9 @@ local function ${build}(base)
1022
948
  setmetatable(class, { __index = base })
1023
949
  setmetatable(class.__getters, { __index = base.__getters })
1024
950
  setmetatable(class.__setters, { __index = base.__setters })
951
+ for _, key in ipairs(${metamethods}) do
952
+ class[key] = base[key]
953
+ end
1025
954
  end
1026
955
  return class
1027
956
  end
@@ -1084,6 +1013,7 @@ end
1084
1013
  }
1085
1014
  break;
1086
1015
  case "ClassMethod":
1016
+ if (member2.isAbstract) break;
1087
1017
  out.push(this.functionStatement(
1088
1018
  member(self, member2.name.name),
1089
1019
  member2,
@@ -1105,7 +1035,7 @@ end
1105
1035
  }
1106
1036
  }
1107
1037
  out.push(this.classInit(node, self));
1108
- out.push(this.classNew(node, self));
1038
+ if (!node.isAbstract) out.push(this.classNew(node, self));
1109
1039
  out.push(callStatement(call(identifier(accessors), [self])));
1110
1040
  } finally {
1111
1041
  this.classContext = previous;
@@ -1383,7 +1313,11 @@ end
1383
1313
  }
1384
1314
  switch (node.type) {
1385
1315
  case "Identifier":
1386
- return this.reference(node);
1316
+ if (node.name === "scriptArgs" && this.isLanguageGlobal(node)) {
1317
+ this.usesScriptArgs = true;
1318
+ return identifier("scriptArgs");
1319
+ }
1320
+ return this.loweredGlobalValue(node) ?? this.reference(node);
1387
1321
  case "NilLiteral":
1388
1322
  return nil();
1389
1323
  case "BooleanLiteral":
@@ -1392,8 +1326,6 @@ end
1392
1326
  return number(node.value, this.numberRaw(node));
1393
1327
  case "StringLiteral":
1394
1328
  return string(node.value);
1395
- case "VarargExpression":
1396
- return vararg();
1397
1329
  // Only a recovering parse makes one, and a syntax error stops the build.
1398
1330
  case "ErrorExpression":
1399
1331
  this.report(node, "Syntax error");
@@ -1414,11 +1346,6 @@ end
1414
1346
  }
1415
1347
  case "UnaryExpression":
1416
1348
  return unary(node.operator, this.expression(node.argument));
1417
- case "NewExpression":
1418
- return call(
1419
- member(this.expression(node.callee), "new"),
1420
- this.values(node.arguments)
1421
- );
1422
1349
  case "SuperExpression":
1423
1350
  return this.superClassReference(node);
1424
1351
  case "SpreadElement":
@@ -1426,12 +1353,15 @@ end
1426
1353
  return nil();
1427
1354
  case "ClassExpression":
1428
1355
  return this.classExpression(node);
1429
- case "MemberExpression":
1430
- case "IndexExpression":
1431
1356
  case "CallExpression":
1432
- case "MethodCallExpression": {
1357
+ case "MethodCallExpression":
1358
+ return this.callValues(node);
1359
+ case "MemberExpression":
1360
+ case "IndexExpression": {
1433
1361
  const chain = optionalChain(node);
1434
1362
  if (chain) return this.optionalChainExpression(chain);
1363
+ const global = node.type === "MemberExpression" ? this.loweredGlobalValue(node) : void 0;
1364
+ if (global) return global;
1435
1365
  return this.link(node, this.expression(linkObject(node)));
1436
1366
  }
1437
1367
  case "ParenthesizedExpression":
@@ -1451,14 +1381,47 @@ end
1451
1381
  };
1452
1382
  }
1453
1383
  }
1384
+ /** A call. */
1385
+ callValues(node) {
1386
+ const chain = optionalChain(node);
1387
+ if (chain) return this.optionalChainExpression(chain);
1388
+ const object = linkObject(node);
1389
+ if (node.type === "CallExpression" && object.type === "Identifier") {
1390
+ return this.link(node, this.reference(object));
1391
+ }
1392
+ return this.link(node, this.expression(object));
1393
+ }
1394
+ /** A call is one value in tilua, and Luau would hand on every value one
1395
+ * in the last place of an argument list or an array returns. Types say
1396
+ * how many a call returns; where a type says nothing — `any`, `unknown`
1397
+ * — the call is kept to one: `f((g()))`. */
1398
+ keptToOne(nodes, values) {
1399
+ const node = nodes[nodes.length - 1];
1400
+ const last = values[values.length - 1];
1401
+ if (!node || !last || !expandsToMany(last)) return values;
1402
+ const type = this.options.types?.typeOf.get(node);
1403
+ if (type && type.kind !== "any" && type.kind !== "unknown") return values;
1404
+ return [...values.slice(0, -1), parenthesized(last)];
1405
+ }
1406
+ /** Is this `scriptArgs` the language's own, not a name the file declared? */
1407
+ isLanguageGlobal(node) {
1408
+ const id = this.bindingIdOf(node);
1409
+ return id === void 0 || this.scopes.bindings.get(id)?.kind === "global";
1410
+ }
1454
1411
  /** A list of values — a call's arguments, a `return`'s, a declaration's,
1455
1412
  * an assignment's. `f(a, ...xs)` is Lua's own last-value expansion,
1456
1413
  * `f(a, table.unpack(xs))`; a spread anywhere else cannot be, since only
1457
1414
  * the last value of a list expands, so the whole list is built as an
1458
- * array first and that is what expands. */
1459
- values(list) {
1415
+ * array first and that is what expands.
1416
+ *
1417
+ * `arguments` is set for a call's arguments, where Luau hands on every
1418
+ * value the last one returns — see `keptToOne`. */
1419
+ values(list, asArguments = false) {
1460
1420
  const spreads = list.filter((a) => a.type === "SpreadElement");
1461
- if (!spreads.length) return list.map((a) => this.expression(a));
1421
+ if (!spreads.length) {
1422
+ const lowered = list.map((a) => this.expression(a));
1423
+ return asArguments ? this.keptToOne(list, lowered) : lowered;
1424
+ }
1462
1425
  const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
1463
1426
  const last = list[list.length - 1];
1464
1427
  if (spreads.length === 1 && last.type === "SpreadElement") {
@@ -1477,15 +1440,21 @@ end
1477
1440
  return member(object, node.property.name);
1478
1441
  case "IndexExpression":
1479
1442
  return index(object, this.expression(node.index));
1480
- case "CallExpression":
1481
- return call(object, this.values(node.arguments));
1443
+ case "CallExpression": {
1444
+ const lowered = this.loweredGlobalCall(node);
1445
+ const args = this.values(node.arguments, true);
1446
+ if (!lowered) return call(object, args);
1447
+ return call(calleePath(lowered.callee), [...lowered.prepend, ...args]);
1448
+ }
1482
1449
  case "MethodCallExpression": {
1483
- const consoleCall = this.consoleCall(node);
1484
- if (consoleCall) return consoleCall;
1485
- const args = this.values(node.arguments);
1486
1450
  const lowered = this.loweredMethodCall(node);
1451
+ const args = this.values(node.arguments, true);
1487
1452
  if (lowered) {
1488
- return call(calleePath(lowered.callee), lowered.passReceiver ? [object, ...args] : args);
1453
+ return call(calleePath(lowered.callee), [
1454
+ ...lowered.prepend,
1455
+ ...lowered.passReceiver ? [object] : [],
1456
+ ...args
1457
+ ]);
1489
1458
  }
1490
1459
  if (!isLuauName(node.method.name)) {
1491
1460
  this.report(node.method, `'${node.method.name}' is a Luau keyword and cannot be called with ':'`);
@@ -1713,7 +1682,8 @@ end
1713
1682
  * runs of plain elements and the spread arrays are joined by `concat`. */
1714
1683
  arrayExpression(node) {
1715
1684
  if (!node.elements.some((e) => e.type === "SpreadElement")) {
1716
- return table(node.elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) })));
1685
+ const elements = node.elements;
1686
+ return table(this.keptToOne(elements, elements.map((e) => this.expression(e))).map((value) => ({ type: "TableFieldPositional", value })));
1717
1687
  }
1718
1688
  const parts = [];
1719
1689
  let current = [];
@@ -1898,10 +1868,6 @@ function mentions(expression, name) {
1898
1868
  walk(expression);
1899
1869
  return found;
1900
1870
  }
1901
- function isFunctionType(type) {
1902
- if (type.kind === "function") return true;
1903
- return type.kind === "intersection" && type.types.some((t) => t.kind === "function");
1904
- }
1905
1871
  function allNodes(root) {
1906
1872
  const out = [];
1907
1873
  const visit = (value) => {
@@ -1916,6 +1882,24 @@ function allNodes(root) {
1916
1882
  visit(root);
1917
1883
  return out;
1918
1884
  }
1885
+ var CLASS_METAMETHODS = [
1886
+ "__add",
1887
+ "__sub",
1888
+ "__mul",
1889
+ "__div",
1890
+ "__idiv",
1891
+ "__mod",
1892
+ "__pow",
1893
+ "__unm",
1894
+ "__concat",
1895
+ "__len",
1896
+ "__eq",
1897
+ "__lt",
1898
+ "__le",
1899
+ "__call",
1900
+ "__tostring",
1901
+ "__iter"
1902
+ ];
1919
1903
 
1920
1904
  // src/lowering.ts
1921
1905
  import { pathToFileURL } from "url";
@@ -1960,7 +1944,7 @@ import {
1960
1944
  ParseError,
1961
1945
  LexError
1962
1946
  } from "@tilua/parser";
1963
- import { parse as parseLuau2, parseExpressionFromSource, print } from "luau-parser";
1947
+ import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
1964
1948
 
1965
1949
  // src/linemap.ts
1966
1950
  function statementsOf(node) {
@@ -2043,16 +2027,19 @@ async function bundle(options) {
2043
2027
  const file = pending.shift();
2044
2028
  if (modules.has(file)) continue;
2045
2029
  const source = sources.get(file);
2030
+ const shown = relative(root, file).replace(/\\/g, "/");
2046
2031
  const lowered = lower(source.program, source.scopes, {
2047
2032
  names,
2048
2033
  types: analysis.types.get(file),
2049
2034
  lowerings,
2050
2035
  target: options.target ?? config?.target,
2051
2036
  source: source.text,
2037
+ file: shown,
2052
2038
  module: {
2053
2039
  name: source.name,
2054
2040
  require: requireExpression,
2055
2041
  lines: `${G}.lines`,
2042
+ fail: `${G}.fail`,
2056
2043
  resolve: (specifier) => {
2057
2044
  const target = resolveModulePath(file, specifier, config);
2058
2045
  if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
@@ -2062,7 +2049,6 @@ async function bundle(options) {
2062
2049
  }
2063
2050
  });
2064
2051
  for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2065
- const shown = relative(root, file).replace(/\\/g, "/");
2066
2052
  for (const statement of allStatements(lowered.statements)) {
2067
2053
  const line = lowered.origins.get(statement);
2068
2054
  if (line !== void 0) origins.set(statement, { file: shown, line });
@@ -2071,7 +2057,8 @@ async function bundle(options) {
2071
2057
  name: source.name,
2072
2058
  statements: lowered.statements,
2073
2059
  exportsName: lowered.exportsName,
2074
- info: lowered.module
2060
+ info: lowered.module,
2061
+ usesScriptArgs: lowered.usesScriptArgs
2075
2062
  });
2076
2063
  }
2077
2064
  for (const [file, fileDirectives] of directives) {
@@ -2096,6 +2083,9 @@ async function bundle(options) {
2096
2083
  }
2097
2084
  const program2 = parseLuau2(runtime(G));
2098
2085
  const assignment = program2.body.statements[1];
2086
+ if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2087
+ program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2088
+ }
2099
2089
  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;
2100
2090
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
2101
2091
  for (const module of modules.values()) {
@@ -2112,13 +2102,23 @@ async function bundle(options) {
2112
2102
  fields.push(field("load", functionExpression(functionBody([module.exportsName], module.statements, true))));
2113
2103
  modulesTable.fields.push({ type: "TableFieldComputed", key: string(module.name), value: table(fields) });
2114
2104
  }
2115
- const start = call(requireExpression, [string(sources.get(entry).name)]);
2116
- program2.body.statements.push(modules.get(entry).info.exports ? returns([start]) : callStatement(start));
2105
+ const ok = names.fresh("ok");
2106
+ const result = names.fresh("result");
2107
+ const entryStatements = parseLuau2(`
2108
+ local ${ok}, ${result} = xpcall(function()
2109
+ return ${G}.require(${JSON.stringify(sources.get(entry).name)})
2110
+ end, ${G}.fail)
2111
+ if not ${ok} then
2112
+ error(${result}, 0)
2113
+ end
2114
+ ${modules.get(entry).info.exports ? `return ${result}` : ""}
2115
+ `).body.statements;
2116
+ program2.body.statements.push(...entryStatements);
2117
2117
  const map = buildLineMap(program2, parseLuau2(print(program2)), origins);
2118
2118
  if (map.size) {
2119
- program2.body.statements.splice(program2.body.statements.length - 1, 0, assign(
2119
+ program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, assign(
2120
2120
  [member(identifier(G), "lines")],
2121
- [parseExpressionFromSource(lineMapSource(map))]
2121
+ [parseExpressionFromSource2(lineMapSource(map))]
2122
2122
  ));
2123
2123
  }
2124
2124
  return { code: print(program2) + "\n", modules: moduleNames, diagnostics };
@@ -2128,6 +2128,52 @@ function runtime(G) {
2128
2128
  local ${G}
2129
2129
  ${G} = {
2130
2130
  modules = {},
2131
+ -- Filled in just before the entry runs: bundle line -> { file, line }.
2132
+ lines = nil,
2133
+ -- What an error nothing caught says, in the project's files: the bundle's
2134
+ -- own positions in the message are replaced by the places they came from,
2135
+ -- and a traceback of those places follows. A message that is not a string
2136
+ -- is left as it is, and a position that is not this bundle's (a library
2137
+ -- that raised its error with one already mapped) is left alone.
2138
+ fail = function(message)
2139
+ if type(message) ~= "string" then
2140
+ return message
2141
+ end
2142
+ local lines = ${G}.lines
2143
+ if lines == nil then
2144
+ return debug.traceback(message, 2)
2145
+ end
2146
+ local chunk
2147
+ if debug.info ~= nil then
2148
+ chunk = debug.info(1, "s")
2149
+ else
2150
+ chunk = debug.getinfo(1, "S").short_src
2151
+ end
2152
+ local prefix = chunk:gsub("%p", "%%%0")
2153
+ local function place(line)
2154
+ local origin = lines[tonumber(line)]
2155
+ if origin == nil then
2156
+ return nil
2157
+ end
2158
+ return origin[1] .. ":" .. tostring(origin[2])
2159
+ end
2160
+ local mapped = message:gsub(prefix .. ":(%d+)", function(line)
2161
+ return place(line)
2162
+ end)
2163
+ local frames = {}
2164
+ for frame in debug.traceback("", 2):gmatch("[^\\n]+") do
2165
+ local line = frame:match(prefix .. ":(%d+)")
2166
+ local at = line ~= nil and place(line) or nil
2167
+ if at ~= nil then
2168
+ local name = frame:match("function ([%w_.:]+)")
2169
+ frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2170
+ end
2171
+ end
2172
+ if #frames == 0 then
2173
+ return mapped
2174
+ end
2175
+ return mapped .. "\\n" .. table.concat(frames, "\\n")
2176
+ end,
2131
2177
  records = {},
2132
2178
  require = function(name)
2133
2179
  local record = ${G}.records[name]