cluaupp 1.5.0 → 1.5.1
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/CHANGELOG.md +47 -0
- package/README.md +10 -5
- package/api/CHANGELOG.md +3 -0
- package/api/chat.js +74 -0
- package/api/fixtures/roblox/README.md +6 -0
- package/api/fixtures/roblox/aux/sample.d.luau +6 -0
- package/api/fixtures/roblox/mini/Mini-API-Dump.json +72 -0
- package/api/generated/bench-latest.json +39 -0
- package/api/generated/capability-profile.json +76 -0
- package/api/generated/lsp-index.json +1 -0
- package/api/generated/luau/classes.d.luau +11221 -0
- package/api/generated/luau/datatypes.d.luau +327 -0
- package/api/generated/luau/enums.d.luau +4918 -0
- package/api/generated/luau/globals.d.luau +12 -0
- package/api/manifests/roblox-target.manifest.json +22 -0
- package/api/normalized/roblox-target.profile.json +237461 -0
- package/api/overrides/classes.json +8 -0
- package/api/overrides/compatibility.json +9 -0
- package/api/overrides/datatypes.json +1579 -0
- package/api/overrides/enums.json +8 -0
- package/api/overrides/globals.json +8 -0
- package/api/overrides/type-mappings.json +9 -0
- package/api/roblox-api.lock.json +9 -0
- package/data/Mini-API-Dump.json +1 -0
- package/editors/vscode/README.md +12 -10
- package/editors/vscode/extension.js +82 -0
- package/editors/vscode/icons/axiom.svg +4 -0
- package/editors/vscode/icons/bloom.svg +5 -0
- package/editors/vscode/icons/helm.svg +4 -0
- package/editors/vscode/icons/hive.svg +5 -0
- package/editors/vscode/icons/mint.svg +4 -0
- package/editors/vscode/icons/shift.svg +6 -0
- package/editors/vscode/language-configuration-schema.json +24 -0
- package/editors/vscode/package.json +172 -12
- package/editors/vscode/snippets/axiom.json +12 -0
- package/editors/vscode/snippets/bloom.json +12 -0
- package/editors/vscode/snippets/helm.json +12 -0
- package/editors/vscode/snippets/hive.json +12 -0
- package/editors/vscode/snippets/mint.json +12 -0
- package/editors/vscode/snippets/shift.json +17 -0
- package/editors/vscode/syntaxes/axiom.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/bloom.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/helm.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/hive.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/mint.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/schema.tmLanguage.json +89 -0
- package/editors/vscode/syntaxes/shift.tmLanguage.json +5 -0
- package/examples/game/.cluaupp/compile-cache/ReplicatedStorage__Shared__Constants__Datas__TemplateData.clh.json +1 -0
- package/examples/game/.cluaupp/compile-cache/ServerScriptService__Boot__DataBoot.server.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/ServerScriptService__Configuration__ServerConfig.clp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/ServerScriptService__Handlers__Guard.server.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/ServerScriptService__Handlers__PlayerHandler.server.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/StarterPlayer__StarterCharacterScripts__Character__CharacterReady.client.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/StarterPlayer__StarterPlayerScripts__Controllers__DataController.client.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/index.json +12 -0
- package/examples/game/.cluaupp/datamodel-profile.json +94 -0
- package/examples/game/.vscode/settings.json +12 -1
- package/examples/game/include/clpp/datatypes.clh +337 -373
- package/examples/game/include/clpp/generated/datamodel.clh +25 -0
- package/examples/game/include/clpp/generated/enums.clh +1 -1
- package/examples/game/include/clpp/generated/instances.clh +41135 -41064
- package/examples/game/include/clpp/libs/axiom.clh +1 -1
- package/examples/game/include/clpp/libs/math.clh +14 -0
- package/examples/game/include/clpp/libs/spring.clh +17 -0
- package/examples/game/include/clpp/libs/ward.clh +1 -0
- package/examples/game/include/clpp/roblox.clh +3 -4
- package/examples/game/libs/ArrayIndexer/init.luau +209 -0
- package/examples/game/libs/Axiom/init.luau +608 -0
- package/examples/game/libs/Bloom/init.luau +433 -0
- package/examples/game/libs/Coil/init.luau +215 -0
- package/examples/game/libs/Crest/init.luau +426 -0
- package/examples/game/libs/Echo/init.luau +417 -0
- package/examples/game/libs/Ember/init.luau +365 -0
- package/examples/game/libs/Flare/init.luau +717 -0
- package/examples/game/libs/Gleam/init.luau +729 -0
- package/examples/game/libs/Guide/init.luau +215 -0
- package/examples/game/libs/Helm/init.luau +377 -0
- package/examples/game/libs/Hive/init.luau +241 -0
- package/examples/game/libs/Keep/Client.luau +114 -0
- package/examples/game/libs/Keep/Data.luau +883 -0
- package/examples/game/libs/Keep/Engine.luau +211 -0
- package/examples/game/libs/Keep/Enum.luau +6 -0
- package/examples/game/libs/Keep/Enums.luau +10 -0
- package/examples/game/libs/Keep/Path.luau +236 -0
- package/examples/game/libs/Keep/Replicator.luau +298 -0
- package/examples/game/libs/Keep/Server.luau +251 -0
- package/examples/game/libs/Keep/Store.luau +652 -0
- package/examples/game/libs/Keep/Trade.luau +455 -0
- package/examples/game/libs/Keep/init.luau +205 -0
- package/examples/game/libs/Lens/init.luau +609 -0
- package/examples/game/libs/Mint/init.luau +279 -0
- package/examples/game/libs/Net/init.luau +156 -0
- package/examples/game/libs/Net/src/codec.luau +224 -0
- package/examples/game/libs/Occlude/init.luau +117 -0
- package/examples/game/libs/Pin/init.luau +309 -0
- package/examples/game/libs/Promise/LICENSE +21 -0
- package/examples/game/libs/Promise/_impl/LICENSE +21 -0
- package/examples/game/libs/Promise/_impl/init.lua +1991 -0
- package/examples/game/libs/Promise/init.lua +1990 -0
- package/examples/game/libs/Promise/init.luau +13 -0
- package/examples/game/libs/Promise/types.luau +46 -0
- package/examples/game/libs/Roster/init.luau +686 -0
- package/examples/game/libs/SOURCES.md +31 -0
- package/examples/game/libs/Shift/init.luau +238 -0
- package/examples/game/libs/Spark/init.luau +487 -0
- package/examples/game/libs/Stage/init.luau +178 -0
- package/examples/game/libs/Sweep/init.luau +259 -0
- package/examples/game/libs/Trace/init.luau +213 -0
- package/examples/game/libs/Ward/init.luau +211 -0
- package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau +29 -0
- package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau.map.json +13 -0
- package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau +83 -0
- package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau +18 -0
- package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau +14 -0
- package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau +15 -0
- package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau +50 -0
- package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau.map.json +13 -0
- package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau +12 -0
- package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau.map.json +13 -0
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau +16 -0
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau.map.json +13 -0
- package/examples/game/src/ReplicatedStorage/Shared/Net/Net.clh +26 -0
- package/examples/game/src/ServerScriptService/Boot/DataBoot.server.clpp +1 -1
- package/examples/game/src/ServerScriptService/Handlers/PlayerHandler.server.clpp +1 -1
- package/examples/game/stylua.toml +9 -0
- package/generated/api/build-profile.js +115 -0
- package/generated/api/cache.js +44 -0
- package/generated/api/coverage.js +32 -0
- package/generated/api/diff.js +125 -0
- package/generated/api/generators/clpp-headers.js +353 -0
- package/generated/api/generators/docs-reference.js +7 -0
- package/generated/api/generators/lsp-index.js +81 -0
- package/generated/api/generators/luau-defs.js +94 -0
- package/generated/api/ide.js +76 -0
- package/generated/api/identifier.js +47 -0
- package/generated/api/index.js +324 -0
- package/generated/api/loader.js +57 -0
- package/generated/api/merge.js +88 -0
- package/generated/api/model.js +5 -0
- package/generated/api/normalize.js +231 -0
- package/generated/api/overrides-validate.js +58 -0
- package/generated/api/perf.js +12 -0
- package/generated/api/registry.js +136 -0
- package/generated/api/release.js +44 -0
- package/generated/api/resolver.js +250 -0
- package/generated/api/sources/aux-types-diff.js +75 -0
- package/generated/api/sources/client-tracker.js +11 -0
- package/generated/api/sources/creator-docs.js +42 -0
- package/generated/api/sources/luau-types.js +34 -0
- package/generated/api/sources/mini-dump.js +17 -0
- package/generated/api/sources/studio-api.js +8 -0
- package/generated/api/sources/studio-apiv2.js +11 -0
- package/generated/api/sources/studio-full-api.js +8 -0
- package/generated/api/studio-smoke.js +80 -0
- package/generated/api/type-parse.js +49 -0
- package/generated/api/understander.js +51 -0
- package/generated/api/validate.js +135 -0
- package/generated/api-cli.js +421 -0
- package/generated/cli.js +16 -5
- package/generated/clpp/contract.js +3 -3
- package/generated/clpp/format-luau.js +236 -0
- package/generated/clpp/index.js +13 -1
- package/generated/clpp/modules.js +227 -0
- package/generated/clpp/postprocess.js +86 -5
- package/generated/clpp/rewrite-janitor.js +145 -0
- package/generated/clpp/runner.js +14 -2
- package/generated/compile.js +82 -0
- package/generated/flare/parse.js +12 -1
- package/generated/intellisense.js +57 -1
- package/generated/native/index.js +217 -16
- package/generated/target/authority-check.js +136 -0
- package/generated/target/authority-graph.js +74 -0
- package/generated/target/build-cache.js +112 -0
- package/generated/target/capabilities.js +89 -0
- package/generated/target/capability-profile.js +30 -0
- package/generated/target/component-contracts.js +60 -0
- package/generated/target/context-check.js +160 -0
- package/generated/target/datamodel-check.js +170 -0
- package/generated/target/datamodel-generate.js +74 -0
- package/generated/target/datamodel.js +233 -0
- package/generated/target/diagnostics.js +63 -0
- package/generated/target/doctor.js +152 -0
- package/generated/target/flare-version.js +58 -0
- package/generated/target/get-service.js +69 -0
- package/generated/target/lifetime-check.js +47 -0
- package/generated/target/luau-stamp.js +39 -0
- package/generated/target/optimizer-advise.js +88 -0
- package/generated/target/optimizer-apply.js +44 -0
- package/generated/target/optimizer-soa.js +254 -0
- package/generated/target/parallel-check.js +136 -0
- package/generated/target/pgo.js +148 -0
- package/generated/target/project-docs.js +69 -0
- package/generated/target/project-graph.js +71 -0
- package/generated/target/roblox.js +63 -0
- package/generated/target/security-check.js +41 -0
- package/generated/target/source-map.js +25 -0
- package/generated/target/studio-bridge.js +112 -0
- package/generated/utils/project.js +178 -8
- package/generated/utils/rojo-mapper.js +11 -3
- package/include/clpp/datatypes.clh +337 -373
- package/include/clpp/generated/enums.clh +1 -1
- package/include/clpp/generated/instances.clh +41135 -41064
- package/include/clpp/libs/axiom.clh +1 -1
- package/include/clpp/libs/hive.clh +1 -0
- package/include/clpp/libs/keep.clh +2 -0
- package/include/clpp/libs/math.clh +14 -0
- package/include/clpp/libs/spring.clh +17 -0
- package/include/clpp/libs/ward.clh +1 -0
- package/include/clpp/roblox.clh +3 -4
- package/package.json +10 -3
- package/plugins/CluauppNav/Plugin.luau +207 -0
- package/plugins/CluauppNav/ProfileLogger.luau +27 -0
- package/plugins/CluauppNav/README.md +24 -0
- package/runtime/Axiom/init.luau +10 -7
- package/runtime/Coil/init.luau +8 -2
- package/runtime/Crest/init.luau +1 -0
- package/runtime/Flare/init.luau +2 -1
- package/runtime/Gleam/init.luau +15 -4
- package/runtime/Hive/init.luau +36 -14
- package/runtime/Pin/init.luau +5 -2
- package/runtime/Roster/init.luau +2 -0
- package/runtime/Spark/init.luau +3 -0
- package/runtime/Sweep/init.luau +2 -0
- package/runtime/Trace/init.luau +3 -0
- package/runtime/Ward/init.luau +38 -5
- package/scripts/postinstall-editor.js +17 -0
- package/skills/README.md +13 -0
- package/skills/clpp-hive-flare/SKILL.md +79 -0
- package/skills/clpp-language/SKILL.md +123 -0
- package/skills/clpp-language/compiler.md +89 -0
- package/skills/clpp-style/SKILL.md +95 -0
- package/src/api/build-profile.ts +138 -0
- package/src/api/cache.ts +45 -0
- package/src/api/coverage.ts +46 -0
- package/src/api/diff.ts +143 -0
- package/src/api/generators/clpp-headers.ts +377 -0
- package/src/api/generators/docs-reference.ts +11 -0
- package/src/api/generators/lsp-index.ts +106 -0
- package/src/api/generators/luau-defs.ts +98 -0
- package/src/api/ide.ts +87 -0
- package/src/api/identifier.ts +47 -0
- package/src/api/index.ts +269 -0
- package/src/api/loader.ts +60 -0
- package/src/api/merge.ts +126 -0
- package/src/api/model.ts +283 -0
- package/src/api/normalize.ts +257 -0
- package/src/api/overrides-validate.ts +58 -0
- package/src/api/perf.ts +15 -0
- package/src/api/registry.ts +162 -0
- package/src/api/release.ts +47 -0
- package/src/api/resolver.ts +255 -0
- package/src/api/sources/aux-types-diff.ts +81 -0
- package/src/api/sources/client-tracker.ts +10 -0
- package/src/api/sources/creator-docs.ts +37 -0
- package/src/api/sources/luau-types.ts +30 -0
- package/src/api/sources/mini-dump.ts +50 -0
- package/src/api/sources/studio-api.ts +7 -0
- package/src/api/sources/studio-apiv2.ts +10 -0
- package/src/api/sources/studio-full-api.ts +7 -0
- package/src/api/studio-smoke.ts +82 -0
- package/src/api/type-parse.ts +48 -0
- package/src/api/understander.ts +72 -0
- package/src/api/validate.ts +160 -0
- package/src/api-cli.ts +493 -0
- package/src/api.generated.js +37 -22
- package/src/cli.ts +21 -6
- package/src/clpp/contract.ts +34 -3
- package/src/clpp/format-luau.ts +262 -0
- package/src/clpp/index.ts +13 -2
- package/src/clpp/modules.ts +252 -0
- package/src/clpp/postprocess.ts +100 -5
- package/src/clpp/rewrite-janitor.ts +156 -0
- package/src/clpp/runner.ts +14 -2
- package/src/compile.ts +87 -0
- package/src/flare/parse.ts +14 -1
- package/src/flare/types.ts +2 -0
- package/src/intellisense.ts +58 -1
- package/src/native/index.ts +225 -16
- package/src/target/authority-check.ts +162 -0
- package/src/target/authority-graph.ts +80 -0
- package/src/target/build-cache.ts +129 -0
- package/src/target/capabilities.ts +115 -0
- package/src/target/capability-profile.ts +25 -0
- package/src/target/component-contracts.ts +64 -0
- package/src/target/context-check.ts +177 -0
- package/src/target/datamodel-check.ts +188 -0
- package/src/target/datamodel-generate.ts +78 -0
- package/src/target/datamodel.ts +261 -0
- package/src/target/diagnostics.ts +67 -0
- package/src/target/doctor.ts +171 -0
- package/src/target/flare-version.ts +61 -0
- package/src/target/get-service.ts +85 -0
- package/src/target/lifetime-check.ts +45 -0
- package/src/target/luau-stamp.ts +33 -0
- package/src/target/optimizer-advise.ts +93 -0
- package/src/target/optimizer-apply.ts +63 -0
- package/src/target/optimizer-soa.ts +270 -0
- package/src/target/parallel-check.ts +135 -0
- package/src/target/pgo.ts +157 -0
- package/src/target/project-docs.ts +68 -0
- package/src/target/project-graph.ts +75 -0
- package/src/target/roblox.ts +78 -0
- package/src/target/security-check.ts +40 -0
- package/src/target/source-map.ts +36 -0
- package/src/target/studio-bridge.ts +140 -0
- package/src/types.ts +14 -0
- package/src/utils/project.ts +183 -9
- package/src/utils/rojo-mapper.ts +12 -3
- package/stylua.toml +9 -0
- package/templates/game/.vscode/settings.json +7 -1
- package/templates/game/include/clpp/datatypes.clh +337 -373
- package/templates/game/include/clpp/generated/enums.clh +1 -1
- package/templates/game/include/clpp/generated/instances.clh +41135 -41064
- package/templates/game/include/clpp/libs/axiom.clh +1 -1
- package/templates/game/include/clpp/libs/ward.clh +1 -0
- package/templates/game/include/clpp/roblox.clh +3 -4
- package/templates/game/src/ServerScriptService/Boot/DataBoot.server.clpp +1 -1
- package/templates/game/src/ServerScriptService/Handlers/PlayerHandler.server.clpp +1 -1
- package/templates/game/stylua.toml +9 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamp emitted Luau with CL++ provenance and --!native when source used #pragma native.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function stampLuauFromClpp(luau: string, sourceRel: string, clppSource?: string): string {
|
|
6
|
+
let next = String(luau);
|
|
7
|
+
const stamp = `-- cluaupp-source: ${sourceRel.replace(/\\/g, "/")}`;
|
|
8
|
+
if (!next.includes("cluaupp-source:")) {
|
|
9
|
+
if (/^--!/.test(next.trimStart())) {
|
|
10
|
+
const lines = next.split(/\r?\n/);
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < lines.length && /^--!/.test(lines[i])) i++;
|
|
13
|
+
lines.splice(i, 0, stamp);
|
|
14
|
+
next = lines.join("\n");
|
|
15
|
+
} else if (next.startsWith("--")) {
|
|
16
|
+
next = `${stamp}\n${next}`;
|
|
17
|
+
} else {
|
|
18
|
+
next = `${stamp}\n${next}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const wantsNative = Boolean(clppSource && /#pragma\s+native\b/.test(clppSource));
|
|
23
|
+
if (wantsNative && !/--!native\b/.test(next)) {
|
|
24
|
+
if (/^\s*--!strict\b/m.test(next)) {
|
|
25
|
+
next = next.replace(/^\s*--!strict\b/m, "--!native\n--!strict");
|
|
26
|
+
} else if (/^\s*--!optimize\b/m.test(next)) {
|
|
27
|
+
next = `--!native\n${next}`;
|
|
28
|
+
} else {
|
|
29
|
+
next = `--!native\n${next}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluaupp Optimizer advisor (v1) — suggestions only; does not rewrite Luau yet.
|
|
3
|
+
* Philosophy: write idiomatic CL++; compiler advises best representation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type PlatformDiagnostic, lineColAt, stripCommentsKeepStrings } from "./diagnostics.js";
|
|
7
|
+
|
|
8
|
+
export interface OptimizeAdvice {
|
|
9
|
+
kind: "storage" | "native" | "parallel" | "layout";
|
|
10
|
+
target: string;
|
|
11
|
+
message: string;
|
|
12
|
+
severity: "info" | "warning";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function adviseOptimizer(source: string, fileName: string): OptimizeAdvice[] {
|
|
16
|
+
const clean = stripCommentsKeepStrings(source);
|
|
17
|
+
const advice: OptimizeAdvice[] = [];
|
|
18
|
+
|
|
19
|
+
const structRe = /struct\s+([A-Za-z_]\w*)\s*\{([^}]*)\}/g;
|
|
20
|
+
let m: RegExpExecArray | null;
|
|
21
|
+
while ((m = structRe.exec(clean))) {
|
|
22
|
+
const name = m[1];
|
|
23
|
+
const body = m[2];
|
|
24
|
+
const fields = body.split(";").map((s) => s.trim()).filter(Boolean);
|
|
25
|
+
if (fields.length < 2) continue;
|
|
26
|
+
const numeric = fields.filter((f) => /^(float|double|int|i32|f32|f64|bool)\b/.test(f) || /\bVector3\b/.test(f));
|
|
27
|
+
const ratio = numeric.length / fields.length;
|
|
28
|
+
if (ratio >= 0.75 && fields.length >= 3) {
|
|
29
|
+
advice.push({
|
|
30
|
+
kind: "storage",
|
|
31
|
+
target: name,
|
|
32
|
+
message: `Hot numeric struct ${name} (${numeric.length}/${fields.length} fields) — candidate for SoA / dense storage (Optimizer Level 1–3)`,
|
|
33
|
+
severity: "info",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const bools = fields.filter((f) => /^bool\b/.test(f));
|
|
37
|
+
if (bools.length >= 4) {
|
|
38
|
+
advice.push({
|
|
39
|
+
kind: "layout",
|
|
40
|
+
target: name,
|
|
41
|
+
message: `${name} has ${bools.length} bool fields — candidate for internal bitset packing`,
|
|
42
|
+
severity: "info",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fnRe = /(?:void|int|float|double|bool|auto)\s+([A-Za-z_]\w*)\s*\([^)]*\)\s*\{/g;
|
|
48
|
+
while ((m = fnRe.exec(clean))) {
|
|
49
|
+
const name = m[1];
|
|
50
|
+
if (name === "init") continue;
|
|
51
|
+
const start = m.index + m[0].length;
|
|
52
|
+
let depth = 1;
|
|
53
|
+
let i = start;
|
|
54
|
+
for (; i < clean.length && depth > 0; i++) {
|
|
55
|
+
if (clean[i] === "{") depth++;
|
|
56
|
+
else if (clean[i] === "}") depth--;
|
|
57
|
+
}
|
|
58
|
+
const body = clean.slice(start, i);
|
|
59
|
+
const apiHits = (body.match(/GetService|WaitForChild|Instance|FindFirstChild|FireServer|FireClient/g) || []).length;
|
|
60
|
+
const numericHits = (body.match(/\b(sin|cos|sqrt|Vector3|CFrame|math::)\b|[+\-*/]=|[+\-*/]/g) || []).length;
|
|
61
|
+
if (numericHits >= 6 && apiHits <= 1 && body.length > 40) {
|
|
62
|
+
advice.push({
|
|
63
|
+
kind: "native",
|
|
64
|
+
target: name,
|
|
65
|
+
message: `${name} looks numeric-hot / low Instance API — good @native / --!native candidate (CLUAU-PERF-001)`,
|
|
66
|
+
severity: "info",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (apiHits === 0 && /for\s*\(/.test(body) && !/shared|static/.test(body)) {
|
|
70
|
+
advice.push({
|
|
71
|
+
kind: "parallel",
|
|
72
|
+
target: name,
|
|
73
|
+
message: `${name} may be parallelizable (independent loop, no shared Instance API) — review ThreadSafety`,
|
|
74
|
+
severity: "info",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void fileName;
|
|
80
|
+
void lineColAt;
|
|
81
|
+
return advice;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function adviceAsDiagnostics(advice: OptimizeAdvice[], fileName: string): PlatformDiagnostic[] {
|
|
85
|
+
return advice.map((a) => ({
|
|
86
|
+
code: a.kind === "native" ? "CLUAU_PERF001" : a.kind === "parallel" ? "CLUAU_PAR_ADVISE" : "CLUAU_OPT001",
|
|
87
|
+
message: a.message,
|
|
88
|
+
line: 1,
|
|
89
|
+
column: 1,
|
|
90
|
+
severity: a.severity === "warning" ? "warning" : "info",
|
|
91
|
+
file: fileName,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply Optimizer transforms to CL++ source:
|
|
3
|
+
* - `#pragma native` on numeric-hot functions
|
|
4
|
+
* - `#pragma layout soa` + SoA Luau modules when --layout
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { adviseOptimizer } from "./optimizer-advise.js";
|
|
8
|
+
import type { CluauppProfile } from "./pgo.js";
|
|
9
|
+
import { rankAdvice } from "./pgo.js";
|
|
10
|
+
import { applyLayoutPragmas, writeSoaArtifacts, type SoaPlan } from "./optimizer-soa.js";
|
|
11
|
+
|
|
12
|
+
export interface ApplyResult {
|
|
13
|
+
source: string;
|
|
14
|
+
changed: boolean;
|
|
15
|
+
applied: string[];
|
|
16
|
+
soaPlans?: SoaPlan[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function applyOptimizerHints(
|
|
20
|
+
source: string,
|
|
21
|
+
fileName: string,
|
|
22
|
+
profile: CluauppProfile | null = null,
|
|
23
|
+
opts: { layout?: boolean; useBuffer?: boolean; projectRoot?: string } = {},
|
|
24
|
+
): ApplyResult {
|
|
25
|
+
const advice = rankAdvice(adviseOptimizer(source, fileName), profile);
|
|
26
|
+
const nativeTargets = new Set(advice.filter((a) => a.kind === "native").map((a) => a.target));
|
|
27
|
+
let out = source;
|
|
28
|
+
const applied: string[] = [];
|
|
29
|
+
|
|
30
|
+
for (const name of nativeTargets) {
|
|
31
|
+
const re = new RegExp(
|
|
32
|
+
`((?:^|\\n)(?:\\s*))((?:void|int|float|double|bool|auto)\\s+${name}\\s*\\()`,
|
|
33
|
+
"m",
|
|
34
|
+
);
|
|
35
|
+
const m = out.match(re);
|
|
36
|
+
if (!m) continue;
|
|
37
|
+
const before = out.slice(0, m.index! + m[1].length);
|
|
38
|
+
if (
|
|
39
|
+
/#pragma\s+native\s*$/m.test(before.slice(-80)) ||
|
|
40
|
+
/#pragma\s+native/.test(out.slice(Math.max(0, m.index! - 40), m.index!))
|
|
41
|
+
) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
out = out.replace(re, `$1#pragma native\n$1$2`);
|
|
45
|
+
applied.push(`#pragma native → ${name}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let soaPlans: SoaPlan[] | undefined;
|
|
49
|
+
if (opts.layout) {
|
|
50
|
+
const layout = applyLayoutPragmas(out, fileName, profile);
|
|
51
|
+
out = layout.source;
|
|
52
|
+
applied.push(...layout.applied);
|
|
53
|
+
soaPlans = layout.plans;
|
|
54
|
+
if (opts.projectRoot && layout.plans.length) {
|
|
55
|
+
const files = writeSoaArtifacts(opts.projectRoot, layout.plans, { useBuffer: opts.useBuffer });
|
|
56
|
+
for (const f of files) {
|
|
57
|
+
applied.push(`SoA emit → ${f.replace(/\\/g, "/")}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { source: out, changed: applied.length > 0 || out !== source, applied, soaPlans };
|
|
63
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in SoA / dense storage codegen.
|
|
3
|
+
* Triggered by `#pragma layout soa StructName` (or optimize --apply --layout).
|
|
4
|
+
* Emits a Luau ModuleScript companion — does not silently rewrite gameplay structs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { adviseOptimizer } from "./optimizer-advise.js";
|
|
10
|
+
import type { CluauppProfile } from "./pgo.js";
|
|
11
|
+
import { rankAdvice } from "./pgo.js";
|
|
12
|
+
|
|
13
|
+
export type SoaScalar = "f32" | "i32" | "bool" | "Vector3";
|
|
14
|
+
|
|
15
|
+
export interface SoaField {
|
|
16
|
+
name: string;
|
|
17
|
+
kind: SoaScalar;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SoaPlan {
|
|
21
|
+
structName: string;
|
|
22
|
+
fields: SoaField[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const FIELD_RE =
|
|
26
|
+
/^(?:float|f32|double|f64)\s+([A-Za-z_]\w*)|^(?:int|i32)\s+([A-Za-z_]\w*)|^bool\s+([A-Za-z_]\w*)|^Vector3\s+([A-Za-z_]\w*)/;
|
|
27
|
+
|
|
28
|
+
export function parseSoaFields(structBody: string): SoaField[] {
|
|
29
|
+
const fields: SoaField[] = [];
|
|
30
|
+
for (const piece of structBody.split(";")) {
|
|
31
|
+
const line = piece.trim().replace(/=.*/, "").trim();
|
|
32
|
+
if (!line) continue;
|
|
33
|
+
const m = line.match(FIELD_RE);
|
|
34
|
+
if (!m) continue;
|
|
35
|
+
if (m[1]) fields.push({ name: m[1], kind: "f32" });
|
|
36
|
+
else if (m[2]) fields.push({ name: m[2], kind: "i32" });
|
|
37
|
+
else if (m[3]) fields.push({ name: m[3], kind: "bool" });
|
|
38
|
+
else if (m[4]) fields.push({ name: m[4], kind: "Vector3" });
|
|
39
|
+
}
|
|
40
|
+
return fields;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function findStructBody(source: string, structName: string): string | null {
|
|
44
|
+
const re = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`);
|
|
45
|
+
const m = source.match(re);
|
|
46
|
+
return m ? m[1] : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function listLayoutTargets(source: string): string[] {
|
|
50
|
+
const names = new Set<string>();
|
|
51
|
+
const re = /#pragma\s+layout\s+soa\s+([A-Za-z_]\w*)/g;
|
|
52
|
+
let m: RegExpExecArray | null;
|
|
53
|
+
while ((m = re.exec(source))) {
|
|
54
|
+
names.add(m[1]);
|
|
55
|
+
}
|
|
56
|
+
return [...names];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function planSoa(source: string, structName: string): SoaPlan | null {
|
|
60
|
+
const body = findStructBody(source, structName);
|
|
61
|
+
if (!body) return null;
|
|
62
|
+
const fields = parseSoaFields(body);
|
|
63
|
+
if (fields.length < 2) return null;
|
|
64
|
+
return { structName, fields };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function defaultFor(kind: SoaScalar): string {
|
|
68
|
+
if (kind === "f32") return "0";
|
|
69
|
+
if (kind === "i32") return "0";
|
|
70
|
+
if (kind === "bool") return "false";
|
|
71
|
+
return "Vector3.zero";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Generate a standalone Luau SoA module (table-columns; buffer pack optional for floats). */
|
|
75
|
+
export function generateSoaLuau(plan: SoaPlan, useBuffer = false): string {
|
|
76
|
+
const { structName, fields } = plan;
|
|
77
|
+
const cols = fields
|
|
78
|
+
.map((f) => {
|
|
79
|
+
if (useBuffer && (f.kind === "f32" || f.kind === "i32")) {
|
|
80
|
+
return `\t\t${f.name} = buffer.create(0), -- grown on ensure`;
|
|
81
|
+
}
|
|
82
|
+
return `\t\t${f.name} = table.create(capacity),`;
|
|
83
|
+
})
|
|
84
|
+
.join("\n");
|
|
85
|
+
|
|
86
|
+
const pushLines = fields
|
|
87
|
+
.map((f) => {
|
|
88
|
+
if (useBuffer && f.kind === "f32") {
|
|
89
|
+
return `\tbuffer.writef32(store.${f.name}, (i - 1) * 4, row.${f.name} or 0)`;
|
|
90
|
+
}
|
|
91
|
+
if (useBuffer && f.kind === "i32") {
|
|
92
|
+
return `\tbuffer.writei32(store.${f.name}, (i - 1) * 4, row.${f.name} or 0)`;
|
|
93
|
+
}
|
|
94
|
+
return `\tstore.${f.name}[i] = row.${f.name}`;
|
|
95
|
+
})
|
|
96
|
+
.join("\n");
|
|
97
|
+
|
|
98
|
+
const getLines = fields
|
|
99
|
+
.map((f) => {
|
|
100
|
+
if (useBuffer && f.kind === "f32") {
|
|
101
|
+
return `\t\t${f.name} = buffer.readf32(store.${f.name}, (i - 1) * 4),`;
|
|
102
|
+
}
|
|
103
|
+
if (useBuffer && f.kind === "i32") {
|
|
104
|
+
return `\t\t${f.name} = buffer.readi32(store.${f.name}, (i - 1) * 4),`;
|
|
105
|
+
}
|
|
106
|
+
return `\t\t${f.name} = store.${f.name}[i],`;
|
|
107
|
+
})
|
|
108
|
+
.join("\n");
|
|
109
|
+
|
|
110
|
+
const ensureBuf = useBuffer
|
|
111
|
+
? fields
|
|
112
|
+
.filter((f) => f.kind === "f32" || f.kind === "i32")
|
|
113
|
+
.map(
|
|
114
|
+
(f) => `
|
|
115
|
+
local need = capacity * 4
|
|
116
|
+
if buffer.len(store.${f.name}) < need then
|
|
117
|
+
local nb = buffer.create(need)
|
|
118
|
+
buffer.copy(nb, 0, store.${f.name})
|
|
119
|
+
store.${f.name} = nb
|
|
120
|
+
end`,
|
|
121
|
+
)
|
|
122
|
+
.join("\n")
|
|
123
|
+
: "";
|
|
124
|
+
|
|
125
|
+
return `--!native
|
|
126
|
+
--!optimize 2
|
|
127
|
+
--!strict
|
|
128
|
+
-- cluaupp generated SoA for ${structName} — do not edit by hand.
|
|
129
|
+
-- Use via: local ${structName}Soa = require(...${structName}Soa)
|
|
130
|
+
|
|
131
|
+
export type ${structName}Row = {
|
|
132
|
+
${fields.map((f) => `\t${f.name}: ${f.kind === "f32" ? "number" : f.kind === "i32" ? "number" : f.kind === "bool" ? "boolean" : "Vector3"},`).join("\n")}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ${structName}Store = {
|
|
136
|
+
len: number,
|
|
137
|
+
capacity: number,
|
|
138
|
+
${fields.map((f) => `\t${f.name}: ${useBuffer && (f.kind === "f32" || f.kind === "i32") ? "buffer" : "{}"},`).join("\n")}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
local ${structName}Soa = {}
|
|
142
|
+
|
|
143
|
+
function ${structName}Soa.new(capacity: number): ${structName}Store
|
|
144
|
+
capacity = math.max(1, capacity or 64)
|
|
145
|
+
local store = {
|
|
146
|
+
len = 0,
|
|
147
|
+
capacity = capacity,
|
|
148
|
+
${cols}
|
|
149
|
+
}
|
|
150
|
+
return store
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
function ${structName}Soa.ensure(store: ${structName}Store, capacity: number)
|
|
154
|
+
if capacity <= store.capacity then
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
store.capacity = capacity
|
|
158
|
+
${ensureBuf}
|
|
159
|
+
${useBuffer ? "" : fields.map((f) => `\t-- table.create columns grow on index assign`).join("\n")}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
function ${structName}Soa.push(store: ${structName}Store, row: ${structName}Row): number
|
|
163
|
+
local i = store.len + 1
|
|
164
|
+
${structName}Soa.ensure(store, i)
|
|
165
|
+
store.len = i
|
|
166
|
+
${pushLines}
|
|
167
|
+
return i
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
function ${structName}Soa.get(store: ${structName}Store, i: number): ${structName}Row
|
|
171
|
+
return {
|
|
172
|
+
${getLines}
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
function ${structName}Soa.set(store: ${structName}Store, i: number, row: ${structName}Row)
|
|
177
|
+
${pushLines}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
return ${structName}Soa
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface LayoutApplyResult {
|
|
185
|
+
source: string;
|
|
186
|
+
changed: boolean;
|
|
187
|
+
applied: string[];
|
|
188
|
+
plans: SoaPlan[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Insert `#pragma layout soa Name` before advised storage structs. */
|
|
192
|
+
export function applyLayoutPragmas(
|
|
193
|
+
source: string,
|
|
194
|
+
fileName: string,
|
|
195
|
+
profile: CluauppProfile | null = null,
|
|
196
|
+
): LayoutApplyResult {
|
|
197
|
+
const advice = rankAdvice(adviseOptimizer(source, fileName), profile);
|
|
198
|
+
const targets = new Set(advice.filter((a) => a.kind === "storage").map((a) => a.target));
|
|
199
|
+
const applied: string[] = [];
|
|
200
|
+
const plans: SoaPlan[] = [];
|
|
201
|
+
let out = source;
|
|
202
|
+
|
|
203
|
+
for (const name of targets) {
|
|
204
|
+
if (new RegExp(`#pragma\\s+layout\\s+soa\\s+${name}\\b`).test(out)) {
|
|
205
|
+
const plan = planSoa(out, name);
|
|
206
|
+
if (plan) plans.push(plan);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const re = new RegExp(`((?:^|\\n)(?:\\s*))(struct\\s+${name}\\s*\\{)`, "m");
|
|
210
|
+
if (!re.test(out)) continue;
|
|
211
|
+
out = out.replace(re, `$1#pragma layout soa ${name}\n$1$2`);
|
|
212
|
+
applied.push(`#pragma layout soa → ${name}`);
|
|
213
|
+
const plan = planSoa(out, name);
|
|
214
|
+
if (plan) plans.push(plan);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const name of listLayoutTargets(out)) {
|
|
218
|
+
if (plans.some((p) => p.structName === name)) continue;
|
|
219
|
+
const plan = planSoa(out, name);
|
|
220
|
+
if (plan) plans.push(plan);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { source: out, changed: applied.length > 0, applied, plans };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function writeSoaArtifacts(
|
|
227
|
+
projectRoot: string,
|
|
228
|
+
plans: SoaPlan[],
|
|
229
|
+
opts: { useBuffer?: boolean } = {},
|
|
230
|
+
): string[] {
|
|
231
|
+
if (!plans.length) return [];
|
|
232
|
+
const outDir = path.join(projectRoot, ".cluaupp", "generated", "soa");
|
|
233
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
234
|
+
const written: string[] = [];
|
|
235
|
+
for (const plan of plans) {
|
|
236
|
+
const file = path.join(outDir, `${plan.structName}Soa.luau`);
|
|
237
|
+
fs.writeFileSync(file, generateSoaLuau(plan, opts.useBuffer === true), "utf8");
|
|
238
|
+
written.push(file);
|
|
239
|
+
}
|
|
240
|
+
return written;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Collect SoA plans from all sources that already have layout pragmas. */
|
|
244
|
+
export function collectSoaPlansFromProject(projectRoot: string, rootDir = "src"): SoaPlan[] {
|
|
245
|
+
const srcDir = path.join(projectRoot, rootDir);
|
|
246
|
+
if (!fs.existsSync(srcDir)) return [];
|
|
247
|
+
const plans: SoaPlan[] = [];
|
|
248
|
+
const seen = new Set<string>();
|
|
249
|
+
const walk = (dir: string) => {
|
|
250
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
251
|
+
const full = path.join(dir, entry.name);
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
walk(full);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!/\.(clpp|clp|clh)$/i.test(entry.name)) continue;
|
|
257
|
+
const source = fs.readFileSync(full, "utf8");
|
|
258
|
+
for (const name of listLayoutTargets(source)) {
|
|
259
|
+
if (seen.has(name)) continue;
|
|
260
|
+
const plan = planSoa(source, name);
|
|
261
|
+
if (plan) {
|
|
262
|
+
seen.add(name);
|
|
263
|
+
plans.push(plan);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
walk(srcDir);
|
|
269
|
+
return plans;
|
|
270
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel Luau / ThreadSafety diagnostics (CLUAU-PAR*).
|
|
3
|
+
* Flags Unsafe mutations inside parallel { } / desynchronize regions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { projectRoot } from "../package-info.js";
|
|
9
|
+
import type { RobloxTargetProfile, ThreadSafety } from "../api/model.js";
|
|
10
|
+
import { type PlatformDiagnostic, lineColAt, stripCommentsKeepStrings } from "./diagnostics.js";
|
|
11
|
+
|
|
12
|
+
let cachedUnsafeMembers: Set<string> | null = null;
|
|
13
|
+
|
|
14
|
+
function loadUnsafeMemberNames(): Set<string> {
|
|
15
|
+
if (cachedUnsafeMembers) {
|
|
16
|
+
return cachedUnsafeMembers;
|
|
17
|
+
}
|
|
18
|
+
const set = new Set<string>(["Parent", "Name"]); // Parent writes always unsafe in parallel
|
|
19
|
+
const profilePath = path.join(projectRoot, "api", "normalized", "roblox-target.profile.json");
|
|
20
|
+
if (fs.existsSync(profilePath)) {
|
|
21
|
+
try {
|
|
22
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, "utf8")) as RobloxTargetProfile;
|
|
23
|
+
for (const cls of Object.values(profile.classes || {})) {
|
|
24
|
+
for (const prop of Object.values(cls.properties || {})) {
|
|
25
|
+
if (prop.threadSafety === "Unsafe") {
|
|
26
|
+
set.add(prop.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const method of Object.values(cls.methods || {})) {
|
|
30
|
+
if (method.threadSafety === "Unsafe") {
|
|
31
|
+
set.add(method.name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* fallback set only */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
cachedUnsafeMembers = set;
|
|
40
|
+
return set;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractParallelRegions(source: string): Array<{ start: number; body: string }> {
|
|
44
|
+
const regions: Array<{ start: number; body: string }> = [];
|
|
45
|
+
const reParallel = /\bparallel\s*\{/g;
|
|
46
|
+
let m: RegExpExecArray | null;
|
|
47
|
+
while ((m = reParallel.exec(source))) {
|
|
48
|
+
const open = m.index + m[0].length - 1;
|
|
49
|
+
let depth = 0;
|
|
50
|
+
for (let i = open; i < source.length; i++) {
|
|
51
|
+
if (source[i] === "{") depth++;
|
|
52
|
+
else if (source[i] === "}") {
|
|
53
|
+
depth--;
|
|
54
|
+
if (depth === 0) {
|
|
55
|
+
regions.push({ start: m.index, body: source.slice(open + 1, i) });
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const reDesync = /task\s*::\s*desynchronize\s*\(\s*\)/g;
|
|
62
|
+
while ((m = reDesync.exec(source))) {
|
|
63
|
+
const rest = source.slice(m.index);
|
|
64
|
+
const sync = rest.search(/task\s*::\s*synchronize\s*\(\s*\)/);
|
|
65
|
+
const body = sync >= 0 ? rest.slice(0, sync) : rest.slice(0, Math.min(rest.length, 800));
|
|
66
|
+
regions.push({ start: m.index, body });
|
|
67
|
+
}
|
|
68
|
+
if (/\bConnectParallel\b/.test(source)) {
|
|
69
|
+
regions.push({ start: source.indexOf("ConnectParallel"), body: source });
|
|
70
|
+
}
|
|
71
|
+
return regions;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function checkParallelSafety(source: string, fileName = "input.clpp"): PlatformDiagnostic[] {
|
|
75
|
+
const clean = stripCommentsKeepStrings(source);
|
|
76
|
+
const unsafe = loadUnsafeMemberNames();
|
|
77
|
+
const out: PlatformDiagnostic[] = [];
|
|
78
|
+
const regions = extractParallelRegions(clean);
|
|
79
|
+
if (!regions.length) {
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
for (const region of regions) {
|
|
83
|
+
const parentAssign = /\.Parent\s*=/g;
|
|
84
|
+
let pm: RegExpExecArray | null;
|
|
85
|
+
while ((pm = parentAssign.exec(region.body))) {
|
|
86
|
+
const { line, column } = lineColAt(clean, region.start + pm.index);
|
|
87
|
+
out.push({
|
|
88
|
+
code: "CLUAU_PAR001",
|
|
89
|
+
message: "Unsafe Instance write `.Parent =` inside parallel / desynchronized region",
|
|
90
|
+
line,
|
|
91
|
+
column,
|
|
92
|
+
severity: "error",
|
|
93
|
+
file: fileName,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const memberWrite = /\.([A-Za-z_]\w*)\s*=/g;
|
|
97
|
+
while ((pm = memberWrite.exec(region.body))) {
|
|
98
|
+
const name = pm[1];
|
|
99
|
+
if (name === "Parent") continue;
|
|
100
|
+
if (!unsafe.has(name)) continue;
|
|
101
|
+
const { line, column } = lineColAt(clean, region.start + pm.index);
|
|
102
|
+
out.push({
|
|
103
|
+
code: "CLUAU_PAR002",
|
|
104
|
+
message: `API member \`.${name}\` is ThreadSafety=Unsafe inside parallel region`,
|
|
105
|
+
line,
|
|
106
|
+
column,
|
|
107
|
+
severity: "error",
|
|
108
|
+
file: fileName,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const destroy = /\.Destroy\s*\(/g;
|
|
112
|
+
while ((pm = destroy.exec(region.body))) {
|
|
113
|
+
const { line, column } = lineColAt(clean, region.start + pm.index);
|
|
114
|
+
out.push({
|
|
115
|
+
code: "CLUAU_PAR003",
|
|
116
|
+
message: "`Destroy()` is Unsafe inside parallel region",
|
|
117
|
+
line,
|
|
118
|
+
column,
|
|
119
|
+
severity: "error",
|
|
120
|
+
file: fileName,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function assertParallelSafe(source: string, fileName?: string): void {
|
|
128
|
+
const errors = checkParallelSafety(source, fileName).filter((d) => d.severity === "error");
|
|
129
|
+
if (!errors.length) return;
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Parallel safety:\n${errors.map((e) => ` ${e.line}:${e.column} ${e.code} ${e.message}`).join("\n")}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type { ThreadSafety };
|