rbxts-transform-boost 0.1.0 → 1.1.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
@@ -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 all without needing `//!native` in every file.
7
+ > **What's new:** `--!optimize 2` on every file, `game:GetService()` hoisting to module-level locals, repeated property chain 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 but the implementation is non-trivial. The old package also had reliability issues that this rewrite addresses.
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 roblox-ts transformer that automatically applies Luau performance directives at compile time — no runtime cost, no code changes required.
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 — rotor preserves it.
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
- -- Compiled with rotor v2.2.0
83
- local function encodeFixed(buf: buffer, offset: number, value: number, scale: number)
84
- local fixed = math.floor(value * scale)
85
- local clamped = math.clamp(fixed, -32768, 32767)
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 in rotor output — pay the registry lookup cost zero times at runtime.
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
- local _Players = game:GetService("Players") -- hoisted once at module load
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 -- engine call #1
150
- local look = camera.CFrame.LookVector -- engine call #2
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
- local _cache0 = camera.CFrame -- one engine call
160
- local pos = _cache0.Position -- local read
161
- local look = _cache0.LookVector -- local read
162
- local fov = camera.FieldOfView
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
- local _cache0 = a.Y -- each component read twice → all hoisted
195
- local _cache1 = b.Z
196
- local _cache2 = a.Z
197
- local _cache3 = b.Y
198
- local _cache4 = b.X
199
- local _cache5 = a.X
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,
@@ -209,106 +204,109 @@ end
209
204
 
210
205
  ---
211
206
 
212
- ### Loop bounds hoisting
207
+ ### Luau type annotation injection
213
208
 
214
- `arr.size()` in a `for` loop condition is re-evaluated on every iteration in rotor's output. The loops pass hoists it to a `const` before the loop.
209
+ 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.
215
210
 
216
211
  ```typescript
217
212
  // TypeScript source
218
- export function sumWeighted(values: Array<number>, weights: Array<number>): number {
219
- let total = 0;
220
- for (let i = 0; i < values.size(); i++) {
221
- total += values[i] * weights[i];
222
- }
223
- return total;
213
+ export function dot(a: Vector3, b: Vector3): number {
214
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
224
215
  }
225
216
  ```
226
217
 
227
218
  ```lua
228
219
  -- Without transformer
229
- local function sumWeighted(values, weights)
230
- local total = 0
231
- for i = 0, #values - 1 do -- #values evaluated once per iteration
232
- total += values[i + 1] * weights[i + 1]
233
- end
234
- return total
220
+ local function dot(a, b)
221
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z
235
222
  end
236
223
  ```
237
224
 
238
225
  ```lua
239
226
  -- With transformer
240
- local function sumWeighted(values: {number}, weights: {number})
241
- local total = 0
242
- local _len_values = #values -- hoisted: evaluated once total
243
- for i = 0, _len_values - 1 do
244
- total += values[i + 1] * weights[i + 1]
245
- end
246
- return total
227
+ local function dot(a: Vector3, b: Vector3): number
228
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z
247
229
  end
248
230
  ```
249
231
 
250
- **2.3× faster** (combined with `--!native` and type annotations on `values`/`weights`).
232
+ Supported types: `number`, `string`, `boolean`, `Vector3`, `Vector2`, `CFrame`, `UDim2`, `Color3`, `buffer`, `Instance`, `BasePart`, `Player`, `Camera`, `RunService`, `Players`, `Workspace`, and array forms (`{number}`, `{Vector3}`, etc.).
251
233
 
252
234
  ---
253
235
 
254
- ### Luau type annotation injection
236
+ ### Output formatting
255
237
 
256
- After rotor writes `.luau` files, the transformer injects Luau type annotations on function parameters. This lets the native compiler generate specialized code for numeric and Roblox value types.
238
+ 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.
239
+
240
+ **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:
257
241
 
258
242
  ```typescript
259
- // TypeScript source
260
- export function dot(a: Vector3, b: Vector3): number {
261
- return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
262
- }
263
- ```
243
+ // Shared
244
+ import * as utils from "../shared/utils";
264
245
 
265
- ```lua
266
- -- Without transformer
267
- local function dot(a, b)
268
- return a.X * b.X + a.Y * b.Y + a.Z * b.Z
269
- end
246
+ // Server
247
+ import * as data from "../server/data";
270
248
  ```
271
249
 
