@tilua/compiler 1.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 +232 -0
- package/dist/chunk-S5C4VH6A.js +2310 -0
- package/dist/cli.cjs +2344 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +54 -0
- package/dist/index.cjs +2369 -0
- package/dist/index.d.cts +233 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +55 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# @tilua/compiler
|
|
2
|
+
|
|
3
|
+
Compiles a [tilua](https://marketplace.visualstudio.com/search?term=tilua)
|
|
4
|
+
project to one Luau file. Each file is parsed by
|
|
5
|
+
[`@tilua/parser`](https://www.npmjs.com/package/@tilua/parser), its AST lowered
|
|
6
|
+
to a `luau-parser` AST, and that AST printed by `luau-parser`.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i -D @tilua/compiler @tilua-types/roblox
|
|
10
|
+
npx tilua src/main.tilua --out build/main.luau
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
tilua <entry> [--out <file>] [--config <tilua.config.json>] [--noCheck] [--target luau|lua51]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Every module the entry imports is bundled in. Problems are reported as
|
|
18
|
+
`file:line:column message`. Type errors are checked against the config's
|
|
19
|
+
`types`: they are reported, but the bundle is still written (`--noCheck` skips
|
|
20
|
+
the check). A syntax error or a missing module stops the build.
|
|
21
|
+
|
|
22
|
+
Without `--out`, the entry's own path with `.luau` is written. Without
|
|
23
|
+
`--config`, the nearest `tilua.config.json` above the entry applies — see the
|
|
24
|
+
[`@tilua/parser`](https://www.npmjs.com/package/@tilua/parser) README for what
|
|
25
|
+
goes in it.
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { bundle, compile } from "@tilua/compiler"
|
|
31
|
+
|
|
32
|
+
const result = await bundle({
|
|
33
|
+
entry: "src/main.tilua",
|
|
34
|
+
// A config file's path, or its contents:
|
|
35
|
+
config: { types: ["roblox"], paths: { "@shared/*": ["src/shared/*"] } },
|
|
36
|
+
})
|
|
37
|
+
result.code // the Luau bundle, unless a module failed
|
|
38
|
+
result.diagnostics // { file, line, column, message, category: "syntax" | "module" | "type" | "config" }
|
|
39
|
+
|
|
40
|
+
// One file, no imports. The config is optional here too, and brings the same
|
|
41
|
+
// type libraries — including what they lower.
|
|
42
|
+
const one = await compile("const names = [1]\n", "tilua.config.json")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`config` is typed `string | TiluaConfigJson`. Left out, the nearest
|
|
46
|
+
`tilua.config.json` above the entry applies — and for `compile`, nothing at
|
|
47
|
+
all.
|
|
48
|
+
|
|
49
|
+
Both are `async`: a type library may ship its own lowering, which is a
|
|
50
|
+
JavaScript module the build loads.
|
|
51
|
+
|
|
52
|
+
## What becomes what
|
|
53
|
+
|
|
54
|
+
| tilua | Luau |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `const { a, b } = value` | `local a, b = value.a, value.b` |
|
|
57
|
+
| `const [x, ...rest] = list` | `local x = list[1]; local rest = table.move(list, 2, #list, 1, {})` |
|
|
58
|
+
| `const { a = 1 } = t` | `local a = t.a; if a == nil then a = 1 end` |
|
|
59
|
+
| `[1, 2]` / `{ a: 1, b }` | `{ 1, 2 }` / `{ a = 1, b = b }` |
|
|
60
|
+
| `{ ...base, a: 1 }` / `[...xs, 1]` | a small helper copying the parts in order |
|
|
61
|
+
| `` `${a} any` `` | `("%s any"):format(tostring(a))` |
|
|
62
|
+
| `a?.b` | `if a == nil then nil else a.b` |
|
|
63
|
+
| `a?:m()` as a statement | `if a ~= nil then a:m() end` |
|
|
64
|
+
| `f()?.b?:m(x)` | a function call that keeps each link in a local, so every link runs once, in order, and none after a nil |
|
|
65
|
+
| `function f(n = 1, { x })` | `function f(n, arg) if n == nil then n = 1 end local x = arg.x ...` |
|
|
66
|
+
| `const` / `let` | `local` |
|
|
67
|
+
| `class C { … }` | one table for the class, one per instance — see below |
|
|
68
|
+
| `new C(x)` | `C.new(x)` |
|
|
69
|
+
| `[...]` | `{...}` — the varargs as an array |
|
|
70
|
+
| `function f(a, ...rest)` | `function f(a, ...) local rest = {...}` |
|
|
71
|
+
| `f(a, ...xs)` / `return ...xs` | `table.unpack(xs)` in the last position |
|
|
72
|
+
| `f(...xs, a)` | the list built first, then `table.unpack` of it |
|
|
73
|
+
| `x as T`, `x satisfies T`, types, `declare` | removed |
|
|
74
|
+
| `names:filter(f)` | whatever the type library that declared `filter` says — see below |
|
|
75
|
+
|
|
76
|
+
## Classes
|
|
77
|
+
|
|
78
|
+
`class` is sugar over the Lua idiom, and it lowers to that idiom and nothing
|
|
79
|
+
more: one table per class, holding its methods and its statics, and one table
|
|
80
|
+
per instance whose metatable is the class. An instance reaches the class
|
|
81
|
+
directly — nothing is copied per instance.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
class Dog extends Animal {
|
|
85
|
+
breed = "corgi"
|
|
86
|
+
|
|
87
|
+
constructor(name: string) {
|
|
88
|
+
super(name)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function speak(): string {
|
|
92
|
+
return `${super.speak()} woof`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```lua
|
|
98
|
+
local Dog = tilua_class(Animal)
|
|
99
|
+
function Dog.speak(this)
|
|
100
|
+
return ("%s woof"):format(tostring(Animal.speak(this)))
|
|
101
|
+
end
|
|
102
|
+
function Dog.__init(this, name)
|
|
103
|
+
Animal.__init(this, name) -- super(name)
|
|
104
|
+
this.breed = "corgi" -- the field initializers, after super
|
|
105
|
+
end
|
|
106
|
+
function Dog.new(...)
|
|
107
|
+
local this = setmetatable({}, Dog)
|
|
108
|
+
Dog.__init(this, ...)
|
|
109
|
+
return this
|
|
110
|
+
end
|
|
111
|
+
tilua_accessors(Dog)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`__init` is what `super(...)` calls: it runs the constructor on an instance
|
|
115
|
+
that already exists, so a derived class builds one table, not one per level.
|
|
116
|
+
`new` is a real function on the class table — `new Dog(x)` and `Dog.new(x)`
|
|
117
|
+
are the same call.
|
|
118
|
+
|
|
119
|
+
Two links come with every class, and they cost an instance nothing because
|
|
120
|
+
they live on the class table: `ClassObject`, which an instance reads through
|
|
121
|
+
its metatable to reach its own class, and `ParentClass`, which a class reads
|
|
122
|
+
to reach the one it extends.
|
|
123
|
+
|
|
124
|
+
A class written as a value (`const Counter = class { … }`) is the same code,
|
|
125
|
+
inside a function that runs where the class is written and hands the table
|
|
126
|
+
back — an expression has no room for statements. Type parameters leave no
|
|
127
|
+
trace at all: `Box<number>` and `Box<string>` compile to the one `Box`.
|
|
128
|
+
|
|
129
|
+
Two helpers go in at the top of any file that declares a class.
|
|
130
|
+
`tilua_class(base)` makes the table, points `__index` at it, and chains it to
|
|
131
|
+
the base — for the statics, and for the getter and setter tables. Getters and
|
|
132
|
+
setters are what the second helper is for: `tilua_accessors(class)` replaces
|
|
133
|
+
`__index` with a function *only* when the class or one it extends declares an
|
|
134
|
+
accessor. Every other class keeps the plain `__index = class` lookup, which is
|
|
135
|
+
the fast one.
|
|
136
|
+
|
|
137
|
+
## What a type library lowers
|
|
138
|
+
|
|
139
|
+
The compiler lowers tilua. What a *library* gives a value, the library also
|
|
140
|
+
says how to run: `names:filter(f)` is a call to a function because
|
|
141
|
+
[`@tilua-types/lua`](https://www.npmjs.com/package/@tilua-types/lua) declares
|
|
142
|
+
the method and ships the Luau behind it.
|
|
143
|
+
|
|
144
|
+
A library names a module in its package.json (`"tilua": { "lowering":
|
|
145
|
+
"lowering.mjs" }`) whose default export answers for a call. The contract is
|
|
146
|
+
`LoweringPlugin`, declared in `@tilua/parser` and re-exported here, so a
|
|
147
|
+
library can be checked against it — by JSDoc, or in TypeScript — without
|
|
148
|
+
depending on the compiler:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
// @ts-check
|
|
152
|
+
/** @type {import("@tilua/parser").LoweringPlugin} */
|
|
153
|
+
const plugin = {
|
|
154
|
+
runtime: { array: "local __NAME__ = {}\nfunction __NAME__.filter(t, test) ... end" },
|
|
155
|
+
methodCall({ method, receiver, use }) {
|
|
156
|
+
if (receiver?.kind === "array" && method === "filter") {
|
|
157
|
+
return { callee: `${use("array")}.filter` }
|
|
158
|
+
}
|
|
159
|
+
return undefined
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
export default plugin
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`receiver` is the tilua type the analyzer worked out; `use(key)` names the
|
|
166
|
+
table from `runtime`, emitted once at the top of the output and only if a call
|
|
167
|
+
needed it; the receiver becomes the call's first argument unless
|
|
168
|
+
`passReceiver: false`. The last library loaded is asked first, and `undefined`
|
|
169
|
+
leaves an ordinary Luau method call.
|
|
170
|
+
|
|
171
|
+
## Modules
|
|
172
|
+
|
|
173
|
+
Roblox's `require` takes an Instance, so the bundle has its own. Each module
|
|
174
|
+
is an entry in one table:
|
|
175
|
+
|
|
176
|
+
```lua
|
|
177
|
+
local G
|
|
178
|
+
G = {
|
|
179
|
+
modules = {
|
|
180
|
+
["src/util"] = {
|
|
181
|
+
names = { clamp = true },
|
|
182
|
+
load = function(exports)
|
|
183
|
+
function exports.clamp(x, lo, hi) ... end
|
|
184
|
+
end,
|
|
185
|
+
},
|
|
186
|
+
["src/main"] = {
|
|
187
|
+
load = function(exports)
|
|
188
|
+
local util
|
|
189
|
+
util = G.require("src/util")
|
|
190
|
+
print(util.clamp(5, 0, 1))
|
|
191
|
+
end,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
records = {},
|
|
195
|
+
require = function(name) ... end,
|
|
196
|
+
}
|
|
197
|
+
G.require("src/main")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Modules are named by their path from the project folder. An entry that
|
|
201
|
+
exports ends with `return G.require(...)`, so the bundle can itself be a
|
|
202
|
+
ModuleScript.
|
|
203
|
+
|
|
204
|
+
Modules follow ES module rules, including when they import each other:
|
|
205
|
+
|
|
206
|
+
- **Loading state.** A module's record exists, marked `loading`, before its
|
|
207
|
+
code runs. When `a` imports `b` and `b` imports `a` back, `b` gets `a`'s
|
|
208
|
+
exports as they stand instead of loading `a` again.
|
|
209
|
+
- **Hoisting.** Every top-level function is defined before a module's imports
|
|
210
|
+
run, so `b` can call `a`'s functions in the middle of that cycle.
|
|
211
|
+
- **Initialization.** Reading an export its module has not initialized yet is
|
|
212
|
+
an error — `Cannot access 'x' before initialization` — as reading a `let`
|
|
213
|
+
before its declaration is in JavaScript.
|
|
214
|
+
- **Live bindings.** Exports live on the module's `exports` table and imports
|
|
215
|
+
are read through it (`util.clamp`, never a copy), so a value set later — or
|
|
216
|
+
a `let` changed later — is what the importer sees. Re-exports
|
|
217
|
+
(`export { x } from`) and `export *` are references too: they read the
|
|
218
|
+
other module on every access.
|
|
219
|
+
- **Read-only imports.** Assigning to an imported name, or to a member of a
|
|
220
|
+
namespace (`import * as M`, then `M.x = 1`), is an error.
|
|
221
|
+
- **Types only.** `import type` is erased whatever it names, and so is an
|
|
222
|
+
import used only as a type. A module reached only that way is left out of
|
|
223
|
+
the bundle and never runs.
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npm test # with a Luau interpreter on the PATH (or named by LUAU),
|
|
229
|
+
# the tests also run each bundle and check its output
|
|
230
|
+
npm run build
|
|
231
|
+
npm run typecheck
|
|
232
|
+
```
|