@tilua/compiler 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,9 +54,10 @@ JavaScript module the build loads.
54
54
  | tilua | Luau |
55
55
  |---|---|
56
56
  | `const { a, b } = value` | `local a, b = value.a, value.b` |
57
- | `const [x, ...rest] = list` | `local x = list[1]; local rest = table.move(list, 2, #list, 1, {})` |
57
+ | `const [x, ...rest] = list` | `local x = list[1]; local rest = table.move(list, 2, #list, 1, {})` — `{}` written down as an array |
58
58
  | `const { a = 1 } = t` | `local a = t.a; if a == nil then a = 1 end` |
59
- | `[1, 2]` / `{ a: 1, b }` | `{ 1, 2 }` / `{ a = 1, b = b }` |
59
+ | `[1, 2]` / `{ a: 1, b }` | `tilua_asArray({ 1, 2 })` / `{ a = 1, b = b }` — see `typeof` below |
60
+ | `typeof v` | `tilua_typeof(v)`, or `tilua_typeof(v, "array")` where `v`'s type already says which table it is |
60
61
  | `{ ...base, a: 1 }` / `[...xs, 1]` | a small helper copying the parts in order |
61
62
  | `` `${a} any` `` | `("%s any"):format(tostring(a))` |
62
63
  | `a?.b` | `if a == nil then nil else a.b` |
@@ -77,6 +78,26 @@ JavaScript module the build loads.
77
78
  | `x as T`, `x satisfies T`, types, `declare` | removed |
78
79
  | `names:filter(f)` | whatever the type library that declared `filter` says — see below |
79
80
 
81
+ ## `typeof`, arrays and objects
82
+
83
+ Luau cannot tell an array from an object: both are tables, and an empty one
84
+ has nothing to go by. So every array the program makes is written down as
85
+ one where it is made — `[a, b]`, a rest parameter, `scriptArgs`, what
86
+ `:map`, `:filter`, `:keys` and the rest answer — in a table that holds its
87
+ keys weakly, one for the whole bundle. `tilua_asArray(t)` does that and hands
88
+ `t` back.
89
+
90
+ `typeof v` is Luau's `typeof` (Lua 5.1's `type`) for anything but a table.
91
+ For a table it answers, in order:
92
+
93
+ 1. what `v`'s type says, where every table it can be is an array, or every
94
+ one an object — passed by the compiler, so nothing is looked up;
95
+ 2. `"array"` if it was written down as one;
96
+ 3. `"object"` if it has a metatable — an instance of a class;
97
+ 4. `"array"` if it has a first element, and `"object"` otherwise: a table the
98
+ program did not make (one `JSONDecode` answered) is read by its shape, and
99
+ an empty one of those is an `"object"`.
100
+
80
101
  ## Classes
81
102
 
82
103
  `class` is sugar over the Lua idiom, and it lowers to that idiom and nothing
@@ -185,7 +206,7 @@ argument (`arguments`: type, the code it was written as, and what a name was
185
206
  declared as). An answer can `prepend` Luau expressions ahead of the written
186
207
  arguments. `globalCall` and `globalValue` answer for a global, such as
187
208
  `print(x)` or a bare `print`. A runtime can read `__LINES__` to reach the
188
- bundle's line map. See the `@tilua/parser` README for the details.
209
+ bundle's line map, and wraps a new array it answers with in `__ARRAY__(...)`. See the `@tilua/parser` README for the details.
189
210
  `console:log` is not built into the compiler: `@tilua-types/lua` implements it
190
211
  with these hooks.
191
212
 
@@ -14,6 +14,7 @@ import { formatType } from "@tilua/parser";
14
14
  import { parse as parseLuau, parseExpressionFromSource } from "luau-parser";
15
15
 
16
16
  // src/luau.ts
17
+ import { LUAU_KEYWORDS, isLuauName } from "@tilua/parser";
17
18
  var NO_SPAN = { line: { start: 0, end: 0 }, column: { start: 0, end: 0 } };
18
19
  var span = () => ({ line: { ...NO_SPAN.line }, column: { ...NO_SPAN.column } });
19
20
  var identifier = (name) => ({ type: "Identifier", name, ...span() });
@@ -74,32 +75,6 @@ var functionBody = (params, statements, hasVarargs = false) => ({
74
75
  });
75
76
  var localFunction = (name, func) => ({ type: "LocalFunctionStatement", name: identifier(name), func, ...span() });
76
77
  var program = (statements) => ({ type: "Program", body: block(statements), ...span() });
