@tilua/compiler 3.1.0 → 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
 
@@ -210,11 +210,11 @@ var Lowerer = class {
210
210
  origins = /* @__PURE__ */ new WeakMap();
211
211
  run() {
212
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() }]))])] : [];
213
214
  const helpers = this.helperDefinitions();
214
215
  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
216
  return {
217
- statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
217
+ statements: [...captured, ...helpers, ...scriptArgs, ...this.classRuntimeStatements(), ...body],
218
218
  exportsName: this.exportsName,
219
219
  module: this.info,
220
220
  diagnostics: this.diagnostics,
@@ -264,11 +264,36 @@ var Lowerer = class {
264
264
  helper(kind) {
265
265
  let name = this.helpers.get(kind);
266
266
  if (!name) {
267
- name = this.names.fresh(`tilua_${kind}`);
267
+ name = this.names.fresh(`tilua_${kind === "array" ? "asArray" : kind}`);
268
268
  this.helpers.set(kind, name);
269
269
  }
270
270
  return identifier(name);
271
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
+ }
272
297
  /** The local a library's runtime table gets in this file, emitting it the
273
298
  * first time something asks. Keyed by library and by the key the library
274
299
  * itself chose, so two libraries' tables never collide. */
@@ -463,6 +488,46 @@ var Lowerer = class {
463
488
  }
464
489
  helperDefinitions() {
465
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
+ }
466
531
  const assign2 = this.helpers.get("assign");
467
532
  if (assign2) {
468
533
  out.push(localFunction(assign2, functionBody(["target"], [
@@ -477,25 +542,11 @@ var Lowerer = class {
477
542
  returns([identifier("target")])
478
543
  ], true)));
479
544
  }
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
- }
545
+ out.push(...runtimes);
495
546
  const concat = this.helpers.get("concat");
496
547
  if (concat) {
497
548
  out.push(localFunction(concat, functionBody([], [
498
- local(["result"], [table([])]),
549
+ local(["result"], [this.array(table([]))]),
499
550
  numericFor("i", number(1), selectCount(this.builtin("select")), [
500
551
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
501
552
  callStatement(call(member(this.builtin("table"), "move"), [
@@ -511,6 +562,26 @@ var Lowerer = class {
511
562
  }
512
563
  return out;
513
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
+ }
514
585
  // --------------------------------------------------------
515
586
  // Modules
516
587
  // --------------------------------------------------------
@@ -1263,7 +1334,7 @@ end
1263
1334
  number(pattern.elements.length + 1),
1264
1335
  unary("#", source),
1265
1336
  number(1),
1266
- table([])
1337
+ this.array(table([]))
1267
1338
  ]);
1268
1339
  const rest = this.restTarget(pattern.rest, elements, mode, after);
1269
1340
  after.push(...rest.then);
@@ -1317,7 +1388,7 @@ end
1317
1388
  const names = [];
1318
1389
  for (const p of params) {
1319
1390
  if (p.rest) {
1320
- 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() }]))]));
1321
1392
  continue;
1322
1393
  }
1323
1394
  const name = p.pattern ? this.names.fresh("arg") : this.name(p.name);
@@ -1375,6 +1446,7 @@ end
1375
1446
  return binary(node.operator, left, right);
1376
1447
  }
1377
1448
  case "UnaryExpression":
1449
+ if (node.operator === "typeof") return this.typeofExpression(node);
1378
1450
  return unary(node.operator, this.expression(node.argument));
1379
1451
  case "SuperExpression":
1380
1452
  return this.superClassReference(node);
@@ -1429,7 +1501,7 @@ end
1429
1501
  * kept to one there, `f((g()))`. Every value is taken only where the
1430
1502
  * program asks for them, as an array's last element: `[g()]`. */
1431
1503
  keptToOne(value) {
1432
- return expandsToMany(value) ? parenthesized(value) : value;
1504
+ return expandsToMany(value) && !this.marked.has(value) ? parenthesized(value) : value;
1433
1505
  }
1434
1506
  /** Is this `scriptArgs` the language's own, not a name the file declared? */
1435
1507
  isLanguageGlobal(node) {
@@ -1710,7 +1782,7 @@ end
1710
1782
  arrayExpression(node) {
1711
1783
  const elements = packs(node.elements).items;
1712
1784
  if (!elements.some((e) => e.type === "SpreadElement")) {
1713
- 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) }))));
1714
1786
  }
