@tilua/parser 2.4.0 → 3.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
@@ -260,22 +260,51 @@ brings in names that are types and nothing else: unlike TypeScript, using one
260
260
  as a value is an error, and only type positions — `typeof A` included — may
261
261
  name it. Compiled code keeps no trace of it.
262
262
 
263
- **Array and string methods** — an array and a string answer to methods
264
- written with `:`, the way JavaScript writes them:
263
+ **Methods of strings, arrays and tables** — a value answers to the methods
264
+ its metatable's `__index` has, written with `:`:
265
265
 
266
266
  ```luau
267
267
  const long = names:filter(n => #n > 3):map(string.upper)
268
- const first = names:find(n => n:startsWith("A"))
269
- print(names:join(", "), text:trim(), text:replaceAll(",", ";"))
268
+ print(names:join(", "), text:trim(), text:upper())
269
+ const point = { x: 1, y: "up" }
270
+ const [x, y] = point:values() -- [number, string]
270
271
  ```
271
272
 
272
- Which methods those are is not the language's business. The analyzer looks for
273
- two types by name — `ArrayMethods<T>` and `StringMethods` — and reads an
274
- array's or a string's members out of whichever type library declared them;
275
- without such a library an array has no methods at all.
273
+ Strings, arrays and tables are the language's own, so their metatables are
274
+ too: the prelude declares them, in tilua, and they are there with no type
275
+ library at all. A string's is Lua's (`upper`, `sub`, with one value where Lua
276
+ answers several: `find` is an array) plus the JavaScript-shaped ones (`trim`,
277
+ `startsWith`, ...); an array's is JavaScript's (`filter`, `map`, `join`, ...),
278
+ with Lua's indices — the first element is 1, `indexOf` answers `nil` rather
279
+ than -1 — and `push`, `pop`, `shift`, `unshift`, `sort` and `reverse` change
280
+ the array. A table's are `keys`, `values` and `entries`, which answer a tuple
281
+ in the order the table's type wrote its members, not `string[]` as
282
+ JavaScript's `Object.keys` does; the analyzer supplies that order as
283
+ `ObjectKeys<T>`, `ObjectValues<T>` and `ObjectEntries<T>`. A member of a
284
+ value's own with the same name is what a call reaches, and a class instance,
285
+ which has a metatable of its own, has none of them.
286
+
287
+ No Lua array or table really has these methods, so the compiler lowers each
288
+ call into its own runtime (`names:filter(f)` becomes `array.filter(names, f)`);
289
+ a string's Lua methods stay plain method calls.
290
+
291
+ A library adds to a metatable by declaring the same target again, and a
292
+ metatable can be declared for any type:
276
293
 
277
- Running them is that library's business too. A library points at a JavaScript
278
- module in its package.json, and the compiler asks it what a call becomes:
294
+ ```luau
295
+ declare metatable string: { __index: { split: (self: string, separator?: string) => string[] } }
296
+ declare metatable<T> T[]: { __index: { first: (self: T[]) => T | nil } }
297
+ declare metatable<T extends {}> T: { __index: { size: (self: T) => number } }
298
+ ```
299
+
300
+ `<T> T[]` is every array, `T` what it holds; `<T extends C> T` every value
301
+ that is a `C`, `T` the value itself; a target with no parameters, every value
302
+ of that type. The most specific target a value matches is its metatable.
303
+
304
+ A method a library's metatable gives may be one the value really has, as
305
+ Luau's `split` is. When it is not, the library says how it runs. It points at
306
+ a JavaScript module in its package.json, and the compiler asks it — and only
307
+ it — what a call through a metatable it declared becomes:
279
308
 