272
250
  ```lua
273
- -- With transformer
274
- local function dot(a: Vector3, b: Vector3)
275
- return a.X * b.X + a.Y * b.Y + a.Z * b.Z
276
- end
251
+ --!optimize 2
252
+ --!native
253
+
254
+ -- Compiled with rotor v2.2.0
255
+
256
+ -- Runtime
257
+ local TS = require(...)
258
+
259
+ -- Services
260
+ local _ReplicatedStorage = game:GetService("ReplicatedStorage")
261
+ local _Workspace = game:GetService("Workspace")
262
+
263
+ -- Shared
264
+ local utils = TS.import(script, ...)
265
+
266
+ -- Server
267
+ local data = TS.import(script, ...)
277
268
  ```
278
269
 
279
- Supported types: `number`, `string`, `boolean`, `Vector3`, `Vector2`, `CFrame`, `UDim2`, `Color3`, `buffer`, `Instance`, `BasePart`, `Player`, `Camera`, `RunService`, `Players`, `Workspace`, and array forms (`{number}`, `{Vector3}`, etc.).
270
+ **Spacing inside functions** blank lines are added so blocks breathe:
271
+
272
+ - Before `return` when it's not the only statement in the function
273
+ - After `end` blocks when the next line is not another `end`, `else`, or `elseif`
274
+ - Before block starters (`do`/`while`/`for`/`if`) when preceded by a group of `local`/`const` assignments
275
+ - At `const` → `local` transitions
276
+
277
+ **`--!` directives** are sorted by length and separated from the rotor header comment with a blank line.
280
278
 
281
279
  ---
282
280
 
283
281
  ## Benchmarks
284
282
 
285
- 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.
283
+ Measured in Roblox Studio server context. 100,000 iterations per benchmark (10,000 for `cfLookAt`). Both suites use `//!native` the only variable is whether the transformer is applied, so the numbers reflect what the transformer itself contributes on top of native.
286
284
 
287
285
  | Benchmark | With transformer | Without | Speedup | Driver |
288
286
  |-----------|-----------------|---------|---------|--------|
289
- | integrate (Verlet) | 0.042 µs | 0.055 µs | **1.3×** | `--!native` + type annotations |
290
- | dot (V3 manual) | 0.016 µs | 0.032 µs | **2.0×** | `--!native` |
291
- | cross (V3 manual) | 0.018 µs | 0.049 µs | **2.7×** | `--!native` + 6× field hoisting |
292
- | lerpVec3 (V3 manual) | 0.015 µs | 0.047 µs | **3.1×** | `--!native` + 3× field hoisting |
293
- | encodeFixed (buf+math) | 0.015 µs | 0.032 µs | **2.1×** | `--!native` |
294
- | encodePacket (3× fixed) | 0.018 µs | 0.067 µs | **3.7×** | `--!native` stacked across 3 calls |
295
- | sumWeighted (loop) | 0.046 µs | 0.107 µs | **2.3×** | `--!native` + loop bounds hoist |
296
- | dotProduct (loop) | 0.067 µs | 0.109 µs | **1.6×** | `--!native` + loop bounds hoist |
297
- | norm (loop+sqrt) | 0.048 µs | 0.111 µs | **2.3×** | `--!native` + loop bounds hoist |
298
- | mathHeavy (trig+sqrt) | 0.038 µs | 0.052 µs | **1.4×** | `--!native` |
299
- | fib(20) (iter) | 0.048 µs | 0.155 µs | **3.2×** | `--!native` on integer loop |
300
- | cfLookAt (ctor) | 0.079 µs | 0.079 µs | 1.0× | C++ floor — no Luau work |
301
- | cfChain (mul+angles) | 0.076 µs | 0.077 µs | 1.0× | C++ floor — no Luau work |
302
- | serviceWork (GetService ×2) | 0.185 µs | 0.440 µs | **2.4×** | GetService hoisting |
303
- | multiSvc (GetService ×3) | 0.142 µs | 0.480 µs | **3.4×** | GetService hoisting |
304
- | cameraWork (prop chain) | 0.148 µs | 0.215 µs | **1.5×** | `camera.CFrame` hoisted (2 reads → 1) |
305
- | formatStats (template) | 0.170 µs | 0.175 µs | ~1× | String — no arithmetic |
306
- | buildKey (template) | 0.068 µs | 0.086 µs | **1.3×** | `--!native` |
287
+ | integrate (Verlet) | 0.058 µs | 0.071 µs | **1.2×** | type annotations |
288
+ | dot (V3 manual) | 0.025 µs | 0.046 µs | **1.8×** | type annotations |
289
+ | cross (V3 manual) | 0.024 µs | 0.072 µs | **3.0×** | 6× field hoisting + type annotations |
290
+ | lerpVec3 (V3 manual) | 0.026 µs | 0.061 µs | **2.3×** | 3× field hoisting + type annotations |
291
+ | encodeFixed (buf+math) | 0.025 µs | 0.026 µs | ~1× | |
292
+ | encodePacket (3× fixed) | 0.030 µs | 0.028 µs | ~1× | |
293
+ | sumWeighted (loop) | 0.051 µs | 0.054 µs | ~1× | type annotations |
294
+ | dotProduct (loop) | 0.050 µs | 0.060 µs | **1.2×** | type annotations |
295
+ | norm (loop+sqrt) | 0.052 µs | 0.058 µs | **1.1×** | type annotations |
296
+ | mathHeavy (trig+sqrt) | 0.044 µs | 0.050 µs | **1.1×** | type annotations |
297
+ | fib(20) (iter) | 0.062 µs | 0.071 µs | **1.1×** | type annotations |
298
+ | cfLookAt (ctor) | 0.087 µs | 0.082 µs | ~1× | C++ floor — no Luau work |
299
+ | cfChain (mul+angles) | 0.102 µs | 0.092 µs | ~1× | C++ floor — no Luau work |
300
+ | serviceWork (GetService ×2) | 0.243 µs | 0.481 µs | **2.0×** | GetService hoisting |
301
+ | multiSvc (GetService ×3) | 0.154 µs | 0.505 µs | **3.3×** | GetService hoisting |
302
+ | cameraWork (prop chain) | 0.185 µs | 0.218 µs | **1.2×** | `camera.CFrame` hoisted (2 reads → 1) |
303
+ | formatStats (template) | 0.191 µs | 0.187 µs | ~1× | string — no arithmetic |
304
+ | buildKey (template) | 0.085 µs | 0.079 µs | ~1× | |
307
305
 
