luaut-parser 4.0.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 +296 -20
- package/dist/index.cjs +1535 -121
- package/dist/index.d.cts +143 -20
- package/dist/index.d.ts +143 -20
- package/dist/index.js +1534 -121
- package/package.json +1 -1
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
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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() ...
|
|
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 = {}
|
|
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
|
|
@@ -164,8 +234,8 @@ name it. Compiled code keeps no trace of it.
|
|
|
164
234
|
written with `:`, the way JavaScript writes them:
|
|
165
235
|
|
|
166
236
|
```luau
|
|
167
|
-
const long = names:filter(
|
|
168
|
-
const first = names:find(
|
|
237
|
+
const long = names:filter(n => #n > 3):map(string.upper)
|
|
238
|
+
const first = names:find(n => n:startsWith("A"))
|
|
169
239
|
print(names:join(", "), text:trim(), text:replaceAll(",", ";"))
|
|
170
240
|
```
|
|
171
241
|
|
|
@@ -181,10 +251,10 @@ module in its package.json, and the compiler asks it what a call becomes:
|
|
|
181
251
|
"luaut": { "types": "index.d.luaut", "lowering": "lowering.mjs" }
|
|
182
252
|
```
|
|
183
253
|
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const plugin
|
|
254
|
+
```js
|
|
255
|
+
// @ts-check
|
|
256
|
+
/** @type {import("luaut-parser").LoweringPlugin} */ // the contract, declared here
|
|
257
|
+
const plugin = {
|
|
188
258
|
runtime: { array: "local __NAME__ = {}\nfunction __NAME__.filter(t, test) ... end" },
|
|
189
259
|
methodCall({ method, receiver, use }) {
|
|
190
260
|
if (receiver?.kind === "array" && method === "filter") {
|
|
@@ -209,6 +279,14 @@ it.
|
|
|
209
279
|
first element is 1, `indexOf` answers `nil` rather than -1) and `push`, `pop`,
|
|
210
280
|
`shift`, `unshift`, `sort` and `reverse` change the array they are called on.
|
|
211
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
|
+
|
|
212
290
|
**Optionality** — there is no `T?` shorthand. `?` in type position always
|
|
213
291
|
belongs to a conditional type, and in expression position to a ternary or an
|
|
214
292
|
optional chain.
|
|
@@ -226,7 +304,7 @@ nothing further along the chain runs, arguments included: `folder?:FindFirstChil
|
|
|
226
304
|
is a `string | nil`. The `?` must touch the `.` or `:`; `c ? a : b` stays a
|
|
227
305
|
ternary. Parentheses end a chain. A chain cannot be assigned to (`a?.b = 1` is
|
|
228
306
|
an error). A chain that got through narrows what it tested: inside
|
|
229
|
-
`if part?.Parent
|
|
307
|
+
`if (part?.Parent)`, and `if (part?.Name == "Door")`, `part` is not nil.
|
|
230
308
|
|
|
231
309
|
**Classes** — types are structural, except for classes. A definitions file
|
|
232
310
|
declares one with `declare class`, and it is nominal, as Roblox's classes are:
|
|
@@ -243,12 +321,210 @@ has (`{ Name: string }`). It is not a table, though, so `typeof(part)` picks
|
|
|
243
321
|
the `"Instance"` overload, not `"table"`. Members are inherited, and a subclass
|
|
244
322
|
may narrow one (`Parent: SomeFolder`).
|
|
245
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
|
+
|
|
246
522
|
**Callbacks** — a function written where a function type is expected takes
|
|
247
|
-
its parameter types from it: in `signal:Connect(
|
|
523
|
+
its parameter types from it: in `signal:Connect(player => ... )`,
|
|
248
524
|
`player` is typed from `Connect`. The same applies to an annotated `const`
|
|
249
|
-
and to an assignment such as `remote.OnServerInvoke = function(player)
|
|
525
|
+
and to an assignment such as `remote.OnServerInvoke = function(player) { ... }`.
|
|
250
526
|
|
|
251
|
-
**Type packs** — `type Signal<T... = ...any> = { Connect: (self, cb: (T...)
|
|
527
|
+
**Type packs** — `type Signal<T... = ...any> = { Connect: (self, cb: (T...) => ()) => () }`.
|
|
252
528
|
A pack parameter takes every type argument from its position on:
|
|
253
529
|
`Signal<Player, string>`, `Signal<()>` for none.
|
|
254
530
|
|
|
@@ -284,7 +560,7 @@ already get.
|
|
|
284
560
|
|
|
285
561
|
**Narrowing** follows TypeScript's model: references (`x`, `x.a.b`, `x["k"]`)
|
|
286
562
|
rather than just variables, discriminated unions at any depth, `and`/`or`,
|
|
287
|
-
early return, `break`/`continue`, `error()` (declared
|
|
563
|
+
early return, `break`/`continue`, `error()` (declared `=> never`), user type
|
|
288
564
|
guards (`v is T`), and assertion signatures (`asserts v`).
|
|
289
565
|
|
|
290
566
|
Reading a member of, indexing or calling a value that may be nil is an error
|
|
@@ -325,8 +601,8 @@ as in TypeScript 4.9:
|
|
|
325
601
|
type Shape = { kind: "circle" | "rect", size: number }
|
|
326
602
|
const circle = { kind: "circle", size: 2 } satisfies Shape -- { kind: "circle", size: number }
|
|
327
603
|
const handlers = {
|
|
328
|
-
Click:
|
|
329
|
-
} satisfies { [string]: (x: number)
|
|
604
|
+
Click: x => x + 1, -- x: number, from the contract
|
|
605
|
+
} satisfies { [string]: (x: number) => number }
|
|
330
606
|
```
|
|
331
607
|
|
|
332
608
|
The contract types callbacks and empty arrays, and a literal stays a literal
|