@tilua/parser 1.0.0 → 1.0.2

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
@@ -1,702 +1,702 @@
1
- # @tilua/parser
2
-
3
- Front end for **tilua** — a TypeScript-flavoured language that compiles to Luau.
4
- Source → tokens → AST → scope analysis → flow-sensitive type analysis.
5
-
6
- This package is for people building tools: editors, linters, codemods, a
7
- compiler of your own. It parses and analyses; it never emits. **If you want to
8
- *write* tilua, you want [`@tilua/compiler`][compiler] and the
9
- [VS Code extension][vscode] instead** — though the language reference below is
10
- the same one either way.
11
-
12
- | package | for |
13
- |---|---|
14
- | [`@tilua/compiler`][compiler] | building a project to one Luau file |
15
- | [`@tilua/language-server`][server] | editor support over LSP |
16
- | [`@tilua-types/roblox`][roblox], [`@tilua-types/lua`][lua], [`@tilua-types/sunc`][sunc] | the globals a project may use |
17
- | `@tilua/parser` | this: tokens, AST, scopes, types |
18
-
19
- [compiler]: https://www.npmjs.com/package/@tilua/compiler
20
- [server]: https://www.npmjs.com/package/@tilua/language-server
21
- [roblox]: https://www.npmjs.com/package/@tilua-types/roblox
22
- [lua]: https://www.npmjs.com/package/@tilua-types/lua
23
- [sunc]: https://www.npmjs.com/package/@tilua-types/sunc
24
- [vscode]: https://marketplace.visualstudio.com/search?term=tilua
25
-
26
- ```bash
27
- npm install @tilua/parser
28
- ```
29
-
30
- ```ts
31
- import { readFileSync } from "node:fs"
32
- import {
33
- parse, analyzeScopes, analyzeTypes,
34
- findConfig, resolveTypeLibraries, resolveModulePath,
35
- } from "@tilua/parser"
36
-
37
- // The project the file belongs to, and the type libraries it names.
38
- const { config } = findConfig(file)
39
- const libs = config ? resolveTypeLibraries(config).files.map(f => parse(readFileSync(f, "utf8"))) : []
40
- const globals = libs.flatMap(lib => lib.body.statements.flatMap(s => s.type === "DeclareStatement" ? [s.name] : []))
41
-
42
- const program = parse(readFileSync(file, "utf8"))
43
- const scopes = analyzeScopes(program, { builtinGlobals: globals })
44
- const types = analyzeTypes(program, scopes, { libs })
45
-
46
- for (const d of [...scopes.diagnostics, ...types.diagnostics]) console.log(d.message)
47
- ```
48
-
49
- ## The three passes
50
-
51
- | | produces | use it for |
52
- |---|---|---|
53
- | `parse(source)` | `Program` — every node carries `line`/`column` spans | everything |
54
- | `analyzeScopes(program, opts)` | `bindingOf`, `bindings`, `references`, `diagnostics` | go-to-definition, find-references, rename |
55
- | `analyzeTypes(program, scopes, opts)` | `typeOf`, `narrowedTypeOf`, `bindingType`, `typeOfTypeNode`, `expectedTypeOf`, `aliases`, `diagnostics` | hover, completion, type errors |
56
-
57
- Every name in the AST has a node with its own span, down to
58
- `DeclareStatement.id`, `TableTypeProperty.key`, `FunctionTypeParameter.id`,
59
- `GenericTypeParameter.id`, `InferTypeNode.id` and
60
- `MappedTypeNode.parameterId`. `typeOfTypeNode` gives what each type
61
- annotation resolves to, and `expectedTypeOf` what each call argument should
62
- be, so a tool never has to re-derive a type from text.
63
-
64
- `parseWithRecovery(source)` returns `{ program, errors }` instead of throwing —
65
- use it for editors, where the text is usually mid-edit. An error costs as
66
- little of the tree as it can: a broken value becomes an `ErrorExpression`
67
- (typed `any`) in its place, a broken field or argument is skipped to the next
68
- `,`, a missing comma between fields on separate lines, or a missing `)` or
69
- `}`, is recorded and read past — a missing `}` is placed by indentation — and
70
- an unclosed string ends at its line. Skipping never lets a `}` inside a
71
- skipped function or object close the block around it. Valid code parses to
72
- exactly the same tree as `parse`.
73
-
74
- Neither analysis mutates the AST; both return side tables.
75
-
76
- ## Projects
77
-
78
- **No globals are built in** — not `print`, not `string`, not `game`. Only the
79
- language's own utility types are (`Partial`, `Pick`, `Omit`, `Record`,
80
- `ReturnType`, `Truthy`, ...; see `PRELUDE_SOURCE`). A project lists the type
81
- libraries it uses in `tilua.config.json`, the way TypeScript uses `@types/*`:
82
-
83
- ```bash
84
- npm i -D @tilua-types/roblox # Luau + Roblox; or @tilua-types/lua on its own
85
- ```
86
-
87
- ```jsonc
88
- // tilua.config.json
89
- {
90
- "types": ["roblox"], // and what it depends on: @tilua-types/lua
91
- "paths": { "@shared/*": ["src/shared/*"] }, // import aliases, as in tsconfig
92
- "sourceMap": "sourcemap.json" // a Rojo sourcemap, or null
93
- }
94
- ```
95
-
96
- - **Which config applies** — the nearest one in the file's folder or above.
97
- `tilua.config.json` and `tilua.config.jsonc` in the same folder is an error.
98
- Both forms accept comments and trailing commas.
99
- - **`types`** — any name, looked up as the package `@tilua-types/<name>` in
100
- `node_modules` from the config upward; one that is not installed is an
101
- error. A relative path (`"./types"`, `"./defs.d.tilua"`) loads the project's
102
- own definitions. A type library's own type-library dependencies load first.
103
- - **`paths`** — tsconfig rules: an exact pattern wins, then the `*` pattern
104
- with the longest prefix; targets resolve from `baseUrl` (default: the
105
- config's folder).
106
- - **`sourceMap`** — the instance tree becomes types: `game` and `workspace`
107
- follow it, and a file the tree maps gets its own `script`, so
108
- `script.Parent.Remotes` is typed. A `.tilua` file matches the Luau file of
109
- the same path.
110
-
111
- | function | does |
112
- |---|---|
113
- | `findConfig(file, host?)` | the config that applies, problems with it, and every path searched |
114
- | `loadConfig(path, host?)` | read and check one config |
115
- | `resolveTypeLibraries(config, host?)` | the `.d.tilua` files to load, in order |
116
- | `moduleCandidates(from, specifier, config?)` / `resolveModulePath(...)` | what an `import` means |
117
- | `sourceMapTypes(text, path, { classes })` | the tree's types, and `scriptFor(file)` |
118
-
119
- Every problem comes back as `{ file, message, line, column }`, pointing into
120
- the config (or sourcemap) it is about. `host` reads files — pass your own to
121
- read unsaved editor buffers or to record what was read.
122
-
123
- `type` / `typeof` are **not** special-cased in the analyzer either: they are
124
- overload sets in `@tilua-types/lua` and `@tilua-types/roblox`, and narrowing is derived
125
- from them. Without a library that declares them, they narrow nothing.
126
-
127
- Libraries stack: a name declared again *adds* to what an earlier library gave
128
- it — overloads of a function accumulate, and the members of a declared table
129
- merge. That is how `@tilua-types/roblox` extends Lua's `table` and `type` without
130
- restating them.
131
-
132
- ## The language, in brief
133
-
134
- TypeScript syntax and semantics wherever they fit, Lua semantics where they
135
- must.
136
-
137
- **Blocks** — braces, and a condition in parentheses:
138
-
139
- ```luau
140
- if (n < 0) {
141
- return "negative"
142
- } elseif (n == 0) {
143
- return "zero"
144
- } else {
145
- return "positive"
146
- }
147
-
148
- while (going) { step() }
149
- for (i = 1, 10) { total += i }
150
- for (name, value in pairs(t)) { print(name, value) }
151
- repeat { step() } until (done)
152
- do { ... }
153
-
154
- function greet(name: string): string {
155
- return `hello ${name}`
156
- }
157
-
158
- class Dog extends Animal {
159
- function speak(): string { return "woof" }
160
- }
161
- ```
162
-
163
- The parentheses are not decoration: `f {}` is a call with a table argument, so
164
- without them `if ready { ... }` would be a call of `ready` followed by a block.
165
-
166
- There is no `end` in tilua, and `then` is not a word it knows.
167
-
168
- **One statement instead of a block** — an `if`, an `elseif`, an `else`, a
169
- `while` or a `for` may be written with the single statement it does, no braces,
170
- as TypeScript writes it:
171
-
172
- ```luau
173
- if (done) return
174
- if (n < 0) return "negative"
175
- elseif (n == 0) return "zero"
176
- else return "positive"
177
-
178
- for (_, item in items) if (item) print(item)
179
- while (queued > 0) queued -= 1
180
- ```
181
-
182
- It is a block all the same — what it narrows, and what a `break` or `continue`
183
- in it leaves, end with it — so the early-return form reads exactly as the
184
- braced one does. A declaration may not be written there, as in TypeScript:
185
- nothing could reach the name `if (x) const y = 1` binds, so it is an error
186
- rather than a shorthand. A function body and `do` always take braces.
187
-
188
- `and`, `or` and `not` stay words; `{}` is still an object literal everywhere a
189
- value is expected, and `[]` an array.
190
-
191
- **Arrows** — one for both, as TypeScript writes them. `(a: number) => string`
192
- is a function type; `(a: number) => a` is a function. Which one a `=>` makes is
193
- decided by where it stands, since a type and a value never share a place.
194
-
195
- ```luau
196
- type Reducer = (total: number, value: number) => number
197
-
198
- const double = (x: number) => x * 2
199
- const add: Reducer = (a, b) => a + b -- parameters typed by the contract
200
- const shown = (n: number): string => tostring(n)
201
- const identity = <T>(v: T) => v
202
-
203
- each(n => print(n)) -- one parameter needs no parens
204
-
205
- const logged = (n: number) => { -- a block body is a block,
206
- print(n) -- as in TypeScript,
207
- return n
208
- }
209
- const wrap = (n: number) => ({ value: n }) -- so an object is parenthesized
210
- ```
211
-
212
- An arrow is a function expression written short — there is no second kind of
213
- function — so `this` inside one is the `this` of the method around it, which
214
- is what a JavaScript arrow is for.
215
-
216
- **Declarations** — `const` and `let` only; Lua's `local` is gone.
217
-
218
- **Functions** — `function name() { ... }` declares `name` in the enclosing
219
- scope; like a TypeScript function declaration it cannot be reassigned.
220
- `const` and `let` do not apply to functions. `function T.name()` and
221
- `function T:name()` define a member.
222
-
223
- **Hoisting** — a function declaration is visible to its whole block, above
224
- itself too, so `let r: ReturnType<typeof load>` may come before `function
225
- load()`. A closure reads the name its own value is bound to, as in JavaScript
226
- (`let m = { clear: function() { m.items = {} } }`), and a later name in the
227
- same block; the compiler declares such a name before the statement that fills
228
- it. A module's top-level names are visible to code that runs later —
229
- function bodies and `typeof` — wherever that code is written, since a bundle
230
- declares them all before the module runs. At the top level the whole function
231
- is hoisted, and can be called above its declaration. Inside a function only
232
- the name is: other functions can call it, but a call straight in the block
233
- above the declaration is an error, because nothing is there yet.
234
-
235
- **Returns** — a declared return type is checked: what a `return` gives must
236
- fit it, and a function that declared one must return a value (a guard or an
237
- `asserts` function needs none). The declared type also types what is written
238
- there, so a returned callback takes its parameters from it.
239
-
240
- **Overloads** — a `function name(...)` with no body is a signature for the
241
- declaration that follows it, as in TypeScript: the signatures are what a call
242
- sees, and the last one, with the body, is the implementation. `export` goes on
243
- every line of the set or none of them. A parameter of the implementation that
244
- carries no annotation holds what the signatures allow there — under
245
- `get(stat: "hp")` and `get(stat: "name")`, the implementation's `stat` is
246
- `"hp" | "name"` rather than `any`.
247
-
248
- **Modules** — `import { a, b as c } from "./m"`, `import D from "./m"` and
249
- `import * as M from "./m"`; `export const`, `export function`, `export default`,
250
- `export { a as b }`, `export { a } from "./m"` and `export * from "./m"`.
251
- Imports are read-only: assigning to an imported name, or to a member of a
252
- namespace (`M.x = 1`), is an error.
253
-
254
- `import type { A } from "./m"` (also `import type D` and `import type * as M`)
255
- brings in names that are types and nothing else: unlike TypeScript, using one
256
- as a value is an error, and only type positions — `typeof A` included — may
257
- name it. Compiled code keeps no trace of it.
258
-
259
- **Array and string methods** — an array and a string answer to methods
260
- written with `:`, the way JavaScript writes them:
261
-
262
- ```luau
263
- const long = names:filter(n => #n > 3):map(string.upper)
264
- const first = names:find(n => n:startsWith("A"))
265
- print(names:join(", "), text:trim(), text:replaceAll(",", ";"))
266
- ```
267
-
268
- Which methods those are is not the language's business. The analyzer looks for
269
- two types by name — `ArrayMethods<T>` and `StringMethods` — and reads an
270
- array's or a string's members out of whichever type library declared them;
271
- without such a library an array has no methods at all.
272
-
273
- Running them is that library's business too. A library points at a JavaScript
274
- module in its package.json, and the compiler asks it what a call becomes:
275
-
276
- ```json
277
- "tilua": { "types": "index.d.tilua", "lowering": "lowering.mjs" }
278
- ```
279
-
280
- ```js
281
- // @ts-check
282
- /** @type {import("@tilua/parser").LoweringPlugin} */ // the contract, declared here
283
- const plugin = {
284
- runtime: { array: "local __NAME__ = {}\nfunction __NAME__.filter(t, test) ... end" },
285
- methodCall({ method, receiver, use }) {
286
- if (receiver?.kind === "array" && method === "filter") {
287
- return { callee: `${use("array")}.filter` }
288
- }
289
- return undefined
290
- },
291
- }
292
- export default plugin
293
- ```
294
-
295
- `receiver` is the tilua type the analyzer worked out, `use(key)` gives the
296
- local name that table got — emitted once, at the top of the output, only if a
297
- call needed it — and the receiver is passed as the call's first argument. An
298
- answer of `undefined` leaves an ordinary Luau method call, which is what
299
- `text:upper()` wants, since a string already answers to it.
300
-
301
- The compiler lowers the language and nothing else: `filter` appears nowhere in
302
- it.
303
-
304
- `@tilua-types/lua` ships the JavaScript-shaped set; there, indices are Luau's (the
305
- first element is 1, `indexOf` answers `nil` rather than -1) and `push`, `pop`,
306
- `shift`, `unshift`, `sort` and `reverse` change the array they are called on.
307
-
308
- **A `(` that starts a line** continues the statement above it, as in Lua and
309
- in JavaScript — `const v = map[key]` followed by `("A"):upper()` is one
310
- statement, a call of `map[key]`. tilua says so rather than letting it pass:
311
- write `;` before the `(` when a new statement was meant.
312
-
313
- **Object types** — `{ name: T, name?: T, [K]: V }`, and a name that is not an
314
- identifier is quoted, as in the literal: `{ "Respawn After Kill": boolean }`.
315
-
316
- **Optionality** — there is no `T?` shorthand. `?` in type position always
317
- belongs to a conditional type, and in expression position to a ternary or an
318
- optional chain.
319
-
320
- ```luau
321
- name?: T -- may be absent; its type is `T | nil`
322
- name: T | nil -- must be written, but may be nil
323
- ```
324
-
325
- Omitting an argument requires `?` (or a default), as in TypeScript — a
326
- parameter typed `T | nil` still has to be passed something.
327
-
328
- **Optional chaining** — `a?.b` and `a?:m(x)` are nil when `a` is, and then
329
- nothing further along the chain runs, arguments included: `folder?:FindFirstChild("A")?.Name`
330
- is a `string | nil`. The `?` must touch the `.` or `:`; `c ? a : b` stays a
331
- ternary. Parentheses end a chain. A chain cannot be assigned to (`a?.b = 1` is
332
- an error). A chain that got through narrows what it tested: inside
333
- `if (part?.Parent)`, and `if (part?.Name == "Door")`, `part` is not nil.
334
-
335
- **Classes** — types are structural, except for classes. A definitions file
336
- declares one with `declare class`, and it is nominal, as Roblox's classes are:
337
-
338
- ```luau
339
- declare class BasePart extends PVInstance { Size: Vector3 }
340
- declare class Part extends BasePart { Shape: EnumItem }
341
- ```
342
-
343
- A `Part` is a `BasePart` and an `Instance` because it extends them. A
344
- `ReplicatedStorage` is not a `Part`, and no table literal is an `Instance`,
345
- however alike their members. A class still fits a shape that names members it
346
- has (`{ Name: string }`). It is not a table, though, so `typeof(part)` picks
347
- the `"Instance"` overload, not `"table"`. Members are inherited, and a subclass
348
- may narrow one (`Parent: SomeFolder`).
349
-
350
- **`class ... { }`** — a class written in code, rather than declared in a
351
- definitions file. It is sugar over the Lua idiom, and the shape it stands for
352
- is exactly that one: the class is a single table holding the methods and the
353
- statics, and an instance is a table whose metatable points at it. An instance
354
- therefore reaches *the class* — nothing is copied per instance, and there is
355
- no prototype chain of an instance's own.
356
-
357
- ```luau
358
- class Animal {
359
- name: string -- set by the constructor
360
- legs = 4 -- set on every instance, before the body runs
361
- static count = 0 -- on the class table, once
362
-
363
- constructor(name: string) {
364
- this.name = name
365
- Animal.count += 1
366
- }
367
-
368
- function speak(): string {
369
- return `${this.name} makes a sound`
370
- }
371
-
372
- get label(): string { -- read as a property, run as a function
373
- return `<${this.name}>`
374
- }
375
-
376
- set label(value: string) {
377
- this.name = value
378
- }
379
-
380
- static function made(): number {
381
- return Animal.count
382
- }
383
- }
384
-
385
- class Dog extends Animal {
386
- breed: string
387
-
388
- constructor(name: string, breed: string) {
389
- super(name) -- required: it is what fills in the base
390
- this.breed = breed
391
- }
392
-
393
- function speak(): string {
394
- return `${super.speak()} (woof)`
395
- }
396
- }
397
-
398
- const rex = new Dog("Rex", "shiba")
399
- rex:speak() -- the receiver is `this`
400
- rex.label = "Max" -- the setter
401
- Dog.made() -- a static, inherited from Animal
402
- ```
403
-
404
- The receiver is written `this`, and it is an ordinary first parameter: a
405
- method's type is `(this: Dog, ...) => R`, so `rex:speak()` supplies it the way
406
- `function T:m()` supplies `self`. `new Dog(x)` *is* `Dog.new(x)` — the same
407
- function, callable by hand and passable as a value.
408
-
409
- A declaration names two things. As a **type**, `Dog` is the type of its
410
- instances, nominal the same way a `declare class` is: a table with the same
411
- members is not one, and only `Dog` and what extends it are assignable to it.
412
- As a **value**, `Dog` is the class table — its statics, and the `new` that
413
- builds an instance. `export class` exports both.
414
-
415
- Two links are always there, and they are what the memory model promises:
416
-
417
- ```luau
418
- rex.ClassObject == Dog -- an instance names its class
419
- Dog.ParentClass == Animal -- a class names the one it extends
420
- Animal.ParentClass == nil -- and a root class extends nothing
421
- ```
422
-
423
- Both live on the class table, so an instance carries neither.
424
-
425
- **Generic classes** — `class Box<T> { ... }`. The name is then a generic type,
426
- and `Box<number>` and `Box<string>` are both `Box` but neither is the other:
427
-
428
- ```luau
429
- class Box<T> {
430
- value: T
431
- constructor(value: T) {
432
- this.value = value
433
- }
434
- function get(): T {
435
- return this.value
436
- }
437
- function map<R>(f: (value: T) => R): Box<R> {
438
- return new Box(f(this.value))
439
- }
440
- }
441
-
442
- const n = new Box(1) -- Box<number>, read off the argument
443
- const s = new Box<string>("a") -- or written out
444
- const held: number = n:get()
445
-
446
- class Ints extends Box<number> { -- extending one fixes its argument
447
- constructor(n: number) { super(n) }
448
- }
449
- ```
450
-
451
- A function takes the argument off what it is handed: `function unwrap<T>(b:
452
- Box<T>): T` given a `Box<boolean>` returns `boolean`.
453
-
454
- **A class as a value** — `const Counter = class { ... }`, and `export default
455
- class { ... }`. It may be named, and then the name is visible only inside its
456
- own body, as in JavaScript; an anonymous one is known by whatever holds it.
457
- Two of them are different types however alike they look. A class written as a
458
- value takes no type parameters — nothing could write the arguments.
459
-
460
- `export default class Name { ... }` declares `Name` here as well as exporting
461
- it, as TypeScript's does, and importing it brings in the type too:
462
-
463
- ```luau
464
- import Box from "./box" -- `Box` is the class *and* the type
465
- const held: Box<number> = new Box(1)
466
- ```
467
-
468
- Reported: a field with a type that nothing gives a value (`name: string` that
469
- the constructor never assigns), a derived constructor that does not call
470
- `super(...)`, `extends` naming something that is not a class, a chain that
471
- closes on itself, a member written twice, and a member named one of the words
472
- the compiler builds the class table out of (`new`, `ClassObject`,
473
- `ParentClass`, `__init`, `__index`, `__newindex`, `__getters`, `__setters`,
474
- `__dynamic`).
475
-
476
- **Varargs** — `...` is Lua's pack, and every name on the left reads one of
477
- it: with `...: number`, `const a, b = ...` gives two numbers. `[...]` puts the
478
- whole pack in an array.
479
-
480
- **Rest parameters** — `...name: T[]` is JavaScript's: every argument from that
481
- position on, as an array. It is last, and the call signature is the same one
482
- `...: T` describes — the difference is only what the body sees.
483
-
484
- ```luau
485
- function join(separator: string, ...parts: string[]): string {
486
- return table.concat(parts, separator) -- `parts` is a string[] here
487
- }
488
-
489
- join("-", "a", "b") -- and a vararg call out here
490
- join("-", 1) -- Argument of type '1' is not assignable to 'string'
491
-
492
- function firstOf<T>(...items: T[]): T | nil {
493
- return items[1]
494
- }
495
-
496
- firstOf(1, 2) -- number | nil: the arguments say what `T` is
497
- ```
498
-
499
- A type is written the same way: `type Reporter = (level: string, ...lines:
500
- string[]) => ()` describes the same calls as `(level: string, ...string) =>
501
- ()`. Without an annotation a rest parameter is `unknown[]`; annotated with
502
- something that is not an array, it is reported.
503
-
504
- **Spreads** — `...xs` puts what an array holds into any list of values, as
505
- JavaScript does: a call's arguments, a `return`, a declaration, an assignment.
506
- Bare `...` is unchanged, and is still the pack.
507
-
508
- ```luau
509
- join("-", ...names) -- every name
510
- join("-", ...names, "z") -- and one more after them
511
- add3(...nums) -- however many `nums` turns out to hold
512
-
513
- function three(): (number, number, number) {
514
- return ...nums
515
- }
516
-
517
- const first, second = ...names -- one each, as far as the names go
518
- ```
519
-
520
- What the array holds is checked against what it fills. How many it holds is
521
- not known, so nothing is said about the count — unless it is a tuple, which
522
- holds a known value at each position and is checked one by one. Spreading
523
- something that is not a list is reported.
524
-
525
- **Branded types** — nothing in the type model is about branding; an
526
- intersection already means it. `string & { __brand }` is assignable to
527
- `string`, and `string` is not assignable to it, which is the whole of it:
528
-
529
- ```luau
530
- type UserId = string & { readonly __brand: "UserId" }
531
- type PostId = string & { readonly __brand: "PostId" }
532
-
533
- declare function findUser(id: UserId): string
534
-
535
- const id = "raw" as UserId -- `as` is how one is made
536
- findUser(id) -- ok
537
- findUser("raw") -- '"raw"' is not assignable to 'UserId'
538
- findUser(postId) -- 'PostId' is not assignable to 'UserId'
539
-
540
- #id -- still a string: 5
541
- id:upper() -- and its methods, giving a plain string
542
- ```
543
-
544
- Anything that builds a new value builds an unbranded one — which is the point:
545
- the brand says where the value came from. It works on any type (`number & {
546
- __brand }`), and survives being stored, indexed and narrowed.
547
-
548
- **Callbacks** — a function written where a function type is expected takes
549
- its parameter types from it: in `signal:Connect(player => ... )`,
550
- `player` is typed from `Connect`. The same applies to an annotated `const`
551
- and to an assignment such as `remote.OnServerInvoke = function(player) { ... }`.
552
-
553
- **Type packs** — `type Signal<T... = ...any> = { Connect: (self, cb: (T...) => ()) => () }`.
554
- A pack parameter takes every type argument from its position on:
555
- `Signal<Player, string>`, `Signal<()>` for none.
556
-
557
- **Operators** — on a type that declares metamethods (`__add`, `__mul`,
558
- `__unm`, ...), an operator has the metamethod's result, tried on the left
559
- operand and then the right one, as Luau does. So `Vector3 + Vector3` and
560
- `2 * vector` are both `Vector3`.
561
-
562
- **Qualified type names** — a definitions file may declare `Enum.Material`
563
- (`declare class Enum.Material extends EnumItem {}`), and code writes it the
564
- same way.
565
-
566
- **Contextual typing** — an expression takes its type from where it is
567
- written, as in TypeScript: `let queue: thread[] = []` is a `thread[]`, and so
568
- is `[]` passed where one is expected, including inside an object literal.
569
-
570
- **Calls** — every argument is checked against its parameter, and a generic
571
- parameter against its constraint (`GetService<K extends keyof Services>`
572
- rejects `""`). Arguments are checked again once the call's own type arguments
573
- are known, so `pick("Bones", "Blast1")` is caught where `pick`'s second
574
- parameter reads `Extract<Rows, { Page: P }>["Skills"][number]`. A type that
575
- waits on a type parameter — a conditional, an index, `T[K]` — is worked out
576
- where that parameter is.
577
-
578
- **Trailing commas** are allowed wherever TypeScript allows them: parameter
579
- lists, call arguments, generic parameters and type arguments, tables, arrays,
580
- tuples, imports and exports.
581
-
582
- A value read by a key narrows the key: after `const path = paths[stat]`, the
583
- `else` of `if path then` leaves `stat` as exactly the keys `paths` does not
584
- have — the same correlation `pairs` over a record and a destructured union
585
- already get.
586
-
587
- **Narrowing** follows TypeScript's model: references (`x`, `x.a.b`, `x["k"]`)
588
- rather than just variables, discriminated unions at any depth, `and`/`or`,
589
- early return, `break`/`continue`, `error()` (declared `=> never`), user type
590
- guards (`v is T`), and assertion signatures (`asserts v`).
591
-
592
- Reading a member of, indexing or calling a value that may be nil is an error
593
- until a check narrows the nil away, as with TypeScript's `strictNullChecks`:
594
- `FindFirstChild("A"):FindFirstChild("B")` reports that the first call is
595
- possibly nil. Use `?.` / `?:`, or check first. The read is still typed from the
596
- non-nil part.
597
-
598
- Only `nil` and `false` are falsy — `0` and `""` are truthy, unlike JavaScript.
599
-
600
- **Types** — unions, intersections, tuples `[A, B]`, type packs `(A, B)` (the
601
- several values a function returns), `keyof`, `T[K]`, conditional types with
602
- `infer`, mapped types with `as` remapping, template literal types
603
- (`` `on${Event}` ``), and set difference `A - B`. The utility types
604
- (`Partial`, `Pick`, `Omit`, `ReturnType`, `Parameters`, `Exclude`, …) are
605
- built in, and written in tilua on top of those rather than special-cased in
606
- the analyzer. A type library or a file may declare one again; the later
607
- declaration wins.
608
-
609
- A call may write its type arguments out — `find<Folder>("Remotes")`,
610
- `inst:WaitForChild<Folder>("Remotes")` — and a type parameter may have a
611
- default (`<T = Instance>`) for the calls that do not. `a < b > (c)` is still
612
- three operators: only a call after the `>` makes it type arguments.
613
-
614
- `<const T>` infers an argument at its narrowest, as in TypeScript 5.
615
-
616
- `typeof x` in a type is TypeScript's type query — the type of a value
617
- (`typeof config`, `typeof config.volume`, `ReturnType<typeof f>`). Luau's
618
- `typeof(expr)` spelling works too. It is compile-time only, unrelated to the
619
- `typeof(v)` function that returns a string at runtime.
620
-
621
- **Modules** — `import` / `export`, export lists, re-exports and `export *`.
622
-
623
- **`satisfies`** — checks a value against a type without giving it that type,
624
- as in TypeScript 4.9:
625
-
626
- ```luau
627
- type Shape = { kind: "circle" | "rect", size: number }
628
- const circle = { kind: "circle", size: 2 } satisfies Shape -- { kind: "circle", size: number }
629
- const handlers = {
630
- Click: x => x + 1, -- x: number, from the contract
631
- } satisfies { [string]: (x: number) => number }
632
- ```
633
-
634
- The contract types callbacks and empty arrays, and a literal stays a literal
635
- where the contract asks for literals (`kind: "circle"`, not `string`). A value
636
- that does not fit is an error. So is a property the contract does not know —
637
- TypeScript's excess property check, which applies to an object literal written
638
- straight into an annotation (`const s: Shape = { ..., typo: 1 }`) too. A value
639
- that already has a type of its own keeps it exactly: `{ ... } as const
640
- satisfies T` stays readonly and literal. `as` reinterprets instead of
641
- checking, and compiled code keeps neither.
642
-
643
- **Undeclared names** — `analyzeScopes(program, { builtinGlobals, reportUndeclared: true })`
644
- reports each read of a name nothing declares: "Cannot find name 'x'". A global
645
- assigned in the file (`x = 1`) and a `declare` count as declarations.
646
- `analyzeTypes(program, scopes, { reportUnknownTypes: true })` does the same for
647
- type names. Both are off by default, since they are only right when the type
648
- libraries the file names are loaded.
649
-
650
- **Directives** — comments that switch checking off, as TypeScript's
651
- `// @ts-...` do. They silence scope and type errors, never syntax errors:
652
-
653
- ```luau
654
- --@tilua-nocheck -- before the first line of code: the whole file
655
- --@tilua-ignore -- the next line of code
656
- --@tilua-expect-error -- the next line of code, which must have an error
657
- ```
658
-
659
- `parseWithRecovery` returns them as `directives`; `directivesOf(source)` reads
660
- them for a caller that parsed some other way, and
661
- `applyDirectives(directives, diagnostics, lineOf)` filters a list and names
662
- each `expect-error` that had nothing to suppress.
663
-
664
- ## Options
665
-
666
- ```ts
667
- analyzeScopes(program, {
668
- builtinGlobals: ["print", "game"], // names that may be used undeclared
669
- })
670
-
671
- analyzeTypes(program, scopes, {
672
- libs, // parsed `.d.tilua` definitions
673
- globalTypes: { … }, // types for specific globals; wins over `libs`
674
- libTypes: { … }, // extra named types for annotations
675
- diagnostics: true, // emit type errors (default)
676
- resolveModule: specifier => exportsOfThatFile,
677
- // what an `import` sees; without it imports are `any`
678
- })
679
- ```
680
-
681
- ## Known limitations
682
-
683
- - `export * as ns from` and namespace imports (`import * as ns`) are not
684
- supported.
685
- - `setmetatable` and metatables are not modelled.
686
- - Accessing a property a type does not have yields `unknown` rather than an
687
- error; assigning to a `readonly` property is not reported.
688
- - A sourcemap child whose name is not an identifier (`"My Part"`) is not typed.
689
-
690
- ## Development
691
-
692
- ```bash
693
- npm install
694
- npm test # smoketests (types via smoketest/tilua.config.json), then project tests
695
- npm run build
696
- npm run typecheck
697
- ```
698
-
699
- `smoketest/` is the test suite. Each file annotates its bindings with the type
700
- they should infer to, so a wrong result surfaces as a diagnostic rather than
701
- something to eyeball. `scripts/project.test.ts` covers configs, type
702
- libraries, import paths and sourcemaps against an in-memory file system.
1
+ # @tilua/parser
2
+
3
+ Front end for **tilua** — a TypeScript-flavoured language that compiles to Luau.
4
+ Source → tokens → AST → scope analysis → flow-sensitive type analysis.
5
+
6
+ This package is for people building tools: editors, linters, codemods, a
7
+ compiler of your own. It parses and analyses; it never emits. **If you want to
8
+ *write* tilua, you want [`@tilua/compiler`][compiler] and the
9
+ [VS Code extension][vscode] instead** — though the language reference below is
10
+ the same one either way.
11
+
12
+ | package | for |
13
+ |---|---|
14
+ | [`@tilua/compiler`][compiler] | building a project to one Luau file |
15
+ | [`@tilua/language-server`][server] | editor support over LSP |
16
+ | [`@tilua-types/roblox`][roblox], [`@tilua-types/lua`][lua], [`@tilua-types/sunc`][sunc] | the globals a project may use |
17
+ | `@tilua/parser` | this: tokens, AST, scopes, types |
18
+
19
+ [compiler]: https://www.npmjs.com/package/@tilua/compiler
20
+ [server]: https://www.npmjs.com/package/@tilua/language-server
21
+ [roblox]: https://www.npmjs.com/package/@tilua-types/roblox
22
+ [lua]: https://www.npmjs.com/package/@tilua-types/lua
23
+ [sunc]: https://www.npmjs.com/package/@tilua-types/sunc
24
+ [vscode]: https://marketplace.visualstudio.com/search?term=tilua
25
+
26
+ ```bash
27
+ npm install @tilua/parser
28
+ ```
29
+
30
+ ```ts
31
+ import { readFileSync } from "node:fs"
32
+ import {
33
+ parse, analyzeScopes, analyzeTypes,
34
+ findConfig, resolveTypeLibraries, resolveModulePath,
35
+ } from "@tilua/parser"
36
+
37
+ // The project the file belongs to, and the type libraries it names.
38
+ const { config } = findConfig(file)
39
+ const libs = config ? resolveTypeLibraries(config).files.map(f => parse(readFileSync(f, "utf8"))) : []
40
+ const globals = libs.flatMap(lib => lib.body.statements.flatMap(s => s.type === "DeclareStatement" ? [s.name] : []))
41
+
42
+ const program = parse(readFileSync(file, "utf8"))
43
+ const scopes = analyzeScopes(program, { builtinGlobals: globals })
44
+ const types = analyzeTypes(program, scopes, { libs })
45
+
46
+ for (const d of [...scopes.diagnostics, ...types.diagnostics]) console.log(d.message)
47
+ ```
48
+
49
+ ## The three passes
50
+
51
+ | | produces | use it for |
52
+ |---|---|---|
53
+ | `parse(source)` | `Program` — every node carries `line`/`column` spans | everything |
54
+ | `analyzeScopes(program, opts)` | `bindingOf`, `bindings`, `references`, `diagnostics` | go-to-definition, find-references, rename |
55
+ | `analyzeTypes(program, scopes, opts)` | `typeOf`, `narrowedTypeOf`, `bindingType`, `typeOfTypeNode`, `expectedTypeOf`, `aliases`, `diagnostics` | hover, completion, type errors |
56
+
57
+ Every name in the AST has a node with its own span, down to
58
+ `DeclareStatement.id`, `TableTypeProperty.key`, `FunctionTypeParameter.id`,
59
+ `GenericTypeParameter.id`, `InferTypeNode.id` and
60
+ `MappedTypeNode.parameterId`. `typeOfTypeNode` gives what each type
61
+ annotation resolves to, and `expectedTypeOf` what each call argument should
62
+ be, so a tool never has to re-derive a type from text.
63
+
64
+ `parseWithRecovery(source)` returns `{ program, errors }` instead of throwing —
65
+ use it for editors, where the text is usually mid-edit. An error costs as
66
+ little of the tree as it can: a broken value becomes an `ErrorExpression`
67
+ (typed `any`) in its place, a broken field or argument is skipped to the next
68
+ `,`, a missing comma between fields on separate lines, or a missing `)` or
69
+ `}`, is recorded and read past — a missing `}` is placed by indentation — and
70
+ an unclosed string ends at its line. Skipping never lets a `}` inside a
71
+ skipped function or object close the block around it. Valid code parses to
72
+ exactly the same tree as `parse`.
73
+
74
+ Neither analysis mutates the AST; both return side tables.
75
+
76
+ ## Projects
77
+
78
+ **No globals are built in** — not `print`, not `string`, not `game`. Only the
79
+ language's own utility types are (`Partial`, `Pick`, `Omit`, `Record`,
80
+ `ReturnType`, `Truthy`, ...; see `PRELUDE_SOURCE`). A project lists the type
81
+ libraries it uses in `tilua.config.json`, the way TypeScript uses `@types/*`:
82
+
83
+ ```bash
84
+ npm i -D @tilua-types/roblox # Luau + Roblox; or @tilua-types/lua on its own
85
+ ```
86
+
87
+ ```jsonc
88
+ // tilua.config.json
89
+ {
90
+ "types": ["roblox"], // and what it depends on: @tilua-types/lua
91
+ "paths": { "@shared/*": ["src/shared/*"] }, // import aliases, as in tsconfig
92
+ "sourceMap": "sourcemap.json" // a Rojo sourcemap, or null
93
+ }
94
+ ```
95
+
96
+ - **Which config applies** — the nearest one in the file's folder or above.
97
+ `tilua.config.json` and `tilua.config.jsonc` in the same folder is an error.
98
+ Both forms accept comments and trailing commas.
99
+ - **`types`** — any name, looked up as the package `@tilua-types/<name>` in
100
+ `node_modules` from the config upward; one that is not installed is an
101
+ error. A relative path (`"./types"`, `"./defs.d.tilua"`) loads the project's
102
+ own definitions. A type library's own type-library dependencies load first.
103
+ - **`paths`** — tsconfig rules: an exact pattern wins, then the `*` pattern
104
+ with the longest prefix; targets resolve from `baseUrl` (default: the
105
+ config's folder).
106
+ - **`sourceMap`** — the instance tree becomes types: `game` and `workspace`
107
+ follow it, and a file the tree maps gets its own `script`, so
108
+ `script.Parent.Remotes` is typed. A `.tilua` file matches the Luau file of
109
+ the same path.
110
+
111
+ | function | does |
112
+ |---|---|
113
+ | `findConfig(file, host?)` | the config that applies, problems with it, and every path searched |
114
+ | `loadConfig(path, host?)` | read and check one config |
115
+ | `resolveTypeLibraries(config, host?)` | the `.d.tilua` files to load, in order |
116
+ | `moduleCandidates(from, specifier, config?)` / `resolveModulePath(...)` | what an `import` means |
117
+ | `sourceMapTypes(text, path, { classes })` | the tree's types, and `scriptFor(file)` |
118
+
119
+ Every problem comes back as `{ file, message, line, column }`, pointing into
120
+ the config (or sourcemap) it is about. `host` reads files — pass your own to
121
+ read unsaved editor buffers or to record what was read.
122
+
123
+ `type` / `typeof` are **not** special-cased in the analyzer either: they are
124
+ overload sets in `@tilua-types/lua` and `@tilua-types/roblox`, and narrowing is derived
125
+ from them. Without a library that declares them, they narrow nothing.
126
+
127
+ Libraries stack: a name declared again *adds* to what an earlier library gave
128
+ it — overloads of a function accumulate, and the members of a declared table
129
+ merge. That is how `@tilua-types/roblox` extends Lua's `table` and `type` without
130
+ restating them.
131
+
132
+ ## The language, in brief
133
+
134
+ TypeScript syntax and semantics wherever they fit, Lua semantics where they
135
+ must.
136
+
137
+ **Blocks** — braces, and a condition in parentheses:
138
+
139
+ ```luau
140
+ if (n < 0) {
141
+ return "negative"
142
+ } elseif (n == 0) {
143
+ return "zero"
144
+ } else {
145
+ return "positive"
146
+ }
147
+
148
+ while (going) { step() }
149
+ for (i = 1, 10) { total += i }
150
+ for (name, value in pairs(t)) { print(name, value) }
151
+ repeat { step() } until (done)
152
+ do { ... }
153
+
154
+ function greet(name: string): string {
155
+ return `hello ${name}`
156
+ }
157
+
158
+ class Dog extends Animal {
159
+ function speak(): string { return "woof" }
160
+ }
161
+ ```
162
+
163
+ The parentheses are not decoration: `f {}` is a call with a table argument, so
164
+ without them `if ready { ... }` would be a call of `ready` followed by a block.
165
+
166
+ There is no `end` in tilua, and `then` is not a word it knows.
167
+
168
+ **One statement instead of a block** — an `if`, an `elseif`, an `else`, a
169
+ `while` or a `for` may be written with the single statement it does, no braces,
170
+ as TypeScript writes it:
171
+
172
+ ```luau
173
+ if (done) return
174
+ if (n < 0) return "negative"
175
+ elseif (n == 0) return "zero"
176
+ else return "positive"
177
+
178
+ for (_, item in items) if (item) print(item)
179
+ while (queued > 0) queued -= 1
180
+ ```
181
+
182
+ It is a block all the same — what it narrows, and what a `break` or `continue`
183
+ in it leaves, end with it — so the early-return form reads exactly as the
184
+ braced one does. A declaration may not be written there, as in TypeScript:
185
+ nothing could reach the name `if (x) const y = 1` binds, so it is an error
186
+ rather than a shorthand. A function body and `do` always take braces.
187
+
188
+ `and`, `or` and `not` stay words; `{}` is still an object literal everywhere a
189
+ value is expected, and `[]` an array.
190
+
191
+ **Arrows** — one for both, as TypeScript writes them. `(a: number) => string`
192
+ is a function type; `(a: number) => a` is a function. Which one a `=>` makes is
193
+ decided by where it stands, since a type and a value never share a place.
194
+
195
+ ```luau
196
+ type Reducer = (total: number, value: number) => number
197
+
198
+ const double = (x: number) => x * 2
199
+ const add: Reducer = (a, b) => a + b -- parameters typed by the contract
200
+ const shown = (n: number): string => tostring(n)
201
+ const identity = <T>(v: T) => v
202
+
203
+ each(n => print(n)) -- one parameter needs no parens
204
+
205
+ const logged = (n: number) => { -- a block body is a block,
206
+ print(n) -- as in TypeScript,
207
+ return n
208
+ }
209
+ const wrap = (n: number) => ({ value: n }) -- so an object is parenthesized
210
+ ```
211
+
212
+ An arrow is a function expression written short — there is no second kind of
213
+ function — so `this` inside one is the `this` of the method around it, which
214
+ is what a JavaScript arrow is for.
215
+
216
+ **Declarations** — `const` and `let` only; Lua's `local` is gone.
217
+
218
+ **Functions** — `function name() { ... }` declares `name` in the enclosing
219
+ scope; like a TypeScript function declaration it cannot be reassigned.
220
+ `const` and `let` do not apply to functions. `function T.name()` and
221
+ `function T:name()` define a member.
222
+
223
+ **Hoisting** — a function declaration is visible to its whole block, above
224
+ itself too, so `let r: ReturnType<typeof load>` may come before `function
225
+ load()`. A closure reads the name its own value is bound to, as in JavaScript
226
+ (`let m = { clear: function() { m.items = {} } }`), and a later name in the
227
+ same block; the compiler declares such a name before the statement that fills
228
+ it. A module's top-level names are visible to code that runs later —
229
+ function bodies and `typeof` — wherever that code is written, since a bundle
230
+ declares them all before the module runs. At the top level the whole function
231
+ is hoisted, and can be called above its declaration. Inside a function only
232
+ the name is: other functions can call it, but a call straight in the block
233
+ above the declaration is an error, because nothing is there yet.
234
+
235
+ **Returns** — a declared return type is checked: what a `return` gives must
236
+ fit it, and a function that declared one must return a value (a guard or an
237
+ `asserts` function needs none). The declared type also types what is written
238
+ there, so a returned callback takes its parameters from it.
239
+
240
+ **Overloads** — a `function name(...)` with no body is a signature for the
241
+ declaration that follows it, as in TypeScript: the signatures are what a call
242
+ sees, and the last one, with the body, is the implementation. `export` goes on
243
+ every line of the set or none of them. A parameter of the implementation that
244
+ carries no annotation holds what the signatures allow there — under
245
+ `get(stat: "hp")` and `get(stat: "name")`, the implementation's `stat` is
246
+ `"hp" | "name"` rather than `any`.
247
+
248
+ **Modules** — `import { a, b as c } from "./m"`, `import D from "./m"` and
249
+ `import * as M from "./m"`; `export const`, `export function`, `export default`,
250
+ `export { a as b }`, `export { a } from "./m"` and `export * from "./m"`.
251
+ Imports are read-only: assigning to an imported name, or to a member of a
252
+ namespace (`M.x = 1`), is an error.
253
+
254
+ `import type { A } from "./m"` (also `import type D` and `import type * as M`)
255
+ brings in names that are types and nothing else: unlike TypeScript, using one
256
+ as a value is an error, and only type positions — `typeof A` included — may
257
+ name it. Compiled code keeps no trace of it.
258
+
259
+ **Array and string methods** — an array and a string answer to methods
260
+ written with `:`, the way JavaScript writes them:
261
+
262
+ ```luau
263
+ const long = names:filter(n => #n > 3):map(string.upper)
264
+ const first = names:find(n => n:startsWith("A"))
265
+ print(names:join(", "), text:trim(), text:replaceAll(",", ";"))
266
+ ```
267
+
268
+ Which methods those are is not the language's business. The analyzer looks for
269
+ two types by name — `ArrayMethods<T>` and `StringMethods` — and reads an
270
+ array's or a string's members out of whichever type library declared them;
271
+ without such a library an array has no methods at all.
272
+
273
+ Running them is that library's business too. A library points at a JavaScript
274
+ module in its package.json, and the compiler asks it what a call becomes:
275
+
276
+ ```json
277
+ "tilua": { "types": "index.d.tilua", "lowering": "lowering.mjs" }
278
+ ```
279
+
280
+ ```js
281
+ // @ts-check
282
+ /** @type {import("@tilua/parser").LoweringPlugin} */ // the contract, declared here
283
+ const plugin = {
284
+ runtime: { array: "local __NAME__ = {}\nfunction __NAME__.filter(t, test) ... end" },
285
+ methodCall({ method, receiver, use }) {
286
+ if (receiver?.kind === "array" && method === "filter") {
287
+ return { callee: `${use("array")}.filter` }
288
+ }
289
+ return undefined
290
+ },
291
+ }
292
+ export default plugin
293
+ ```
294
+
295
+ `receiver` is the tilua type the analyzer worked out, `use(key)` gives the
296
+ local name that table got — emitted once, at the top of the output, only if a
297
+ call needed it — and the receiver is passed as the call's first argument. An
298
+ answer of `undefined` leaves an ordinary Luau method call, which is what
299
+ `text:upper()` wants, since a string already answers to it.
300
+
301
+ The compiler lowers the language and nothing else: `filter` appears nowhere in
302
+ it.
303
+
304
+ `@tilua-types/lua` ships the JavaScript-shaped set; there, indices are Luau's (the
305
+ first element is 1, `indexOf` answers `nil` rather than -1) and `push`, `pop`,
306
+ `shift`, `unshift`, `sort` and `reverse` change the array they are called on.
307
+
308
+ **A `(` that starts a line** continues the statement above it, as in Lua and
309
+ in JavaScript — `const v = map[key]` followed by `("A"):upper()` is one
310
+ statement, a call of `map[key]`. tilua says so rather than letting it pass:
311
+ write `;` before the `(` when a new statement was meant.
312
+
313
+ **Object types** — `{ name: T, name?: T, [K]: V }`, and a name that is not an
314
+ identifier is quoted, as in the literal: `{ "Respawn After Kill": boolean }`.
315
+
316
+ **Optionality** — there is no `T?` shorthand. `?` in type position always
317
+ belongs to a conditional type, and in expression position to a ternary or an
318
+ optional chain.
319
+
320
+ ```luau
321
+ name?: T -- may be absent; its type is `T | nil`
322
+ name: T | nil -- must be written, but may be nil
323
+ ```
324
+
325
+ Omitting an argument requires `?` (or a default), as in TypeScript — a
326
+ parameter typed `T | nil` still has to be passed something.
327
+
328
+ **Optional chaining** — `a?.b` and `a?:m(x)` are nil when `a` is, and then
329
+ nothing further along the chain runs, arguments included: `folder?:FindFirstChild("A")?.Name`
330
+ is a `string | nil`. The `?` must touch the `.` or `:`; `c ? a : b` stays a
331
+ ternary. Parentheses end a chain. A chain cannot be assigned to (`a?.b = 1` is
332
+ an error). A chain that got through narrows what it tested: inside
333
+ `if (part?.Parent)`, and `if (part?.Name == "Door")`, `part` is not nil.
334
+
335
+ **Classes** — types are structural, except for classes. A definitions file
336
+ declares one with `declare class`, and it is nominal, as Roblox's classes are:
337
+
338
+ ```luau
339
+ declare class BasePart extends PVInstance { Size: Vector3 }
340
+ declare class Part extends BasePart { Shape: EnumItem }
341
+ ```
342
+
343
+ A `Part` is a `BasePart` and an `Instance` because it extends them. A
344
+ `ReplicatedStorage` is not a `Part`, and no table literal is an `Instance`,
345
+ however alike their members. A class still fits a shape that names members it
346
+ has (`{ Name: string }`). It is not a table, though, so `typeof(part)` picks
347
+ the `"Instance"` overload, not `"table"`. Members are inherited, and a subclass
348
+ may narrow one (`Parent: SomeFolder`).
349
+
350
+ **`class ... { }`** — a class written in code, rather than declared in a
351
+ definitions file. It is sugar over the Lua idiom, and the shape it stands for
352
+ is exactly that one: the class is a single table holding the methods and the
353
+ statics, and an instance is a table whose metatable points at it. An instance
354
+ therefore reaches *the class* — nothing is copied per instance, and there is
355
+ no prototype chain of an instance's own.
356
+
357
+ ```luau
358
+ class Animal {
359
+ name: string -- set by the constructor
360
+ legs = 4 -- set on every instance, before the body runs
361
+ static count = 0 -- on the class table, once
362
+
363
+ constructor(name: string) {
364
+ this.name = name
365
+ Animal.count += 1
366
+ }
367
+
368
+ function speak(): string {
369
+ return `${this.name} makes a sound`
370
+ }
371
+
372
+ get label(): string { -- read as a property, run as a function
373
+ return `<${this.name}>`
374
+ }
375
+
376
+ set label(value: string) {
377
+ this.name = value
378
+ }
379
+
380
+ static function made(): number {
381
+ return Animal.count
382
+ }
383
+ }
384
+
385
+ class Dog extends Animal {
386
+ breed: string
387
+
388
+ constructor(name: string, breed: string) {
389
+ super(name) -- required: it is what fills in the base
390
+ this.breed = breed
391
+ }
392
+
393
+ function speak(): string {
394
+ return `${super.speak()} (woof)`
395
+ }
396
+ }
397
+
398
+ const rex = new Dog("Rex", "shiba")
399
+ rex:speak() -- the receiver is `this`
400
+ rex.label = "Max" -- the setter
401
+ Dog.made() -- a static, inherited from Animal
402
+ ```
403
+
404
+ The receiver is written `this`, and it is an ordinary first parameter: a
405
+ method's type is `(this: Dog, ...) => R`, so `rex:speak()` supplies it the way
406
+ `function T:m()` supplies `self`. `new Dog(x)` *is* `Dog.new(x)` — the same
407
+ function, callable by hand and passable as a value.
408
+
409
+ A declaration names two things. As a **type**, `Dog` is the type of its
410
+ instances, nominal the same way a `declare class` is: a table with the same
411
+ members is not one, and only `Dog` and what extends it are assignable to it.
412
+ As a **value**, `Dog` is the class table — its statics, and the `new` that
413
+ builds an instance. `export class` exports both.
414
+
415
+ Two links are always there, and they are what the memory model promises:
416
+
417
+ ```luau
418
+ rex.ClassObject == Dog -- an instance names its class
419
+ Dog.ParentClass == Animal -- a class names the one it extends
420
+ Animal.ParentClass == nil -- and a root class extends nothing
421
+ ```
422
+
423
+ Both live on the class table, so an instance carries neither.
424
+
425
+ **Generic classes** — `class Box<T> { ... }`. The name is then a generic type,
426
+ and `Box<number>` and `Box<string>` are both `Box` but neither is the other:
427
+
428
+ ```luau
429
+ class Box<T> {
430
+ value: T
431
+ constructor(value: T) {
432
+ this.value = value
433
+ }
434
+ function get(): T {
435
+ return this.value
436
+ }
437
+ function map<R>(f: (value: T) => R): Box<R> {
438
+ return new Box(f(this.value))
439
+ }
440
+ }
441
+
442
+ const n = new Box(1) -- Box<number>, read off the argument
443
+ const s = new Box<string>("a") -- or written out
444
+ const held: number = n:get()
445
+
446
+ class Ints extends Box<number> { -- extending one fixes its argument
447
+ constructor(n: number) { super(n) }
448
+ }
449
+ ```
450
+
451
+ A function takes the argument off what it is handed: `function unwrap<T>(b:
452
+ Box<T>): T` given a `Box<boolean>` returns `boolean`.
453
+
454
+ **A class as a value** — `const Counter = class { ... }`, and `export default
455
+ class { ... }`. It may be named, and then the name is visible only inside its
456
+ own body, as in JavaScript; an anonymous one is known by whatever holds it.
457
+ Two of them are different types however alike they look. A class written as a
458
+ value takes no type parameters — nothing could write the arguments.
459
+
460
+ `export default class Name { ... }` declares `Name` here as well as exporting
461
+ it, as TypeScript's does, and importing it brings in the type too:
462
+
463
+ ```luau
464
+ import Box from "./box" -- `Box` is the class *and* the type
465
+ const held: Box<number> = new Box(1)
466
+ ```
467
+
468
+ Reported: a field with a type that nothing gives a value (`name: string` that
469
+ the constructor never assigns), a derived constructor that does not call
470
+ `super(...)`, `extends` naming something that is not a class, a chain that
471
+ closes on itself, a member written twice, and a member named one of the words
472
+ the compiler builds the class table out of (`new`, `ClassObject`,
473
+ `ParentClass`, `__init`, `__index`, `__newindex`, `__getters`, `__setters`,
474
+ `__dynamic`).
475
+
476
+ **Varargs** — `...` is Lua's pack, and every name on the left reads one of
477
+ it: with `...: number`, `const a, b = ...` gives two numbers. `[...]` puts the
478
+ whole pack in an array.
479
+
480
+ **Rest parameters** — `...name: T[]` is JavaScript's: every argument from that
481
+ position on, as an array. It is last, and the call signature is the same one
482
+ `...: T` describes — the difference is only what the body sees.
483
+
484
+ ```luau
485
+ function join(separator: string, ...parts: string[]): string {
486
+ return table.concat(parts, separator) -- `parts` is a string[] here
487
+ }
488
+
489
+ join("-", "a", "b") -- and a vararg call out here
490
+ join("-", 1) -- Argument of type '1' is not assignable to 'string'
491
+
492
+ function firstOf<T>(...items: T[]): T | nil {
493
+ return items[1]
494
+ }
495
+
496
+ firstOf(1, 2) -- number | nil: the arguments say what `T` is
497
+ ```
498
+
499
+ A type is written the same way: `type Reporter = (level: string, ...lines:
500
+ string[]) => ()` describes the same calls as `(level: string, ...string) =>
501
+ ()`. Without an annotation a rest parameter is `unknown[]`; annotated with
502
+ something that is not an array, it is reported.
503
+
504
+ **Spreads** — `...xs` puts what an array holds into any list of values, as
505
+ JavaScript does: a call's arguments, a `return`, a declaration, an assignment.
506
+ Bare `...` is unchanged, and is still the pack.
507
+
508
+ ```luau
509
+ join("-", ...names) -- every name
510
+ join("-", ...names, "z") -- and one more after them
511
+ add3(...nums) -- however many `nums` turns out to hold
512
+
513
+ function three(): (number, number, number) {
514
+ return ...nums
515
+ }
516
+
517
+ const first, second = ...names -- one each, as far as the names go
518
+ ```
519
+
520
+ What the array holds is checked against what it fills. How many it holds is
521
+ not known, so nothing is said about the count — unless it is a tuple, which
522
+ holds a known value at each position and is checked one by one. Spreading
523
+ something that is not a list is reported.
524
+
525
+ **Branded types** — nothing in the type model is about branding; an
526
+ intersection already means it. `string & { __brand }` is assignable to
527
+ `string`, and `string` is not assignable to it, which is the whole of it:
528
+
529
+ ```luau
530
+ type UserId = string & { readonly __brand: "UserId" }
531
+ type PostId = string & { readonly __brand: "PostId" }
532
+
533
+ declare function findUser(id: UserId): string
534
+
535
+ const id = "raw" as UserId -- `as` is how one is made
536
+ findUser(id) -- ok
537
+ findUser("raw") -- '"raw"' is not assignable to 'UserId'
538
+ findUser(postId) -- 'PostId' is not assignable to 'UserId'
539
+
540
+ #id -- still a string: 5
541
+ id:upper() -- and its methods, giving a plain string
542
+ ```
543
+
544
+ Anything that builds a new value builds an unbranded one — which is the point:
545
+ the brand says where the value came from. It works on any type (`number & {
546
+ __brand }`), and survives being stored, indexed and narrowed.
547
+
548
+ **Callbacks** — a function written where a function type is expected takes
549
+ its parameter types from it: in `signal:Connect(player => ... )`,
550
+ `player` is typed from `Connect`. The same applies to an annotated `const`
551
+ and to an assignment such as `remote.OnServerInvoke = function(player) { ... }`.
552
+
553
+ **Type packs** — `type Signal<T... = ...any> = { Connect: (self, cb: (T...) => ()) => () }`.
554
+ A pack parameter takes every type argument from its position on:
555
+ `Signal<Player, string>`, `Signal<()>` for none.
556
+
557
+ **Operators** — on a type that declares metamethods (`__add`, `__mul`,
558
+ `__unm`, ...), an operator has the metamethod's result, tried on the left
559
+ operand and then the right one, as Luau does. So `Vector3 + Vector3` and
560
+ `2 * vector` are both `Vector3`.
561
+
562
+ **Qualified type names** — a definitions file may declare `Enum.Material`
563
+ (`declare class Enum.Material extends EnumItem {}`), and code writes it the
564
+ same way.
565
+
566
+ **Contextual typing** — an expression takes its type from where it is
567
+ written, as in TypeScript: `let queue: thread[] = []` is a `thread[]`, and so
568
+ is `[]` passed where one is expected, including inside an object literal.
569
+
570
+ **Calls** — every argument is checked against its parameter, and a generic
571
+ parameter against its constraint (`GetService<K extends keyof Services>`
572
+ rejects `""`). Arguments are checked again once the call's own type arguments
573
+ are known, so `pick("Bones", "Blast1")` is caught where `pick`'s second
574
+ parameter reads `Extract<Rows, { Page: P }>["Skills"][number]`. A type that
575
+ waits on a type parameter — a conditional, an index, `T[K]` — is worked out
576
+ where that parameter is.
577
+
578
+ **Trailing commas** are allowed wherever TypeScript allows them: parameter
579
+ lists, call arguments, generic parameters and type arguments, tables, arrays,
580
+ tuples, imports and exports.
581
+
582
+ A value read by a key narrows the key: after `const path = paths[stat]`, the
583
+ `else` of `if path then` leaves `stat` as exactly the keys `paths` does not
584
+ have — the same correlation `pairs` over a record and a destructured union
585
+ already get.
586
+
587
+ **Narrowing** follows TypeScript's model: references (`x`, `x.a.b`, `x["k"]`)
588
+ rather than just variables, discriminated unions at any depth, `and`/`or`,
589
+ early return, `break`/`continue`, `error()` (declared `=> never`), user type
590
+ guards (`v is T`), and assertion signatures (`asserts v`).
591
+
592
+ Reading a member of, indexing or calling a value that may be nil is an error
593
+ until a check narrows the nil away, as with TypeScript's `strictNullChecks`:
594
+ `FindFirstChild("A"):FindFirstChild("B")` reports that the first call is
595
+ possibly nil. Use `?.` / `?:`, or check first. The read is still typed from the
596
+ non-nil part.
597
+
598
+ Only `nil` and `false` are falsy — `0` and `""` are truthy, unlike JavaScript.
599
+
600
+ **Types** — unions, intersections, tuples `[A, B]`, type packs `(A, B)` (the
601
+ several values a function returns), `keyof`, `T[K]`, conditional types with
602
+ `infer`, mapped types with `as` remapping, template literal types
603
+ (`` `on${Event}` ``), and set difference `A - B`. The utility types
604
+ (`Partial`, `Pick`, `Omit`, `ReturnType`, `Parameters`, `Exclude`, …) are
605
+ built in, and written in tilua on top of those rather than special-cased in
606
+ the analyzer. A type library or a file may declare one again; the later
607
+ declaration wins.
608
+
609
+ A call may write its type arguments out — `find<Folder>("Remotes")`,
610
+ `inst:WaitForChild<Folder>("Remotes")` — and a type parameter may have a
611
+ default (`<T = Instance>`) for the calls that do not. `a < b > (c)` is still
612
+ three operators: only a call after the `>` makes it type arguments.
613
+
614
+ `<const T>` infers an argument at its narrowest, as in TypeScript 5.
615
+
616
+ `typeof x` in a type is TypeScript's type query — the type of a value
617
+ (`typeof config`, `typeof config.volume`, `ReturnType<typeof f>`). Luau's
618
+ `typeof(expr)` spelling works too. It is compile-time only, unrelated to the
619
+ `typeof(v)` function that returns a string at runtime.
620
+
621
+ **Modules** — `import` / `export`, export lists, re-exports and `export *`.
622
+
623
+ **`satisfies`** — checks a value against a type without giving it that type,
624
+ as in TypeScript 4.9:
625
+
626
+ ```luau
627
+ type Shape = { kind: "circle" | "rect", size: number }
628
+ const circle = { kind: "circle", size: 2 } satisfies Shape -- { kind: "circle", size: number }
629
+ const handlers = {
630
+ Click: x => x + 1, -- x: number, from the contract
631
+ } satisfies { [string]: (x: number) => number }
632
+ ```
633
+
634
+ The contract types callbacks and empty arrays, and a literal stays a literal
635
+ where the contract asks for literals (`kind: "circle"`, not `string`). A value
636
+ that does not fit is an error. So is a property the contract does not know —
637
+ TypeScript's excess property check, which applies to an object literal written
638
+ straight into an annotation (`const s: Shape = { ..., typo: 1 }`) too. A value
639
+ that already has a type of its own keeps it exactly: `{ ... } as const
640
+ satisfies T` stays readonly and literal. `as` reinterprets instead of
641
+ checking, and compiled code keeps neither.
642
+
643
+ **Undeclared names** — `analyzeScopes(program, { builtinGlobals, reportUndeclared: true })`
644
+ reports each read of a name nothing declares: "Cannot find name 'x'". A global
645
+ assigned in the file (`x = 1`) and a `declare` count as declarations.
646
+ `analyzeTypes(program, scopes, { reportUnknownTypes: true })` does the same for
647
+ type names. Both are off by default, since they are only right when the type
648
+ libraries the file names are loaded.
649
+
650
+ **Directives** — comments that switch checking off, as TypeScript's
651
+ `// @ts-...` do. They silence scope and type errors, never syntax errors:
652
+
653
+ ```luau
654
+ --@tilua-nocheck -- before the first line of code: the whole file
655
+ --@tilua-ignore -- the next line of code
656
+ --@tilua-expect-error -- the next line of code, which must have an error
657
+ ```
658
+
659
+ `parseWithRecovery` returns them as `directives`; `directivesOf(source)` reads
660
+ them for a caller that parsed some other way, and
661
+ `applyDirectives(directives, diagnostics, lineOf)` filters a list and names
662
+ each `expect-error` that had nothing to suppress.
663
+
664
+ ## Options
665
+
666
+ ```ts
667
+ analyzeScopes(program, {
668
+ builtinGlobals: ["print", "game"], // names that may be used undeclared
669
+ })
670
+
671
+ analyzeTypes(program, scopes, {
672
+ libs, // parsed `.d.tilua` definitions
673
+ globalTypes: { … }, // types for specific globals; wins over `libs`
674
+ libTypes: { … }, // extra named types for annotations
675
+ diagnostics: true, // emit type errors (default)
676
+ resolveModule: specifier => exportsOfThatFile,
677
+ // what an `import` sees; without it imports are `any`
678
+ })
679
+ ```
680
+
681
+ ## Known limitations
682
+
683
+ - `export * as ns from` and namespace imports (`import * as ns`) are not
684
+ supported.
685
+ - `setmetatable` and metatables are not modelled.
686
+ - Accessing a property a type does not have yields `unknown` rather than an
687
+ error; assigning to a `readonly` property is not reported.
688
+ - A sourcemap child whose name is not an identifier (`"My Part"`) is not typed.
689
+
690
+ ## Development
691
+
692
+ ```bash
693
+ npm install
694
+ npm test # smoketests (types via smoketest/tilua.config.json), then project tests
695
+ npm run build
696
+ npm run typecheck
697
+ ```
698
+
699
+ `smoketest/` is the test suite. Each file annotates its bindings with the type
700
+ they should infer to, so a wrong result surfaces as a diagnostic rather than
701
+ something to eyeball. `scripts/project.test.ts` covers configs, type
702
+ libraries, import paths and sourcemaps against an in-memory file system.