1715
1787
  const parts = [];
1716
1788
  let current = [];
@@ -2100,6 +2172,7 @@ async function bundle(options) {
2100
2172
  require: requireExpression,
2101
2173
  lines: `${G}.lines`,
2102
2174
  fail: `${G}.fail`,
2175
+ arrays: `${G}.arrays`,
2103
2176
  resolve: (specifier) => {
2104
2177
  const target = resolveModulePath(file, specifier, config);
2105
2178
  if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
@@ -2145,6 +2218,7 @@ async function bundle(options) {
2145
2218
  const assignment = program2.body.statements[1];
2146
2219
  if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2147
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);
2148
2222
  }
2149
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;
2150
2224
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
@@ -2188,6 +2262,9 @@ function runtime(G) {
2188
2262
  local ${G}
2189
2263
  ${G} = {
2190
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" }),
2191
2268
  -- Filled in just before the entry runs: bundle line -> { file, line }.
2192
2269
  lines = nil,
2193
2270
  -- What an error nothing caught says, in the project's files: the bundle's
package/dist/cli.cjs CHANGED
@@ -226,11 +226,11 @@ var Lowerer = class {
226
226
  origins = /* @__PURE__ */ new WeakMap();
227
227
  run() {
228
228
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
229
+ const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))])] : [];
229
230
  const helpers = this.helperDefinitions();
230
231
  const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