308
306
  ### What the transformer cannot help with
309
307
 
310
- - **Pure engine API calls** — `CFrame.lookAt`, `CFrame.Angles`, `CFrame` multiplication all execute immediately in C++. `--!native` cannot speed up code that is already running natively. `cfLookAt` and `cfChain` show 1.0× for this reason.
311
- - **Single-access properties** — the cache pass only hoists when a property is read 2+ times in the same function. One read has nothing to eliminate.
308
+ - **Pure engine API calls** — `CFrame.lookAt`, `CFrame.Angles`, `CFrame` multiplication execute immediately in C++. `--!native` cannot speed up code that is already running natively.
309
+ - **Single-access properties** — the cache pass only hoists when a property is read 2+ times in the same function.
312
310
  - **String-heavy functions** — Luau string operations are not meaningfully accelerated by the native compiler.
313
311
 
314
312
  ---
@@ -316,8 +314,10 @@ Measured in Roblox Studio server context. 100,000 iterations per benchmark (10,0
316
314
  ## Development
317
315
 
318
316
  ```bash
319
- npm run build # build the transformer
320
- npm run bench:rbxlx # build both versions + produce bench/benchmark.rbxlx
317
+ npm run build # compile the transformer (tsc → dist/)
318
+ npm run bench:build # build the benchmark suite with transformer applied
319
+ npm run bench:build:baseline # build the baseline suite (no transformer)
320
+ npm run bench:rbxlx # produce bench/benchmark.rbxlx (both suites in one place)
321
321
  ```
322
322
 
323
- Open `bench/benchmark.rbxlx` in Roblox Studio and run the server. The optimized suite prints first, then the baseline suite, sequentially in the output window.
323
+ Open `bench/benchmark.rbxlx` in Roblox Studio and run the server. The optimized suite prints first, then the baseline suite.
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export interface PluginConfig {
2
2
  optimize?: boolean;
3
+ strict?: boolean;
3
4
  hoist?: boolean;
4
5
  }
package/dist/index.js CHANGED
@@ -3,18 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = default_1;
4
4
  const native_1 = require("./passes/native");
5
5
  const cache_1 = require("./passes/cache");
6
- const loops_1 = require("./passes/loops");
7
6
  const annotate_1 = require("./passes/annotate");
8
7
  function default_1(program, config = {}, { ts }) {
9
- const { optimize = true, hoist = true } = config;
8
+ const { optimize = true, strict = true, hoist = true } = config;
10
9
  return (ctx) => (sourceFile) => {
11
10
  (0, annotate_1.annotatePass)(ts, program, sourceFile);
12
11
  let result = sourceFile;
13
12
  if (hoist)
14
13
  result = (0, cache_1.cachePass)(ts, program, ctx, result);
15
- result = (0, loops_1.loopsPass)(ts, program, ctx, result);
16
- if (optimize)
17
- result = (0, native_1.nativePass)(ts, ctx, result);
14
+ if (optimize || strict)
15
+ result = (0, native_1.nativePass)(ts, ctx, result, optimize, strict);
18
16
  return result;
19
17
  };
20
18
  }