rbxts-transform-boost 0.1.0 → 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 +135 -73
- package/bench/out/include/Promise.lua +2068 -0
- package/bench/out/include/RuntimeLib.lua +260 -0
- package/bench/out/src/server/benchmark.server.luau +122 -0
- package/bench/out/src/shared/fns-bare.luau +258 -0
- package/bench/out/src/shared/fns.luau +259 -0
- package/bench/src/server/benchmark.server.ts +30 -30
- package/bench/src/shared/fns.ts +2 -0
- package/bench/tsconfig.notransform.json +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/index.js +3 -3
- package/dist/passes/annotate.js +240 -47
- package/dist/passes/native.d.ts +1 -1
- package/dist/passes/native.js +11 -5
- package/dist/util.d.ts +1 -0
- package/dist/util.js +4 -0
- package/package.json +1 -3
- package/src/config.ts +4 -0
- package/src/index.ts +2 -2
- package/src/passes/annotate.ts +251 -58
- package/src/passes/native.ts +25 -10
- package/src/util.ts +4 -0
- package/bench/package.json +0 -14
- package/scripts/postprocess.js +0 -29
- /package/bench/{benchmark.project.json → default.project.json} +0 -0
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
>
|
|
5
5
|
> **What carried over:** Luau type annotation injection on function parameters for native codegen (primitives, Roblox value types, arrays).
|
|
6
6
|
>
|
|
7
|
-
> **What's new:** `--!optimize 2` on every file, `game:GetService()` hoisting to module-level locals, repeated property chain hoisting, loop bounds hoisting
|
|
7
|
+
> **What's new:** `--!optimize 2` on every file, `game:GetService()` hoisting to module-level locals, repeated property chain hoisting, loop bounds hoisting, `const` keyword for TypeScript `const` declarations, output formatting so compiled files look human-written.
|
|
8
8
|
>
|
|
9
|
-
> **What's different:** The old package also annotated return types, local variable declarations, class methods, and user-defined interfaces/type aliases. Those are not yet in this package — they're planned
|
|
9
|
+
> **What's different:** The old package also annotated return types, local variable declarations, class methods, and user-defined interfaces/type aliases. Those are not yet in this package — they're planned. The old package also had reliability issues that this rewrite addresses.
|
|
10
10
|
|
|
11
|
-
A
|
|
11
|
+
A TypeScript transformer for Roblox that automatically applies Luau performance directives and cleans up compiled output at build time — no runtime cost, no code changes required.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
@@ -24,6 +24,7 @@ npm install --save-dev rbxts-transform-boost
|
|
|
24
24
|
{
|
|
25
25
|
"transform": "rbxts-transform-boost",
|
|
26
26
|
"optimize": true,
|
|
27
|
+
"strict": true,
|
|
27
28
|
"hoist": true
|
|
28
29
|
}
|
|
29
30
|
]
|
|
@@ -36,14 +37,15 @@ npm install --save-dev rbxts-transform-boost
|
|
|
36
37
|
| Option | Type | Default | Description |
|
|
37
38
|
|--------|------|---------|-------------|
|
|
38
39
|
| `optimize` | `boolean` | `true` | Prepend `--!optimize 2` to every file that doesn't already have it |
|
|
40
|
+
| `strict` | `boolean` | `true` | Prepend `--!strict` to every file that doesn't already have it |
|
|
39
41
|
| `hoist` | `boolean` | `true` | Hoist `GetService` calls and repeated property reads to locals |
|
|
40
42
|
|
|
41
|
-
`--!native` is never auto-inserted. Add `//!native` at the top of your TypeScript file for hot paths you've profiled —
|
|
43
|
+
`--!native` is never auto-inserted. Add `//!native` at the top of your TypeScript file for hot paths you've profiled — the compiler preserves it.
|
|
42
44
|
|
|
43
45
|
```typescript
|
|
44
46
|
//!native
|
|
45
47
|
export function integrate(pos: Vector3, vel: Vector3, acc: Vector3, dt: number) {
|
|
46
|
-
...
|
|
48
|
+
// ...
|
|
47
49
|
}
|
|
48
50
|
```
|
|
49
51
|
|
|
@@ -53,21 +55,10 @@ export function integrate(pos: Vector3, vel: Vector3, acc: Vector3, dt: number)
|
|
|
53
55
|
|
|
54
56
|
### `--!optimize 2` — always on top
|
|
55
57
|
|
|
56
|
-
Every file gets `--!optimize 2` prepended if it doesn't already have it. Roblox already runs all scripts at optimization level 2, but the directive makes Studio behaviour match production and signals intent.
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
// TypeScript source
|
|
60
|
-
export function encodeFixed(buf: buffer, offset: number, value: number, scale: number): number {
|
|
61
|
-
const fixed = math.floor(value * scale);
|
|
62
|
-
const clamped = math.clamp(fixed, -32768, 32767);
|
|
63
|
-
buffer.writei16(buf, offset, clamped);
|
|
64
|
-
return offset + 2;
|
|
65
|
-
}
|
|
66
|
-
```
|
|
58
|
+
Every file gets `--!optimize 2` prepended if it doesn't already have it. Roblox already runs all scripts at optimization level 2 in live games, but the directive makes Studio behaviour match production and signals intent.
|
|
67
59
|
|
|
68
60
|
```lua
|
|
69
61
|
-- Without transformer
|
|
70
|
-
-- Compiled with rotor v2.2.0
|
|
71
62
|
local function encodeFixed(buf, offset, value, scale)
|
|
72
63
|
local fixed = math.floor(value * scale)
|
|
73
64
|
local clamped = math.clamp(fixed, -32768, 32767)
|
|
@@ -79,20 +70,41 @@ end
|
|
|
79
70
|
```lua
|
|
80
71
|
-- With transformer
|
|
81
72
|
--!optimize 2
|
|
82
|
-
|
|
83
|
-
local function encodeFixed(buf: buffer, offset: number, value: number, scale: number)
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
|
|
74
|
+
local function encodeFixed(buf: buffer, offset: number, value: number, scale: number): number
|
|
75
|
+
const fixed = math.floor(value * scale)
|
|
76
|
+
const clamped = math.clamp(fixed, -32768, 32767)
|
|
86
77
|
buffer.writei16(buf, offset, clamped)
|
|
78
|
+
|
|
87
79
|
return offset + 2
|
|
88
80
|
end
|
|
89
81
|
```
|
|
90
82
|
|
|
91
83
|
---
|
|
92
84
|
|
|
85
|
+
### `const` for TypeScript `const` declarations
|
|
86
|
+
|
|
87
|
+
TypeScript `const` declarations are emitted as Luau `const` (shipped in Roblox Studio March 2026). TypeScript `let` stays as `local`. Rotor-generated internal variables (`_cache0`, `_shouldIncrement`, etc.) are not affected.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// TypeScript
|
|
91
|
+
const N = 100000;
|
|
92
|
+
let i = 0;
|
|
93
|
+
const elapsed = os.clock() - t0;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```lua
|
|
97
|
+
-- Compiled output
|
|
98
|
+
const N = 100000
|
|
99
|
+
local i = 0
|
|
100
|
+
const elapsed = os.clock() - t0
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
93
105
|
### GetService hoisting
|
|
94
106
|
|
|
95
|
-
Every `game:GetService("X")` call in a file is hoisted to a module-level local on first load. Functions that call `GetService` on every invocation — the most common pattern
|
|
107
|
+
Every `game:GetService("X")` call in a file is hoisted to a module-level local on first load. Functions that call `GetService` on every invocation — the most common compiled pattern — pay the registry lookup cost zero times at runtime.
|
|
96
108
|
|
|
97
109
|
```typescript
|
|
98
110
|
// TypeScript source
|
|
@@ -115,12 +127,15 @@ end
|
|
|
115
127
|
```lua
|
|
116
128
|
-- With transformer
|
|
117
129
|
--!optimize 2
|
|
118
|
-
|
|
130
|
+
|
|
131
|
+
-- Services
|
|
119
132
|
local _RunService = game:GetService("RunService")
|
|
133
|
+
local _Players = game:GetService("Players")
|
|
134
|
+
|
|
135
|
+
local function serviceWork(): string
|
|
136
|
+
const count = #_Players:GetPlayers()
|
|
137
|
+
const running = _RunService:IsRunning()
|
|
120
138
|
|
|
121
|
-
local function serviceWork()
|
|
122
|
-
local count = #_Players:GetPlayers()
|
|
123
|
-
local running = _RunService:IsRunning()
|
|
124
139
|
return `{count}-{running}`
|
|
125
140
|
end
|
|
126
141
|
```
|
|
@@ -146,8 +161,8 @@ export function cameraWork(camera: Camera): number {
|
|
|
146
161
|
```lua
|
|
147
162
|
-- Without transformer
|
|
148
163
|
local function cameraWork(camera)
|
|
149
|
-
local pos = camera.CFrame.Position
|
|
150
|
-
local look = camera.CFrame.LookVector
|
|
164
|
+
local pos = camera.CFrame.Position
|
|
165
|
+
local look = camera.CFrame.LookVector
|
|
151
166
|
local fov = camera.FieldOfView
|
|
152
167
|
return pos.Magnitude + look.X + fov
|
|
153
168
|
end
|
|
@@ -155,48 +170,28 @@ end
|
|
|
155
170
|
|
|
156
171
|
```lua
|
|
157
172
|
-- With transformer
|
|
158
|
-
local function cameraWork(camera: Camera)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
local function cameraWork(camera: Camera): number
|
|
174
|
+
const _cache0 = camera.CFrame
|
|
175
|
+
const pos = _cache0.Position
|
|
176
|
+
const look = _cache0.LookVector
|
|
177
|
+
const fov = camera.FieldOfView
|
|
178
|
+
|
|
163
179
|
return pos.Magnitude + look.X + fov
|
|
164
180
|
end
|
|
165
181
|
```
|
|
166
182
|
|
|
167
183
|
**1.5× faster.** Also hoists value type field reads (`.X`, `.Y`, `.Z`) when they appear multiple times — the `cross` function reads each component twice, so all six are hoisted:
|
|
168
184
|
|
|
169
|
-
```typescript
|
|
170
|
-
// TypeScript source
|
|
171
|
-
export function cross(a: Vector3, b: Vector3): Vector3 {
|
|
172
|
-
return new Vector3(
|
|
173
|
-
a.Y * b.Z - a.Z * b.Y,
|
|
174
|
-
a.Z * b.X - a.X * b.Z,
|
|
175
|
-
a.X * b.Y - a.Y * b.X,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
```lua
|
|
181
|
-
-- Without transformer
|
|
182
|
-
local function cross(a, b)
|
|
183
|
-
return Vector3.new(
|
|
184
|
-
a.Y * b.Z - a.Z * b.Y,
|
|
185
|
-
a.Z * b.X - a.X * b.Z,
|
|
186
|
-
a.X * b.Y - a.Y * b.X
|
|
187
|
-
)
|
|
188
|
-
end
|
|
189
|
-
```
|
|
190
|
-
|
|
191
185
|
```lua
|
|
192
186
|
-- With transformer
|
|
193
|
-
local function cross(a: Vector3, b: Vector3)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
187
|
+
local function cross(a: Vector3, b: Vector3): Vector3
|
|
188
|
+
const _cache0 = a.Y
|
|
189
|
+
const _cache1 = b.Z
|
|
190
|
+
const _cache2 = a.Z
|
|
191
|
+
const _cache3 = b.Y
|
|
192
|
+
const _cache4 = b.X
|
|
193
|
+
const _cache5 = a.X
|
|
194
|
+
|
|
200
195
|
return Vector3.new(
|
|
201
196
|
_cache0 * _cache1 - _cache2 * _cache3,
|
|
202
197
|
_cache2 * _cache4 - _cache5 * _cache1,
|
|
@@ -211,7 +206,7 @@ end
|
|
|
211
206
|
|
|
212
207
|
### Loop bounds hoisting
|
|
213
208
|
|
|
214
|
-
`arr.size()` in a `for` loop condition is re-evaluated on every iteration in
|
|
209
|
+
`arr.size()` in a `for` loop condition is re-evaluated on every iteration in compiled output. The loops pass hoists it to a local before the loop.
|
|
215
210
|
|
|
216
211
|
```typescript
|
|
217
212
|
// TypeScript source
|
|
@@ -228,7 +223,7 @@ export function sumWeighted(values: Array<number>, weights: Array<number>): numb
|
|
|
228
223
|
-- Without transformer
|
|
229
224
|
local function sumWeighted(values, weights)
|
|
230
225
|
local total = 0
|
|
231
|
-
for i = 0, #values - 1 do
|
|
226
|
+
for i = 0, #values - 1 do
|
|
232
227
|
total += values[i + 1] * weights[i + 1]
|
|
233
228
|
end
|
|
234
229
|
return total
|
|
@@ -237,12 +232,32 @@ end
|
|
|
237
232
|
|
|
238
233
|
```lua
|
|
239
234
|
-- With transformer
|
|
240
|
-
local function sumWeighted(values: {number}, weights: {number})
|
|
235
|
+
local function sumWeighted(values: {number}, weights: {number}): number
|
|
241
236
|
local total = 0
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
|
|
238
|
+
do
|
|
239
|
+
const _len_values = #values
|
|
240
|
+
|
|
241
|
+
do
|
|
242
|
+
local i = 0
|
|
243
|
+
local _shouldIncrement = false
|
|
244
|
+
|
|
245
|
+
while true do
|
|
246
|
+
if _shouldIncrement then
|
|
247
|
+
i += 1
|
|
248
|
+
else
|
|
249
|
+
_shouldIncrement = true
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
if not (i < _len_values) then
|
|
253
|
+
break
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
total += values[i + 1] * weights[i + 1]
|
|
257
|
+
end
|
|
258
|
+
end
|
|
245
259
|
end
|
|
260
|
+
|
|
246
261
|
return total
|
|
247
262
|
end
|
|
248
263
|
```
|
|
@@ -253,7 +268,7 @@ end
|
|
|
253
268
|
|
|
254
269
|
### Luau type annotation injection
|
|
255
270
|
|
|
256
|
-
After
|
|
271
|
+
After the compiler writes `.luau` files, the transformer injects Luau type annotations on function parameters and return types. This lets the native compiler generate specialized code for numeric and Roblox value types.
|
|
257
272
|
|
|
258
273
|
```typescript
|
|
259
274
|
// TypeScript source
|
|
@@ -271,7 +286,7 @@ end
|
|
|
271
286
|
|
|
272
287
|
```lua
|
|
273
288
|
-- With transformer
|
|
274
|
-
local function dot(a: Vector3, b: Vector3)
|
|
289
|
+
local function dot(a: Vector3, b: Vector3): number
|
|
275
290
|
return a.X * b.X + a.Y * b.Y + a.Z * b.Z
|
|
276
291
|
end
|
|
277
292
|
```
|
|
@@ -280,6 +295,51 @@ Supported types: `number`, `string`, `boolean`, `Vector3`, `Vector2`, `CFrame`,
|
|
|
280
295
|
|
|
281
296
|
---
|
|
282
297
|
|
|
298
|
+
### Output formatting
|
|
299
|
+
|
|
300
|
+
This one's a personal pet peeve — yes, most people will never open a compiled `.luau` file. But the transformer post-processes every compiled `.luau` file so the output looks like a human wrote it anyway, not a compiler.
|
|
301
|
+
|
|
302
|
+
**Preamble organisation** — top-level declarations are sorted into labeled sections in dependency order. Sections are sorted by line length (longest first). If you put a comment before a group of imports in TypeScript, that comment becomes the section label:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// Shared
|
|
306
|
+
import * as utils from "../shared/utils";
|
|
307
|
+
|
|
308
|
+
// Server
|
|
309
|
+
import * as data from "../server/data";
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
```lua
|
|
313
|
+
--!optimize 2
|
|
314
|
+
--!native
|
|
315
|
+
|
|
316
|
+
-- Compiled with rotor v2.2.0
|
|
317
|
+
|
|
318
|
+
-- Runtime
|
|
319
|
+
local TS = require(...)
|
|
320
|
+
|
|
321
|
+
-- Services
|
|
322
|
+
local _ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
323
|
+
local _Workspace = game:GetService("Workspace")
|
|
324
|
+
|
|
325
|
+
-- Shared
|
|
326
|
+
local utils = TS.import(script, ...)
|
|
327
|
+
|
|
328
|
+
-- Server
|
|
329
|
+
local data = TS.import(script, ...)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Spacing inside functions** — blank lines are added so blocks breathe:
|
|
333
|
+
|
|
334
|
+
- Before `return` when it's not the only statement in the function
|
|
335
|
+
- After `end` blocks when the next line is not another `end`, `else`, or `elseif`
|
|
336
|
+
- Before block starters (`do`/`while`/`for`/`if`) when preceded by a group of `local`/`const` assignments
|
|
337
|
+
- At `const` → `local` transitions
|
|
338
|
+
|
|
339
|
+
**`--!` directives** are sorted by length and separated from the rotor header comment with a blank line.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
283
343
|
## Benchmarks
|
|
284
344
|
|
|
285
345
|
Measured in Roblox Studio server context. 100,000 iterations per benchmark (10,000 for `cfLookAt`). Same TypeScript source compiled two ways — with and without the transformer.
|
|
@@ -316,8 +376,10 @@ Measured in Roblox Studio server context. 100,000 iterations per benchmark (10,0
|
|
|
316
376
|
## Development
|
|
317
377
|
|
|
318
378
|
```bash
|
|
319
|
-
npm run build
|
|
320
|
-
npm run bench:
|
|
379
|
+
npm run build # compile the transformer (tsc → dist/)
|
|
380
|
+
npm run bench:build # build the benchmark suite with transformer applied
|
|
381
|
+
npm run bench:build:baseline # build the baseline suite (no transformer)
|
|
382
|
+
npm run bench:rbxlx # produce bench/benchmark.rbxlx (both suites in one place)
|
|
321
383
|
```
|
|
322
384
|
|
|
323
|
-
Open `bench/benchmark.rbxlx` in Roblox Studio and run the server. The optimized suite prints first, then the baseline suite
|
|
385
|
+
Open `bench/benchmark.rbxlx` in Roblox Studio and run the server. The optimized suite prints first, then the baseline suite.
|