231
- const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: vararg() }])])] : [];
232
232
  return {
233
- statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
233
+ statements: [...captured, ...helpers, ...scriptArgs, ...this.classRuntimeStatements(), ...body],
234
234
  exportsName: this.exportsName,
235
235
  module: this.info,
236
236
  diagnostics: this.diagnostics,
@@ -280,11 +280,36 @@ var Lowerer = class {
280
280
  helper(kind) {
281
281
  let name = this.helpers.get(kind);
282
282
  if (!name) {
283
- name = this.names.fresh(`tilua_${kind}`);
283
+ name = this.names.fresh(`tilua_${kind === "array" ? "asArray" : kind}`);
284
284
  this.helpers.set(kind, name);
285
285
  }
286
286
  return identifier(name);
287
287
  }
288
+ // `typeof v` tells an array from an object, which Luau cannot: both are
289
+ // tables. So every array the program makes — `[a, b]`, a rest parameter,
290
+ // what `:map` answers — is written down as one, in a table that holds its
291
+ // keys weakly, and `typeof` looks there. A table nothing wrote down (one a
292
+ // Lua library made) is told by its type where that says, and by its shape
293
+ // where it does not.
294
+ /** `value`, a new array, written down as one. */
295
+ array(value) {
296
+ const marked = call(this.helper("array"), [value]);
297
+ this.marked.add(marked);
298
+ return marked;
299
+ }
300
+ /** The calls `array` made: each answers the one table it was given. */
301
+ marked = /* @__PURE__ */ new WeakSet();
302
+ /** Luau reaching the record of arrays: the bundle's, or this file's own. */
303
+ arrays() {
304
+ return this.options.module?.arrays ?? this.helper("arrays").name;
305
+ }
306
+ typeofExpression(node) {
307
+ const known = this.options.types?.typeofTables.get(node);
308
+ let argument = node.argument;
309
+ while (argument.type === "ParenthesizedExpression") argument = argument.expression;
310
+ const value = this.expression(argument);
311
+ return call(this.helper("typeof"), known ? [value, string(known)] : [this.keptToOne(value)]);
312
+ }
288
313
  /** The local a library's runtime table gets in this file, emitting it the
289
314
  * first time something asks. Keyed by library and by the key the library
290
315
  * itself chose, so two libraries' tables never collide. */
@@ -479,6 +504,46 @@ var Lowerer = class {
479
504
  }
480
505
  helperDefinitions() {
481
506
  const out2 = [];
507
+ const runtimes = this.runtimeDefinitions();
508
+ if (this.helpers.has("concat")) this.helper("array");
509
+ const marker = this.helpers.get("array");
510
+ const typeOf = this.helpers.get("typeof");
511
+ if (marker || typeOf) {
512
+ const arrays = this.arrays();
513
+ const own = this.helpers.get("arrays");
514
+ if (own) {
515
+ out2.push(...(0, import_luau_parser.parse)(`local ${own} = ${this.builtin("setmetatable").name}({}, { __mode = "k" })`).body.statements);
516
+ }
517
+ if (marker) {
518
+ out2.push(...(0, import_luau_parser.parse)(`
519
+ local function ${marker}(t)
520
+ ${arrays}[t] = true
521
+ return t
522
+ end`).body.statements);
523
+ }
524
+ if (typeOf) {
525
+ out2.push(...(0, import_luau_parser.parse)(`
526
+ local function ${typeOf}(value, known)
527
+ local name = ${this.builtin(this.lua51 ? "type" : "typeof").name}(value)
528
+ if name ~= "table" then
529
+ return name
530
+ end
531
+ if known ~= nil then
532
+ return known
533
+ end
534
+ if ${arrays}[value] then
535
+ return "array"
536
+ end
537
+ if ${this.builtin("getmetatable").name}(value) ~= nil then
538
+ return "object"
539
+ end
540
+ if #value > 0 then
541
+ return "array"
542
+ end
543
+ return "object"
544
+ end`).body.statements);
545
+ }
546
+ }
482
547
  const assign2 = this.helpers.get("assign");
483
548
  if (assign2) {
484
549
  out2.push(localFunction(assign2, functionBody(["target"], [
@@ -493,25 +558,11 @@ var Lowerer = class {
493
558
  returns([identifier("target")])
494
559
  ], true)));
495
560
  }
496
- for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
497
- const { plugin: plugin2, from } = this.lowerings[index2];
498
- const source = plugin2.runtime?.[runtime2];
499
- if (source === void 0) {
500
- this.diagnostics.push({
501
- message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
502
- line: 1,
503
- column: 1
504
- });
505
- continue;
506
- }
507
- const lines = this.options.module?.lines ?? "nil";
508
- const fail = this.options.module?.fail ?? "nil";
509
- out2.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
510
- }
561
+ out2.push(...runtimes);
511
562
  const concat = this.helpers.get("concat");
512
563
  if (concat) {
513
564
  out2.push(localFunction(concat, functionBody([], [
514
- local(["result"], [table([])]),
565
+ local(["result"], [this.array(table([]))]),
515
566
  numericFor("i", number(1), selectCount(this.builtin("select")), [
516
567
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
517
568
  callStatement(call(member(this.builtin("table"), "move"), [
@@ -527,6 +578,26 @@ var Lowerer = class {
527
578
  }
528
579
  return out2;
529
580
  }
581
+ /** The runtime tables the file's libraries were asked for. */
582
+ runtimeDefinitions() {
583
+ const out2 = [];
584
+ for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
585
+ const { plugin: plugin2, from } = this.lowerings[index2];
586
+ const source = plugin2.runtime?.[runtime2];
587
+ if (source === void 0) {
588
+ this.diagnostics.push({
589
+ message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
590
+ line: 1,
591
+ column: 1
592
+ });
593
+ continue;
594
+ }
595
+ const lines = this.options.module?.lines ?? "nil";
596
+ const fail = this.options.module?.fail ?? "nil";
597
+ out2.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail).replace(/__ARRAY__/g, () => this.helper("array").name)).body.statements);
598
+ }
599
+ return out2;
600
+ }
530
601
  // --------------------------------------------------------
531
602
  // Modules
532
603
  // --------------------------------------------------------
@@ -1279,7 +1350,7 @@ end
1279
1350
  number(pattern.elements.length + 1),
1280
1351
  unary("#", source),
1281
1352
  number(1),
1282
- table([])
1353
+ this.array(table([]))
1283
1354
  ]);
1284
1355
  const rest = this.restTarget(pattern.rest, elements, mode, after);
1285
1356
  after.push(...rest.then);
@@ -1333,7 +1404,7 @@ end
1333
1404
  const names = [];
1334
1405
  for (const p of params) {
1335
1406
  if (p.rest) {
1336
- prelude.push(local([this.name(p.name)], [table([{ type: "TableFieldPositional", value: vararg() }])]));
1407
+ prelude.push(local([this.name(p.name)], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))]));
1337
1408
  continue;
1338
1409
  }
1339
1410
  const name = p.pattern ? this.names.fresh("arg") : this.name(p.name);
@@ -1391,6 +1462,7 @@ end
1391
1462
  return binary(node.operator, left, right);
1392
1463
  }
1393
1464
  case "UnaryExpression":
1465
+ if (node.operator === "typeof") return this.typeofExpression(node);
1394
1466
  return unary(node.operator, this.expression(node.argument));
1395
1467
  case "SuperExpression":
1396
1468
  return this.superClassReference(node);
@@ -1445,7 +1517,7 @@ end
1445
1517
  * kept to one there, `f((g()))`. Every value is taken only where the
1446
1518
  * program asks for them, as an array's last element: `[g()]`. */
1447
1519
  keptToOne(value) {
1448
- return expandsToMany(value) ? parenthesized(value) : value;
1520
+ return expandsToMany(value) && !this.marked.has(value) ? parenthesized(value) : value;
1449
1521
  }
1450
1522
  /** Is this `scriptArgs` the language's own, not a name the file declared? */
1451
1523
  isLanguageGlobal(node) {
@@ -1726,7 +1798,7 @@ end
1726
1798
  arrayExpression(node) {
1727
1799
  const elements = packs(node.elements).items;
1728
1800
  if (!elements.some((e) => e.type === "SpreadElement")) {
1729
- return table(elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) })));
1801
+ return this.array(table(elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) }))));
1730
1802
  }
