cluaupp 1.5.2 → 2.0.0-beta.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 +3 -0
- package/README.md +27 -40
- package/api/generated/bench-latest.json +43 -21
- package/api/generated/lsp-index.json +1 -1
- package/api/manifests/roblox-target.manifest.json +5 -5
- package/api/normalized/roblox-target.profile.json +8729 -267
- package/api/roblox/luau-builtins.d.luau +185 -0
- package/api/roblox/surface.json +1 -0
- package/api/roblox-api.lock.json +11 -2
- package/bin/cluaupp.js +38 -0
- package/editors/vscode/syntaxes/clpp.tmLanguage.json +211 -0
- package/examples/README.md +9 -9
- package/examples/game/cluaupp.config.json +2 -2
- package/examples/game/default.project.json +4 -15
- package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau +8 -0
- package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Leaderstats.server.luau +26 -0
- package/examples/game/out/ServerScriptService/Leaderstats.server.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Main.server.luau +15 -0
- package/examples/game/out/ServerScriptService/Main.server.luau.map.json +13 -0
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau +10 -0
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau.map.json +13 -0
- package/examples/game/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
- package/examples/game/src/ServerScriptService/Leaderstats.server.clp +18 -0
- package/examples/game/src/ServerScriptService/Main.server.clp +9 -0
- package/examples/game/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
- package/generated/api/build-profile.js +1 -1
- package/generated/api/loader.js +2 -2
- package/generated/api/surface/build.js +328 -0
- package/generated/api/surface/luau-defs.js +595 -0
- package/generated/api/surface/model.js +8 -0
- package/generated/clpp/contract.js +6 -3
- package/generated/clpp/handshake.js +36 -0
- package/generated/clpp/modules.js +20 -9
- package/generated/clpp/runner.js +62 -31
- package/generated/compile.js +18 -1
- package/generated/compiler/ast.js +3 -0
- package/generated/compiler/checker/calls.js +999 -0
- package/generated/compiler/checker/checker.js +1215 -0
- package/generated/compiler/checker/exprs.js +791 -0
- package/generated/compiler/checker/flow.js +168 -0
- package/generated/compiler/checker/members.js +763 -0
- package/generated/compiler/checker/statements.js +792 -0
- package/generated/compiler/checker/symbols.js +90 -0
- package/generated/compiler/checker/typeops.js +348 -0
- package/generated/compiler/checker/yields.js +115 -0
- package/generated/compiler/diagnostics.js +138 -0
- package/generated/compiler/emit/emitter.js +2356 -0
- package/generated/compiler/emit/luau-types.js +137 -0
- package/generated/compiler/emit/names.js +130 -0
- package/generated/compiler/emit/runtime.js +432 -0
- package/generated/compiler/lexer.js +403 -0
- package/generated/compiler/parser.js +1881 -0
- package/generated/compiler/project.js +332 -0
- package/generated/compiler/roblox/api.js +388 -0
- package/generated/compiler/roblox/context.js +100 -0
- package/generated/compiler/source.js +59 -0
- package/generated/compiler/stdlib.js +232 -0
- package/generated/compiler/types.js +250 -0
- package/generated/target/build-cache.js +4 -4
- package/generated/target/gen-rojo-tree.js +15 -0
- package/generated/target/source-map.js +61 -8
- package/generated/target/studio-bridge.js +1 -0
- package/generated/utils/project.js +83 -25
- package/native/bin/.gitkeep +1 -0
- package/package.json +17 -6
- package/plugins/CluauppNav/Plugin.luau +28 -4
- package/rust/cluaupp/Cargo.lock +795 -0
- package/rust/cluaupp/Cargo.toml +17 -0
- package/rust/cluaupp/src/main.rs +717 -0
- package/scripts/postinstall-native.js +56 -0
- package/skills/README.md +3 -3
- package/skills/clpp-hive-flare/SKILL.md +30 -26
- package/skills/clpp-language/SKILL.md +67 -74
- package/skills/clpp-language/compiler.md +24 -36
- package/skills/clpp-style/SKILL.md +31 -44
- package/src/api/build-profile.ts +1 -1
- package/src/api/loader.ts +2 -2
- package/src/api/surface/build.ts +374 -0
- package/src/api/surface/luau-defs.ts +635 -0
- package/src/api/surface/model.ts +96 -0
- package/src/clpp/contract.ts +20 -3
- package/src/clpp/handshake.ts +25 -0
- package/src/clpp/modules.ts +17 -10
- package/src/clpp/runner.ts +52 -34
- package/src/compile.ts +20 -2
- package/src/compiler/ast.ts +336 -0
- package/src/compiler/checker/calls.ts +1025 -0
- package/src/compiler/checker/checker.ts +1352 -0
- package/src/compiler/checker/exprs.ts +805 -0
- package/src/compiler/checker/flow.ts +174 -0
- package/src/compiler/checker/members.ts +793 -0
- package/src/compiler/checker/statements.ts +802 -0
- package/src/compiler/checker/symbols.ts +219 -0
- package/src/compiler/checker/typeops.ts +355 -0
- package/src/compiler/checker/yields.ts +120 -0
- package/src/compiler/diagnostics.ts +163 -0
- package/src/compiler/emit/emitter.ts +2486 -0
- package/src/compiler/emit/luau-types.ts +143 -0
- package/src/compiler/emit/names.ts +130 -0
- package/src/compiler/emit/runtime.ts +438 -0
- package/src/compiler/lexer.ts +444 -0
- package/src/compiler/parser.ts +1943 -0
- package/src/compiler/project.ts +354 -0
- package/src/compiler/roblox/api.ts +408 -0
- package/src/compiler/roblox/context.ts +108 -0
- package/src/compiler/source.ts +67 -0
- package/src/compiler/stdlib.ts +256 -0
- package/src/compiler/types.ts +313 -0
- package/src/target/build-cache.ts +5 -8
- package/src/target/gen-rojo-tree.ts +15 -0
- package/src/target/source-map.ts +65 -27
- package/src/target/studio-bridge.ts +3 -0
- package/src/types.ts +3 -0
- package/src/utils/project.ts +79 -31
- package/templates/README.md +7 -0
- package/templates/{game → game-modern}/cluaupp.config.json +2 -2
- package/templates/{game → game-modern}/package.json +1 -2
- package/templates/game-modern/rokit.toml +2 -0
- package/templates/game-modern/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
- package/templates/game-modern/src/ServerScriptService/Handlers/Guard.server.clp +4 -0
- package/templates/game-modern/src/ServerScriptService/Main.server.clp +8 -0
- package/templates/game-modern/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
- package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau +0 -29
- package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau.map.json +0 -13
- package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau +0 -83
- package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau.map.json +0 -13
- package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau +0 -18
- package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau.map.json +0 -13
- package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau +0 -14
- package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau.map.json +0 -13
- package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau +0 -15
- package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau.map.json +0 -13
- package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau +0 -50
- package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau.map.json +0 -13
- package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau +0 -12
- package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau.map.json +0 -13
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau +0 -16
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau.map.json +0 -13
- package/examples/game/src/Client/Controllers/DataController.client.clpp +0 -7
- package/examples/game/src/Client/Main.client.clpp +0 -4
- package/examples/game/src/Declarations/Net.flare +0 -5
- package/examples/game/src/Include/GameTypes.clh +0 -40
- package/examples/game/src/Modules/Core/Guard.server.clpp +0 -7
- package/examples/game/src/Modules/Core/PlayerHandler.server.clpp +0 -23
- package/examples/game/src/Server/Boot/DataBoot.server.clpp +0 -13
- package/examples/game/src/Server/Main.server.clpp +0 -4
- package/templates/game/.clangd +0 -25
- package/templates/game/.vscode/extensions.json +0 -8
- package/templates/game/.vscode/settings.json +0 -17
- package/templates/game/Packages/.gitkeep +0 -0
- package/templates/game/compile_flags.txt +0 -6
- package/templates/game/default.project.json +0 -41
- package/templates/game/include/clpp/datatypes.clh +0 -575
- package/templates/game/include/clpp/generated/enums.clh +0 -4920
- package/templates/game/include/clpp/generated/instances.clh +0 -87182
- package/templates/game/include/clpp/libs/arrayindexer.clh +0 -12
- package/templates/game/include/clpp/libs/axiom.clh +0 -154
- package/templates/game/include/clpp/libs/bloom.clh +0 -58
- package/templates/game/include/clpp/libs/coil.clh +0 -37
- package/templates/game/include/clpp/libs/crest.clh +0 -22
- package/templates/game/include/clpp/libs/echo.clh +0 -52
- package/templates/game/include/clpp/libs/ember.clh +0 -17
- package/templates/game/include/clpp/libs/flare.clh +0 -65
- package/templates/game/include/clpp/libs/gleam.clh +0 -111
- package/templates/game/include/clpp/libs/guide.clh +0 -42
- package/templates/game/include/clpp/libs/helm.clh +0 -31
- package/templates/game/include/clpp/libs/hive.clh +0 -20
- package/templates/game/include/clpp/libs/keep.clh +0 -158
- package/templates/game/include/clpp/libs/lens.clh +0 -24
- package/templates/game/include/clpp/libs/mint.clh +0 -28
- package/templates/game/include/clpp/libs/net.clh +0 -31
- package/templates/game/include/clpp/libs/occlude.clh +0 -15
- package/templates/game/include/clpp/libs/pin.clh +0 -28
- package/templates/game/include/clpp/libs/promise.clh +0 -24
- package/templates/game/include/clpp/libs/roster.clh +0 -123
- package/templates/game/include/clpp/libs/shift.clh +0 -22
- package/templates/game/include/clpp/libs/spark.clh +0 -28
- package/templates/game/include/clpp/libs/stage.clh +0 -23
- package/templates/game/include/clpp/libs/sweep.clh +0 -23
- package/templates/game/include/clpp/libs/trace.clh +0 -18
- package/templates/game/include/clpp/libs/ward.clh +0 -19
- package/templates/game/include/clpp/libs.clh +0 -31
- package/templates/game/include/clpp/roblox.clh +0 -41
- package/templates/game/rokit.toml +0 -5
- package/templates/game/src/Client/Controllers/DataController.client.clpp +0 -7
- package/templates/game/src/Client/Main.client.clpp +0 -4
- package/templates/game/src/Declarations/Net.flare +0 -5
- package/templates/game/src/Include/GameTypes.clh +0 -40
- package/templates/game/src/Modules/Core/Guard.server.clpp +0 -7
- package/templates/game/src/Modules/Core/PlayerHandler.server.clpp +0 -24
- package/templates/game/src/Server/Boot/DataBoot.server.clpp +0 -13
- package/templates/game/src/Server/Main.server.clpp +0 -4
- package/templates/game/stylua.toml +0 -9
- package/templates/game/wally.toml +0 -12
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** CL++ types → Luau type annotations. `null` means "omit the annotation" (let Luau infer). */
|
|
2
|
+
|
|
3
|
+
import { prune, type Type } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export interface TypeNaming {
|
|
6
|
+
/** Luau name for a struct type (`Fighter`, `Combat.Fighter`), or null when it cannot be named here. */
|
|
7
|
+
struct(id: string): string | null;
|
|
8
|
+
/** Luau type of a payload enum, or null for plain enums (numbers). */
|
|
9
|
+
enumType(id: string): string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function luauType(t: Type, names: TypeNaming, depth = 0): string | null {
|
|
13
|
+
const p = prune(t);
|
|
14
|
+
if (depth > 8) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const sub = (x: Type) => luauType(x, names, depth + 1);
|
|
18
|
+
switch (p.k) {
|
|
19
|
+
case "int":
|
|
20
|
+
case "float":
|
|
21
|
+
return "number";
|
|
22
|
+
case "bool":
|
|
23
|
+
return "boolean";
|
|
24
|
+
case "string":
|
|
25
|
+
return "string";
|
|
26
|
+
case "lit":
|
|
27
|
+
return typeof p.value === "string" ? JSON.stringify(p.value) : typeof p.value === "boolean" ? String(p.value) : "number";
|
|
28
|
+
case "null":
|
|
29
|
+
return "nil";
|
|
30
|
+
case "void":
|
|
31
|
+
return "()";
|
|
32
|
+
case "any":
|
|
33
|
+
return "any";
|
|
34
|
+
case "buffer":
|
|
35
|
+
return "buffer";
|
|
36
|
+
case "thread":
|
|
37
|
+
return "thread";
|
|
38
|
+
case "list": {
|
|
39
|
+
const e = sub(p.elem);
|
|
40
|
+
return e ? `{ ${e} }` : null;
|
|
41
|
+
}
|
|
42
|
+
case "dict": {
|
|
43
|
+
const k = sub(p.key);
|
|
44
|
+
const v = sub(p.value);
|
|
45
|
+
return k && v ? `{ [${k}]: ${v} }` : null;
|
|
46
|
+
}
|
|
47
|
+
case "table": {
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
for (const [k, v] of Object.entries(p.fields)) {
|
|
50
|
+
const vt = sub(v);
|
|
51
|
+
if (!vt) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
parts.push(`${/^[A-Za-z_][A-Za-z0-9_]*$/.test(k) ? k : `[${JSON.stringify(k)}]`}: ${vt}`);
|
|
55
|
+
}
|
|
56
|
+
return `{ ${parts.join(", ")} }`;
|
|
57
|
+
}
|
|
58
|
+
case "tuple": {
|
|
59
|
+
const parts = p.elems.map(sub);
|
|
60
|
+
return parts.every(Boolean) ? `(${parts.join(", ")})` : null;
|
|
61
|
+
}
|
|
62
|
+
case "struct": {
|
|
63
|
+
const base = names.struct(p.id);
|
|
64
|
+
if (!base) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (p.args.length) {
|
|
68
|
+
const args = p.args.map(sub);
|
|
69
|
+
return args.every(Boolean) ? `${base}<${args.join(", ")}>` : null;
|
|
70
|
+
}
|
|
71
|
+
return base;
|
|
72
|
+
}
|
|
73
|
+
case "enum":
|
|
74
|
+
return names.enumType(p.id) ?? "number";
|
|
75
|
+
case "variant": {
|
|
76
|
+
const parts = p.alternatives.map(sub);
|
|
77
|
+
return parts.every(Boolean) && parts.length ? [...new Set(parts)].join(" | ") : null;
|
|
78
|
+
}
|
|
79
|
+
case "option": {
|
|
80
|
+
const inner = sub(p.inner);
|
|
81
|
+
if (!inner) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (inner === "any") {
|
|
85
|
+
return "any";
|
|
86
|
+
}
|
|
87
|
+
return /->|\|/.test(inner) ? `(${inner})?` : `${inner}?`;
|
|
88
|
+
}
|
|
89
|
+
case "result": {
|
|
90
|
+
const ok = sub(p.ok) ?? "any";
|
|
91
|
+
const err = sub(p.err) ?? "any";
|
|
92
|
+
const opt = (x: string) => (x === "any" ? x : /->|\|/.test(x) ? `(${x})?` : `${x}?`);
|
|
93
|
+
return `{ ok: boolean, value: ${opt(ok)}, error: ${opt(err)} }`;
|
|
94
|
+
}
|
|
95
|
+
case "task": {
|
|
96
|
+
const r = sub(p.result);
|
|
97
|
+
return `ClTask<${r && r !== "()" ? r : "any"}>`;
|
|
98
|
+
}
|
|
99
|
+
case "func": {
|
|
100
|
+
const params = p.params.map((x) => {
|
|
101
|
+
const pt = sub(x.type);
|
|
102
|
+
return pt ? (x.optional && !pt.endsWith("?") ? (/->|\|/.test(pt) ? `(${pt})?` : `${pt}?`) : pt) : null;
|
|
103
|
+
});
|
|
104
|
+
if (!params.every(Boolean)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (p.variadic) {
|
|
108
|
+
const v = sub(p.variadic);
|
|
109
|
+
params.push(`...${v ?? "any"}`);
|
|
110
|
+
}
|
|
111
|
+
const ret = prune(p.ret).k === "void" ? "()" : sub(p.ret);
|
|
112
|
+
if (!ret) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return `(${params.join(", ")}) -> ${ret}`;
|
|
116
|
+
}
|
|
117
|
+
case "union": {
|
|
118
|
+
const parts = p.members.map(sub);
|
|
119
|
+
return parts.every(Boolean) ? [...new Set(parts)].join(" | ") : null;
|
|
120
|
+
}
|
|
121
|
+
case "param":
|
|
122
|
+
return p.name;
|
|
123
|
+
case "instance":
|
|
124
|
+
return p.cls === "Object" ? "Instance" : p.cls;
|
|
125
|
+
case "datatype":
|
|
126
|
+
if (p.name === "__Mutex") {
|
|
127
|
+
return "ClMutex";
|
|
128
|
+
}
|
|
129
|
+
return p.name;
|
|
130
|
+
case "rbxEnumItem":
|
|
131
|
+
return p.enumName === "*" ? "EnumItem" : `Enum.${p.enumName}`;
|
|
132
|
+
case "rbxEnum":
|
|
133
|
+
return "Enum";
|
|
134
|
+
case "rbxEnums":
|
|
135
|
+
return "Enums";
|
|
136
|
+
case "rbxSignal":
|
|
137
|
+
return "RBXScriptSignal";
|
|
138
|
+
case "clSignal":
|
|
139
|
+
return null;
|
|
140
|
+
default:
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Luau-safe identifiers: keywords, globals the output relies on, and non-ASCII names. */
|
|
2
|
+
|
|
3
|
+
import type { RobloxApi } from "../roblox/api.js";
|
|
4
|
+
|
|
5
|
+
export const LUAU_KEYWORDS = new Set([
|
|
6
|
+
"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or",
|
|
7
|
+
"repeat", "return", "then", "true", "until", "while",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
/** Names emitted code may reference as globals; user symbols are renamed away from them. */
|
|
11
|
+
export function reservedGlobals(api: RobloxApi): Set<string> {
|
|
12
|
+
const out = new Set<string>([
|
|
13
|
+
"self", "continue", "type", "typeof", "export",
|
|
14
|
+
"game", "workspace", "script", "plugin", "shared", "Enum", "Instance", "Vector3", "Vector2",
|
|
15
|
+
"print", "warn", "error", "assert", "tostring", "tonumber", "select", "pairs", "ipairs", "next", "pcall",
|
|
16
|
+
"xpcall", "require", "setmetatable", "getmetatable", "rawget", "rawset", "rawequal", "rawlen", "unpack",
|
|
17
|
+
"tick", "time", "wait", "delay", "spawn",
|
|
18
|
+
]);
|
|
19
|
+
for (const name of Object.keys(api.libraries)) {
|
|
20
|
+
out.add(name);
|
|
21
|
+
}
|
|
22
|
+
for (const name of Object.keys(api.datatypes)) {
|
|
23
|
+
out.add(name);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const LUAU_BUILTIN_TYPES = new Set(["number", "string", "boolean", "any", "unknown", "never", "nil", "thread", "buffer", "userdata", "vector", "Enum", "EnumItem", "Instance"]);
|
|
29
|
+
|
|
30
|
+
/** Names that may not be used for an emitted type alias. */
|
|
31
|
+
export function isReservedTypeName(api: RobloxApi, name: string): boolean {
|
|
32
|
+
return LUAU_BUILTIN_TYPES.has(name) || api.hasClass(name) || api.hasDatatype(name) || name === "RBXScriptSignal" || name === "RBXScriptConnection" || name.startsWith("Cl");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sanitizeIdentifier(name: string): string {
|
|
36
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
let out = "";
|
|
40
|
+
for (const ch of name) {
|
|
41
|
+
if (/[A-Za-z0-9_]/.test(ch)) {
|
|
42
|
+
out += ch;
|
|
43
|
+
} else {
|
|
44
|
+
out += `_${ch.codePointAt(0)!.toString(16)}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return /^[0-9]/.test(out) ? `_${out}` : out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isIdentifier(name: string): boolean {
|
|
51
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !LUAU_KEYWORDS.has(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Luau string literal with the shortest correct escaping. */
|
|
55
|
+
export function luauString(value: string): string {
|
|
56
|
+
let out = '"';
|
|
57
|
+
for (const ch of value) {
|
|
58
|
+
const code = ch.codePointAt(0)!;
|
|
59
|
+
switch (ch) {
|
|
60
|
+
case '"':
|
|
61
|
+
out += '\\"';
|
|
62
|
+
break;
|
|
63
|
+
case "\\":
|
|
64
|
+
out += "\\\\";
|
|
65
|
+
break;
|
|
66
|
+
case "\n":
|
|
67
|
+
out += "\\n";
|
|
68
|
+
break;
|
|
69
|
+
case "\r":
|
|
70
|
+
out += "\\r";
|
|
71
|
+
break;
|
|
72
|
+
case "\t":
|
|
73
|
+
out += "\\t";
|
|
74
|
+
break;
|
|
75
|
+
case "\0":
|
|
76
|
+
out += "\\0";
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
out += code < 32 || code === 127 ? `\\${code}` : ch;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return `${out}"`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Escape literal text inside a Luau interpolated string. */
|
|
86
|
+
export function interpolationText(value: string): string {
|
|
87
|
+
let out = "";
|
|
88
|
+
for (const ch of value) {
|
|
89
|
+
switch (ch) {
|
|
90
|
+
case "`":
|
|
91
|
+
out += "\\`";
|
|
92
|
+
break;
|
|
93
|
+
case "{":
|
|
94
|
+
out += "\\{";
|
|
95
|
+
break;
|
|
96
|
+
case "}":
|
|
97
|
+
out += "\\}";
|
|
98
|
+
break;
|
|
99
|
+
case "\\":
|
|
100
|
+
out += "\\\\";
|
|
101
|
+
break;
|
|
102
|
+
case "\n":
|
|
103
|
+
out += "\\n";
|
|
104
|
+
break;
|
|
105
|
+
case "\r":
|
|
106
|
+
out += "\\r";
|
|
107
|
+
break;
|
|
108
|
+
case "\t":
|
|
109
|
+
out += "\\t";
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
out += ch;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Number literal: integers stay integral, floats use the shortest round-trip form. */
|
|
119
|
+
export function luauNumber(value: number): string {
|
|
120
|
+
if (Number.isNaN(value)) {
|
|
121
|
+
return "(0 / 0)";
|
|
122
|
+
}
|
|
123
|
+
if (!Number.isFinite(value)) {
|
|
124
|
+
return value > 0 ? "math.huge" : "-math.huge";
|
|
125
|
+
}
|
|
126
|
+
if (Number.isInteger(value) && Math.abs(value) < 2 ** 53) {
|
|
127
|
+
return String(value);
|
|
128
|
+
}
|
|
129
|
+
return String(value);
|
|
130
|
+
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-local runtime helpers. Only the helpers a file uses are emitted (with their dependencies),
|
|
3
|
+
* as `local function`s so `--!optimize 2` can inline them. No shared runtime module, no require.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Helper {
|
|
7
|
+
/** Luau source (one or more top-level statements). */
|
|
8
|
+
code: string;
|
|
9
|
+
deps?: string[];
|
|
10
|
+
/** Type aliases the helper declares. */
|
|
11
|
+
types?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const HELPERS: Record<string, Helper> = {
|
|
15
|
+
__cl_idiv: {
|
|
16
|
+
code: `local function __cl_idiv(a: number, b: number): number
|
|
17
|
+
local q = a / b
|
|
18
|
+
return if q < 0 then math.ceil(q) else math.floor(q)
|
|
19
|
+
end`,
|
|
20
|
+
},
|
|
21
|
+
__cl_trunc: {
|
|
22
|
+
code: `local function __cl_trunc(x: number): number
|
|
23
|
+
return if x < 0 then math.ceil(x) else math.floor(x)
|
|
24
|
+
end`,
|
|
25
|
+
},
|
|
26
|
+
__cl_toint: {
|
|
27
|
+
deps: ["__cl_tonumber", "__cl_trunc"],
|
|
28
|
+
code: `local function __cl_toint(text: string): number
|
|
29
|
+
return __cl_trunc(__cl_tonumber(text))
|
|
30
|
+
end`,
|
|
31
|
+
},
|
|
32
|
+
__cl_tonumber: {
|
|
33
|
+
code: `local function __cl_tonumber(text: string): number
|
|
34
|
+
local n = tonumber(text)
|
|
35
|
+
if n == nil then
|
|
36
|
+
error(\`not a number: "{text}"\`, 2)
|
|
37
|
+
end
|
|
38
|
+
return n
|
|
39
|
+
end`,
|
|
40
|
+
},
|
|
41
|
+
__cl_strlen: {
|
|
42
|
+
code: `local function __cl_strlen(s: string): number
|
|
43
|
+
return utf8.len(s) or #s
|
|
44
|
+
end`,
|
|
45
|
+
},
|
|
46
|
+
__cl_char: {
|
|
47
|
+
code: `local function __cl_char(s: string, i: number): string
|
|
48
|
+
local from = utf8.offset(s, i + 1)
|
|
49
|
+
if from == nil or from > #s then
|
|
50
|
+
error(\`index out of range: {i} (size {utf8.len(s) or #s})\`, 2)
|
|
51
|
+
end
|
|
52
|
+
local to = (utf8.offset(s, i + 2) or #s + 1) - 1
|
|
53
|
+
return string.sub(s, from, to)
|
|
54
|
+
end`,
|
|
55
|
+
},
|
|
56
|
+
__cl_slice: {
|
|
57
|
+
code: `local function __cl_slice<T>(x: T, a: number, b: number): T
|
|
58
|
+
if type(x) == "string" then
|
|
59
|
+
local s = x :: any
|
|
60
|
+
local from = utf8.offset(s, a + 1) or #s + 1
|
|
61
|
+
local to = (utf8.offset(s, b + 1) or #s + 1) - 1
|
|
62
|
+
return string.sub(s, from, to) :: any
|
|
63
|
+
end
|
|
64
|
+
return table.move(x :: any, a + 1, b, 1, {}) :: any
|
|
65
|
+
end`,
|
|
66
|
+
},
|
|
67
|
+
__cl_count: {
|
|
68
|
+
code: `local function __cl_count(t: { [any]: any }): number
|
|
69
|
+
local n = 0
|
|
70
|
+
for _ in t do
|
|
71
|
+
n += 1
|
|
72
|
+
end
|
|
73
|
+
return n
|
|
74
|
+
end`,
|
|
75
|
+
},
|
|
76
|
+
__cl_take: {
|
|
77
|
+
code: `local function __cl_take<K, V>(t: { [K]: V }, key: K): V
|
|
78
|
+
local value = t[key]
|
|
79
|
+
t[key] = nil
|
|
80
|
+
return value
|
|
81
|
+
end`,
|
|
82
|
+
},
|
|
83
|
+
__cl_sorted: {
|
|
84
|
+
code: `local function __cl_sorted<T>(list: { T }): { T }
|
|
85
|
+
local out = table.clone(list)
|
|
86
|
+
table.sort(out :: any)
|
|
87
|
+
return out
|
|
88
|
+
end`,
|
|
89
|
+
},
|
|
90
|
+
__cl_str: {
|
|
91
|
+
code: `local function __cl_str(value: any): string
|
|
92
|
+
local kind = typeof(value)
|
|
93
|
+
if kind == "string" then
|
|
94
|
+
return value
|
|
95
|
+
elseif kind == "boolean" or kind == "number" or kind == "nil" then
|
|
96
|
+
return if value == nil then "null" else tostring(value)
|
|
97
|
+
elseif kind == "Vector3" then
|
|
98
|
+
return \`({value.X}, {value.Y}, {value.Z})\`
|
|
99
|
+
elseif kind == "Vector2" then
|
|
100
|
+
return \`({value.X}, {value.Y})\`
|
|
101
|
+
elseif kind ~= "table" then
|
|
102
|
+
return tostring(value)
|
|
103
|
+
end
|
|
104
|
+
local meta = getmetatable(value)
|
|
105
|
+
local fields = if type(meta) == "table" then rawget(meta, "__fields") else nil
|
|
106
|
+
local parts = {}
|
|
107
|
+
local function item(v: any): string
|
|
108
|
+
return if type(v) == "string" then \`"{v}"\` else __cl_str(v)
|
|
109
|
+
end
|
|
110
|
+
if fields then
|
|
111
|
+
for i, name in fields do
|
|
112
|
+
parts[i] = __cl_str(value[name])
|
|
113
|
+
end
|
|
114
|
+
return "(" .. table.concat(parts, ", ") .. ")"
|
|
115
|
+
end
|
|
116
|
+
if rawget(value, "tag") ~= nil and type(rawget(value, "tag")) == "number" then
|
|
117
|
+
return if value.value ~= nil then __cl_str(value.value) else tostring(value.tag)
|
|
118
|
+
end
|
|
119
|
+
if #value > 0 or next(value) == nil then
|
|
120
|
+
for i, v in value do
|
|
121
|
+
parts[i] = item(v)
|
|
122
|
+
end
|
|
123
|
+
return "[" .. table.concat(parts, ", ") .. "]"
|
|
124
|
+
end
|
|
125
|
+
for k, v in value do
|
|
126
|
+
table.insert(parts, \`{k}: {__cl_str(v)}\`)
|
|
127
|
+
end
|
|
128
|
+
return "{" .. table.concat(parts, ", ") .. "}"
|
|
129
|
+
end`,
|
|
130
|
+
},
|
|
131
|
+
__cl_copy: {
|
|
132
|
+
code: `local function __cl_copy<T>(value: T): T
|
|
133
|
+
if type(value) ~= "table" then
|
|
134
|
+
return value
|
|
135
|
+
end
|
|
136
|
+
local source = value :: any
|
|
137
|
+
local meta = getmetatable(source)
|
|
138
|
+
if meta ~= nil and (type(meta) ~= "table" or rawget(meta, "__fields") == nil) then
|
|
139
|
+
return value -- foreign object: shared by reference
|
|
140
|
+
end
|
|
141
|
+
local out = table.clone(source)
|
|
142
|
+
for k, v in out do
|
|
143
|
+
if type(v) == "table" then
|
|
144
|
+
out[k] = __cl_copy(v)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
if meta ~= nil then
|
|
148
|
+
setmetatable(out, meta)
|
|
149
|
+
end
|
|
150
|
+
return out :: any
|
|
151
|
+
end`,
|
|
152
|
+
},
|
|
153
|
+
__cl_eq: {
|
|
154
|
+
code: `local function __cl_eq(a: any, b: any): boolean
|
|
155
|
+
if a == b then
|
|
156
|
+
return true
|
|
157
|
+
end
|
|
158
|
+
if type(a) ~= "table" or type(b) ~= "table" or getmetatable(a) ~= getmetatable(b) then
|
|
159
|
+
return false
|
|
160
|
+
end
|
|
161
|
+
for k, v in a do
|
|
162
|
+
if not __cl_eq(v, b[k]) then
|
|
163
|
+
return false
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
for k in b do
|
|
167
|
+
if a[k] == nil then
|
|
168
|
+
return false
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
return true
|
|
172
|
+
end`,
|
|
173
|
+
},
|
|
174
|
+
__cl_findeq: {
|
|
175
|
+
deps: ["__cl_eq"],
|
|
176
|
+
code: `local function __cl_findeq(list: { any }, value: any): number
|
|
177
|
+
for i, v in list do
|
|
178
|
+
if __cl_eq(v, value) then
|
|
179
|
+
return i - 1
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
return -1
|
|
183
|
+
end`,
|
|
184
|
+
},
|
|
185
|
+
__cl_task: {
|
|
186
|
+
types: `type ClTask<T> = { done: boolean, ok: boolean, value: T?, err: any, waiting: { thread } }`,
|
|
187
|
+
code: `local function __cl_spawn<T>(work: () -> T): ClTask<T>
|
|
188
|
+
local job: ClTask<T> = { done = false, ok = true, value = nil, err = nil, waiting = {} }
|
|
189
|
+
task.spawn(function()
|
|
190
|
+
local ok, result = pcall(work)
|
|
191
|
+
job.ok = ok
|
|
192
|
+
if ok then
|
|
193
|
+
job.value = result
|
|
194
|
+
else
|
|
195
|
+
job.err = result
|
|
196
|
+
end
|
|
197
|
+
job.done = true
|
|
198
|
+
for _, waiter in job.waiting do
|
|
199
|
+
task.spawn(waiter)
|
|
200
|
+
end
|
|
201
|
+
end)
|
|
202
|
+
return job
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
local function __cl_await<T>(job: ClTask<T>): T
|
|
206
|
+
if not job.done then
|
|
207
|
+
table.insert(job.waiting, coroutine.running())
|
|
208
|
+
coroutine.yield()
|
|
209
|
+
end
|
|
210
|
+
if not job.ok then
|
|
211
|
+
error(job.err, 0)
|
|
212
|
+
end
|
|
213
|
+
return job.value :: T
|
|
214
|
+
end`,
|
|
215
|
+
},
|
|
216
|
+
__cl_parallel: {
|
|
217
|
+
deps: ["__cl_task"],
|
|
218
|
+
code: `local function __cl_parallel<T>(jobs: { ClTask<T> }): { T }
|
|
219
|
+
local results = table.create(#jobs)
|
|
220
|
+
for i, job in jobs do
|
|
221
|
+
results[i] = __cl_await(job)
|
|
222
|
+
end
|
|
223
|
+
return results
|
|
224
|
+
end`,
|
|
225
|
+
},
|
|
226
|
+
__cl_mutex: {
|
|
227
|
+
types: `type ClMutex = { locked: boolean, queue: { thread } }`,
|
|
228
|
+
code: `local function __cl_mutex(): ClMutex
|
|
229
|
+
return { locked = false, queue = {} }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
local function __cl_lock(m: ClMutex)
|
|
233
|
+
if m.locked then
|
|
234
|
+
table.insert(m.queue, coroutine.running())
|
|
235
|
+
coroutine.yield()
|
|
236
|
+
end
|
|
237
|
+
m.locked = true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
local function __cl_unlock(m: ClMutex)
|
|
241
|
+
local nextThread = table.remove(m.queue, 1)
|
|
242
|
+
if nextThread then
|
|
243
|
+
task.spawn(nextThread)
|
|
244
|
+
else
|
|
245
|
+
m.locked = false
|
|
246
|
+
end
|
|
247
|
+
end`,
|
|
248
|
+
},
|
|
249
|
+
__cl_errmsg: {
|
|
250
|
+
code: `local function __cl_errmsg(err: any): string
|
|
251
|
+
if type(err) == "string" then
|
|
252
|
+
return (string.gsub(err, "^[^:]+:%d+: ", ""))
|
|
253
|
+
end
|
|
254
|
+
return tostring(err)
|
|
255
|
+
end`,
|
|
256
|
+
},
|
|
257
|
+
trim: {
|
|
258
|
+
code: `local function __cl_trim(text: string): string
|
|
259
|
+
return (string.match(text, "^%s*(.-)%s*$")) or ""
|
|
260
|
+
end`,
|
|
261
|
+
},
|
|
262
|
+
replace: {
|
|
263
|
+
code: `local function __cl_replace(text: string, target: string, replacement: string): string
|
|
264
|
+
if target == "" then
|
|
265
|
+
return text
|
|
266
|
+
end
|
|
267
|
+
local out = {}
|
|
268
|
+
local start = 1
|
|
269
|
+
while true do
|
|
270
|
+
local i, j = string.find(text, target, start, true)
|
|
271
|
+
if i == nil or j == nil then
|
|
272
|
+
break
|
|
273
|
+
end
|
|
274
|
+
table.insert(out, string.sub(text, start, i - 1))
|
|
275
|
+
table.insert(out, replacement)
|
|
276
|
+
start = j + 1
|
|
277
|
+
end
|
|
278
|
+
table.insert(out, string.sub(text, start))
|
|
279
|
+
return table.concat(out)
|
|
280
|
+
end`,
|
|
281
|
+
},
|
|
282
|
+
endsWith: {
|
|
283
|
+
code: `local function __cl_endsWith(text: string, part: string): boolean
|
|
284
|
+
return part == "" or string.sub(text, -#part) == part
|
|
285
|
+
end`,
|
|
286
|
+
},
|
|
287
|
+
toNumber: {
|
|
288
|
+
deps: ["__cl_tonumber"],
|
|
289
|
+
code: "",
|
|
290
|
+
},
|
|
291
|
+
indexOf: {
|
|
292
|
+
code: `local function __cl_indexOf(text: string, part: string): number
|
|
293
|
+
local at = string.find(text, part, 1, true)
|
|
294
|
+
if at == nil then
|
|
295
|
+
return -1
|
|
296
|
+
end
|
|
297
|
+
return (utf8.len(string.sub(text, 1, at - 1)) or (at - 1))
|
|
298
|
+
end`,
|
|
299
|
+
},
|
|
300
|
+
joinAll: {
|
|
301
|
+
deps: ["__cl_str"],
|
|
302
|
+
code: `local function __cl_joinAll(parts: { any }, separator: string): string
|
|
303
|
+
local out = table.create(#parts)
|
|
304
|
+
for i, part in parts do
|
|
305
|
+
out[i] = __cl_str(part)
|
|
306
|
+
end
|
|
307
|
+
return table.concat(out, separator)
|
|
308
|
+
end`,
|
|
309
|
+
},
|
|
310
|
+
jsonParse: {
|
|
311
|
+
code: `local function __cl_jsonParse(text: string): any
|
|
312
|
+
return game:GetService("HttpService"):JSONDecode(text)
|
|
313
|
+
end`,
|
|
314
|
+
},
|
|
315
|
+
jsonValid: {
|
|
316
|
+
code: `local function __cl_jsonValid(text: string): boolean
|
|
317
|
+
return (pcall(function()
|
|
318
|
+
return game:GetService("HttpService"):JSONDecode(text)
|
|
319
|
+
end))
|
|
320
|
+
end`,
|
|
321
|
+
},
|
|
322
|
+
jsonStringify: {
|
|
323
|
+
code: `local function __cl_jsonStringify(value: any): string
|
|
324
|
+
return game:GetService("HttpService"):JSONEncode(value)
|
|
325
|
+
end`,
|
|
326
|
+
},
|
|
327
|
+
jsonPretty: {
|
|
328
|
+
deps: ["jsonStringify"],
|
|
329
|
+
code: `local function __cl_jsonPretty(value: any): string
|
|
330
|
+
local text = __cl_jsonStringify(value)
|
|
331
|
+
local out, depth, inString = {}, 0, false
|
|
332
|
+
for i = 1, #text do
|
|
333
|
+
local ch = string.sub(text, i, i)
|
|
334
|
+
if ch == '"' and string.sub(text, i - 1, i - 1) ~= "\\\\" then
|
|
335
|
+
inString = not inString
|
|
336
|
+
end
|
|
337
|
+
if inString then
|
|
338
|
+
table.insert(out, ch)
|
|
339
|
+
elseif ch == "{" or ch == "[" then
|
|
340
|
+
depth += 1
|
|
341
|
+
table.insert(out, ch .. "\\n" .. string.rep(" ", depth))
|
|
342
|
+
elseif ch == "}" or ch == "]" then
|
|
343
|
+
depth -= 1
|
|
344
|
+
table.insert(out, "\\n" .. string.rep(" ", depth) .. ch)
|
|
345
|
+
elseif ch == "," then
|
|
346
|
+
table.insert(out, ",\\n" .. string.rep(" ", depth))
|
|
347
|
+
elseif ch == ":" then
|
|
348
|
+
table.insert(out, ": ")
|
|
349
|
+
else
|
|
350
|
+
table.insert(out, ch)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
return table.concat(out)
|
|
354
|
+
end`,
|
|
355
|
+
},
|
|
356
|
+
jsonGet: {
|
|
357
|
+
code: `local function __cl_jsonGet(value: any, path: string): any
|
|
358
|
+
local node = value
|
|
359
|
+
for key in string.gmatch(path, "[^%.]+") do
|
|
360
|
+
if type(node) ~= "table" then
|
|
361
|
+
return nil
|
|
362
|
+
end
|
|
363
|
+
local index = tonumber(key)
|
|
364
|
+
node = if index ~= nil and node[index + 1] ~= nil then node[index + 1] else node[key]
|
|
365
|
+
end
|
|
366
|
+
return node
|
|
367
|
+
end`,
|
|
368
|
+
},
|
|
369
|
+
jsonHas: {
|
|
370
|
+
deps: ["jsonGet"],
|
|
371
|
+
code: `local function __cl_jsonHas(value: any, path: string): boolean
|
|
372
|
+
return __cl_jsonGet(value, path) ~= nil
|
|
373
|
+
end`,
|
|
374
|
+
},
|
|
375
|
+
jsonSet: {
|
|
376
|
+
code: `local function __cl_jsonSet(target: any, key: string, value: any): any
|
|
377
|
+
local copy = if type(target) == "table" then table.clone(target) else {}
|
|
378
|
+
local index = tonumber(key)
|
|
379
|
+
if index ~= nil and #copy > 0 then
|
|
380
|
+
copy[index + 1] = value
|
|
381
|
+
else
|
|
382
|
+
copy[key] = value
|
|
383
|
+
end
|
|
384
|
+
return copy
|
|
385
|
+
end`,
|
|
386
|
+
},
|
|
387
|
+
jsonKeys: {
|
|
388
|
+
code: `local function __cl_jsonKeys(object: any): { string }
|
|
389
|
+
local keys = {}
|
|
390
|
+
if type(object) == "table" then
|
|
391
|
+
for k in object do
|
|
392
|
+
table.insert(keys, tostring(k))
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
return keys
|
|
396
|
+
end`,
|
|
397
|
+
},
|
|
398
|
+
urlHost: {
|
|
399
|
+
code: `local function __cl_urlHost(url: string): string
|
|
400
|
+
local rest = string.match(url, "^%a[%w+.-]*://(.*)$") or url
|
|
401
|
+
local host = string.match(rest, "^[^/?#]*") or ""
|
|
402
|
+
host = string.match(host, "^[^@]*@(.*)$") or host
|
|
403
|
+
return (string.match(host, "^([^:]*)")) or host
|
|
404
|
+
end`,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/** Luau name a std helper is called by. */
|
|
409
|
+
export function helperFunctionName(helper: string): string {
|
|
410
|
+
if (helper === "toNumber") {
|
|
411
|
+
return "__cl_tonumber";
|
|
412
|
+
}
|
|
413
|
+
return helper.startsWith("__cl_") ? helper : `__cl_${helper}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Ordered, de-duplicated helper set including dependencies. */
|
|
417
|
+
export function resolveHelpers(used: Iterable<string>): string[] {
|
|
418
|
+
const out: string[] = [];
|
|
419
|
+
const seen = new Set<string>();
|
|
420
|
+
const visit = (name: string) => {
|
|
421
|
+
if (seen.has(name)) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
seen.add(name);
|
|
425
|
+
const h = HELPERS[name];
|
|
426
|
+
if (!h) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
for (const d of h.deps || []) {
|
|
430
|
+
visit(d);
|
|
431
|
+
}
|
|
432
|
+
out.push(name);
|
|
433
|
+
};
|
|
434
|
+
for (const u of used) {
|
|
435
|
+
visit(u);
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|