luaut-parser 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,24 +45,32 @@ annotation resolves to, and `expectedTypeOf` what each call argument should
45
45
  be, so a tool never has to re-derive a type from text.
46
46
 
47
47
  `parseWithRecovery(source)` returns `{ program, errors }` instead of throwing —
48
- use it for editors, where the text is usually mid-edit.
48
+ use it for editors, where the text is usually mid-edit. An error costs as
49
+ little of the tree as it can: a broken value becomes an `ErrorExpression`
50
+ (typed `any`) in its place, a broken field or argument is skipped to the next
51
+ `,`, a missing comma between fields on separate lines, or a missing `)`, `}`,
52
+ `then`, `do` or `end`, is recorded and read past — a missing `end` is placed
53
+ by indentation — and an unclosed string ends at its line. Skipping never lets
54
+ an `end` or `}` inside a skipped function or object close the block around
55
+ it. Valid code parses to exactly the same tree as `parse`.
49
56
 
50
57
  Neither analysis mutates the AST; both return side tables.
51
58
 
52
59
  ## Projects
53
60
 
54
- **No types are built in** — not `print`, not `string`, not `game`. A project
55
- lists the type libraries it uses in `luaut.config.json`, the way TypeScript
56
- uses `@types/*`:
61
+ **No globals are built in** — not `print`, not `string`, not `game`. Only the
62
+ language's own utility types are (`Partial`, `Pick`, `Omit`, `Record`,
63
+ `ReturnType`, `Truthy`, ...; see `PRELUDE_SOURCE`). A project lists the type
64
+ libraries it uses in `luaut.config.json`, the way TypeScript uses `@types/*`:
57
65
 
58
66
  ```bash
59
- npm i -D @luaut/roblox # or just @luaut/luau
67
+ npm i -D @luaut/roblox # Luau + Roblox; or @luaut/lua on its own
60
68
  ```
61
69
 