1731
1803
  const parts = [];
1732
1804
  let current = [];
@@ -2097,6 +2169,7 @@ async function bundle(options) {
2097
2169
  require: requireExpression,
2098
2170
  lines: `${G}.lines`,
2099
2171
  fail: `${G}.fail`,
2172
+ arrays: `${G}.arrays`,
2100
2173
  resolve: (specifier) => {
2101
2174
  const target = (0, import_parser6.resolveModulePath)(file, specifier, config2);
2102
2175
  if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
@@ -2142,6 +2215,7 @@ async function bundle(options) {
2142
2215
  const assignment = program.body.statements[1];
2143
2216
  if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2144
2217
  program.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2218
+ program.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
2145
2219
  }
2146
2220
  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;
2147
2221
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
@@ -2185,6 +2259,9 @@ function runtime(G) {
2185
2259
  local ${G}
2186
2260
  ${G} = {
2187
2261
  modules = {},
2262
+ -- Every array the program made, so \`typeof\` can tell one from an object.
2263
+ -- The keys are held weakly: being written down keeps no array alive.
2264
+ arrays = setmetatable({}, { __mode = "k" }),
2188
2265
  -- Filled in just before the entry runs: bundle line -> { file, line }.
2189
2266
  lines = nil,
2190
2267
  -- What an error nothing caught says, in the project's files: the bundle's
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  bundle
4
- } from "./chunk-VANZBLCL.js";
4
+ } from "./chunk-VZ7XC4BG.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { writeFileSync } from "fs";
package/dist/index.cjs CHANGED
@@ -250,11 +250,11 @@ var Lowerer = class {
250
250
  origins = /* @__PURE__ */ new WeakMap();
251
251
  run() {
252
252
  const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
253
+ const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))])] : [];
253
254
  const helpers = this.helperDefinitions();
254
255
  const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
255
- const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: vararg() }])])] : [];
256
256
  return {
257
- statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
257
+ statements: [...captured, ...helpers, ...scriptArgs, ...this.classRuntimeStatements(), ...body],
258
258
  exportsName: this.exportsName,
259
259
  module: this.info,
260
260
  diagnostics: this.diagnostics,
@@ -304,11 +304,36 @@ var Lowerer = class {
304
304
  helper(kind) {
305
305
  let name = this.helpers.get(kind);
306
306
  if (!name) {
307
- name = this.names.fresh(`tilua_${kind}`);
307
+ name = this.names.fresh(`tilua_${kind === "array" ? "asArray" : kind}`);
308
308
  this.helpers.set(kind, name);
309
309
  }
310
310
  return identifier(name);
311
311
  }
312
+ // `typeof v` tells an array from an object, which Luau cannot: both are
313
+ // tables. So every array the program makes — `[a, b]`, a rest parameter,
314
+ // what `:map` answers — is written down as one, in a table that holds its
315
+ // keys weakly, and `typeof` looks there. A table nothing wrote down (one a
316
+ // Lua library made) is told by its type where that says, and by its shape
317
+ // where it does not.
318
+ /** `value`, a new array, written down as one. */
319
+ array(value) {
320
+ const marked = call(this.helper("array"), [value]);
321
+ this.marked.add(marked);
322
+ return marked;
323
+ }
324
+ /** The calls `array` made: each answers the one table it was given. */
325
+ marked = /* @__PURE__ */ new WeakSet();
326
+ /** Luau reaching the record of arrays: the bundle's, or this file's own. */
327
+ arrays() {
328
+ return this.options.module?.arrays ?? this.helper("arrays").name;
329
+ }
330
+ typeofExpression(node) {
331
+ const known = this.options.types?.typeofTables.get(node);
332
+ let argument = node.argument;
333
+ while (argument.type === "ParenthesizedExpression") argument = argument.expression;
334
+ const value = this.expression(argument);
335
+ return call(this.helper("typeof"), known ? [value, string(known)] : [this.keptToOne(value)]);
336
+ }
312
337
  /** The local a library's runtime table gets in this file, emitting it the
313
338
  * first time something asks. Keyed by library and by the key the library
314
339
  * itself chose, so two libraries' tables never collide. */
@@ -503,6 +528,46 @@ var Lowerer = class {
503
528
  }
504
529
  helperDefinitions() {
505
530
  const out = [];
531
+ const runtimes = this.runtimeDefinitions();
532
+ if (this.helpers.has("concat")) this.helper("array");
533
+ const marker = this.helpers.get("array");
534
+ const typeOf = this.helpers.get("typeof");
535
+ if (marker || typeOf) {
536
+ const arrays = this.arrays();
537
+ const own = this.helpers.get("arrays");
538
+ if (own) {
539
+ out.push(...(0, import_luau_parser.parse)(`local ${own} = ${this.builtin("setmetatable").name}({}, { __mode = "k" })`).body.statements);
540
+ }
541
+ if (marker) {
542
+ out.push(...(0, import_luau_parser.parse)(`
543
+ local function ${marker}(t)
544
+ ${arrays}[t] = true
545
+ return t
546
+ end`).body.statements);
547
+ }
548
+ if (typeOf) {
549
+ out.push(...(0, import_luau_parser.parse)(`
550
+ local function ${typeOf}(value, known)
551
+ local name = ${this.builtin(this.lua51 ? "type" : "typeof").name}(value)
552
+ if name ~= "table" then
553
+ return name
554
+ end
555
+ if known ~= nil then
556
+ return known
557
+ end
558
+ if ${arrays}[value] then
559
+ return "array"
560
+ end
561
+ if ${this.builtin("getmetatable").name}(value) ~= nil then
562
+ return "object"
563
+ end
564
+ if #value > 0 then
565
+ return "array"
566
+ end
567
+ return "object"
568
+ end`).body.statements);
569
+ }
570
+ }
506
571
  const assign2 = this.helpers.get("assign");
507
572
  if (assign2) {
508
573
  out.push(localFunction(assign2, functionBody(["target"], [
@@ -517,25 +582,11 @@ var Lowerer = class {
517
582
  returns([identifier("target")])
518
583
  ], true)));
519
584
  }
520
- for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
521
- const { plugin: plugin2, from } = this.lowerings[index2];
522
- const source = plugin2.runtime?.[runtime2];
523
- if (source === void 0) {
524
- this.diagnostics.push({
525
- message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
526
- line: 1,
527
- column: 1
528
- });
529
- continue;
530
- }
531
- const lines = this.options.module?.lines ?? "nil";
532
- const fail = this.options.module?.fail ?? "nil";
533
- out.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
534
- }
585
+ out.push(...runtimes);
535
586
  const concat = this.helpers.get("concat");
536
587
  if (concat) {
537
588
  out.push(localFunction(concat, functionBody([], [
538
- local(["result"], [table([])]),
589
+ local(["result"], [this.array(table([]))]),
539
590
  numericFor("i", number(1), selectCount(this.builtin("select")), [
540
591
  local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
541
592
  callStatement(call(member(this.builtin("table"), "move"), [
@@ -551,6 +602,26 @@ var Lowerer = class {
551
602
  }
552
603
  return out;
553
604
  }
605
+ /** The runtime tables the file's libraries were asked for. */
606
+ runtimeDefinitions() {
607
+ const out = [];
608
+ for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
609
+ const { plugin: plugin2, from } = this.lowerings[index2];
610
+ const source = plugin2.runtime?.[runtime2];
611
+ if (source === void 0) {
612
+ this.diagnostics.push({
613
+ message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
614
+ line: 1,
615
+ column: 1
616
+ });
617
+ continue;
618
+ }
619
+ const lines = this.options.module?.lines ?? "nil";
620
+ const fail = this.options.module?.fail ?? "nil";
621
+ out.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail).replace(/__ARRAY__/g, () => this.helper("array").name)).body.statements);
622
+ }
623
+ return out;
624
+ }
554
625
  // --------------------------------------------------------
555
626
  // Modules
556
627
  // --------------------------------------------------------
@@ -1303,7 +1374,7 @@ end
1303
1374
  number(pattern.elements.length + 1),
1304
1375
  unary("#", source),
1305
1376
  number(1),
1306
- table([])
1377
+ this.array(table([]))
1307
1378
  ]);
1308
1379
  const rest = this.restTarget(pattern.rest, elements, mode, after);
1309
1380
  after.push(...rest.then);
@@ -1357,7 +1428,7 @@ end
1357
1428
  const names = [];
1358
1429
  for (const p of params) {
1359
1430
  if (p.rest) {
1360
- prelude.push(local([this.name(p.name)], [table([{ type: "TableFieldPositional", value: vararg() }])]));
1431
+ prelude.push(local([this.name(p.name)], [this.array(table([{ type: "TableFieldPositional", value: vararg() }]))]));
1361
1432
  continue;
1362
1433
  }
1363
1434
  const name = p.pattern ? this.names.fresh("arg") : this.name(p.name);
@@ -1415,6 +1486,7 @@ end
1415
1486
  return binary(node.operator, left, right);
1416
1487
  }
1417
1488
  case "UnaryExpression":
1489
+ if (node.operator === "typeof") return this.typeofExpression(node);
1418
1490
  return unary(node.operator, this.expression(node.argument));
1419
1491
  case "SuperExpression":
1420
1492
  return this.superClassReference(node);
@@ -1469,7 +1541,7 @@ end
1469
1541
  * kept to one there, `f((g()))`. Every value is taken only where the
1470
1542
  * program asks for them, as an array's last element: `[g()]`. */
1471
1543
  keptToOne(value) {
1472
- return expandsToMany(value) ? parenthesized(value) : value;
1544
+ return expandsToMany(value) && !this.marked.has(value) ? parenthesized(value) : value;
1473
1545
  }
1474
1546
  /** Is this `scriptArgs` the language's own, not a name the file declared? */
1475
1547
  isLanguageGlobal(node) {
@@ -1750,7 +1822,7 @@ end
1750
1822
  arrayExpression(node) {
1751
1823
  const elements = packs(node.elements).items;
1752
1824
  if (!elements.some((e) => e.type === "SpreadElement")) {
1753
- return table(elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) })));
1825
+ return this.array(table(elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) }))));
1754
1826
  }
1755
1827
  const parts = [];
1756
1828
  let current = [];
@@ -2121,6 +2193,7 @@ async function bundle(options) {
2121
2193
  require: requireExpression,
2122
2194
  lines: `${G}.lines`,
2123
2195
  fail: `${G}.fail`,
2196
+ arrays: `${G}.arrays`,
2124
2197
  resolve: (specifier) => {
2125
2198
  const target = (0, import_parser6.resolveModulePath)(file, specifier, config);
2126
2199
  if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
@@ -2166,6 +2239,7 @@ async function bundle(options) {
2166
2239
  const assignment = program2.body.statements[1];
2167
2240
  if ([...modules.values()].some((m) => m.usesScriptArgs)) {
2168
2241
  program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
2242
+ program2.body.statements.push(...(0, import_luau_parser2.parse)(`${G}.arrays[scriptArgs] = true`).body.statements);
2169
2243
  }
2170
2244
  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;
2171
2245
  if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
@@ -2209,6 +2283,9 @@ function runtime(G) {
2209
2283
  local ${G}
2210
2284
  ${G} = {
2211
2285
  modules = {},
2286
+ -- Every array the program made, so \`typeof\` can tell one from an object.
2287
+ -- The keys are held weakly: being written down keeps no array alive.
2288
+ arrays = setmetatable({}, { __mode = "k" }),
2212
2289
  -- Filled in just before the entry runs: bundle line -> { file, line }.
2213
2290
  lines = nil,
2214
2291
  -- What an error nothing caught says, in the project's files: the bundle's
package/dist/index.d.cts CHANGED
@@ -100,6 +100,9 @@ interface ModuleContext {
100
100
  * reads, as `__FAIL__`, to report an error from a callback the way an
101
101
  * error that reached the entry is reported. */
102
102
  readonly fail?: string;
103
+ /** Luau reaching the bundle's record of which tables are arrays, so that
104
+ * every module marks and reads the same one (`typeof`). */
105
+ readonly arrays?: string;
103
106
  }
104
107
  interface LowerOptions {
105
108
  /** Lower the file as a module of a bundle. Without it, `import` and
package/dist/index.d.ts CHANGED
@@ -100,6 +100,9 @@ interface ModuleContext {
100
100
  * reads, as `__FAIL__`, to report an error from a callback the way an
101
101
  * error that reached the entry is reported. */
102
102
  readonly fail?: string;
103
+ /** Luau reaching the bundle's record of which tables are arrays, so that
104
+ * every module marks and reads the same one (`typeof`). */
105
+ readonly arrays?: string;
103
106
  }
104
107
  interface LowerOptions {
105
108
  /** Lower the file as a module of a bundle. Without it, `import` and
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  lower,
5
5
  program,
6
6
  resolveConfig
7
- } from "./chunk-VANZBLCL.js";
7
+ } from "./chunk-VZ7XC4BG.js";
8
8
 
9
9
  // src/compile.ts
10
10
  import { readFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tilua/compiler",
3
- "version": "3.1.0",
3
+ "version": "4.0.0",
4
4
  "description": "Compiles tilua to Luau: lowers the @tilua/parser AST to a luau-parser AST and prints it",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "type": "module",
39
39
  "dependencies": {
40
40
  "luau-parser": "^1.0.3",
41
- "@tilua/parser": "^3.1.0"
41
+ "@tilua/parser": "^4.0.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^22.20.2",
@@ -19,7 +19,7 @@ function __NAME__.findIndex(t, test)
19
19
  end
20
20
 
21
21
  function __NAME__.filter(t, test)
22
- local out = {}
22
+ local out = __ARRAY__({})
23
23
  for i = 1, #t do
24
24
  if test(t[i], i) then
25
25
  out[#out + 1] = t[i]
@@ -29,7 +29,7 @@ function __NAME__.filter(t, test)
29
29
  end
30
30
 
31
31
  function __NAME__.map(t, transform)
32
- local out = {}
32
+ local out = __ARRAY__({})
33
33
  for i = 1, #t do
34
34
  out[i] = transform(t[i], i)
35
35
  end
@@ -95,7 +95,7 @@ function __NAME__.join(t, separator)
95
95
  end
96
96
 
97
97
  function __NAME__.concat(t, ...)
98
- local out = {}
98
+ local out = __ARRAY__({})
99
99
  table.move(t, 1, #t, 1, out)
100
100
  for i = 1, select("#", ...) do
101
101
  local part = select(i, ...)
@@ -114,7 +114,7 @@ function __NAME__.slice(t, start, stop)
114
114
  if to < 0 then to = n + to + 1 end
115
115
  if from < 1 then from = 1 end
116
116
  if to > n then to = n end
117
- local out = {}
117
+ local out = __ARRAY__({})
118
118
  for i = from, to do
119
119
  out[#out + 1] = t[i]
120
120
  end
@@ -123,7 +123,7 @@ end
123
123
 
124
124
  -- One level, as JavaScript's `flat()` does by default.
125
125
  function __NAME__.flat(t)
126
- local out = {}
126
+ local out = __ARRAY__({})
127
127
  for i = 1, #t do
128
128
  local value = t[i]
129
129
  if type(value) == "table" then
@@ -5,7 +5,7 @@ local __NAME__ = {}
5
5
  -- follow the named ones. With no one order to give (`known` is nil), every
6
6
  -- key is listed, in the order `pairs` walks them.
7
7
  local function keysOf(known, open, t)
8
- local out = {}
8
+ local out = __ARRAY__({})
9
9
  if known == nil then
10
10
  for key in pairs(t) do
11
11
  out[#out + 1] = key
@@ -37,7 +37,7 @@ end
37
37
  -- rather than moving everything after it up one.
38
38
  function __NAME__.values(known, open, t)
39
39
  local keys = keysOf(known, open, t)
40
- local out = {}
40
+ local out = __ARRAY__({})
41
41
  for i = 1, #keys do
42
42
  out[i] = t[keys[i]]
43
43
  end
@@ -46,9 +46,9 @@ end
46
46
 
47
47
  function __NAME__.entries(known, open, t)
48
48
  local keys = keysOf(known, open, t)
49
- local out = {}
49
+ local out = __ARRAY__({})
50
50
  for i = 1, #keys do
51
- out[i] = { keys[i], t[keys[i]] }
51
+ out[i] = __ARRAY__({ keys[i], t[keys[i]] })
52
52
  end
53
53
  return out
54
54
  end
@@ -88,7 +88,7 @@ local function values(...)
88
88
  if select("#", ...) <= 1 and (...) == nil then
89
89
  return nil
90
90
  end
91
- return { ... }
91
+ return __ARRAY__({ ... })
92
92
  end
93
93
 
94
94
  function __NAME__.find(s, pattern, init, plain)
@@ -107,7 +107,7 @@ function __NAME__.byte(s, i, j)
107
107
  if j == nil then
108
108
  return (string.byte(s, i))
109
109
  end
110
- return { string.byte(s, i, j) }
110
+ return __ARRAY__({ string.byte(s, i, j) })
111
111
  end
112
112
 
113
113
  -- An iterator function: each call is one match's captures, as an array.