77
- var LUAU_KEYWORDS = /* @__PURE__ */ new Set([
78
- "and",
79
- "break",
80
- "do",
81
- "else",
82
- "elseif",
83
- "end",
84
- "false",
85
- "for",
86
- "function",
87
- "if",
88
- "in",
89
- "local",
90
- "nil",
91
- "not",
92
- "or",
93
- "repeat",
94
- "return",
95
- "then",
96
- "true",
97
- "until",
98
- "while"
99
- ]);
100
- function isLuauName(name) {
101
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !LUAU_KEYWORDS.has(name);
102
- }
103
78
 
104
79
  // src/names.ts
105
80
  var Names = class _Names {
@@ -142,6 +117,7 @@ var Names = class _Names {
142
117
 
143
118
  // src/language.ts
144
119
  import { readFileSync } from "fs";
120
+ import { luauString } from "@tilua/parser";
145
121
  var read = (name) => readFileSync(new URL(`../runtime/${name}`, import.meta.url), "utf8");
146
122
  var implemented = (source) => new Set([...source.matchAll(/function __NAME__\.(\w+)/g)].map((m) => m[1]));
147
123
  var array = read("array.luau");
@@ -175,7 +151,7 @@ function objectKeys(type) {
175
151
  orders.push(keys);
176
152
  }
177
153
  const order = JSON.stringify(orders[0] ?? []);
178
- const known = orders.length && orders.every((keys) => JSON.stringify(keys) === order) ? `{ ${orders[0].map((key) => JSON.stringify(key)).join(", ")} }` : "nil";
154
+ const known = orders.length && orders.every((keys) => JSON.stringify(keys) === order) ? `{ ${orders[0].map((key) => luauString(key)).join(", ")} }` : "nil";
179
155
  return { known, open: String(open) };
180
156
  }
181
157
  var plugin = {
@@ -234,11 +210,11 @@ var Lowerer = class {
234
210
  origins = /* @__PURE__ */ new WeakMap();
235
211
  run() {
236
212
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
213
+ const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))])] : [];
237
214
  const helpers = this.helperDefinitions();
238
215
  const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