62
70
  ```jsonc
63
71
  // luaut.config.json
64
72
  {
65
- "types": ["roblox"], // @luaut/roblox, which brings @luaut/luau
73
+ "types": ["roblox"], // and what it depends on: @luaut/lua
66
74
  "paths": { "@shared/*": ["src/shared/*"] }, // import aliases, as in tsconfig
67
75
  "sourceMap": "sourcemap.json" // a Rojo sourcemap, or null
68
76
  }
@@ -96,8 +104,13 @@ the config (or sourcemap) it is about. `host` reads files — pass your own to
96
104
  read unsaved editor buffers or to record what was read.
97
105
 
98
106
  `type` / `typeof` are **not** special-cased in the analyzer either: they are
99
- overload sets in `@luaut/luau`, and narrowing is derived from them. Without a
100
- library that declares them, they narrow nothing.
107
+ overload sets in `@luaut/lua` and `@luaut/roblox`, and narrowing is derived
108
+ from them. Without a library that declares them, they narrow nothing.
109
+
110
+ Libraries stack: a name declared again *adds* to what an earlier library gave
111
+ it — overloads of a function accumulate, and the members of a declared table
112
+ merge. That is how `@luaut/roblox` extends Lua's `table` and `type` without
113
+ restating them.
101
114
 
102
115
  ## The language, in brief
103
116
 
@@ -111,6 +124,31 @@ scope; like a TypeScript function declaration it cannot be reassigned.
111
124
  `const` and `let` do not apply to functions. `function T.name()` and
112
125
  `function T:name()` define a member.
113
126
 
127
+ **Hoisting** — a function declaration is visible to its whole block, above
128
+ itself too, so `let r: ReturnType<typeof load>` may come before `function
129
+ load()`. A closure reads the name its own value is bound to, as in JavaScript
130
+ (`let m = { clear: function() m.items = {} end }`), and a later name in the
131
+ same block; the compiler declares such a name before the statement that fills
132
+ it. A module's top-level names are visible to code that runs later —
133
+ function bodies and `typeof` — wherever that code is written, since a bundle
134
+ declares them all before the module runs. At the top level the whole function
135
+ is hoisted, and can be called above its declaration. Inside a function only
136
+ the name is: other functions can call it, but a call straight in the block
137
+ above the declaration is an error, because nothing is there yet.
138
+
139
+ **Returns** — a declared return type is checked: what a `return` gives must
140
+ fit it, and a function that declared one must return a value (a guard or an
141
+ `asserts` function needs none). The declared type also types what is written
142
+ there, so a returned callback takes its parameters from it.
143
+
144
+ **Overloads** — a `function name(...)` with no body is a signature for the
145
+ declaration that follows it, as in TypeScript: the signatures are what a call
146
+ sees, and the last one, with the body, is the implementation. `export` goes on
147
+ every line of the set or none of them. A parameter of the implementation that
148
+ carries no annotation holds what the signatures allow there — under
149
+ `get(stat: "hp")` and `get(stat: "name")`, the implementation's `stat` is
150
+ `"hp" | "name"` rather than `any`.
151
+
114
152
  **Modules** — `import { a, b as c } from "./m"`, `import D from "./m"` and
115
153
  `import * as M from "./m"`; `export const`, `export function`, `export default`,
116
154
  `export { a as b }`, `export { a } from "./m"` and `export * from "./m"`.
@@ -122,8 +160,58 @@ brings in names that are types and nothing else: unlike TypeScript, using one
122
160
  as a value is an error, and only type positions — `typeof A` included — may
123
161
  name it. Compiled code keeps no trace of it.
124
162
 
163
+ **Array and string methods** — an array and a string answer to methods
164
+ written with `:`, the way JavaScript writes them:
165
+
166
+ ```luau
167
+ const long = names:filter(function(n) return #n > 3 end):map(string.upper)
168
+ const first = names:find(function(n) return n:startsWith("A") end)
169
+ print(names:join(", "), text:trim(), text:replaceAll(",", ";"))
170
+ ```
171
+
172
+ Which methods those are is not the language's business. The analyzer looks for
173
+ two types by name — `ArrayMethods<T>` and `StringMethods` — and reads an
174
+ array's or a string's members out of whichever type library declared them;
175
+ without such a library an array has no methods at all.
176
+
177
+ Running them is that library's business too. A library points at a JavaScript
178
+ module in its package.json, and the compiler asks it what a call becomes:
179
+
180
+ ```json
181
+ "luaut": { "types": "index.d.luaut", "lowering": "lowering.mjs" }
182
+ ```
183
+
184
+ ```ts
185
+ import type { LoweringPlugin } from "luaut-parser" // the contract, declared here
186
+
187
+ const plugin: LoweringPlugin = {
188
+ runtime: { array: "local __NAME__ = {}\nfunction __NAME__.filter(t, test) ... end" },
189
+ methodCall({ method, receiver, use }) {
190
+ if (receiver?.kind === "array" && method === "filter") {
191
+ return { callee: `${use("array")}.filter` }
192
+ }
193
+ return undefined
194
+ },
195
+ }
196
+ export default plugin
197
+ ```
198
+
199
+ `receiver` is the luaut type the analyzer worked out, `use(key)` gives the
200
+ local name that table got — emitted once, at the top of the output, only if a
201
+ call needed it — and the receiver is passed as the call's first argument. An
202
+ answer of `undefined` leaves an ordinary Luau method call, which is what
203
+ `text:upper()` wants, since a string already answers to it.
204
+
205
+ The compiler lowers the language and nothing else: `filter` appears nowhere in
206
+ it.
207
+
208
+ `@luaut/lua` ships the JavaScript-shaped set; there, indices are Luau's (the
209
+ first element is 1, `indexOf` answers `nil` rather than -1) and `push`, `pop`,
210
+ `shift`, `unshift`, `sort` and `reverse` change the array they are called on.
211
+
125
212
  **Optionality** — there is no `T?` shorthand. `?` in type position always
126
- belongs to a conditional type, and in expression position to a ternary.
213
+ belongs to a conditional type, and in expression position to a ternary or an
214
+ optional chain.
127
215
 
128
216
  ```luau
129
217
  name?: T -- may be absent; its type is `T | nil`
@@ -133,6 +221,13 @@ name: T | nil -- must be written, but may be nil
133
221
  Omitting an argument requires `?` (or a default), as in TypeScript — a
134
222
  parameter typed `T | nil` still has to be passed something.
135
223
 
224
+ **Optional chaining** — `a?.b` and `a?:m(x)` are nil when `a` is, and then
225
+ nothing further along the chain runs, arguments included: `folder?:FindFirstChild("A")?.Name`
226
+ is a `string | nil`. The `?` must touch the `.` or `:`; `c ? a : b` stays a
227
+ ternary. Parentheses end a chain. A chain cannot be assigned to (`a?.b = 1` is
228
+ an error). A chain that got through narrows what it tested: inside
229
+ `if part?.Parent then`, and `if part?.Name == "Door" then`, `part` is not nil.
230
+
136
231
  **Classes** — types are structural, except for classes. A definitions file
137
232
  declares one with `declare class`, and it is nominal, as Roblox's classes are:
138
233
 
@@ -172,13 +267,32 @@ is `[]` passed where one is expected, including inside an object literal.
172
267
 
173
268
  **Calls** — every argument is checked against its parameter, and a generic
174
269
  parameter against its constraint (`GetService<K extends keyof Services>`
175
- rejects `""`).
270
+ rejects `""`). Arguments are checked again once the call's own type arguments
271
+ are known, so `pick("Bones", "Blast1")` is caught where `pick`'s second
272
+ parameter reads `Extract<Rows, { Page: P }>["Skills"][number]`. A type that
273
+ waits on a type parameter — a conditional, an index, `T[K]` — is worked out
274
+ where that parameter is.
275
+
276
+ **Trailing commas** are allowed wherever TypeScript allows them: parameter
277
+ lists, call arguments, generic parameters and type arguments, tables, arrays,
278
+ tuples, imports and exports.
279
+
280
+ A value read by a key narrows the key: after `const path = paths[stat]`, the
281
+ `else` of `if path then` leaves `stat` as exactly the keys `paths` does not
282
+ have — the same correlation `pairs` over a record and a destructured union
283
+ already get.
176
284
 
177
285
  **Narrowing** follows TypeScript's model: references (`x`, `x.a.b`, `x["k"]`)
178
286
  rather than just variables, discriminated unions at any depth, `and`/`or`,
179
287
  early return, `break`/`continue`, `error()` (declared `-> never`), user type
180
288
  guards (`v is T`), and assertion signatures (`asserts v`).
181
289
 
290
+ Reading a member of, indexing or calling a value that may be nil is an error
291
+ until a check narrows the nil away, as with TypeScript's `strictNullChecks`:
292
+ `FindFirstChild("A"):FindFirstChild("B")` reports that the first call is
293
+ possibly nil. Use `?.` / `?:`, or check first. The read is still typed from the
294
+ non-nil part.
295
+
182
296
  Only `nil` and `false` are falsy — `0` and `""` are truthy, unlike JavaScript.
183
297
 
184
298
  **Types** — unions, intersections, tuples `[A, B]`, type packs `(A, B)` (the
@@ -186,7 +300,14 @@ several values a function returns), `keyof`, `T[K]`, conditional types with
186
300
  `infer`, mapped types with `as` remapping, template literal types
187
301
  (`` `on${Event}` ``), and set difference `A - B`. The utility types
188
302
  (`Partial`, `Pick`, `Omit`, `ReturnType`, `Parameters`, `Exclude`, …) are
189
- written in luaut on top of those, not built in.
303
+ built in, and written in luaut on top of those rather than special-cased in
304
+ the analyzer. A type library or a file may declare one again; the later
305
+ declaration wins.
306
+
307
+ A call may write its type arguments out — `find<Folder>("Remotes")`,
308
+ `inst:WaitForChild<Folder>("Remotes")` — and a type parameter may have a
309
+ default (`<T = Instance>`) for the calls that do not. `a < b > (c)` is still
310
+ three operators: only a call after the `>` makes it type arguments.
190
311
 
191
312
  `<const T>` infers an argument at its narrowest, as in TypeScript 5.
192
313
 
@@ -197,6 +318,47 @@ written in luaut on top of those, not built in.
197
318
 
198
319
  **Modules** — `import` / `export`, export lists, re-exports and `export *`.
199
320
 
321
+ **`satisfies`** — checks a value against a type without giving it that type,
322
+ as in TypeScript 4.9:
323
+
324
+ ```luau
325
+ type Shape = { kind: "circle" | "rect", size: number }
326
+ const circle = { kind: "circle", size: 2 } satisfies Shape -- { kind: "circle", size: number }
327
+ const handlers = {
328
+ Click: function(x) return x + 1 end, -- x: number, from the contract
329
+ } satisfies { [string]: (x: number) -> number }
330
+ ```
331
+
332
+ The contract types callbacks and empty arrays, and a literal stays a literal
333
+ where the contract asks for literals (`kind: "circle"`, not `string`). A value
334
+ that does not fit is an error. So is a property the contract does not know —
335
+ TypeScript's excess property check, which applies to an object literal written
336
+ straight into an annotation (`const s: Shape = { ..., typo: 1 }`) too. A value
337
+ that already has a type of its own keeps it exactly: `{ ... } as const
338
+ satisfies T` stays readonly and literal. `as` reinterprets instead of
339
+ checking, and compiled code keeps neither.
340
+
341
+ **Undeclared names** — `analyzeScopes(program, { builtinGlobals, reportUndeclared: true })`
342
+ reports each read of a name nothing declares: "Cannot find name 'x'". A global
343
+ assigned in the file (`x = 1`) and a `declare` count as declarations.
344
+ `analyzeTypes(program, scopes, { reportUnknownTypes: true })` does the same for
345
+ type names. Both are off by default, since they are only right when the type
346
+ libraries the file names are loaded.
347
+
348
+ **Directives** — comments that switch checking off, as TypeScript's
349
+ `// @ts-...` do. They silence scope and type errors, never syntax errors:
350
+
351
+ ```luau
352
+ --@luaut-nocheck -- before the first line of code: the whole file
353
+ --@luaut-ignore -- the next line of code
354
+ --@luaut-expect-error -- the next line of code, which must have an error
355
+ ```
356
+
357
+ `parseWithRecovery` returns them as `directives`; `directivesOf(source)` reads
358
+ them for a caller that parsed some other way, and
359
+ `applyDirectives(directives, diagnostics, lineOf)` filters a list and names
360
+ each `expect-error` that had nothing to suppress.
361
+
200
362
  ## Options
201
363
 
202
364
  ```ts