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.
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.annotatePass = annotatePass;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- // Luau type names for TypeScript type strings rotor knows about
40
39
  const LUAU_TYPE = {
41
40
  number: "number",
42
41
  string: "string",
@@ -58,7 +57,6 @@ const LUAU_TYPE = {
58
57
  Region3: "Region3",
59
58
  Ray: "Ray",
60
59
  buffer: "buffer",
61
- // Roblox service types
62
60
  Instance: "Instance",
63
61
  BasePart: "BasePart",
64
62
  Part: "Part",
@@ -68,21 +66,9 @@ const LUAU_TYPE = {
68
66
  Workspace: "Workspace",
69
67
  RunService: "RunService",
70
68
  Players: "Players",
71
- // Luau numeric arrays — kept as {number} in Luau
72
69
  };
73
- // Global sidecar: outLuauPath → list of function annotations for that file
74
70
  const sidecar = new Map();
75
71
  let hooked = false;
76
- function luauTypeForTsType(ts, checker, node) {
77
- if (node.type) {
78
- const mapped = mapTypeNode(ts, node.type);
79
- if (mapped)
80
- return mapped;
81
- }
82
- const type = checker.getTypeAtLocation(node);
83
- const name = checker.typeToString(type);
84
- return LUAU_TYPE[name] ?? null;
85
- }
86
72
  function mapTypeNode(ts, typeNode) {
87
73
  if (ts.isTypeReferenceNode(typeNode)) {
88
74
  const name = ts.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : null;
@@ -90,7 +76,6 @@ function mapTypeNode(ts, typeNode) {
90
76
  return null;
91
77
  if (LUAU_TYPE[name])
92
78
  return LUAU_TYPE[name];
93
- // Array<T> → {T}
94
79
  if ((name === "Array" || name === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
95
80
  const inner = mapTypeNode(ts, typeNode.typeArguments[0]);
96
81
  return inner ? `{${inner}}` : "{any}";
@@ -110,22 +95,40 @@ function mapTypeNode(ts, typeNode) {
110
95
  return kw[typeNode.kind];
111
96
  return null;
112
97
  }
98
+ function luauTypeForParam(ts, checker, node) {
99
+ if (node.type) {
100
+ const mapped = mapTypeNode(ts, node.type);
101
+ if (mapped)
102
+ return mapped;
103
+ }
104
+ const name = checker.typeToString(checker.getTypeAtLocation(node));
105
+ return LUAU_TYPE[name] ?? null;
106
+ }
107
+ function luauTypeForReturn(ts, checker, node) {
108
+ if (node.type) {
109
+ const mapped = mapTypeNode(ts, node.type);
110
+ if (mapped)
111
+ return mapped;
112
+ }
113
+ const sig = checker.getSignatureFromDeclaration(node);
114
+ if (!sig)
115
+ return null;
116
+ const ret = checker.getReturnTypeOfSignature(sig);
117
+ const name = checker.typeToString(ret);
118
+ return LUAU_TYPE[name] ?? null;
119
+ }
113
120
  function outPathForSource(sourceFile, program) {
114
121
  const options = program.getCompilerOptions();
115
122
  const outDir = options.outDir;
116
123
  if (!outDir)
117
124
  return null;
118
- // Compute rootDir: explicit option or the common root of all source files
119
- const rootDir = options.rootDir
120
- ?? commonRoot(program.getRootFileNames());
125
+ const rootDir = options.rootDir ?? commonRoot(program.getRootFileNames());
121
126
  if (!rootDir)
122
127
  return null;
123
128
  const rel = path.relative(rootDir, sourceFile.fileName);
124
129
  if (rel.startsWith(".."))
125
130
  return null;
126
- // Change .ts / .tsx extension to .luau
127
- const luauRel = rel.replace(/\.tsx?$/, ".luau");
128
- return path.join(outDir, luauRel);
131
+ return path.join(outDir, rel.replace(/\.tsx?$/, ".luau"));
129
132
  }
130
133
  function commonRoot(files) {
131
134
  if (files.length === 0)
@@ -142,45 +145,239 @@ function commonRoot(files) {
142
145
  return root.join(path.sep) || undefined;
143
146
  }
144
147
  function collectAnnotations(ts, checker, sourceFile, outPath) {
145
- const fileMap = sidecar.get(outPath) ?? new Map();
146
- sidecar.set(outPath, fileMap);
148
+ const entry = sidecar.get(outPath) ?? { fns: new Map(), consts: new Set() };
149
+ sidecar.set(outPath, entry);
147
150
  function visit(node) {
148
151
  if (ts.isFunctionDeclaration(node) && node.name) {
149
- const fnName = node.name.text;
150
- const params = node.parameters.map(p => luauTypeForTsType(ts, checker, p));
151
- if (params.some(p => p !== null)) {
152
- fileMap.set(fnName, { params });
152
+ const params = node.parameters.map(p => luauTypeForParam(ts, checker, p));
153
+ const ret = luauTypeForReturn(ts, checker, node);
154
+ if (params.some(p => p !== null) || ret !== null) {
155
+ entry.fns.set(node.name.text, { params, ret });
156
+ }
157
+ }
158
+ if (ts.isVariableStatement(node)) {
159
+ const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
160
+ if (isConst) {
161
+ for (const decl of node.declarationList.declarations) {
162
+ if (ts.isIdentifier(decl.name)) {
163
+ entry.consts.add(decl.name.text);
164
+ }
165
+ }
153
166
  }
154
167
  }
155
168
  ts.forEachChild(node, visit);
156
169
  }
157
170
  visit(sourceFile);
158
171
  }
159
- function injectAnnotations(luauPath, fileMap) {
172
+ function byLengthDesc(a, b) {
173
+ return b.length - a.length;
174
+ }
175
+ function organizePreamble(src) {
176
+ const lines = src.split("\n");
177
+ let i = 0;
178
+ // Collect --! directives separately from other leading comments
179
+ const shebang = [];
180
+ const header = [];
181
+ while (i < lines.length && lines[i].startsWith("--")) {
182
+ if (lines[i].startsWith("--!")) {
183
+ shebang.push(lines[i++]);
184
+ }
185
+ else {
186
+ header.push(lines[i++]);
187
+ }
188
+ }
189
+ shebang.sort(byLengthDesc);
190
+ const services = [];
191
+ const runtime = [];
192
+ const importGroups = [];
193
+ const bindings = [];
194
+ let pendingLabel = null;
195
+ let currentImports = [];
196
+ function flushImports() {
197
+ if (currentImports.length > 0) {
198
+ importGroups.push({ label: pendingLabel ?? "-- Imports", lines: [...currentImports] });
199
+ currentImports = [];
200
+ }
201
+ pendingLabel = null;
202
+ }
203
+ while (i < lines.length) {
204
+ const line = lines[i];
205
+ if (line.trim() === "") {
206
+ // Blank line = end of current import group
207
+ flushImports();
208
+ i++;
209
+ continue;
210
+ }
211
+ if (/^--!/.test(line)) {
212
+ // Rotor can emit --!native after preamble locals — hoist it up
213
+ shebang.push(line);
214
+ shebang.sort(byLengthDesc);
215
+ i++;
216
+ }
217
+ else if (/^--/.test(line)) {
218
+ // User comment becomes the label for the next import group
219
+ flushImports();
220
+ pendingLabel = line;
221
+ i++;
222
+ }
223
+ else if (/^local \w+ = game:GetService\(/.test(line)) {
224
+ flushImports();
225
+ services.push(line);
226
+ i++;
227
+ }
228
+ else if (/^local \w+ = require\(/.test(line)) {
229
+ flushImports();
230
+ runtime.push(line);
231
+ i++;
232
+ }
233
+ else if (/^local \w+ = TS\.import\(/.test(line)) {
234
+ currentImports.push(line);
235
+ i++;
236
+ }
237
+ else if (/^local \w+ = \w+[\.\[]/.test(line) && !/^local function/.test(line)) {
238
+ flushImports();
239
+ bindings.push(line);
240
+ i++;
241
+ }
242
+ else {
243
+ break;
244
+ }
245
+ }
246
+ flushImports();
247
+ services.sort(byLengthDesc);
248
+ bindings.sort(byLengthDesc);
249
+ const out = [...shebang];
250
+ if (header.length > 0)
251
+ out.push("", ...header);
252
+ if (services.length > 0)
253
+ out.push("", "-- Services", ...services);
254
+ if (runtime.length > 0)
255
+ out.push("", "-- Runtime", ...runtime);
256
+ for (const group of importGroups) {
257
+ group.lines.sort(byLengthDesc);
258
+ out.push("", group.label, ...group.lines);
259
+ }
260
+ if (bindings.length > 0)
261
+ out.push("", "-- Bindings", ...bindings);
262
+ if (i < lines.length)
263
+ out.push("", ...lines.slice(i));
264
+ return out.join("\n");
265
+ }
266
+ function hoistGetService(src) {
267
+ // Count occurrences of each game:GetService("X") call
268
+ const re = /game:GetService\("([^"]+)"\)/g;
269
+ const counts = new Map();
270
+ for (const m of src.matchAll(re)) {
271
+ counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
272
+ }
273
+ const toHoist = [...counts.entries()].filter(([, n]) => n >= 2).map(([svc]) => svc);
274
+ if (toHoist.length === 0)
275
+ return src;
276
+ // Build locals and replace
277
+ const decls = toHoist
278
+ .map(svc => `local _${svc} = game:GetService("${svc}")`)
279
+ .join("\n");
280
+ for (const svc of toHoist) {
281
+ src = src.split(`game:GetService("${svc}")`).join(`_${svc}`);
282
+ }
283
+ // Insert after any leading --! directives and the rotor header comment
284
+ const insertAt = src.search(/^(?!--[!\s]|--\s*Compiled)/m);
285
+ if (insertAt === -1)
286
+ return decls + "\n" + src;
287
+ return src.slice(0, insertAt) + decls + "\n" + src.slice(insertAt);
288
+ }
289
+ function addSpacing(src) {
290
+ const lines = src.split("\n");
291
+ const out = [];
292
+ for (let i = 0; i < lines.length; i++) {
293
+ const line = lines[i];
294
+ const trimmed = line.trim();
295
+ const prevOut = out.length > 0 ? out[out.length - 1] : "";
296
+ const prevTrimmed = prevOut.trim();
297
+ const alreadyBlank = prevTrimmed === "";
298
+ if (!alreadyBlank) {
299
+ // Blank before top-level local function
300
+ if (/^local function /.test(trimmed)) {
301
+ out.push("");
302
+ }
303
+ // Blank before return when it's not the first statement in its block
304
+ else if (/^return\b/.test(trimmed) &&
305
+ !/\b(then|do|repeat)$/.test(prevTrimmed) &&
306
+ !/function\s*\([^)]*\)$/.test(prevTrimmed) &&
307
+ !/^local function /.test(prevTrimmed)) {
308
+ out.push("");
309
+ }
310
+ // Blank before a block starter (do/while/for/if/repeat) when prev is local/const
311
+ else if (/^(do\b|while |for |if |repeat\b)/.test(trimmed) && /^(local |const )/.test(prevTrimmed)) {
312
+ out.push("");
313
+ }
314
+ // Blank on const → local transition
315
+ else if (/^local /.test(trimmed) && /^const /.test(prevTrimmed)) {
316
+ out.push("");
317
+ }
318
+ }
319
+ out.push(line);
320
+ // Blank after `end` when next non-blank line is not end/else/elseif/until
321
+ if (trimmed === "end") {
322
+ const next = lines[i + 1]?.trim() ?? "";
323
+ if (next !== "" && !/^(end\b|else\b|elseif\b|until\b)/.test(next)) {
324
+ out.push("");
325
+ }
326
+ }
327
+ }
328
+ return out.join("\n");
329
+ }
330
+ function injectAnnotations(luauPath, entry) {
160
331
  if (!fs.existsSync(luauPath))
161
332
  return;
162
333
  let src = fs.readFileSync(luauPath, "utf8");
163
334
  let changed = false;
164
- for (const [fnName, ann] of fileMap) {
165
- if (ann.params.every(p => p === null))
335
+ // Inject param + return type annotations
336
+ for (const [fnName, ann] of entry.fns) {
337
+ if (ann.params.every(p => p === null) && ann.ret === null)
166
338
  continue;
167
- // Match: local function fnName(a, b, c)
168
- // Captures the param list so we can replace individual names
169
- const re = new RegExp(`(local function ${escapeRegex(fnName)}\\()([^)]*)(\\.\\.\\.\\))?\\)`);
170
- src = src.replace(re, (_match, open, rawParams, vararg) => {
339
+ const re = new RegExp(`(local function ${escapeRegex(fnName)}\\()([^)]*)(\\.\\.\\.)?(\\))`);
340
+ src = src.replace(re, (_m, open, rawParams, vararg, close) => {
171
341
  const names = rawParams.split(",").map((s) => s.trim()).filter(Boolean);
172
342
  const annotated = names.map((name, i) => {
173
- // strip any existing annotation
174
343
  const bare = name.split(":")[0].trim();
175
344
  const typ = ann.params[i];
176
345
  return typ ? `${bare}: ${typ}` : bare;
177
346
  });
178
347
  if (vararg)
179
348
  annotated.push("...");
349
+ const retSuffix = ann.ret ? `: ${ann.ret}` : "";
180
350
  changed = true;
181
- return `${open}${annotated.join(", ")})`;
351
+ return `${open}${annotated.join(", ")}${close}${retSuffix}`;
182
352
  });
183
353
  }
354
+ // Replace local → const for TypeScript const declarations
355
+ for (const name of entry.consts) {
356
+ const re = new RegExp(`^(\\t*)local (${escapeRegex(name)}) =`, "m");
357
+ const next = src.replace(re, `$1const $2 =`);
358
+ if (next !== src) {
359
+ src = next;
360
+ changed = true;
361
+ }
362
+ }
363
+ // Hoist any repeated game:GetService() calls injected by the compiler
364
+ const hoisted = hoistGetService(src);
365
+ if (hoisted !== src) {
366
+ src = hoisted;
367
+ changed = true;
368
+ }
369
+ // Organize preamble into labeled sections
370
+ const organized = organizePreamble(src);
371
+ if (organized !== src) {
372
+ src = organized;
373
+ changed = true;
374
+ }
375
+ // Add blank lines between top-level blocks for readability
376
+ const spaced = addSpacing(src);
377
+ if (spaced !== src) {
378
+ src = spaced;
379
+ changed = true;
380
+ }
184
381
  if (changed)
185
382
  fs.writeFileSync(luauPath, src, "utf8");
186
383
  }
@@ -198,16 +395,14 @@ function installWatcher(outDir) {
198
395
  const full = path.join(outDir, filename);
199
396
  if (seen.has(full))
200
397
  return;
201
- const fileMap = sidecar.get(full);
202
- if (!fileMap)
398
+ const entry = sidecar.get(full);
399
+ if (!entry)
203
400
  return;
204
401
  seen.add(full);
205
- if (fileMap.size > 0) {
206
- try {
207
- injectAnnotations(full, fileMap);
208
- }
209
- catch { /* ignore */ }
402
+ try {
403
+ injectAnnotations(full, entry);
210
404
  }
405
+ catch { /* ignore */ }
211
406
  });
212
407
  watcher.unref();
213
408
  }
@@ -215,8 +410,6 @@ function annotatePass(ts, program, sourceFile) {
215
410
  const outPath = outPathForSource(sourceFile, program);
216
411
  if (!outPath)
217
412
  return;
218
- const checker = program.getTypeChecker();
219
- collectAnnotations(ts, checker, sourceFile, outPath);
220
- const outDir = program.getCompilerOptions().outDir;
221
- installWatcher(outDir);
413
+ collectAnnotations(ts, program.getTypeChecker(), sourceFile, outPath);
414
+ installWatcher(program.getCompilerOptions().outDir);
222
415
  }
@@ -1,2 +1,2 @@
1
1
  import type ts from "typescript";
2
- export declare function nativePass(ts: typeof import("typescript"), ctx: ts.TransformationContext, sourceFile: ts.SourceFile): ts.SourceFile;
2
+ export declare function nativePass(ts: typeof import("typescript"), ctx: ts.TransformationContext, sourceFile: ts.SourceFile, optimize: boolean, strict: boolean): ts.SourceFile;
@@ -2,10 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.nativePass = nativePass;
4
4
  const util_1 = require("../util");
5
- function nativePass(ts, ctx, sourceFile) {
6
- if ((0, util_1.hasOptimizeDirective)(sourceFile))
7
- return sourceFile;
5
+ function nativePass(ts, ctx, sourceFile, optimize, strict) {
8
6
  const factory = ctx.factory;
9
- const optimize = ts.addSyntheticLeadingComment(factory.createNotEmittedStatement(sourceFile), ts.SyntaxKind.SingleLineCommentTrivia, "!optimize 2", true);
10
- return factory.updateSourceFile(sourceFile, [optimize, ...Array.from(sourceFile.statements)]);
7
+ const prepend = [];
8
+ if (strict && !(0, util_1.hasStrictDirective)(sourceFile)) {
9
+ prepend.push(ts.addSyntheticLeadingComment(factory.createNotEmittedStatement(sourceFile), ts.SyntaxKind.SingleLineCommentTrivia, "!strict", true));
10
+ }
11
+ if (optimize && !(0, util_1.hasOptimizeDirective)(sourceFile)) {
12
+ prepend.push(ts.addSyntheticLeadingComment(factory.createNotEmittedStatement(sourceFile), ts.SyntaxKind.SingleLineCommentTrivia, "!optimize 2", true));
13
+ }
14
+ if (prepend.length === 0)
15
+ return sourceFile;
16
+ return factory.updateSourceFile(sourceFile, [...prepend, ...Array.from(sourceFile.statements)]);
11
17
  }
package/dist/util.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type ts from "typescript";
2
2
  export declare function hasOptimizeDirective(sourceFile: ts.SourceFile): boolean;
3
+ export declare function hasStrictDirective(sourceFile: ts.SourceFile): boolean;
3
4
  export declare function chainKey(ts: typeof import("typescript"), node: ts.Expression): string | undefined;
4
5
  export declare function walk(ts: typeof import("typescript"), node: ts.Node, visitor: (n: ts.Node) => void): void;
5
6
  export declare function isAssignmentTarget(ts: typeof import("typescript"), node: ts.Node): boolean;
package/dist/util.js CHANGED
@@ -1,12 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.hasOptimizeDirective = hasOptimizeDirective;
4
+ exports.hasStrictDirective = hasStrictDirective;
4
5
  exports.chainKey = chainKey;
5
6
  exports.walk = walk;
6
7
  exports.isAssignmentTarget = isAssignmentTarget;
7
8
  function hasOptimizeDirective(sourceFile) {
8
9
  return /^--!optimize\b/m.test(sourceFile.text) || /^\/\/!optimize\b/m.test(sourceFile.text);
9
10
  }
11
+ function hasStrictDirective(sourceFile) {
12
+ return /^--!strict\b/m.test(sourceFile.text) || /^\/\/!strict\b/m.test(sourceFile.text);
13
+ }
10
14
  function chainKey(ts, node) {
11
15
  if (ts.isIdentifier(node))
12
16
  return node.text;
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "rbxts-transform-boost",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "roblox-ts transformer: automatic --!native, GetService hoisting, property chain caching, loop bounds hoisting, and Luau type annotation injection",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
7
11
  "scripts": {
8
12
  "build": "tsc",
9
- "dev": "tsc --watch",
10
13
  "bench:build": "npm run build && cd bench && ~/.rokit/bin/rotor build",
11
14
  "bench:build:baseline": "cd bench && ~/.rokit/bin/rotor build -p tsconfig.notransform.json -i out/include",
12
- "bench:dev": "cd bench && ~/.rokit/bin/rotor dev",
13
- "bench:rbxlx": "npm run bench:build && npm run bench:build:baseline && cd bench && ~/.rokit/bin/rojo build benchmark.project.json -o benchmark.rbxlx"
15
+ "bench:rbxlx": "npm run bench:build && npm run bench:build:baseline && cd bench && ~/.rokit/bin/rojo build default.project.json -o benchmark.rbxlx"
14
16
  },
15
17
  "keywords": [
16
18
  "roblox-ts",
@@ -24,6 +26,7 @@
24
26
  "typescript": ">=5.0.0"
25
27
  },
26
28
  "devDependencies": {
27
- "@types/node": "^26.0.0"
29
+ "@types/node": "^26.0.0",
30
+ "typescript": "^5.9.3"
28
31
  }
29
32
  }
@@ -1,26 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
- workflow_dispatch:
8
-
9
- jobs:
10
- publish:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v4
14
-
15
- - uses: actions/setup-node@v4
16
- with:
17
- node-version: 20
18
- registry-url: https://registry.npmjs.org
19
-
20
- - run: npm ci
21
-
22
- - run: npm run build
23
-
24
- - run: npm publish --access public
25
- env:
26
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,20 +0,0 @@
1
- {
2
- "name": "perf-benchmark",
3
- "emitLegacyScripts": false,
4
- "tree": {
5
- "$className": "DataModel",
6
- "ServerScriptService": {
7
- "$className": "ServerScriptService",
8
- "$path": "out/src/server"
9
- },
10
- "ReplicatedStorage": {
11
- "$className": "ReplicatedStorage",
12
- "shared": {
13
- "$path": "out/src/shared"
14
- },
15
- "include": {
16
- "$path": "out/include"
17
- }
18
- }
19
- }
20
- }
@@ -1,59 +0,0 @@
1
- {
2
- "name": "rbxts-transform-perf-test",
3
- "lockfileVersion": 3,
4
- "requires": true,
5
- "packages": {
6
- "": {
7
- "name": "rbxts-transform-perf-test",
8
- "devDependencies": {
9
- "@rbxts/compiler-types": "^3.0.0-types.0",
10
- "@rbxts/types": "^1.0.908",
11
- "rbxts-transform-boost": "file:../",
12
- "typescript": "=5.5.3"
13
- }
14
- },
15
- "..": {
16
- "name": "rbxts-transform-boost",
17
- "version": "0.1.0",
18
- "dev": true,
19
- "devDependencies": {
20
- "@types/node": "^26.0.0"
21
- },
22
- "peerDependencies": {
23
- "typescript": ">=5.0.0"
24
- }
25
- },
26
- "node_modules/@rbxts/compiler-types": {
27
- "version": "3.0.0-types.0",
28
- "resolved": "https://registry.npmjs.org/@rbxts/compiler-types/-/compiler-types-3.0.0-types.0.tgz",
29
- "integrity": "sha512-VGOHJPoL7+56NTatMGqQj3K7xWuzEV+aP4QD5vZiHu+bcff3kiTmtoadaF6NkJrmwfFAvbsd4Dg764ZjWNceag==",
30
- "dev": true,
31
- "license": "MIT"
32
- },
33
- "node_modules/@rbxts/types": {
34
- "version": "1.0.928",
35
- "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.928.tgz",
36
- "integrity": "sha512-GzmzBNn8fyn0ed9kVQkXUbBXpUg9Gfyf9AZJKT51VGqyrf4w6J0X72eSSGxt6YN2BMHNbqhzHCBn/fM8bdqMTg==",
37
- "dev": true,
38
- "license": "MIT"
39
- },
40
- "node_modules/rbxts-transform-boost": {
41
- "resolved": "..",
42
- "link": true
43
- },
44
- "node_modules/typescript": {
45
- "version": "5.5.3",
46
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
47
- "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
48
- "dev": true,
49
- "license": "Apache-2.0",
50
- "bin": {
51
- "tsc": "bin/tsc",
52
- "tsserver": "bin/tsserver"
53
- },
54
- "engines": {
55
- "node": ">=14.17"
56
- }
57
- }
58
- }
59
- }
@@ -1,14 +0,0 @@
1
- {
2
- "name": "rbxts-transform-perf-test",
3
- "private": true,
4
- "scripts": {
5
- "compile": "lumen src -o out/src -i out/include -p default.project.json",
6
- "watch": "lumen src -o out/src -i out/include -p default.project.json --watch"
7
- },
8
- "devDependencies": {
9
- "@rbxts/compiler-types": "^3.0.0-types.0",
10
- "@rbxts/types": "^1.0.908",
11
- "typescript": "=5.5.3",
12
- "rbxts-transform-boost": "file:../"
13
- }
14
- }
@@ -1,57 +0,0 @@
1
- import * as opt from "../shared/fns";
2
- import * as base from "../shared/fns-bare";
3
-
4
- function bench(label: string, n: number, fn: () => void): void {
5
- task.wait(0.05);
6
- const t0 = os.clock();
7
- for (let i = 0; i < n; i++) fn();
8
- const elapsed = os.clock() - t0;
9
- print(` ${label}: ${string.format("%.3f", (elapsed / n) * 1e6)} us/iter`);
10
- }
11
-
12
- const N = 100000;
13
- const NS = 10000;
14
-
15
- const pos = new Vector3(1, 2, 3);
16
- const vel = new Vector3(0, 1, 0);
17
- const acc = new Vector3(0, -9.8, 0);
18
- const vecA = new Vector3(1, 0, 0);
19
- const vecB = new Vector3(0, 1, 0);
20
- const eye = new Vector3(0, 5, 10);
21
- const tgt = new Vector3(0, 0, 0);
22
- const cf = CFrame.lookAt(eye, tgt);
23
- const buf = buffer.create(256);
24
- const vals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
25
- const wts = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
26
- const cam = game.GetService("Workspace").CurrentCamera!;
27
-
28
- function runSuite(fns: typeof opt): void {
29
- bench("integrate (verlet)", N, () => fns.integrate(pos, vel, acc, 1 / 60));
30
- bench("dot (V3 manual)", N, () => fns.dot(vecA, vecB));
31
- bench("cross (V3 manual)", N, () => fns.cross(vecA, vecB));
32
- bench("lerpVec3 (V3 manual)", N, () => fns.lerpVec3(pos, eye, 0.5));
33
- bench("encodeFixed (buf+math)", N, () => fns.encodeFixed(buf, 0, 3.14, 100));
34
- bench("encodePacket(3x fixed)", N, () => fns.encodePacket(buf, 1.1, 2.2, 3.3, 100));
35
- bench("sumWeighted (loop)", N, () => fns.sumWeighted(vals, wts));
36
- bench("dotProduct (loop)", N, () => fns.dotProduct(vals, vals));
37
- bench("norm (loop+sqrt)", N, () => fns.norm(vals));
38
- bench("mathHeavy (trig+sqrt)", N, () => fns.mathHeavy(1.23, 4.56));
39
- bench("fib(20) (iter)", N, () => fns.fib(20));
40
- bench("cfLookAt (ctor)", NS, () => fns.cfLookAt(eye, tgt));
41
- bench("cfChain (mul+angles)", N, () => fns.cfChain(cf, 0.016));
42
- bench("serviceWork (GetService x2)", N, () => fns.serviceWork());
43
- bench("multiSvc (GetService x3)", N, () => fns.multiService());
44
- bench("cameraWork (prop chain)", N, () => fns.cameraWork(cam));
45
- bench("formatStats (template)", N, () => fns.formatStats("speed", 9.81, "m/s"));
46
- bench("buildKey (template)", N, () => fns.buildKey("player", 42, "data"));
47
- }
48
-
49
- print("\n=== optimized (--!native + transformer) ===");
50
- runSuite(opt);
51
- print("===========================================\n");
52
-
53
- task.wait(1);
54
-
55
- print("\n=== baseline (plain rotor, no transformer) ===");
56
- runSuite(base);
57
- print("==============================================\n");