@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 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
+ ```