280
309
  ```json
281
310
  "tilua": { "types": "index.d.tilua", "lowering": "lowering.mjs" }
@@ -285,11 +314,9 @@ module in its package.json, and the compiler asks it what a call becomes:
285
314
  // @ts-check
286
315
  /** @type {import("@tilua/parser").LoweringPlugin} */ // the contract, declared here
287
316
  const plugin = {
288
- runtime: { array: "local __NAME__ = {}\nfunction __NAME__.filter(t, test) ... end" },
289
- methodCall({ method, receiver, use }) {
290
- if (receiver?.kind === "array" && method === "filter") {
291
- return { callee: `${use("array")}.filter` }
292
- }
317
+ runtime: { array: "local __NAME__ = {}\nfunction __NAME__.first(t) return t[1] end" },
318
+ methodCall({ method, use }) {
319
+ if (method === "first") return { callee: `${use("array")}.first` }
293
320
  return undefined
294
321
  },
295
322
  }
@@ -300,7 +327,9 @@ export default plugin
300
327
  local name that table got — emitted once, at the top of the output, only if a
301
328
  call needed it — and the receiver is passed as the call's first argument. An
302
329
  answer of `undefined` leaves an ordinary Luau method call, which is what
303
- `text:upper()` wants, since a string already answers to it.
330
+ `text:split(",")` wants, since a Luau string already answers to it. A method
331
+ of a value's own (`console:log`, a member of a declared table) is asked of
332
+ every library, the last loaded first.
304
333
 
305
334
  A call can also carry more than was written. Every hook is told `at`, where
306
335
  the call was written (`{ file, line, column }`), and `arguments`: each one's
@@ -314,13 +343,6 @@ hook is asked about it. A runtime can read `__LINES__`, the bundle's line map
314
343
  project. `console:log`, `print` and `error` in `@tilua-types/lua` are built on
315
344
  these hooks.
316
345
 
317
- The compiler lowers the language and nothing else: `filter` appears nowhere in
318
- it.
319
-
320
- `@tilua-types/lua` ships the JavaScript-shaped set; there, indices are Luau's (the
321
- first element is 1, `indexOf` answers `nil` rather than -1) and `push`, `pop`,
322
- `shift`, `unshift`, `sort` and `reverse` change the array they are called on.
323
-
324
346
  **A `(` that starts a line** continues the statement above it, as in Lua and
325
347
  in JavaScript — `const v = map[key]` followed by `("A"):upper()` is one
326
348
  statement, a call of `map[key]`. tilua says so rather than letting it pass:
package/dist/index.cjs CHANGED
@@ -734,7 +734,8 @@ var DECLARATION_STATEMENTS = /* @__PURE__ */ new Set([
734
734
  "ExportNamedStatement",
735
735
  "ExportAllStatement",
736
736
  "DeclareStatement",
737
- "DeclareClassStatement"
737
+ "DeclareClassStatement",
738
+ "DeclareMetatableStatement"
738
739
  ]);
739
740
  function spanFrom(start, end) {
740
741
  return {
@@ -1352,6 +1353,9 @@ var Parser = class {
1352
1353
  if (p1.type === "Identifier" && p1.value === "class" && this.peek(2).type === "Identifier") {
1353
1354
  return this.parseDeclareClassStatement();
1354
1355
  }
1356
+ if (p1.type === "Identifier" && p1.value === "metatable" && !this.punctuatorAt(2, ":")) {
1357
+ return this.parseDeclareMetatableStatement();
1358
+ }
1355
1359
  if (p1.type === "Identifier" || p1.type === "Keyword" && p1.value === "function") {
1356
1360
  return this.parseDeclareStatement();
1357
1361
  }
@@ -1404,6 +1408,17 @@ var Parser = class {
1404
1408
  }
1405
1409
  return tokenIdentifier(first);
1406
1410
  }
1411
+ // `declare metatable<T> T[]: { __index: { ... } }`
1412
+ parseDeclareMetatableStatement() {
1413
+ const start = this.current();
1414
+ this.advance();
1415
+ this.advance();
1416
+ const generics = this.checkOperator("<") ? this.parseGenericTypeParameterList() : [];
1417
+ const target = this.parseType();
1418
+ this.expectPunctuator(":");
1419
+ const metatable = this.parseType();
1420
+ return { type: "DeclareMetatableStatement", generics, target, metatable, ...spanFrom(start, this.previous()) };
1421
+ }
1407
1422
  // `declare class Name extends Base { member: T, ... }`
1408
1423
  parseDeclareClassStatement() {
1409
1424
  const start = this.current();
@@ -2112,6 +2127,14 @@ var Parser = class {
2112
2127
  parseExpressionStatement() {
2113
2128
  const start = this.current();
2114
2129
  if (this.checkPunctuator("{") || this.checkPunctuator("[")) {
2130
+ const call = this.tryParse(() => {
2131
+ const errors = this.errors.length;
2132
+ const expression2 = this.parseAtom();
2133
+ if (expression2.type !== "MethodCallExpression" && expression2.type !== "CallExpression") throw new ParseRecover("not a call");
2134
+ if (this.errors.length > errors) throw new ParseRecover("not a call");
2135
+ return expression2;
2136
+ });
2137
+ if (call) return { type: "CallStatement", expression: call, ...spanFrom(start, this.previous()) };
2115
2138
  const target = this.checkPunctuator("{") ? this.parseObjectPattern(true) : this.parseArrayPattern(true);
2116
2139
  return this.parseAssignmentRest(start, target);
2117
2140
  }
@@ -2433,11 +2456,9 @@ var Parser = class {
2433
2456
  if (t.type === "Identifier" && t.value === "class") {
2434
2457
  return this.parseClassExpression();
2435
2458
  }
2436
- if (t.type === "Punctuator" && t.value === "{") {
2437
- return this.parseTableExpression();
2438
- }
2439
- if (t.type === "Punctuator" && t.value === "[") {
2440
- return this.parseArrayExpression();
2459
+ if (t.type === "Punctuator" && (t.value === "{" || t.value === "[")) {
2460
+ const literal2 = t.value === "{" ? this.parseTableExpression() : this.parseArrayExpression();
2461
+ return this.checkPunctuator(":") && this.startsMethodCall() ? this.parseSuffixes(literal2) : literal2;
2441
2462
  }
2442
2463
  if (this.checkType("Identifier") && this.punctuatorAt(1, "=>")) return this.parseArrow();
2443
2464
  if (this.checkPunctuator("(") || this.checkOperator("<")) {
@@ -2514,6 +2535,11 @@ var Parser = class {
2514
2535
  } else {
2515
2536
  this.error("Expected identifier or '('");
2516
2537
  }
2538
+ return this.parseSuffixes(base);
2539
+ }
2540
+ /** What follows a prefix expression's base: `.name`, `[key]`, `:m(...)`,
2541
+ * a call, and their optional forms. */
2542
+ parseSuffixes(base) {
2517
2543
  while (true) {
2518
2544
  if (this.checkPunctuator("?") && this.touchesNext()) {
2519
2545
  const next = this.peek(1);
@@ -4264,6 +4290,11 @@ var Analyzer = class {
4264
4290
  case "DeclareClassStatement":
4265
4291
  this.visitType(stmt.body, scope);
4266
4292
  return;
4293
+ case "DeclareMetatableStatement":
4294
+ this.visitGenerics(stmt.generics, scope);
4295
+ this.visitType(stmt.target, scope);
4296
+ this.visitType(stmt.metatable, scope);
4297
+ return;
4267
4298
  case "TypeAliasStatement":
4268
4299
  case "ExportTypeAliasStatement":
4269
4300
  this.visitGenerics(
@@ -4529,6 +4560,101 @@ type Mutable<T> = { -readonly [K in keyof T]: T[K] }
4529
4560
  type Pick<T, K> = { [P in K]: T[P] }
4530
4561
  type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
4531
4562
  type Record<K, V> = { [P in K]: V }
4563
+
4564
+ -- ============================================================
4565
+ -- The metatables of the language's own values
4566
+ -- ============================================================
4567
+
4568
+ -- A string's methods. The first group is Lua's string library, which a
4569
+ -- string really answers to; where Lua answers several values, tilua answers
4570
+ -- one, as the global functions do (\`find\` is an array, \`gsub\` the new
4571
+ -- string). The second group is written the way JavaScript writes it.
4572
+ --
4573
+ -- Positions are Lua's: \`sub\` and \`slice\` count from 1, and \`indexOf\` is
4574
+ -- \`nil\` when the text is not there.
4575
+ declare metatable string: {
4576
+ __index: {
4577
+ upper: (self: string) => string,
4578
+ lower: (self: string) => string,
4579
+ len: (self: string) => number,
4580
+ sub: (self: string, i: number, j?: number) => string,
4581
+ rep: (self: string, n: number, separator?: string) => string,
4582
+ reverse: (self: string) => string,
4583
+ format: (self: string, ...args: unknown[]) => string,
4584
+ byte: ((self: string, i?: number) => number) & ((self: string, i: number, j: number) => number[]),
4585
+ find: (self: string, pattern: string, init?: number, plain?: boolean) => [number, number, ...string[]] | nil,
4586
+ match: (self: string, pattern: string, init?: number) => [...string[]] | nil,
4587
+ gmatch: (self: string, pattern: string) => () => [string, ...string[]] | nil,
4588
+ gsub: (self: string, pattern: string, replacement: unknown, n?: number) => string,
4589
+
4590
+ trim: (self: string) => string,
4591
+ trimStart: (self: string) => string,
4592
+ trimEnd: (self: string) => string,
4593
+ startsWith: (self: string, text: string) => boolean,
4594
+ endsWith: (self: string, text: string) => boolean,
4595
+ includes: (self: string, text: string) => boolean,
4596
+ indexOf: (self: string, text: string, start?: number) => number | nil,
4597
+ slice: (self: string, start?: number, stop?: number) => string,
4598
+ replace: (self: string, text: string, replacement: string) => string,
4599
+ replaceAll: (self: string, text: string, replacement: string) => string,
4600
+ padStart: (self: string, length: number, padding?: string) => string,
4601
+ padEnd: (self: string, length: number, padding?: string) => string,
4602
+ },
4603
+ }
4604
+
4605
+ -- An array's methods. Lua gives an array no metatable: the compiler lowers
4606
+ -- each call to a plain function, so they work on any array, one a Lua
4607
+ -- library made included.
4608
+ --
4609
+ -- Indices are Lua's: the first element is 1, and \`indexOf\` / \`findIndex\`
4610
+ -- answer \`nil\` rather than JavaScript's -1. \`sort\` takes Lua's comparator
4611
+ -- (true when \`a\` comes first). \`push\`, \`pop\`, \`shift\`, \`unshift\`,
4612
+ -- \`sort\` and \`reverse\` change the array; everything else returns a new one.
4613
+ declare metatable<T> T[]: {
4614
+ __index: {
4615
+ find: (self: T[], test: (value: T, index: number) => boolean) => T | nil,
4616
+ findIndex: (self: T[], test: (value: T, index: number) => boolean) => number | nil,
4617
+ filter: (self: T[], test: (value: T, index: number) => boolean) => T[],
4618
+ map: <U>(self: T[], transform: (value: T, index: number) => U) => U[],
4619
+ forEach: (self: T[], visit: (value: T, index: number) => nil) => nil,
4620
+ some: (self: T[], test: (value: T, index: number) => boolean) => boolean,
4621
+ every: (self: T[], test: (value: T, index: number) => boolean) => boolean,
4622
+ reduce: <U>(self: T[], step: (total: U, value: T, index: number) => U, initial: U) => U,
4623
+ includes: (self: T[], value: T) => boolean,
4624
+ indexOf: (self: T[], value: T, start?: number) => number | nil,
4625
+ join: (self: T[], separator?: string) => string,
4626
+ concat: (self: T[], ...others: T[][]) => T[],
4627
+ slice: (self: T[], start?: number, stop?: number) => T[],
4628
+ flat: (self: T[]) => T[],
4629
+ reverse: (self: T[]) => T[],
4630
+ sort: (self: T[], compare?: (a: T, b: T) => boolean) => T[],
4631
+ push: (self: T[], ...args: T[]) => number,
4632
+ pop: (self: T[]) => T | nil,
4633
+ shift: (self: T[]) => T | nil,
4634
+ unshift: (self: T[], ...args: T[]) => number,
4635
+ },
4636
+ }
4637
+
4638
+ -- A table's methods. Unlike JavaScript's \`Object.keys\`, they answer a
4639
+ -- tuple, in the order the table's type wrote its members:
4640
+ --
4641
+ -- const point = { x: 1, y: "up" }
4642
+ -- point:keys() -- ["x", "y"]
4643
+ -- point:values() -- [number, string]
4644
+ -- point:entries() -- [["x", number], ["y", string]]
4645
+ --
4646
+ -- A Lua table has no order of its own, so the compiler hands its runtime the
4647
+ -- keys as the type lists them. An optional member keeps its place, holding
4648
+ -- \`nil\` when it is missing; an indexer's keys follow the named ones. A
4649
+ -- member of the table's own with one of these names is what a call reaches,
4650
+ -- and a class instance, which has a metatable of its own, has none of them.
4651
+ declare metatable<T extends {}> T: {
4652
+ __index: {
4653
+ keys: (self: T) => ObjectKeys<T>,
4654
+ values: (self: T) => ObjectValues<T>,
4655
+ entries: (self: T) => ObjectEntries<T>,
4656
+ },
4657
+ }
4532
4658
  `;
4533
4659
  var prelude;
4534
4660
  function preludeProgram() {
@@ -5894,6 +6020,10 @@ var TypeAnalyzer = class {
5894
6020
  typeOfTypeNode = /* @__PURE__ */ new Map();
5895
6021
  expectedTypeOf = /* @__PURE__ */ new Map();
5896
6022
  loops = /* @__PURE__ */ new Map();
6023
+ /** Every `declare metatable`, libraries first, in declaration order. */
6024
+ metatables = [];
6025
+ metatableCache = /* @__PURE__ */ new WeakMap();
6026
+ methodSources = /* @__PURE__ */ new Map();
5897
6027
  /** Public: each alias resolved once (generic aliases keep their params as
5898
6028
  * `typeParam` nodes in the body). */
5899
6029
  aliases = new LazyMap();
@@ -5960,6 +6090,9 @@ var TypeAnalyzer = class {
5960
6090
  this.registerNestedClasses();
5961
6091
  this.registerNestedAliases();
5962
6092
  for (const lib of this.options.libs ?? []) this.harvestDeclares(lib.body);
6093
+ this.harvestMetatables(preludeProgram().body, { origin: "language" });
6094
+ (this.options.libs ?? []).forEach((lib, i) => this.harvestMetatables(lib.body, { origin: "library", library: i }));
6095
+ this.harvestMetatables(this.program.body, { origin: "program" });
5963
6096
  this.registerImportedTypes();
5964
6097
  this.resolveAllAliases();
5965
6098
  this.harvestDeclares(this.program.body, true);
@@ -5991,6 +6124,8 @@ var TypeAnalyzer = class {
5991
6124
  expectedTypeOf: this.expectedTypeOf,
5992
6125
  aliases: this.resolveDeferredAliases(),
5993
6126
  loops: this.loops,
6127
+ methodSources: this.methodSources,
6128
+ metatableMembers: (type) => this.metatableMembers(type),
5994
6129
  diagnostics: this.diagnostics
5995
6130
  };
5996
6131
  }
@@ -6048,8 +6183,7 @@ var TypeAnalyzer = class {
6048
6183
  }
6049
6184
  /** `layering` is on for the prelude and for definitions files: a second
6050
6185
  * library that declares an alias already declared *adds* to it, the way a
6051
- * second `declare` of a table's name does, so `@tilua-types/roblox` can give
6052
- * `StringMethods` Luau's `split` without restating Lua's. The file being
6186
+ * second `declare` of a table's name does. The file being
6053
6187
  * analysed is not a layer: its own alias replaces what the libraries
6054
6188
  * gave, which is how a project opts out of a set. */
6055
6189
  registerAliasDefs(block, layering = false) {
@@ -6967,7 +7101,7 @@ var TypeAnalyzer = class {
6967
7101
  if (record.type === "TypeReference" && typeof record.base === "string") {
6968
7102
  const name = typeof record.namespace === "string" ? `${record.namespace}.${record.base}` : record.base;
6969
7103
  const resolved = this.typeOfTypeNode.get(node);
6970
- const unresolved = resolved?.kind === "genericRef" && resolved.name === name && !this.aliasDefs.has(name) && !this.importedTypes.has(name) && this.options.libTypes?.[name] === void 0 && !STRING_INTRINSICS.has(name) && !this.importedNames().has(name.split(".")[0]);
7104
+ const unresolved = resolved?.kind === "genericRef" && resolved.name === name && !this.aliasDefs.has(name) && !this.importedTypes.has(name) && this.options.libTypes?.[name] === void 0 && !INTRINSICS.has(name) && !this.importedNames().has(name.split(".")[0]);
6971
7105
  const at = node;
6972
7106
  const key = `${at.line.start}:${at.column.start}`;
6973
7107
  if (unresolved && !reported.has(key)) {
@@ -7085,7 +7219,7 @@ var TypeAnalyzer = class {
7085
7219
  const tp = this.lookupTypeParam(node.base);
7086
7220
  if (tp) return typeParam(tp.name, tp.constraint, tp.isConst);
7087
7221
  if (node.typeArguments.length === 1 && !this.aliasDefs.has(node.base)) {
7088
- const intrinsic = this.applyStringIntrinsic(
7222
+ const intrinsic = this.applyIntrinsic(
7089
7223
  node.base,
7090
7224
  this.resolveType(node.typeArguments[0])
7091
7225
  );
@@ -7292,7 +7426,7 @@ var TypeAnalyzer = class {
7292
7426
  return difference(this.reduceType(t.base), this.reduceType(t.excluded));
7293
7427
  case "genericRef": {
7294
7428
  if (t.typeArguments.length !== 1 || this.aliasDefs.has(t.name)) return t;
7295
- return this.applyStringIntrinsic(t.name, t.typeArguments[0]) ?? t;
7429
+ return this.applyIntrinsic(t.name, t.typeArguments[0]) ?? t;
7296
7430
  }
7297
7431
  case "conditional":
7298
7432
  return this.reduceConditional(t);
@@ -7405,6 +7539,41 @@ var TypeAnalyzer = class {
7405
7539
  }
7406
7540
  return union(combos.map((c) => literal(c)));
7407
7541
  }
7542
+ /** A type function the analyzer supplies, since tilua cannot write it. */
7543
+ applyIntrinsic(name, arg) {
7544
+ return this.applyStringIntrinsic(name, arg) ?? this.applyObjectIntrinsic(name, arg);
7545
+ }
7546
+ /** `ObjectKeys<T>`, `ObjectValues<T>`, `ObjectEntries<T>`: an object's
7547
+ * members as a tuple, in the order they were written — `{ a: 1, b: "x" }`
7548
+ * has keys `["a", "b"]`, values `[number, string]` and entries
7549
+ * `[["a", number], ["b", string]]`. TypeScript answers `string[]` for
7550
+ * `Object.keys`, since its objects have no order; tilua's lowering hands
7551
+ * the runtime the written order, so the tuple is what runs. An indexer's
7552
+ * keys follow the named ones, as the tuple's rest. */
7553
+ applyObjectIntrinsic(name, arg) {
7554
+ if (!OBJECT_INTRINSICS.has(name)) return void 0;
7555
+ const t = this.expand(this.reduceType(arg));
7556
+ if (containsTypeParam(t)) return void 0;
7557
+ if (t.kind === "union") return union(t.types.map((m) => this.applyObjectIntrinsic(name, m) ?? unknownType));
7558
+ const parts = t.kind === "intersection" ? t.types.map((m) => this.expand(m)) : [t];
7559
+ if (!parts.every((m) => m.kind === "object" && !m.class)) return unknownType;
7560
+ const properties = /* @__PURE__ */ new Map();
7561
+ let indexer;
7562
+ for (const part of parts) {
7563
+ for (const [key, property] of part.properties) properties.set(key, property);
7564
+ indexer ??= part.indexer;
7565
+ }
7566
+ const entries = [...properties].map(([key, p]) => [literal(key), p.optional ? optional(p.type) : p.type]);
7567
+ const listed = (elements, rest) => !elements.length && rest ? arrayOf(rest) : tuple(elements, rest);
7568
+ switch (name) {
7569
+ case "ObjectKeys":
7570
+ return listed(entries.map(([key]) => key), indexer?.key);
7571
+ case "ObjectValues":
7572
+ return listed(entries.map(([, value]) => value), indexer?.value);
7573
+ default:
7574
+ return listed(entries.map((entry) => tuple([...entry])), indexer && tuple([indexer.key, indexer.value]));
7575
+ }
7576
+ }
7408
7577
  /** TypeScript's four string intrinsics. They cannot be written in tilua —
7409
7578
  * there is no character-level type arithmetic — so the analyzer supplies
7410
7579
  * them, and only them, as named type functions. */
@@ -9086,28 +9255,122 @@ var TypeAnalyzer = class {
9086
9255
  this.resolvingAliases.delete(t.name);
9087
9256
  }
9088
9257
  }
9089
- /** `names:filter(f)`, `text:trim()` — the methods arrays and strings have.
9090
- * They are written in the prelude as `ArrayMethods<T>` and
9091
- * `StringMethods`, so a file (or a type library) that declares one of
9092
- * those names again replaces the whole set, and nothing here is a special
9093
- * case in the analyzer. The build lowers each call to a plain function. */
9094
- builtInMethod(t, name) {
9095
- const parts = (t.kind === "union" ? t.types : [t]).map((m) => this.expand(m));
9096
- const elements = parts.map((m) => m.kind === "array" ? m.element : m.kind === "tuple" ? union(m.elements) : void 0);
9097
- const element = elements.every((e) => e !== void 0) ? union(elements) : void 0;
9098
- const isString = (m) => m.kind === "primitive" && m.name === "string" || m.kind === "literal" && m.base === "string" || m.kind === "templateLiteral";
9099
- const methodTable = element !== void 0 ? "ArrayMethods" : parts.every(isString) ? "StringMethods" : void 0;
9100
- const def = methodTable === void 0 ? void 0 : this.aliasDefs.get(methodTable);
9101
- if (!def || def.class) return void 0;
9102
- const table = this.expand(this.instantiateAlias(def, element !== void 0 ? [element] : []));
9103
- const layers = table.kind === "intersection" ? table.types.map((m) => this.expand(m)) : [table];
9104
- for (let i = layers.length - 1; i >= 0; i--) {
9105
- const part = layers[i];
9106
- const property = part.kind === "object" ? part.properties.get(name) : void 0;
9107
- if (property) return property.type;
9258
+ // --------------------------------------------------------
9259
+ // Metatables
9260
+ // --------------------------------------------------------
9261
+ harvestMetatables(block, source) {
9262
+ for (const stmt of block.statements) {
9263
+ if (stmt.type === "DeclareMetatableStatement") this.metatables.push({ node: stmt, source });
9264
+ }
9265
+ }
9266
+ resolvedMetatable(entry) {
9267
+ entry.resolved ??= this.withTypeParams(entry.node.generics, () => ({
9268
+ target: this.resolveType(entry.node.target),
9269
+ metatable: this.resolveType(entry.node.metatable)
9270
+ }));
9271
+ return entry.resolved;
9272
+ }
9273
+ /** Does a value (`whole`, whose union members are `parts`) have the
9274
+ * metatable `entry` declares — and if so how specifically, and what its
9275
+ * type parameters stand for there. Three kinds of target:
9276
+ *
9277
+ * - `<T> T[]`: every array and tuple, `T` what they hold;
9278
+ * - `<T extends C> T`: every value that is a `C` other than a class
9279
+ * instance, which has a metatable of its own; `T` the value itself;
9280
+ * - a type with no parameters (`string`): every value that is one.
9281
+ *
9282
+ * A bare parameter matches the most, so it is the least specific: an
9283
+ * array that is also a `{}` takes an array's metatable. */
9284
+ matchMetatable(entry, whole, parts) {
9285
+ if (!parts.length || parts.some((m) => m.kind === "any" || m.kind === "unknown" || m.kind === "never")) return void 0;
9286
+ const { target } = this.resolvedMetatable(entry);
9287
+ if (target.kind === "array" && target.element.kind === "typeParam") {
9288
+ const elements = parts.map((m) => m.kind === "array" ? m.element : m.kind === "tuple" ? union(tupleMembers(m)) : void 0);
9289
+ if (!elements.every((e) => e !== void 0)) return void 0;
9290
+ return { specificity: 2, bindings: /* @__PURE__ */ new Map([[target.element.name, union(elements)]]) };
9291
+ }
9292
+ if (target.kind === "typeParam") {
9293
+ const constraint = target.constraint;
9294
+ if (parts.some((m) => m.kind === "object" && m.class)) return void 0;
9295
+ if (constraint && !parts.every((m) => isAssignable(m, constraint))) return void 0;
9296
+ return { specificity: 1, bindings: /* @__PURE__ */ new Map([[target.name, whole]]) };
9297
+ }
9298
+ if (containsTypeParam(target)) return void 0;
9299
+ return parts.every((m) => isAssignable(m, target)) ? { specificity: 2, bindings: /* @__PURE__ */ new Map() } : void 0;
9300
+ }
9301
+ /** The metatables `t` has: every declaration of the most specific target
9302
+ * it matches, instantiated for it, in declaration order — a later one
9303
+ * adds to an earlier, the way a library layers on another. A type
9304
+ * parameter is read at its constraint. */
9305
+ metatablesOf(t) {
9306
+ if (!this.metatables.length) return [];
9307
+ const cached = this.metatableCache.get(t);
9308
+ if (cached) return cached;
9309
+ const expanded = this.expand(t);
9310
+ const parts = (expanded.kind === "union" ? expanded.types : [expanded]).map((m) => this.expand(m)).map((m) => m.kind === "typeParam" && m.constraint ? this.expand(m.constraint) : m);
9311
+ const whole = union(parts);
9312
+ const matches = this.metatables.flatMap((entry) => {
9313
+ const match = this.matchMetatable(entry, whole, parts);
9314
+ return match ? [{ entry, ...match }] : [];
9315
+ });
9316
+ const best = Math.max(0, ...matches.map((m) => m.specificity));
9317
+ const found = matches.filter((m) => m.specificity === best).map(({ entry, bindings }) => ({
9318
+ type: this.expand(this.reduceType(substitute(this.resolvedMetatable(entry).metatable, bindings))),
9319
+ source: entry.source
9320
+ }));
9321
+ this.metatableCache.set(t, found);
9322
+ return found;
9323
+ }
9324
+ /** The `__index` tables of `t`'s metatables, each with the library that
9325
+ * declared it, in declaration order. */
9326
+ metatableIndexes(t) {
9327
+ return this.metatablesOf(t).flatMap(({ type, source }) => {
9328
+ const parts = type.kind === "intersection" ? type.types.map((m) => this.expand(m)) : [type];
9329
+ return parts.flatMap((part) => {
9330
+ const index = part.kind === "object" ? part.properties.get("__index") : void 0;
9331
+ if (!index) return [];
9332
+ const table = this.expand(index.type);
9333
+ return (table.kind === "intersection" ? table.types.map((m) => this.expand(m)) : [table]).map((m) => ({ table: m, source }));
9334
+ });
9335
+ });
9336
+ }
9337
+ /** `name` read through `t`'s metatable: `text:upper()` is `upper` in the
9338
+ * string metatable's `__index`. The last declaration to give the name
9339
+ * wins. The language's own metatables are declared in the prelude, in
9340
+ * tilua, like any library's; a library adds to them the same way. */
9341
+ metatableMember(t, name) {
9342
+ const indexes = this.metatableIndexes(t);
9343
+ for (let i = indexes.length - 1; i >= 0; i--) {
9344
+ const { table, source } = indexes[i];
9345
+ const property = table.kind === "object" ? table.properties.get(name) : void 0;
9346
+ if (property) return { type: property.type, source };
9108
9347
  }
9109
9348
  return void 0;
9110
9349
  }
9350
+ metatableMethod(t, name) {
9351
+ return this.metatableMember(t, name)?.type;
9352
+ }
9353
+ hasMetatable(t) {
9354
+ return this.metatablesOf(t).length > 0;
9355
+ }
9356
+ metatableMembers(t) {
9357
+ const members = /* @__PURE__ */ new Map();
9358
+ for (const { table } of this.metatableIndexes(t)) {
9359
+ if (table.kind !== "object") continue;
9360
+ for (const [name, property] of table.properties) members.set(name, property.type);
9361
+ }
9362
+ return members;
9363
+ }
9364
+ /** `text:upper()`, `scores:keys()`: the method a value's metatable gives
9365
+ * it, when the value has no member of that name of its own. Called with
9366
+ * `:`, it answers before an object's indexer, since a metatable's
9367
+ * `__index` is only read for a key the table does not hold. */
9368
+ receiverMetatableMember(objType, name) {
9369
+ const t = this.expand(objType);
9370
+ const parts = (t.kind === "union" ? t.types : [t]).map((m) => this.expand(m));
9371
+ if (parts.some((m) => m.kind === "object" && m.properties.has(name))) return void 0;
9372
+ return this.metatableMember(t, name);
9373
+ }
9111
9374
  /** The most a deferred type could turn out to be. A conditional is one of
9112
9375
  * its branches, and the true branch stands for a member of what was
9113
9376
  * tested (`T` in `T extends U ? T : never`); an indexed access reads
@@ -9173,7 +9436,7 @@ var TypeAnalyzer = class {
9173
9436
  if (index.kind !== "literal" || typeof index.value !== "string") return void 0;
9174
9437
  const parts = this.stringParts(object);
9175
9438
  if (!parts) return void 0;
9176
- const found = parts.map((part) => this.builtInMethod(part, index.value));
9439
+ const found = parts.map((part) => this.metatableMethod(part, index.value));
9177
9440
  return found.every((t) => t !== void 0) ? union(found) : void 0;
9178
9441
  }
9179
9442
  /** The members of `t` when every one of them is a string — `"a" | "b"` is
@@ -9194,13 +9457,13 @@ var TypeAnalyzer = class {
9194
9457
  * not one of them is a mistake worth reporting, rather than the nil Lua
9195
9458
  * would hand back. */
9196
9459
  checkStringMember(node, object, key) {
9197
- if (!this.emitDiagnostics || !this.aliasDefs.has("StringMethods")) return;
9460
+ if (!this.emitDiagnostics || !this.hasMetatable(stringType)) return;
9198
9461
  const parts = this.stringParts(object);
9199
9462
  if (!parts) return;
9200
9463
  const keys = (key.kind === "union" ? key.types : [key]).map((m) => this.expand(m));
9201
9464
  if (!keys.length || !keys.every((m) => m.kind === "literal" && typeof m.value === "string")) return;
9202
9465
  const names = keys.map((m) => String(m.value));
9203
- if (names.some((name) => parts.some((part) => this.builtInMethod(part, name)))) return;
9466
+ if (names.some((name) => parts.some((part) => this.metatableMethod(part, name)))) return;
9204
9467
  this.diagnostics.push({
9205
9468
  node,
9206
9469
  message: names.length === 1 ? `'${names[0]}' does not exist on a string` : `'${briefType(key)}' does not name a member of a string`
@@ -9232,24 +9495,24 @@ var TypeAnalyzer = class {
9232
9495
  switch (part.kind) {
9233
9496
  case "primitive":
9234
9497
  if (part.name === "nil") continue;
9235
- if (part.name === "string" && !this.aliasDefs.has("StringMethods")) return false;
9236
- if (part.name === "string" && this.builtInMethod(part, name)) continue;
9498
+ if (part.name === "string" && !this.hasMetatable(part)) return false;
9499
+ if (part.name === "string" && this.metatableMethod(part, name)) continue;
9237
9500
  break;
9238
9501
  case "literal":
9239
9502
  case "templateLiteral":
9240
9503
  if (part.kind === "templateLiteral" || part.base === "string") {
9241
- if (!this.aliasDefs.has("StringMethods")) return false;
9504
+ if (!this.hasMetatable(part)) return false;
9242
9505
  }
9243
- if (this.builtInMethod(part, name)) continue;
9506
+ if (this.metatableMethod(part, name)) continue;
9244
9507
  break;
9245
9508
  case "array":
9246
9509
  case "tuple":
9247
- if (!this.aliasDefs.has("ArrayMethods")) return false;
9248
- if (this.builtInMethod(part, name)) continue;
9510
+ if (!this.hasMetatable(part)) return false;
9511
+ if (this.metatableMethod(part, name)) continue;
9249
9512
  break;
9250
9513
  case "object":
9251
9514
  if (part.properties.has(name)) continue;
9252
- if (part.indexer || part.properties.size === 0 && !part.class || this.builtInMethod(part, name)) return false;
9515
+ if (part.indexer || part.properties.size === 0 && !part.class || this.metatableMethod(part, name)) return false;
9253
9516
  break;
9254
9517
  default:
9255
9518
  return false;
@@ -9384,10 +9647,10 @@ var TypeAnalyzer = class {
9384
9647
  if (p) return p.optional ? optional(p.type) : p.type;
9385
9648
  if (t.indexer) return t.indexer.value;
9386
9649
  }
9387
- const built = this.builtInMethod(t, name);
9650
+ const built = this.metatableMethod(t, name);
9388
9651
  if (built) return built;
9389
9652
  if (t.kind === "union") {
9390
- const built2 = this.builtInMethod(t, name);
9653
+ const built2 = this.metatableMethod(t, name);
9391
9654
  if (built2) return built2;
9392
9655
  return union(t.types.map((m) => this.propertyType(m, name)));
9393
9656
  }
@@ -9788,7 +10051,9 @@ var TypeAnalyzer = class {
9788
10051
  this.checkPrivateMember(expr.method, objType, expr.method.name);
9789
10052
  this.checkStringMember(expr.method, objType, literal(expr.method.name));
9790
10053
  this.checkMissingMember(expr.method, objType, expr.method.name);
9791
- const method = this.propertyType(objType, expr.method.name);
10054
+ const fromMetatable = this.receiverMetatableMember(objType, expr.method.name);
10055
+ if (fromMetatable) this.methodSources.set(expr, fromMetatable.source);
10056
+ const method = fromMetatable?.type ?? this.propertyType(objType, expr.method.name);
9792
10057
  const united = this.unionSignatures(method);
9793
10058
  const fns = united ?? this.overloadsOf(method);
9794
10059
  const explicit = this.explicitTypeArguments(expr, fns);
@@ -11030,6 +11295,8 @@ function containsTypeQuery(node) {
11030
11295
  return found;
11031
11296
  }
11032
11297
  var STRING_INTRINSICS = /* @__PURE__ */ new Set(["Uppercase", "Lowercase", "Capitalize", "Uncapitalize"]);
11298
+ var OBJECT_INTRINSICS = /* @__PURE__ */ new Set(["ObjectKeys", "ObjectValues", "ObjectEntries"]);
11299
+ var INTRINSICS = /* @__PURE__ */ new Set([...STRING_INTRINSICS, ...OBJECT_INTRINSICS]);
11033
11300
  function briefType(t) {
11034
11301
  if (t.kind === "union" && t.types.length > 8) {
11035
11302
  const shown = t.types.slice(0, 6).map(formatType).join(" | ");
@@ -11237,7 +11504,8 @@ function resolveTypeLibraries(config, host = nodeHost) {
11237
11504
  }
11238
11505
  addFile(entryFile);
11239
11506
  const lowering = loweringModule(directory, host, problems, config);
11240
- if (lowering) lowerings.push(lowering);
11507
+ const library = files.findIndex((file) => pathKey(file) === pathKey(entryFile));
11508
+ if (lowering) lowerings.push({ ...lowering, library });
11241
11509
  };
11242
11510
  for (const entry of config.types) {
11243
11511
  const relative = entry.startsWith("./") || entry.startsWith("../") || entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry);