luaut-parser 3.1.0 → 5.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
@@ -48,11 +48,11 @@ be, so a tool never has to re-derive a type from text.
48
48
  use it for editors, where the text is usually mid-edit. An error costs as
49
49
  little of the tree as it can: a broken value becomes an `ErrorExpression`
50
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`.
51
+ `,`, a missing comma between fields on separate lines, or a missing `)` or
52
+ `}`, is recorded and read past — a missing `}` is placed by indentation — and
53
+ an unclosed string ends at its line. Skipping never lets a `}` inside a
54
+ skipped function or object close the block around it. Valid code parses to
55
+ exactly the same tree as `parse`.
56
56
 
57
57
  Neither analysis mutates the AST; both return side tables.
58
58
 
@@ -117,9 +117,79 @@ restating them.
117
117
  TypeScript syntax and semantics wherever they fit, Lua semantics where they
118
118
  must.
119
119
 
120
+ **Blocks** — braces, and a condition in parentheses:
121
+
122
+ ```luau
123
+ if (n < 0) {
124
+ return "negative"
125
+ } elseif (n == 0) {
126
+ return "zero"
127
+ } else {
128
+ return "positive"
129
+ }
130
+
131
+ while (going) { step() }
132
+ for (i = 1, 10) { total += i }
133
+ for (name, value in pairs(t)) { print(name, value) }
134
+ repeat { step() } until (done)
135
+ do { ... }
136
+
137
+ function greet(name: string): string {
138
+ return `hello ${name}`
139
+ }
140
+
141
+ class Dog extends Animal {
142
+ function speak(): string { return "woof" }
143
+ }
144
+ ```
145
+
146
+ The parentheses are not decoration: `f {}` is a call with a table argument, so
147
+ without them `if ready { ... }` would be a call of `ready` followed by a block.
148
+
149
+ There is no `end` in luaut, and `then` is not a word it knows.
150
+
151
+ `and`, `or` and `not` stay words; `{}` is still an object literal everywhere a
152
+ value is expected, and `[]` an array.
153
+
154
+ **Arrows** — one for both, as TypeScript writes them. `(a: number) => string`
155
+ is a function type; `(a: number) => a` is a function. Which one a `=>` makes is
156
+ decided by where it stands, since a type and a value never share a place.
157
+
158
+ ```luau
159
+ type Reducer = (total: number, value: number) => number
160
+
161
+ const double = (x: number) => x * 2
162
+ const add: Reducer = (a, b) => a + b -- parameters typed by the contract
163
+ const shown = (n: number): string => tostring(n)
164
+ const identity = <T>(v: T) => v
165
+
166
+ each(n => print(n)) -- one parameter needs no parens
167
+
168
+ const logged = (n: number) => { -- a block body is a block,
169
+ print(n) -- as in TypeScript,
170
+ return n
171
+ }
172
+ const wrap = (n: number) => ({ value: n }) -- so an object is parenthesized
173
+ ```
174
+
175
+ An arrow is a function expression written short — there is no second kind of
176
+ function — so `this` inside one is the `this` of the method around it, which
177
+ is what a JavaScript arrow is for.
178
+
179
+ **Moving a file over** — `end`, `then`, `do` as a block opener, and `->` for a
180
+ function type are Lua's spellings, and luaut no longer reads any of them.
181
+ `npx tsx scripts/to-braces.ts <file|dir>` rewrites a file written in them,
182
+ and `scripts/to-braces-sources.ts <file>` does the same for luaut written
183
+ inside a TypeScript file, which is where a test suite keeps most of it. Each
184
+ rewrite is parsed and compared with the tree the original made before it is
185
+ written; a file it cannot say the same thing about is left alone.
186
+
187
+ Both scripts need a parser that still reads the old spellings, so run them
188
+ from a checkout of the commit before they were dropped.
189
+
120
190
  **Declarations** — `const` and `let` only; Lua's `local` is gone.
121
191
 
122
- **Functions** — `function name() ... end` declares `name` in the enclosing
192
+ **Functions** — `function name() { ... }` declares `name` in the enclosing
123
193
  scope; like a TypeScript function declaration it cannot be reassigned.
124
194
  `const` and `let` do not apply to functions. `function T.name()` and
125
195
  `function T:name()` define a member.
@@ -127,7 +197,7 @@ scope; like a TypeScript function declaration it cannot be reassigned.
127
197
  **Hoisting** — a function declaration is visible to its whole block, above
128
198
  itself too, so `let r: ReturnType<typeof load>` may come before `function
129
199
  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
200
+ (`let m = { clear: function() { m.items = {} } }`), and a later name in the
131
201
  same block; the compiler declares such a name before the statement that fills
132
202
  it. A module's top-level names are visible to code that runs later —
133
203
  function bodies and `typeof` — wherever that code is written, since a bundle
@@ -160,6 +230,63 @@ brings in names that are types and nothing else: unlike TypeScript, using one
160
230
  as a value is an error, and only type positions — `typeof A` included — may
161
231
  name it. Compiled code keeps no trace of it.
162
232
 
233
+ **Array and string methods** — an array and a string answer to methods
234
+ written with `:`, the way JavaScript writes them:
235
+
236
+ ```luau
237
+ const long = names:filter(n => #n > 3):map(string.upper)
238
+ const first = names:find(n => n:startsWith("A"))
239
+ print(names:join(", "), text:trim(), text:replaceAll(",", ";"))
240
+ ```
241
+
242
+ Which methods those are is not the language's business. The analyzer looks for
243
+ two types by name — `ArrayMethods<T>` and `StringMethods` — and reads an
244
+ array's or a string's members out of whichever type library declared them;
245
+ without such a library an array has no methods at all.
246
+
247
+ Running them is that library's business too. A library points at a JavaScript
248
+ module in its package.json, and the compiler asks it what a call becomes:
249
+
250
+ ```json
251
+ "luaut": { "types": "index.d.luaut", "lowering": "lowering.mjs" }
252
+ ```
253
+
254
+ ```js
255
+ // @ts-check
256
+ /** @type {import("luaut-parser").LoweringPlugin} */ // the contract, declared here
257
+ const plugin = {
258
+ runtime: { array: "local __NAME__ = {}\nfunction __NAME__.filter(t, test) ... end" },
259
+ methodCall({ method, receiver, use }) {
260
+ if (receiver?.kind === "array" && method === "filter") {
261
+ return { callee: `${use("array")}.filter` }
262
+ }
263
+ return undefined
264
+ },
265
+ }
266
+ export default plugin
267
+ ```
268
+
269
+ `receiver` is the luaut type the analyzer worked out, `use(key)` gives the
270
+ local name that table got — emitted once, at the top of the output, only if a
271
+ call needed it — and the receiver is passed as the call's first argument. An
272
+ answer of `undefined` leaves an ordinary Luau method call, which is what
273
+ `text:upper()` wants, since a string already answers to it.
274
+
275
+ The compiler lowers the language and nothing else: `filter` appears nowhere in
276
+ it.
277
+
278
+ `@luaut/lua` ships the JavaScript-shaped set; there, indices are Luau's (the
279
+ first element is 1, `indexOf` answers `nil` rather than -1) and `push`, `pop`,
280
+ `shift`, `unshift`, `sort` and `reverse` change the array they are called on.
281
+
282
+ **A `(` that starts a line** continues the statement above it, as in Lua and
283
+ in JavaScript — `const v = map[key]` followed by `("A"):upper()` is one
284
+ statement, a call of `map[key]`. luaut says so rather than letting it pass:
285
+ write `;` before the `(` when a new statement was meant.
286
+
287
+ **Object types** — `{ name: T, name?: T, [K]: V }`, and a name that is not an
288
+ identifier is quoted, as in the literal: `{ "Respawn After Kill": boolean }`.
289
+
163
290
  **Optionality** — there is no `T?` shorthand. `?` in type position always
164
291
  belongs to a conditional type, and in expression position to a ternary or an
165
292
  optional chain.
@@ -177,7 +304,7 @@ nothing further along the chain runs, arguments included: `folder?:FindFirstChil
177
304
  is a `string | nil`. The `?` must touch the `.` or `:`; `c ? a : b` stays a
178
305
  ternary. Parentheses end a chain. A chain cannot be assigned to (`a?.b = 1` is
179
306
  an error). A chain that got through narrows what it tested: inside
180
- `if part?.Parent then`, and `if part?.Name == "Door" then`, `part` is not nil.
307
+ `if (part?.Parent)`, and `if (part?.Name == "Door")`, `part` is not nil.
181
308
 
182
309
  **Classes** — types are structural, except for classes. A definitions file
183
310
  declares one with `declare class`, and it is nominal, as Roblox's classes are:
@@ -194,12 +321,210 @@ has (`{ Name: string }`). It is not a table, though, so `typeof(part)` picks
194
321
  the `"Instance"` overload, not `"table"`. Members are inherited, and a subclass
195
322
  may narrow one (`Parent: SomeFolder`).
196
323
 
324
+ **`class ... { }`** — a class written in code, rather than declared in a
325
+ definitions file. It is sugar over the Lua idiom, and the shape it stands for
326
+ is exactly that one: the class is a single table holding the methods and the
327
+ statics, and an instance is a table whose metatable points at it. An instance
328
+ therefore reaches *the class* — nothing is copied per instance, and there is
329
+ no prototype chain of an instance's own.
330
+
331
+ ```luau
332
+ class Animal {
333
+ name: string -- set by the constructor
334
+ legs = 4 -- set on every instance, before the body runs
335
+ static count = 0 -- on the class table, once
336
+
337
+ constructor(name: string) {
338
+ this.name = name
339
+ Animal.count += 1
340
+ }
341
+
342
+ function speak(): string {
343
+ return `${this.name} makes a sound`
344
+ }
345
+
346
+ get label(): string { -- read as a property, run as a function
347
+ return `<${this.name}>`
348
+ }
349
+
350
+ set label(value: string) {
351
+ this.name = value
352
+ }
353
+
354
+ static function made(): number {
355
+ return Animal.count
356
+ }
357
+ }
358
+
359
+ class Dog extends Animal {
360
+ breed: string
361
+
362
+ constructor(name: string, breed: string) {
363
+ super(name) -- required: it is what fills in the base
364
+ this.breed = breed
365
+ }
366
+
367
+ function speak(): string {
368
+ return `${super.speak()} (woof)`
369
+ }
370
+ }
371
+
372
+ const rex = new Dog("Rex", "shiba")
373
+ rex:speak() -- the receiver is `this`
374
+ rex.label = "Max" -- the setter
375
+ Dog.made() -- a static, inherited from Animal
376
+ ```
377
+
378
+ The receiver is written `this`, and it is an ordinary first parameter: a
379
+ method's type is `(this: Dog, ...) => R`, so `rex:speak()` supplies it the way
380
+ `function T:m()` supplies `self`. `new Dog(x)` *is* `Dog.new(x)` — the same
381
+ function, callable by hand and passable as a value.
382
+
383
+ A declaration names two things. As a **type**, `Dog` is the type of its
384
+ instances, nominal the same way a `declare class` is: a table with the same
385
+ members is not one, and only `Dog` and what extends it are assignable to it.
386
+ As a **value**, `Dog` is the class table — its statics, and the `new` that
387
+ builds an instance. `export class` exports both.
388
+
389
+ Two links are always there, and they are what the memory model promises:
390
+
391
+ ```luau
392
+ rex.ClassObject == Dog -- an instance names its class
393
+ Dog.ParentClass == Animal -- a class names the one it extends
394
+ Animal.ParentClass == nil -- and a root class extends nothing
395
+ ```
396
+
397
+ Both live on the class table, so an instance carries neither.
398
+
399
+ **Generic classes** — `class Box<T> { ... }`. The name is then a generic type,
400
+ and `Box<number>` and `Box<string>` are both `Box` but neither is the other:
401
+
402
+ ```luau
403
+ class Box<T> {
404
+ value: T
405
+ constructor(value: T) {
406
+ this.value = value
407
+ }
408
+ function get(): T {
409
+ return this.value
410
+ }
411
+ function map<R>(f: (value: T) => R): Box<R> {
412
+ return new Box(f(this.value))
413
+ }
414
+ }
415
+
416
+ const n = new Box(1) -- Box<number>, read off the argument
417
+ const s = new Box<string>("a") -- or written out
418
+ const held: number = n:get()
419
+
420
+ class Ints extends Box<number> { -- extending one fixes its argument
421
+ constructor(n: number) { super(n) }
422
+ }
423
+ ```
424
+
425
+ A function takes the argument off what it is handed: `function unwrap<T>(b:
426
+ Box<T>): T` given a `Box<boolean>` returns `boolean`.
427
+
428
+ **A class as a value** — `const Counter = class { ... }`, and `export default
429
+ class { ... }`. It may be named, and then the name is visible only inside its
430
+ own body, as in JavaScript; an anonymous one is known by whatever holds it.
431
+ Two of them are different types however alike they look. A class written as a
432
+ value takes no type parameters — nothing could write the arguments.
433
+
434
+ `export default class Name { ... }` declares `Name` here as well as exporting
435
+ it, as TypeScript's does, and importing it brings in the type too:
436
+
437
+ ```luau
438
+ import Box from "./box" -- `Box` is the class *and* the type
439
+ const held: Box<number> = new Box(1)
440
+ ```
441
+
442
+ Reported: a field with a type that nothing gives a value (`name: string` that
443
+ the constructor never assigns), a derived constructor that does not call
444
+ `super(...)`, `extends` naming something that is not a class, a chain that
445
+ closes on itself, a member written twice, and a member named one of the words
446
+ the compiler builds the class table out of (`new`, `ClassObject`,
447
+ `ParentClass`, `__init`, `__index`, `__newindex`, `__getters`, `__setters`,
448
+ `__dynamic`).
449
+
450
+ **Varargs** — `...` is Lua's pack, and every name on the left reads one of
451
+ it: with `...: number`, `const a, b = ...` gives two numbers. `[...]` puts the
452
+ whole pack in an array.
453
+
454
+ **Rest parameters** — `...name: T[]` is JavaScript's: every argument from that
455
+ position on, as an array. It is last, and the call signature is the same one
456
+ `...: T` describes — the difference is only what the body sees.
457
+
458
+ ```luau
459
+ function join(separator: string, ...parts: string[]): string {
460
+ return table.concat(parts, separator) -- `parts` is a string[] here
461
+ }
462
+
463
+ join("-", "a", "b") -- and a vararg call out here
464
+ join("-", 1) -- Argument of type '1' is not assignable to 'string'
465
+
466
+ function firstOf<T>(...items: T[]): T | nil {
467
+ return items[1]
468
+ }
469
+
470
+ firstOf(1, 2) -- number | nil: the arguments say what `T` is
471
+ ```
472
+
473
+ A type is written the same way: `type Reporter = (level: string, ...lines:
474
+ string[]) => ()` describes the same calls as `(level: string, ...string) =>
475
+ ()`. Without an annotation a rest parameter is `unknown[]`; annotated with
476
+ something that is not an array, it is reported.
477
+
478
+ **Spreads** — `...xs` puts what an array holds into any list of values, as
479
+ JavaScript does: a call's arguments, a `return`, a declaration, an assignment.
480
+ Bare `...` is unchanged, and is still the pack.
481
+
482
+ ```luau
483
+ join("-", ...names) -- every name
484
+ join("-", ...names, "z") -- and one more after them
485
+ add3(...nums) -- however many `nums` turns out to hold
486
+
487
+ function three(): (number, number, number) {
488
+ return ...nums
489
+ }
490
+
491
+ const first, second = ...names -- one each, as far as the names go
492
+ ```
493
+
494
+ What the array holds is checked against what it fills. How many it holds is
495
+ not known, so nothing is said about the count — unless it is a tuple, which
496
+ holds a known value at each position and is checked one by one. Spreading
497
+ something that is not a list is reported.
498
+
499
+ **Branded types** — nothing in the type model is about branding; an
500
+ intersection already means it. `string & { __brand }` is assignable to
501
+ `string`, and `string` is not assignable to it, which is the whole of it:
502
+
503
+ ```luau
504
+ type UserId = string & { readonly __brand: "UserId" }
505
+ type PostId = string & { readonly __brand: "PostId" }
506
+
507
+ declare function findUser(id: UserId): string
508
+
509
+ const id = "raw" as UserId -- `as` is how one is made
510
+ findUser(id) -- ok
511
+ findUser("raw") -- '"raw"' is not assignable to 'UserId'
512
+ findUser(postId) -- 'PostId' is not assignable to 'UserId'
513
+
514
+ #id -- still a string: 5
515
+ id:upper() -- and its methods, giving a plain string
516
+ ```
517
+
518
+ Anything that builds a new value builds an unbranded one — which is the point:
519
+ the brand says where the value came from. It works on any type (`number & {
520
+ __brand }`), and survives being stored, indexed and narrowed.
521
+
197
522
  **Callbacks** — a function written where a function type is expected takes
198
- its parameter types from it: in `signal:Connect(function(player) ... end)`,
523
+ its parameter types from it: in `signal:Connect(player => ... )`,
199
524
  `player` is typed from `Connect`. The same applies to an annotated `const`
200
- and to an assignment such as `remote.OnServerInvoke = function(player) ...`.
525
+ and to an assignment such as `remote.OnServerInvoke = function(player) { ... }`.
201
526
 
202
- **Type packs** — `type Signal<T... = ...any> = { Connect: (self, cb: (T...) -> ()) -> () }`.
527
+ **Type packs** — `type Signal<T... = ...any> = { Connect: (self, cb: (T...) => ()) => () }`.
203
528
  A pack parameter takes every type argument from its position on:
204
529
  `Signal<Player, string>`, `Signal<()>` for none.
205
530
 
@@ -235,7 +560,7 @@ already get.
235
560
 
236
561
  **Narrowing** follows TypeScript's model: references (`x`, `x.a.b`, `x["k"]`)
237
562
  rather than just variables, discriminated unions at any depth, `and`/`or`,
238
- early return, `break`/`continue`, `error()` (declared `-> never`), user type
563
+ early return, `break`/`continue`, `error()` (declared `=> never`), user type
239
564
  guards (`v is T`), and assertion signatures (`asserts v`).
240
565
 
241
566
  Reading a member of, indexing or calling a value that may be nil is an error
@@ -276,8 +601,8 @@ as in TypeScript 4.9:
276
601
  type Shape = { kind: "circle" | "rect", size: number }
277
602
  const circle = { kind: "circle", size: 2 } satisfies Shape -- { kind: "circle", size: number }
278
603
  const handlers = {
279
- Click: function(x) return x + 1 end, -- x: number, from the contract
280
- } satisfies { [string]: (x: number) -> number }
604
+ Click: x => x + 1, -- x: number, from the contract
605
+ } satisfies { [string]: (x: number) => number }
281
606
  ```
282
607
 
283
608
  The contract types callbacks and empty arrays, and a literal stays a literal