239
- const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: vararg() }])])] : [];
240
216
  return {
241
- statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
217
+ statements: [...captured, ...helpers, ...scriptArgs, ...this.classRuntimeStatements(), ...body],
242
218
  exportsName: this.exportsName,
243
219
  module: this.info,
244
220
  diagnostics: this.diagnostics,
@@ -288,11 +264,36 @@ var Lowerer = class {
288
264
  helper(kind) {
289
265
  let name = this.helpers.get(kind);
290
266
  if (!name) {
291
- name = this.names.fresh(`tilua_${kind}`);
267
+ name = this.names.fresh(`tilua_${kind === "array" ? "asArray" : kind}`);
292
268
  this.helpers.set(kind, name);
293
269
  }
294
270
  return identifier(name);
295
271
  }
272
+ // `typeof v` tells an array from an object, which Luau cannot: both are
273
+ // tables. So every array the program makes — `[a, b]`, a rest parameter,
274
+ // what `:map` answers — is written down as one, in a table that holds its
275
+ // keys weakly, and `typeof` looks there. A table nothing wrote down (one a
276
+ // Lua library made) is told by its type where that says, and by its shape
277
+ // where it does not.
278
+ /** `value`, a new array, written down as one. */
279
+ array(value) {
280
+ const marked = call(this.helper("array"), [value]);
281
+ this.marked.add(marked);
282
+ return marked;
283
+ }
284
+ /** The calls `array` made: each answers the one table it was given. */
285
+ marked = /* @__PURE__ */ new WeakSet();
286
+ /** Luau reaching the record of arrays: the bundle's, or this file's own. */
287
+ arrays() {
288
+ return this.options.module?.arrays ?? this.helper("arrays").name;
289
+ }
290
+ typeofExpression(node) {
291
+ const known = this.options.types?.typeofTables.get(node);
292
+ let argument = node.argument;
293
+ while (argument.type === "ParenthesizedExpression") argument = argument.expression;
294
+ const value = this.expression(argument);
295
+ return call(this.helper("typeof"), known ? [value, string(known)] : [this.keptToOne(value)]);
296
+ }
296
297
  /** The local a library's runtime table gets in this file, emitting it the
297
298
  * first time something asks. Keyed by library and by the key the library
298
299
  * itself chose, so two libraries' tables never collide. */
@@ -487,6 +488,46 @@ var Lowerer = class {
487
488
  }
488
489
  helperDefinitions() {
489
490
  const out = [];
491
+ const runtimes = this.runtimeDefinitions();
492
+ if (this.helpers.has("concat")) this.helper("array");
493
+ const marker = this.helpers.get("array");
494
+ const typeOf = this.helpers.get("typeof");
495
+ if (marker || typeOf) {
496
+ const arrays = this.arrays();
497
+ const own = this.helpers.get("arrays");
498
+ if (own) {
499
+ out.push(...parseLuau(`local ${own} = ${this.builtin("setmetatable").name}({}, { __mode = "k" })`).body.statements);
500
+ }
501
+ if (marker) {
502
+ out.push(...parseLuau(`
503
+ local function ${marker}(t)
504
+ ${arrays}[t] = true
505
+ return t
506
+ end`).body.statements);
507
+ }
508
+ if (typeOf) {
509
+ out.push(...parseLuau(`
510
+ local function ${typeOf}(value, known)
511
+ local name = ${this.builtin(this.lua51 ? "type" : "typeof").name}(value)
512
+ if name ~= "table" then
513
+ return name
514
+ end
515
+ if known ~= nil then
516
+ return known
517
+ end
518
+ if ${arrays}[value] then
519
+ return "array"
520
+ end
521
+ if ${this.builtin("getmetatable").name}(value) ~= nil then
522
+ return "object"
523
+ end
524
+ if #value > 0 then
525
+ return "array"
526
+ end
527
+ return "object"
528
+ end`).body.statements);
529
+ }
530
+ }
490
531
  const assign2 = this.helpers.get("assign");
491
532
  if (assign2) {
492
533
  out.push(localFunction(assign2, functionBody(["target"], [
@@ -501,25 +542,11 @@ var Lowerer = class {
501
542
  returns([identifier("target")])
502
543
  ], true)));
503
544
  }
504
- for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
505
- const { plugin: plugin2, from } = this.lowerings[index2];
506
- const source = plugin2.runtime?.[runtime2];
507
- if (source === void 0) {
508
- this.diagnostics.push({
509
- message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
510
- line: 1,
511
- column: 1
512
- });
513
- continue;
514
- }
515
- const lines = this.options.module?.lines ?? "nil";
516
- const fail = this.options.module?.fail ?? "nil";
517
- out.push(...parseLuau(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
518
- }
545
+ out.push(...runtimes);
519
546
  const concat = this.helpers.get("concat");
520
547
  if (concat) {
521
548
  out.push(localFunction(concat, functionBody([], [
522
- local(["result"], [table([])]),
549
+ local(["result"], [this.array(table([]))]),
523
550
  numericFor("i", number(1), selectCount(this.builtin("select")), [
524
551
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
525
552
  callStatement(call(member(this.builtin("table"), "move"), [
@@ -535,6 +562,26 @@ var Lowerer = class {
535
562
  }
536
563
  return out;
537
564
  }
565
+ /** The runtime tables the file's libraries were asked for. */
566
+ runtimeDefinitions() {
567
+ const out = [];
568
+ for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
569
+ const { plugin: plugin2, from } = this.lowerings[index2];
570
+ const source = plugin2.runtime?.[runtime2];
571
+ if (source === void 0) {
572
+ this.diagnostics.push({
573
+ message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
574
+ line: 1,
575
+ column: 1
576
+ });
577
+ continue;
578
+ }
579
+ const lines = this.options.module?.lines ?? "nil";
580
+ const fail = this.options.module?.fail ?? "nil";
581
+ 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);
582
+ }
583
+ return out;
584
+ }
538
585
  // --------------------------------------------------------
539
586
  // Modules
540
587
  // --------------------------------------------------------
@@ -1287,7 +1334,7 @@ end
1287
1334
  number(pattern.elements.length + 1),
1288
1335
  unary("#", source),
1289
1336
  number(1),
1290
- table([])
1337
+ this.array(table([]))
1291
1338
  ]);
1292
1339
  const rest = this.restTarget(pattern.rest, elements, mode, after);
1293
1340
  after.push(...rest.then);
@@ -1341,7 +1388,7 @@ end
1341
1388
  const names = [];
1342
1389
  for (const p of params) {
1343
1390
  if (p.rest) {
1344
- prelude.push(local([this.name(p.name)], [table([{ type: "TableFieldPositional", value: vararg() }])]));
1391
+ prelude.push(local([this.name(p.name)], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))]));
1345
1392
  continue;
1346
1393
  }
1347
1394
  const name = p.pattern ? this.names.fresh("arg") : this.name(p.name);
@@ -1399,6 +1446,7 @@ end
1399
1446
  return binary(node.operator, left, right);
1400
1447
  }
1401
1448
  case "UnaryExpression":
1449
+ if (node.operator === "typeof") return this.typeofExpression(node);
1402
1450
  return unary(node.operator, this.expression(node.argument));
1403
1451
  case "SuperExpression":
1404
1452
  return this.superClassReference(node);
@@ -1439,6 +1487,9 @@ end
1439
1487
  callValues(node) {
1440
1488
  const chain = optionalChain(node);
1441
1489
  if (chain) return this.optionalChainExpression(chain);
1490
+ if (node.type === "CallExpression" && (node.callee.type === "SuperExpression" || node.callee.type === "MemberExpression" && node.callee.object.type === "SuperExpression")) {
1491
+ return this.expression(node);
1492
+ }
1442
1493
  const object2 = linkObject(node);
1443
1494
  if (node.type === "CallExpression" && object2.type === "Identifier") {
1444
1495
  return this.link(node, this.reference(object2));
@@ -1450,7 +1501,7 @@ end
1450
1501
  * kept to one there, `f((g()))`. Every value is taken only where the
1451
1502
  * program asks for them, as an array's last element: `[g()]`. */
1452
1503
  keptToOne(value) {
1453
- return expandsToMany(value) ? parenthesized(value) : value;
1504
+ return expandsToMany(value) && !this.marked.has(value) ? parenthesized(value) : value;
1454
1505
  }
1455
1506
  /** Is this `scriptArgs` the language's own, not a name the file declared? */
1456
1507
  isLanguageGlobal(node) {
@@ -1731,7 +1782,7 @@ end
1731
1782
  arrayExpression(node) {
1732
1783
  const elements = packs(node.elements).items;
1733
1784
  if (!elements.some((e) => e.type === "SpreadElement")) {
1734
- return table(elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) })));
1785
+ return this.array(table(elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) }))));
1735
1786
  }
1736
1787
  const parts = [];
1737
1788
  let current = [];
@@ -2021,11 +2072,13 @@ import {
2021
2072
  applyDirectives,
2022
2073
  UNUSED_EXPECT_ERROR,
2023
2074
  ParseError,
2024
- LexError
2075
+ LexError,
2076
+ luauString as luauString3
2025
2077
  } from "@tilua/parser";
2026
2078
  import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
2027
2079
 
2028
2080
  // src/linemap.ts
2081
+ import { luauString as luauString2 } from "@tilua/parser";
2029
2082
  function statementsOf(node) {
2030
2083
  const out = [];
2031
2084
  const visit = (value) => {
@@ -2058,7 +2111,7 @@ function buildLineMap(emitted, printed, origins) {
2058
2111
  }
2059
2112
  function lineMapSource(map) {
2060
2113
  const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
2061
- const quoted = (s) => JSON.stringify(s.replace(/\\/g, "/"));
2114
+ const quoted = (s) => luauString2(s.replace(/\\/g, "/"));
2062
2115
  return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
2063
2116
  }
2064
2117
 
@@ -2119,6 +2172,7 @@ async function bundle(options) {
2119
2172
  require: requireExpression,
2120
2173
  lines: `${G}.lines`,
2121
2174
  fail: `${G}.fail`,
2175
+ arrays: `${G}.arrays`,
2122
2176
  resolve: (specifier) => {
2123
2177
  const target = resolveModulePath(file, specifier, config);
2124
2178
  if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
@@ -2164,6 +2218,7 @@ async function bundle(options) {
2164
2218
  const assignment = program2.body.statements[1];
2165
2219
  if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2166
2220
  program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2221
+ program2.body.statements.push(...parseLuau2(`${G}.arrays[scriptArgs] = true`).body.statements);
2167
2222
  }
2168
2223
  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;
2169
2224
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
@@ -2185,7 +2240,7 @@ async function bundle(options) {
2185
2240
  const result = names.fresh("result");
2186
2241
  const entryStatements = parseLuau2(`
2187
2242
  local ${ok}, ${result} = xpcall(function()
2188
- return ${G}.require(${JSON.stringify(sources.get(entry).name)})
2243
+ return ${G}.require(${luauString3(sources.get(entry).name)})
2189
2244
  end, ${G}.fail)
2190
2245
  if not ${ok} then
2191
2246
  error(${result}, 0)
@@ -2207,6 +2262,9 @@ function runtime(G) {
2207
2262
  local ${G}
2208
2263
  ${G} = {
2209
2264
  modules = {},
2265
+ -- Every array the program made, so \`typeof\` can tell one from an object.
2266
+ -- The keys are held weakly: being written down keeps no array alive.
2267
+ arrays = setmetatable({}, { __mode = "k" }),
2210
2268
  -- Filled in just before the entry runs: bundle line -> { file, line }.
2211
2269
  lines = nil,
2212
2270
  -- What an error nothing caught says, in the project's files: the bundle's
@@ -2214,19 +2272,64 @@ ${G} = {
2214
2272
  -- and a traceback of those places follows. A message that is not a string
2215
2273
  -- is left as it is, and a position that is not this bundle's (a library
2216
2274
  -- that raised its error with one already mapped) is left alone.
2275
+ --
2276
+ -- It is an \`xpcall\` handler, and a handler that fails loses the error
2277
+ -- altogether: Luau reports only "error in error handling". So the work is
2278
+ -- done under \`pcall\`, leaning on nothing it has not checked is there (an
2279
+ -- executor may lack \`debug.info\`, or refuse it), and an error it cannot
2280
+ -- map is still answered as Luau raised it, with why.
2217
2281
  fail = function(message)
2282
+ -- Taken first, before anything that can fail. Level 3 is the frame
2283
+ -- that raised: past \`pcall\` and this handler.
2284
+ local trace
2285
+ if debug ~= nil and debug.traceback ~= nil then
2286
+ local ok, text = pcall(debug.traceback, "", 3)
2287
+ if ok and type(text) == "string" then
2288
+ trace = text
2289
+ end
2290
+ end
2291
+ local ok, described = pcall(${G}.describe, message, trace)
2292
+ if ok then
2293
+ return described
2294
+ end
2295
+ if type(message) ~= "string" then
2296
+ return message
2297
+ end
2298
+ local why = type(described) == "string" and described or "unknown"
2299
+ return message .. "\\n(tilua could not map this error to the project's files: " .. why .. ")" .. (trace or "")
2300
+ end,
2301
+ -- \`fail\`'s work. \`trace\` is a traceback from where the error was
2302
+ -- raised, or nil when none could be taken.
2303
+ describe = function(message, trace)
2218
2304
  if type(message) ~= "string" then
2219
2305
  return message
2220
2306
  end
2221
2307
  local lines = ${G}.lines
2222
2308
  if lines == nil then
2223
- return debug.traceback(message, 2)
2309
+ return message .. (trace or "")
2224
2310
  end
2311
+ -- The bundle's name, as a position in a message spells it: where this
2312
+ -- function is, asked of the debug library by the function rather
2313
+ -- than by a level, which \`pcall\` would shift. Failing that, a message
2314
+ -- starts with the position it was raised at.
2225
2315
  local chunk
2226
- if debug.info ~= nil then
2227
- chunk = debug.info(1, "s")
2228
- else
2229
- chunk = debug.getinfo(1, "S").short_src
2316
+ if debug ~= nil and debug.info ~= nil then
2317
+ local found, source = pcall(debug.info, ${G}.describe, "s")
2318
+ if found and type(source) == "string" then
2319
+ chunk = source
2320
+ end
2321
+ end
2322
+ if chunk == nil and debug ~= nil and debug.getinfo ~= nil then
2323
+ local found, info = pcall(debug.getinfo, ${G}.describe, "S")
2324
+ if found and type(info) == "table" and type(info.short_src) == "string" then
2325
+ chunk = info.short_src
2326
+ end
2327
+ end
2328
+ if chunk == nil then
2329
+ chunk = message:match("^(.-):%d+:")
2330
+ end
2331
+ if chunk == nil then
2332
+ return message .. (trace or "")
2230
2333
  end
2231
2334
  local prefix = chunk:gsub("%p", "%%%0")
2232
2335
  local function place(line)
@@ -2240,7 +2343,7 @@ ${G} = {
2240
2343
  return place(line)
2241
2344
  end)
2242
2345
  local frames = {}
2243
- for frame in debug.traceback("", 2):gmatch("[^\\n]+") do
2346
+ for frame in (trace or ""):gmatch("[^\\n]+") do
2244
2347
  local line = frame:match(prefix .. ":(%d+)")
2245
2348
  local at = line ~= nil and place(line) or nil
2246
2349
  if at ~= nil then