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,1215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CL++ checker for the Roblox host. Resolves names, infers and checks types against CL++ rules and
|
|
4
|
+
* the typed Roblox API, enforces server/client authority, and annotates the tree for the emitter.
|
|
5
|
+
*
|
|
6
|
+
* Passes: declare (hoist every top-level name) → resolve (signatures, fields) → check (top-level
|
|
7
|
+
* statements in order, then function bodies on demand so call sites teach unannotated parameters)
|
|
8
|
+
* → finalize (exports, unused names, self mutation).
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.nonNull = exports.exprPathKey = exports.conditionNarrowings = exports.checkCondition = exports.applyNarrowings = exports.Checker = void 0;
|
|
12
|
+
exports.widenLiteral = widenLiteral;
|
|
13
|
+
exports.foldConstant = foldConstant;
|
|
14
|
+
const diagnostics_js_1 = require("../diagnostics.js");
|
|
15
|
+
const context_js_1 = require("../roblox/context.js");
|
|
16
|
+
const types_js_1 = require("../types.js");
|
|
17
|
+
Object.defineProperty(exports, "nonNull", { enumerable: true, get: function () { return types_js_1.nonNull; } });
|
|
18
|
+
const exprs_js_1 = require("./exprs.js");
|
|
19
|
+
Object.defineProperty(exports, "checkCondition", { enumerable: true, get: function () { return exprs_js_1.checkCondition; } });
|
|
20
|
+
Object.defineProperty(exports, "exprPathKey", { enumerable: true, get: function () { return exprs_js_1.exprPathKey; } });
|
|
21
|
+
const flow_js_1 = require("./flow.js");
|
|
22
|
+
Object.defineProperty(exports, "applyNarrowings", { enumerable: true, get: function () { return flow_js_1.applyNarrowings; } });
|
|
23
|
+
Object.defineProperty(exports, "conditionNarrowings", { enumerable: true, get: function () { return flow_js_1.conditionNarrowings; } });
|
|
24
|
+
const symbols_js_1 = require("./symbols.js");
|
|
25
|
+
const typeops_js_1 = require("./typeops.js");
|
|
26
|
+
const statements_js_1 = require("./statements.js");
|
|
27
|
+
let nextSymId = 1;
|
|
28
|
+
class Checker {
|
|
29
|
+
api;
|
|
30
|
+
diags;
|
|
31
|
+
source;
|
|
32
|
+
program;
|
|
33
|
+
input;
|
|
34
|
+
fileScope;
|
|
35
|
+
scope;
|
|
36
|
+
fn = null;
|
|
37
|
+
loopDepth = 0;
|
|
38
|
+
switchDepth = 0;
|
|
39
|
+
structs = new Map();
|
|
40
|
+
enums = new Map();
|
|
41
|
+
variants = new Map();
|
|
42
|
+
links = [];
|
|
43
|
+
services = new Set();
|
|
44
|
+
symbols = [];
|
|
45
|
+
funcSyms = new Map();
|
|
46
|
+
ownStructs = [];
|
|
47
|
+
ownEnums = [];
|
|
48
|
+
exports;
|
|
49
|
+
robloxLinked = false;
|
|
50
|
+
usesShared = false;
|
|
51
|
+
/** Struct/list arguments passed to user functions: copied only if the callee mutates the parameter. */
|
|
52
|
+
deferredCopies = [];
|
|
53
|
+
/** Method calls on a binding; a self-mutating method needs a mutable binding. */
|
|
54
|
+
mutatingCalls = [];
|
|
55
|
+
/** Function bodies not yet checked. */
|
|
56
|
+
pendingBodies = new Map();
|
|
57
|
+
checking = new Set();
|
|
58
|
+
strict;
|
|
59
|
+
constructor(input) {
|
|
60
|
+
this.input = input;
|
|
61
|
+
this.api = input.api;
|
|
62
|
+
this.diags = input.diags;
|
|
63
|
+
this.source = input.source;
|
|
64
|
+
this.program = input.program;
|
|
65
|
+
this.fileScope = new symbols_js_1.Scope(null, "file", 0);
|
|
66
|
+
this.scope = this.fileScope;
|
|
67
|
+
this.strict = input.program.mode === "strict" || (input.strict && input.program.mode !== "nonstrict");
|
|
68
|
+
this.exports = (0, symbols_js_1.emptyExports)(input.moduleId, input.source.path, input.kind, input.context);
|
|
69
|
+
this.exports.dmPath = input.dmPath;
|
|
70
|
+
}
|
|
71
|
+
// ---------- TypeEnv ----------
|
|
72
|
+
struct(id) {
|
|
73
|
+
return this.structs.get(id);
|
|
74
|
+
}
|
|
75
|
+
// ---------- diagnostics helpers ----------
|
|
76
|
+
get nocheck() {
|
|
77
|
+
return this.program.mode === "nocheck";
|
|
78
|
+
}
|
|
79
|
+
error(code, span, message, notes) {
|
|
80
|
+
if (this.nocheck && !code.startsWith("CL1") && !code.startsWith("MOD")) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.diags.error(code, span, message, notes);
|
|
84
|
+
}
|
|
85
|
+
warn(code, span, message, notes) {
|
|
86
|
+
if (this.nocheck) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.diags.warning(code, span, message, notes);
|
|
90
|
+
}
|
|
91
|
+
/** Error in strict mode, warning otherwise. */
|
|
92
|
+
strictError(code, span, message, notes) {
|
|
93
|
+
if (this.strict) {
|
|
94
|
+
this.error(code, span, message, notes);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.warn(code, span, message, notes);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ---------- symbols ----------
|
|
101
|
+
newSym(name, kind, type, span, mutable = false) {
|
|
102
|
+
const sym = {
|
|
103
|
+
id: nextSymId++,
|
|
104
|
+
name,
|
|
105
|
+
kind,
|
|
106
|
+
type,
|
|
107
|
+
mutable,
|
|
108
|
+
span,
|
|
109
|
+
file: this.input.moduleId,
|
|
110
|
+
topLevel: this.scope === this.fileScope,
|
|
111
|
+
depth: this.fn ? this.fn.depth : 0,
|
|
112
|
+
};
|
|
113
|
+
this.symbols.push(sym);
|
|
114
|
+
return sym;
|
|
115
|
+
}
|
|
116
|
+
declare(name, kind, type, span, mutable = false, scope = this.scope) {
|
|
117
|
+
const existing = scope.values.get(name);
|
|
118
|
+
if (existing && existing.kind !== "module") {
|
|
119
|
+
this.error("CL2004", span, `'${name}' is already declared in this scope`, [`first declared at line ${this.source.position(existing.span.start).line}`]);
|
|
120
|
+
}
|
|
121
|
+
else if (!existing) {
|
|
122
|
+
const outer = scope.parent?.lookup(name);
|
|
123
|
+
if (outer && (outer.kind === "var" || outer.kind === "param") && scope.kind !== "file" && !name.startsWith("_")) {
|
|
124
|
+
// shadowing is legal CL++ (blocks create scopes); mention only when it hides a parameter
|
|
125
|
+
if (outer.kind === "param" && outer.depth === (this.fn?.depth ?? 0)) {
|
|
126
|
+
this.warn("CL2010", span, `'${name}' shadows the parameter with the same name`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const sym = this.newSym(name, kind, type, span, mutable);
|
|
131
|
+
sym.topLevel = scope === this.fileScope;
|
|
132
|
+
scope.values.set(name, sym);
|
|
133
|
+
return sym;
|
|
134
|
+
}
|
|
135
|
+
declareType(name, kind, type, span, typeId) {
|
|
136
|
+
const existing = this.fileScope.types.get(name);
|
|
137
|
+
if (existing && existing.file === this.input.moduleId && existing.kind !== "module") {
|
|
138
|
+
this.error("CL2004", span, `type '${name}' is already declared`, [`first declared at line ${this.source.position(existing.span.start).line}`]);
|
|
139
|
+
}
|
|
140
|
+
const sym = this.newSym(name, kind, type, span);
|
|
141
|
+
sym.topLevel = true;
|
|
142
|
+
if (typeId) {
|
|
143
|
+
sym.typeId = typeId;
|
|
144
|
+
}
|
|
145
|
+
this.fileScope.types.set(name, sym);
|
|
146
|
+
return sym;
|
|
147
|
+
}
|
|
148
|
+
pushScope(kind = "block") {
|
|
149
|
+
this.scope = new symbols_js_1.Scope(this.scope, kind, this.fn ? this.fn.depth : 0);
|
|
150
|
+
return this.scope;
|
|
151
|
+
}
|
|
152
|
+
popScope() {
|
|
153
|
+
this.reportUnused(this.scope);
|
|
154
|
+
this.scope = this.scope.parent ?? this.fileScope;
|
|
155
|
+
}
|
|
156
|
+
reportUnused(scope) {
|
|
157
|
+
if (scope === this.fileScope && this.input.kind === "module") {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const sym of scope.values.values()) {
|
|
161
|
+
if (sym.kind === "var" && !sym.used && !sym.name.startsWith("_") && !sym.observable) {
|
|
162
|
+
this.warn("CL2011", sym.span, `'${sym.name}' is declared but never used`, ["prefix it with `_` to keep it on purpose"]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ---------- entry ----------
|
|
167
|
+
run() {
|
|
168
|
+
this.declarePass();
|
|
169
|
+
this.resolvePass();
|
|
170
|
+
this.checkTopLevel();
|
|
171
|
+
this.drainBodies();
|
|
172
|
+
this.finalize();
|
|
173
|
+
return {
|
|
174
|
+
moduleId: this.input.moduleId,
|
|
175
|
+
kind: this.input.kind,
|
|
176
|
+
context: this.input.context,
|
|
177
|
+
mode: this.program.mode,
|
|
178
|
+
directives: this.program.directives,
|
|
179
|
+
robloxLinked: this.robloxLinked,
|
|
180
|
+
links: this.links,
|
|
181
|
+
structs: this.ownStructs,
|
|
182
|
+
enums: this.ownEnums,
|
|
183
|
+
structRegistry: this.structs,
|
|
184
|
+
enumRegistry: this.enums,
|
|
185
|
+
topLevel: [...this.fileScope.values.values()],
|
|
186
|
+
funcSyms: this.funcSyms,
|
|
187
|
+
services: this.services,
|
|
188
|
+
usesShared: this.usesShared,
|
|
189
|
+
exports: this.exports,
|
|
190
|
+
dmPath: this.input.dmPath,
|
|
191
|
+
symbols: this.symbols,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// ---------- pass 1: declare ----------
|
|
195
|
+
declarePass() {
|
|
196
|
+
for (const stmt of this.program.body) {
|
|
197
|
+
if (stmt.kind !== "Decl") {
|
|
198
|
+
if (stmt.kind === "Signal") {
|
|
199
|
+
const sym = this.declare(stmt.name.name, "signal", { k: "clSignal", params: null }, stmt.name.span);
|
|
200
|
+
sym.signalParams = null;
|
|
201
|
+
stmt.name.sym = sym;
|
|
202
|
+
}
|
|
203
|
+
else if (stmt.kind === "Var" && (stmt.declKind === "const" || stmt.declKind === "constexpr") && !stmt.unpack) {
|
|
204
|
+
this.predeclareConst(stmt);
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
this.declareDecl(stmt.decl, stmt.attributes || []);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
predeclareConst(stmt) {
|
|
212
|
+
const name = stmt.names[0];
|
|
213
|
+
const value = stmt.init ? foldConstant(stmt.init) : undefined;
|
|
214
|
+
const type = value === undefined ? types_js_1.T.unknown : value === null ? types_js_1.T.null : typeof value === "string" ? types_js_1.T.string : typeof value === "boolean" ? types_js_1.T.bool : Number.isInteger(value) && !(stmt.init?.kind === "Number" && stmt.init.isFloat) ? types_js_1.T.int : types_js_1.T.float;
|
|
215
|
+
const sym = this.declare(name.name, "const", type, name.span);
|
|
216
|
+
if (value !== undefined) {
|
|
217
|
+
sym.constValue = value;
|
|
218
|
+
}
|
|
219
|
+
name.sym = sym;
|
|
220
|
+
stmt.predeclared = true;
|
|
221
|
+
}
|
|
222
|
+
declareDecl(decl, attrs) {
|
|
223
|
+
switch (decl.kind) {
|
|
224
|
+
case "Link":
|
|
225
|
+
this.declareLink(decl);
|
|
226
|
+
return;
|
|
227
|
+
case "Struct": {
|
|
228
|
+
const id = `${this.input.moduleId}::${decl.name}`;
|
|
229
|
+
const info = {
|
|
230
|
+
id,
|
|
231
|
+
name: decl.name,
|
|
232
|
+
luauName: decl.name,
|
|
233
|
+
moduleId: this.input.moduleId,
|
|
234
|
+
decl,
|
|
235
|
+
typeParams: decl.typeParams,
|
|
236
|
+
bases: [],
|
|
237
|
+
fields: [],
|
|
238
|
+
methods: new Map(),
|
|
239
|
+
subclasses: [],
|
|
240
|
+
isAbstract: decl.isAbstract,
|
|
241
|
+
isFinal: decl.isFinal,
|
|
242
|
+
resolved: false,
|
|
243
|
+
};
|
|
244
|
+
this.structs.set(id, info);
|
|
245
|
+
this.ownStructs.push(info);
|
|
246
|
+
const ty = { k: "struct", id, name: decl.name, args: decl.typeParams.map((p) => ({ k: "param", name: p })) };
|
|
247
|
+
this.declareType(decl.name, "struct", ty, decl.nameSpan, id);
|
|
248
|
+
const value = this.declare(decl.name, "struct", { k: "namespace", ns: { kind: "struct", id, name: decl.name } }, decl.nameSpan);
|
|
249
|
+
value.typeId = id;
|
|
250
|
+
value.doc = decl.doc;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
case "Enum": {
|
|
254
|
+
const id = `${this.input.moduleId}::${decl.name}`;
|
|
255
|
+
const info = {
|
|
256
|
+
id,
|
|
257
|
+
name: decl.name,
|
|
258
|
+
luauName: decl.name,
|
|
259
|
+
moduleId: this.input.moduleId,
|
|
260
|
+
decl,
|
|
261
|
+
cases: decl.cases.map((c) => ({ name: c.name, payload: null })),
|
|
262
|
+
hasPayload: decl.cases.some((c) => c.payload),
|
|
263
|
+
};
|
|
264
|
+
this.enums.set(id, info);
|
|
265
|
+
this.ownEnums.push(info);
|
|
266
|
+
this.declareType(decl.name, "enum", { k: "enum", id, name: decl.name }, decl.nameSpan, id);
|
|
267
|
+
const value = this.declare(decl.name, "enum", { k: "namespace", ns: { kind: "enum", id, name: decl.name } }, decl.nameSpan);
|
|
268
|
+
value.typeId = id;
|
|
269
|
+
value.doc = decl.doc;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
case "Variant": {
|
|
273
|
+
const id = `${this.input.moduleId}::${decl.name}`;
|
|
274
|
+
const info = { id, name: decl.name, alternatives: [] };
|
|
275
|
+
this.variants.set(id, info);
|
|
276
|
+
this.declareType(decl.name, "variant", { k: "variant", id, name: decl.name, alternatives: [] }, decl.nameSpan, id);
|
|
277
|
+
this.declare(decl.name, "variant", types_js_1.T.func([{ name: "value", type: types_js_1.T.any, optional: false }], { k: "variant", id, name: decl.name, alternatives: [] }), decl.nameSpan).typeId = id;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
case "TypeAlias":
|
|
281
|
+
this.declareType(decl.name, "alias", types_js_1.T.unknown, decl.nameSpan);
|
|
282
|
+
return;
|
|
283
|
+
case "Namespace": {
|
|
284
|
+
const sym = this.declare(decl.name, "namespace", { k: "namespace", ns: { kind: "clNamespace", name: decl.name, moduleId: this.input.moduleId } }, decl.nameSpan);
|
|
285
|
+
sym.doc = undefined;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
case "Func": {
|
|
289
|
+
if (decl.isExtern) {
|
|
290
|
+
this.error("RBX5101", decl.nameSpan, `extern func '${decl.name}' has no native provider on Roblox`, [
|
|
291
|
+
"Roblox functions come from `link @clpp.roblox;` — call them directly",
|
|
292
|
+
"declare Luau module functions in a linked module instead",
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
const existing = this.fileScope.values.get(decl.name);
|
|
296
|
+
if (existing && existing.kind === "func") {
|
|
297
|
+
this.error("CL2004", decl.nameSpan, `function '${decl.name}' is already declared`, ["CL++ has no overloading by parameter count across declarations; rename one of them"]);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const sym = this.declare(decl.name, "func", types_js_1.T.func([], types_js_1.T.unknown), decl.nameSpan);
|
|
301
|
+
sym.decl = decl;
|
|
302
|
+
sym.doc = decl.doc;
|
|
303
|
+
const ctx = contextAttribute(attrs.length ? attrs : decl.attributes);
|
|
304
|
+
if (ctx) {
|
|
305
|
+
sym.contextOnly = ctx;
|
|
306
|
+
}
|
|
307
|
+
this.funcSyms.set(decl, sym);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
default:
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
declareLink(decl) {
|
|
315
|
+
const target = this.input.resolveLink(decl);
|
|
316
|
+
const info = { decl, target, sym: null };
|
|
317
|
+
this.links.push(info);
|
|
318
|
+
switch (target.kind) {
|
|
319
|
+
case "error":
|
|
320
|
+
this.error("MOD9001", decl.pathSpan, target.message, target.notes);
|
|
321
|
+
return;
|
|
322
|
+
case "roblox":
|
|
323
|
+
if (this.robloxLinked) {
|
|
324
|
+
this.warn("MOD9002", decl.span, "@clpp.roblox is already linked");
|
|
325
|
+
}
|
|
326
|
+
this.robloxLinked = true;
|
|
327
|
+
return;
|
|
328
|
+
case "std": {
|
|
329
|
+
const sym = this.declare(decl.alias, "module", { k: "namespace", ns: { kind: "stdlib", module: target.module, alias: decl.alias } }, decl.aliasSpan);
|
|
330
|
+
sym.link = target;
|
|
331
|
+
info.sym = sym;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
case "clp": {
|
|
335
|
+
const sym = this.declare(decl.alias, "module", { k: "namespace", ns: { kind: "module", moduleId: target.exports.moduleId, alias: decl.alias } }, decl.aliasSpan);
|
|
336
|
+
sym.link = target;
|
|
337
|
+
info.sym = sym;
|
|
338
|
+
// Types from linked modules are visible with and without the module prefix.
|
|
339
|
+
for (const s of target.exports.structs.values()) {
|
|
340
|
+
this.structs.set(s.id, s);
|
|
341
|
+
if (!this.fileScope.types.has(s.name)) {
|
|
342
|
+
const t = this.newSym(s.name, "struct", { k: "struct", id: s.id, name: s.name, args: s.typeParams.map((p) => ({ k: "param", name: p })) }, decl.span);
|
|
343
|
+
t.typeId = s.id;
|
|
344
|
+
t.file = s.moduleId;
|
|
345
|
+
this.fileScope.types.set(s.name, t);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const e of target.exports.enums.values()) {
|
|
349
|
+
this.enums.set(e.id, e);
|
|
350
|
+
if (!this.fileScope.types.has(e.name)) {
|
|
351
|
+
const t = this.newSym(e.name, "enum", { k: "enum", id: e.id, name: e.name }, decl.span);
|
|
352
|
+
t.typeId = e.id;
|
|
353
|
+
t.file = e.moduleId;
|
|
354
|
+
this.fileScope.types.set(e.name, t);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const v of target.exports.variants.values()) {
|
|
358
|
+
this.variants.set(v.id, v);
|
|
359
|
+
}
|
|
360
|
+
for (const [name, t] of target.exports.aliases) {
|
|
361
|
+
if (!this.fileScope.types.has(name)) {
|
|
362
|
+
const s = this.newSym(name, "alias", t, decl.span);
|
|
363
|
+
s.file = target.exports.moduleId;
|
|
364
|
+
this.fileScope.types.set(name, s);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
this.checkLinkContext(decl, target.exports.context, target.dmPath);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
case "luau": {
|
|
371
|
+
const ns = target.exports ? { kind: "module", moduleId: target.exports.moduleId, alias: decl.alias } : { kind: "luauModule", alias: decl.alias };
|
|
372
|
+
const sym = this.declare(decl.alias, "module", { k: "namespace", ns }, decl.aliasSpan);
|
|
373
|
+
sym.link = target;
|
|
374
|
+
info.sym = sym;
|
|
375
|
+
if (target.exports) {
|
|
376
|
+
for (const s of target.exports.structs.values()) {
|
|
377
|
+
this.structs.set(s.id, s);
|
|
378
|
+
if (!this.fileScope.types.has(s.name)) {
|
|
379
|
+
const t = this.newSym(s.name, "struct", { k: "struct", id: s.id, name: s.name, args: [] }, decl.span);
|
|
380
|
+
t.typeId = s.id;
|
|
381
|
+
this.fileScope.types.set(s.name, t);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
this.checkLinkContext(decl, "shared", target.dmPath);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
default:
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** A LocalScript cannot require something that lives only on the server. */
|
|
393
|
+
checkLinkContext(decl, targetContext, dmPath) {
|
|
394
|
+
const here = this.input.context;
|
|
395
|
+
if (here === "client" && dmPath && (dmPath[0] === "ServerScriptService" || dmPath[0] === "ServerStorage")) {
|
|
396
|
+
this.error("CTX6001", decl.pathSpan, `this module lives in ${dmPath[0]}, which is never replicated to clients`, [
|
|
397
|
+
"move it to ReplicatedStorage (shared) or keep the code on the server",
|
|
398
|
+
]);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (targetContext === "server" || targetContext === "client") {
|
|
402
|
+
this.error("MOD9003", decl.pathSpan, `cannot link a ${targetContext} script; only modules (files without .server/.client) can be linked`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// ---------- pass 2: resolve signatures ----------
|
|
406
|
+
resolvePass() {
|
|
407
|
+
for (const stmt of this.program.body) {
|
|
408
|
+
if (stmt.kind !== "Decl") {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const decl = stmt.decl;
|
|
412
|
+
if (decl.kind === "TypeAlias") {
|
|
413
|
+
const sym = this.fileScope.types.get(decl.name);
|
|
414
|
+
sym.type = this.resolveType(decl.type);
|
|
415
|
+
}
|
|
416
|
+
else if (decl.kind === "Variant") {
|
|
417
|
+
const info = this.variants.get(`${this.input.moduleId}::${decl.name}`);
|
|
418
|
+
info.alternatives = decl.alternatives.map((a) => this.resolveType(a));
|
|
419
|
+
const t = this.fileScope.types.get(decl.name);
|
|
420
|
+
t.type = { k: "variant", id: info.id, name: info.name, alternatives: info.alternatives };
|
|
421
|
+
const v = this.fileScope.values.get(decl.name);
|
|
422
|
+
v.type = types_js_1.T.func([{ name: "value", type: { k: "union", members: info.alternatives }, optional: false }], t.type);
|
|
423
|
+
}
|
|
424
|
+
else if (decl.kind === "Enum") {
|
|
425
|
+
const info = this.enums.get(`${this.input.moduleId}::${decl.name}`);
|
|
426
|
+
info.cases = decl.cases.map((c) => ({ name: c.name, payload: c.payload ? this.resolveType(c.payload) : null }));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
for (const info of this.ownStructs) {
|
|
430
|
+
this.resolveStruct(info, new Set());
|
|
431
|
+
}
|
|
432
|
+
for (const stmt of this.program.body) {
|
|
433
|
+
if (stmt.kind === "Decl" && stmt.decl.kind === "Func") {
|
|
434
|
+
const sym = this.funcSyms.get(stmt.decl);
|
|
435
|
+
if (sym) {
|
|
436
|
+
sym.type = this.signatureOf(stmt.decl, null);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else if (stmt.kind === "Decl" && stmt.decl.kind === "Namespace") {
|
|
440
|
+
const funcs = new Map();
|
|
441
|
+
for (const f of stmt.decl.funcs) {
|
|
442
|
+
const sym = this.newSym(f.name, "func", this.signatureOf(f, null), f.nameSpan);
|
|
443
|
+
sym.decl = f;
|
|
444
|
+
this.funcSyms.set(f, sym);
|
|
445
|
+
funcs.set(f.name, { name: f.name, type: sym.type, doc: f.doc });
|
|
446
|
+
}
|
|
447
|
+
this.exports.namespaces.set(stmt.decl.name, funcs);
|
|
448
|
+
}
|
|
449
|
+
else if (stmt.kind === "Signal" && stmt.params.length && stmt.name.sym) {
|
|
450
|
+
const params = stmt.params.map((p) => this.resolveType(p));
|
|
451
|
+
const signalSym = stmt.name.sym;
|
|
452
|
+
signalSym.signalParams = params;
|
|
453
|
+
signalSym.type = { k: "clSignal", params };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
resolveStruct(info, visiting) {
|
|
458
|
+
if (info.resolved) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (visiting.has(info.id)) {
|
|
462
|
+
this.error("CL3020", info.decl.nameSpan, `struct ${info.name} inherits from itself`);
|
|
463
|
+
info.resolved = true;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
visiting.add(info.id);
|
|
467
|
+
const decl = info.decl;
|
|
468
|
+
const fields = [];
|
|
469
|
+
const seenFields = new Map();
|
|
470
|
+
const methods = new Map();
|
|
471
|
+
for (const baseNode of decl.bases) {
|
|
472
|
+
const baseType = this.resolveType(baseNode);
|
|
473
|
+
if (baseType.k !== "struct") {
|
|
474
|
+
if (baseType.k === "instance") {
|
|
475
|
+
this.error("CL3021", baseNode.span, `a struct cannot inherit from the Roblox class ${baseType.cls}`, [
|
|
476
|
+
"wrap the Instance in a field instead: `struct Door { Part part; }`",
|
|
477
|
+
]);
|
|
478
|
+
}
|
|
479
|
+
else if (!(0, types_js_1.isDynamic)(baseType)) {
|
|
480
|
+
this.error("CL3021", baseNode.span, `'${(0, types_js_1.typeToString)(baseType)}' is not a struct and cannot be inherited`);
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const base = this.structs.get(baseType.id);
|
|
485
|
+
if (!base) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
this.resolveStruct(base, visiting);
|
|
489
|
+
if (base.isFinal) {
|
|
490
|
+
this.error("CL3022", baseNode.span, `struct ${base.name} is final and cannot be inherited`);
|
|
491
|
+
}
|
|
492
|
+
info.bases.push(base.id);
|
|
493
|
+
if (!base.subclasses.includes(info.id)) {
|
|
494
|
+
base.subclasses.push(info.id);
|
|
495
|
+
}
|
|
496
|
+
for (const f of base.fields) {
|
|
497
|
+
const prev = seenFields.get(f.name);
|
|
498
|
+
if (prev) {
|
|
499
|
+
if (prev.owner !== f.owner) {
|
|
500
|
+
this.error("CL3023", baseNode.span, `field '${f.name}' comes from both ${prev.owner} and ${f.owner}`, ["rename one of them to resolve the ambiguity"]);
|
|
501
|
+
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
seenFields.set(f.name, f);
|
|
505
|
+
fields.push(f);
|
|
506
|
+
}
|
|
507
|
+
for (const [name, m] of base.methods) {
|
|
508
|
+
if (!methods.has(name)) {
|
|
509
|
+
methods.set(name, m);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
this.pushTypeParams(info.typeParams);
|
|
514
|
+
for (const f of decl.fields) {
|
|
515
|
+
if (seenFields.has(f.name)) {
|
|
516
|
+
this.error("CL3024", f.nameSpan, `field '${f.name}' is already inherited from ${seenFields.get(f.name).owner}`);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const field = { name: f.name, type: this.resolveType(f.type), owner: info.name, isPrivate: f.isPrivate, defaultValue: f.defaultValue ?? null, span: f.nameSpan, doc: f.doc };
|
|
520
|
+
seenFields.set(f.name, field);
|
|
521
|
+
fields.push(field);
|
|
522
|
+
}
|
|
523
|
+
info.fields = fields;
|
|
524
|
+
const selfType = { k: "struct", id: info.id, name: info.name, args: info.typeParams.map((p) => ({ k: "param", name: p })) };
|
|
525
|
+
for (const m of decl.methods) {
|
|
526
|
+
const inherited = methods.get(m.name);
|
|
527
|
+
if (inherited && inherited.isFinal) {
|
|
528
|
+
this.error("CL3025", m.nameSpan, `method '${m.name}' is final in ${inherited.owner} and cannot be overridden`);
|
|
529
|
+
}
|
|
530
|
+
if (m.isOverride && !inherited) {
|
|
531
|
+
this.error("CL3026", m.nameSpan, `'${m.name}' is marked override but no parent struct declares it`);
|
|
532
|
+
}
|
|
533
|
+
if (fields.some((f) => f.name === m.name)) {
|
|
534
|
+
this.error("CL3027", m.nameSpan, `'${m.name}' is both a field and a method of ${info.name}`);
|
|
535
|
+
}
|
|
536
|
+
const type = this.signatureOf(m, m.isStatic ? null : selfType);
|
|
537
|
+
const method = {
|
|
538
|
+
name: m.name,
|
|
539
|
+
owner: info.name,
|
|
540
|
+
root: inherited ? inherited.root : info.id,
|
|
541
|
+
decl: m,
|
|
542
|
+
type,
|
|
543
|
+
isStatic: m.isStatic,
|
|
544
|
+
isAbstract: m.isAbstract,
|
|
545
|
+
isFinal: m.isFinal,
|
|
546
|
+
isPrivate: m.isPrivate,
|
|
547
|
+
};
|
|
548
|
+
if (m.isAbstract && !info.isAbstract) {
|
|
549
|
+
this.error("CL3028", m.nameSpan, `abstract method '${m.name}' needs an abstract struct`, ["write `abstract struct " + info.name + "`"]);
|
|
550
|
+
}
|
|
551
|
+
if (!m.body && !m.isAbstract) {
|
|
552
|
+
this.error("CL3029", m.nameSpan, `method '${m.name}' has no body`);
|
|
553
|
+
}
|
|
554
|
+
methods.set(m.name, method);
|
|
555
|
+
const sym = this.newSym(m.name, "func", type, m.nameSpan);
|
|
556
|
+
sym.decl = m;
|
|
557
|
+
sym.doc = m.doc;
|
|
558
|
+
this.funcSyms.set(m, sym);
|
|
559
|
+
}
|
|
560
|
+
this.popTypeParams();
|
|
561
|
+
info.methods = methods;
|
|
562
|
+
if (!info.isAbstract) {
|
|
563
|
+
for (const m of methods.values()) {
|
|
564
|
+
if (m.isAbstract) {
|
|
565
|
+
this.error("CL3030", decl.nameSpan, `struct ${info.name} must implement abstract method '${m.name}' from ${m.owner}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
info.resolved = true;
|
|
570
|
+
visiting.delete(info.id);
|
|
571
|
+
}
|
|
572
|
+
typeParamStack = [];
|
|
573
|
+
pushTypeParams(names) {
|
|
574
|
+
this.typeParamStack.push(new Set(names));
|
|
575
|
+
}
|
|
576
|
+
popTypeParams() {
|
|
577
|
+
this.typeParamStack.pop();
|
|
578
|
+
}
|
|
579
|
+
isTypeParam(name) {
|
|
580
|
+
return this.typeParamStack.some((s) => s.has(name));
|
|
581
|
+
}
|
|
582
|
+
/** Signature of a function declaration (params without types learn from call sites). */
|
|
583
|
+
signatureOf(decl, selfType) {
|
|
584
|
+
this.pushTypeParams(decl.typeParams);
|
|
585
|
+
const params = [];
|
|
586
|
+
let variadic = null;
|
|
587
|
+
for (const p of decl.params) {
|
|
588
|
+
if (p.variadic) {
|
|
589
|
+
variadic = p.type ? this.resolveType(p.type) : types_js_1.T.any;
|
|
590
|
+
p.ty = types_js_1.T.list(variadic);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const t = p.type ? this.resolveType(p.type) : { k: "infer", resolved: null };
|
|
594
|
+
p.ty = t;
|
|
595
|
+
params.push({ name: p.name, type: t, optional: p.defaultValue !== null || (0, types_js_1.prune)(t).k === "option" });
|
|
596
|
+
}
|
|
597
|
+
let ret = decl.returnType ? this.resolveType(decl.returnType) : decl.body === null ? types_js_1.T.void : { k: "infer", resolved: null };
|
|
598
|
+
if (decl.isAsync) {
|
|
599
|
+
ret = { k: "task", result: ret };
|
|
600
|
+
}
|
|
601
|
+
this.popTypeParams();
|
|
602
|
+
const fn = { k: "func", params, ret, variadic };
|
|
603
|
+
if (decl.typeParams.length) {
|
|
604
|
+
fn.generics = decl.typeParams;
|
|
605
|
+
}
|
|
606
|
+
void selfType;
|
|
607
|
+
return fn;
|
|
608
|
+
}
|
|
609
|
+
// ---------- type nodes ----------
|
|
610
|
+
resolveType(node) {
|
|
611
|
+
switch (node.kind) {
|
|
612
|
+
case "Literal":
|
|
613
|
+
return { k: "lit", value: node.value };
|
|
614
|
+
case "Optional":
|
|
615
|
+
return types_js_1.T.option(this.resolveType(node.inner));
|
|
616
|
+
case "Union": {
|
|
617
|
+
const members = node.members.map((m) => this.resolveType(m));
|
|
618
|
+
const nonNullMembers = members.filter((m) => m.k !== "null");
|
|
619
|
+
if (nonNullMembers.length !== members.length) {
|
|
620
|
+
return types_js_1.T.option(nonNullMembers.length === 1 ? nonNullMembers[0] : { k: "union", members: nonNullMembers });
|
|
621
|
+
}
|
|
622
|
+
return { k: "union", members };
|
|
623
|
+
}
|
|
624
|
+
case "Intersection": {
|
|
625
|
+
const members = node.members.map((m) => this.resolveType(m));
|
|
626
|
+
return members[0];
|
|
627
|
+
}
|
|
628
|
+
case "Func":
|
|
629
|
+
return types_js_1.T.func(node.params.map((p, i) => ({ name: `arg${i + 1}`, type: this.resolveType(p), optional: false })), node.ret ? this.resolveType(node.ret) : types_js_1.T.void);
|
|
630
|
+
case "Named":
|
|
631
|
+
return this.resolveNamedType(node);
|
|
632
|
+
default:
|
|
633
|
+
return types_js_1.T.unknown;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
resolveNamedType(node) {
|
|
637
|
+
const args = () => node.args.map((a) => this.resolveType(a));
|
|
638
|
+
const arity = (n, what) => {
|
|
639
|
+
if (node.args.length !== n) {
|
|
640
|
+
this.error("CL3001", node.span, `${what} takes ${n} type argument${n === 1 ? "" : "s"}, got ${node.args.length}`);
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
};
|
|
645
|
+
if (node.qualifier) {
|
|
646
|
+
if (node.qualifier === "Enum") {
|
|
647
|
+
this.requireRoblox(node.span, "Enum");
|
|
648
|
+
if (!this.api.hasEnum(node.name)) {
|
|
649
|
+
const s = (0, diagnostics_js_1.suggest)(node.name, Object.keys(this.api.enums));
|
|
650
|
+
this.error("RBX5001", node.span, `Enum.${node.name} is not a Roblox enum`, s ? [`did you mean Enum.${s}?`] : undefined);
|
|
651
|
+
return types_js_1.T.unknown;
|
|
652
|
+
}
|
|
653
|
+
return { k: "rbxEnumItem", enumName: node.name };
|
|
654
|
+
}
|
|
655
|
+
const mod = this.scope.lookup(node.qualifier.split(".")[0]);
|
|
656
|
+
if (mod && mod.kind === "module" && mod.link && (mod.link.kind === "clp" || (mod.link.kind === "luau" && mod.link.exports))) {
|
|
657
|
+
mod.used = true;
|
|
658
|
+
const exp = mod.link.kind === "clp" ? mod.link.exports : mod.link.exports;
|
|
659
|
+
const s = exp.structs.get(node.name);
|
|
660
|
+
if (s) {
|
|
661
|
+
return { k: "struct", id: s.id, name: s.name, args: args() };
|
|
662
|
+
}
|
|
663
|
+
const e = exp.enums.get(node.name);
|
|
664
|
+
if (e) {
|
|
665
|
+
return { k: "enum", id: e.id, name: e.name };
|
|
666
|
+
}
|
|
667
|
+
const a = exp.aliases.get(node.name);
|
|
668
|
+
if (a) {
|
|
669
|
+
return a;
|
|
670
|
+
}
|
|
671
|
+
const v = exp.variants.get(node.name);
|
|
672
|
+
if (v) {
|
|
673
|
+
return { k: "variant", id: v.id, name: v.name, alternatives: v.alternatives };
|
|
674
|
+
}
|
|
675
|
+
this.error("CL2020", node.span, `module ${node.qualifier} has no type '${node.name}'`);
|
|
676
|
+
return types_js_1.T.unknown;
|
|
677
|
+
}
|
|
678
|
+
if (mod && mod.kind === "module") {
|
|
679
|
+
mod.used = true;
|
|
680
|
+
return types_js_1.T.any;
|
|
681
|
+
}
|
|
682
|
+
// unknown qualifier: fall through to the bare name (CL++ ignores module prefixes on types)
|
|
683
|
+
}
|
|
684
|
+
const name = node.name;
|
|
685
|
+
if (this.isTypeParam(name)) {
|
|
686
|
+
return { k: "param", name };
|
|
687
|
+
}
|
|
688
|
+
const local = this.scope.lookupType(name);
|
|
689
|
+
if (local) {
|
|
690
|
+
local.used = true;
|
|
691
|
+
if (local.kind === "struct") {
|
|
692
|
+
const t = local.type;
|
|
693
|
+
const info = this.structs.get(t.id);
|
|
694
|
+
const given = args();
|
|
695
|
+
if (info && given.length && given.length !== info.typeParams.length) {
|
|
696
|
+
this.error("CL3001", node.span, `${name} takes ${info.typeParams.length} type argument(s), got ${given.length}`);
|
|
697
|
+
}
|
|
698
|
+
return { k: "struct", id: t.id, name: t.name, args: given.length ? given : t.args };
|
|
699
|
+
}
|
|
700
|
+
return local.type;
|
|
701
|
+
}
|
|
702
|
+
switch (name) {
|
|
703
|
+
case "int":
|
|
704
|
+
return types_js_1.T.int;
|
|
705
|
+
case "float":
|
|
706
|
+
case "double":
|
|
707
|
+
case "number":
|
|
708
|
+
return types_js_1.T.float;
|
|
709
|
+
case "bool":
|
|
710
|
+
case "boolean":
|
|
711
|
+
return types_js_1.T.bool;
|
|
712
|
+
case "string":
|
|
713
|
+
return types_js_1.T.string;
|
|
714
|
+
case "void":
|
|
715
|
+
return types_js_1.T.void;
|
|
716
|
+
case "null":
|
|
717
|
+
return types_js_1.T.null;
|
|
718
|
+
case "any":
|
|
719
|
+
return types_js_1.T.any;
|
|
720
|
+
case "unknown":
|
|
721
|
+
return types_js_1.T.any;
|
|
722
|
+
case "buffer":
|
|
723
|
+
return types_js_1.T.buffer;
|
|
724
|
+
case "thread":
|
|
725
|
+
return types_js_1.T.thread;
|
|
726
|
+
case "task":
|
|
727
|
+
return { k: "task", result: node.args.length ? this.resolveType(node.args[0]) : types_js_1.T.any };
|
|
728
|
+
case "list":
|
|
729
|
+
return node.args.length ? types_js_1.T.list(this.resolveType(node.args[0])) : types_js_1.T.list(types_js_1.T.any, false);
|
|
730
|
+
case "array":
|
|
731
|
+
return arity(1, "array") ? types_js_1.T.list(this.resolveType(node.args[0])) : types_js_1.T.list(types_js_1.T.unknown);
|
|
732
|
+
case "dictionary":
|
|
733
|
+
case "dict":
|
|
734
|
+
case "map":
|
|
735
|
+
return arity(2, "dictionary") ? types_js_1.T.dict(this.resolveType(node.args[0]), this.resolveType(node.args[1])) : types_js_1.T.dict(types_js_1.T.unknown, types_js_1.T.unknown);
|
|
736
|
+
case "Option":
|
|
737
|
+
return arity(1, "Option") ? types_js_1.T.option(this.resolveType(node.args[0])) : types_js_1.T.unknown;
|
|
738
|
+
case "Result":
|
|
739
|
+
return arity(2, "Result") ? { k: "result", ok: this.resolveType(node.args[0]), err: this.resolveType(node.args[1]) } : types_js_1.T.unknown;
|
|
740
|
+
case "Vector4":
|
|
741
|
+
this.error("RBX5002", node.span, "Vector4 has no Roblox equivalent", ["use Color3 for colors, Rect/UDim2 for 2D boxes, or a struct"]);
|
|
742
|
+
return types_js_1.T.unknown;
|
|
743
|
+
case "signal":
|
|
744
|
+
return { k: "clSignal", params: null };
|
|
745
|
+
case "RBXScriptSignal":
|
|
746
|
+
return { k: "rbxSignal", args: args() };
|
|
747
|
+
default:
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
if (name === "Vector2" || name === "Vector3") {
|
|
751
|
+
return types_js_1.T.datatype(name);
|
|
752
|
+
}
|
|
753
|
+
if (this.api.hasClass(name)) {
|
|
754
|
+
this.requireRoblox(node.span, name);
|
|
755
|
+
return types_js_1.T.instance(name);
|
|
756
|
+
}
|
|
757
|
+
if (this.api.hasDatatype(name) || name === "RBXScriptConnection") {
|
|
758
|
+
this.requireRoblox(node.span, name);
|
|
759
|
+
return types_js_1.T.datatype(name);
|
|
760
|
+
}
|
|
761
|
+
const candidates = [...this.allTypeNames()];
|
|
762
|
+
const s = (0, diagnostics_js_1.suggest)(name, candidates);
|
|
763
|
+
this.error("CL2021", node.span, `unknown type '${name}'`, s ? [`did you mean '${s}'?`] : this.robloxLinked ? undefined : ["Roblox types (Player, Part, CFrame…) need `link @clpp.roblox;`"]);
|
|
764
|
+
return types_js_1.T.unknown;
|
|
765
|
+
}
|
|
766
|
+
*allTypeNames() {
|
|
767
|
+
let cur = this.scope;
|
|
768
|
+
while (cur) {
|
|
769
|
+
yield* cur.types.keys();
|
|
770
|
+
cur = cur.parent;
|
|
771
|
+
}
|
|
772
|
+
yield* ["int", "float", "bool", "string", "void", "list", "array", "dictionary", "Option", "Result", "task", "buffer", "Vector2", "Vector3"];
|
|
773
|
+
if (this.robloxLinked) {
|
|
774
|
+
yield* Object.keys(this.api.classes);
|
|
775
|
+
yield* Object.keys(this.api.datatypes);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/** Roblox names need `link @clpp.roblox;` so pure CL++ modules stay portable to the clpp VM. */
|
|
779
|
+
requireRoblox(span, what) {
|
|
780
|
+
if (!this.robloxLinked) {
|
|
781
|
+
this.error("MOD9004", span, `'${what}' is part of the Roblox API; add \`link @clpp.roblox;\` at the top of the file`, [
|
|
782
|
+
"files without it stay pure CL++ and also run on the clpp VM",
|
|
783
|
+
]);
|
|
784
|
+
this.robloxLinked = true; // report once
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// ---------- pass 3: check ----------
|
|
788
|
+
checkTopLevel() {
|
|
789
|
+
const moduleFile = this.input.kind === "module";
|
|
790
|
+
for (const stmt of this.program.body) {
|
|
791
|
+
if (stmt.kind === "Decl") {
|
|
792
|
+
this.checkDecl(stmt.decl);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (moduleFile && !isModuleLevelStatement(stmt)) {
|
|
796
|
+
this.warn("MOD9010", stmt.span, "this statement runs once, when the module is first required", [
|
|
797
|
+
"CL++ modules only declare things; move the code into a function the scripts call",
|
|
798
|
+
]);
|
|
799
|
+
}
|
|
800
|
+
(0, statements_js_1.checkStatement)(this, stmt);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
checkDecl(decl) {
|
|
804
|
+
switch (decl.kind) {
|
|
805
|
+
case "Func": {
|
|
806
|
+
this.scheduleBody(decl, null);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
case "Struct": {
|
|
810
|
+
const info = this.structs.get(`${this.input.moduleId}::${decl.name}`);
|
|
811
|
+
if (!info) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
for (const f of info.fields) {
|
|
815
|
+
if (f.defaultValue && f.owner === info.name) {
|
|
816
|
+
const t = (0, exprs_js_1.checkExpr)(this, f.defaultValue, f.type);
|
|
817
|
+
this.expectAssignable(f.type, t, f.defaultValue.span, `default value of field '${f.name}'`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
for (const m of decl.methods) {
|
|
821
|
+
this.scheduleBody(m, info);
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
case "Namespace":
|
|
826
|
+
for (const f of decl.funcs) {
|
|
827
|
+
this.scheduleBody(f, null);
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
default:
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
scheduleBody(decl, owner) {
|
|
835
|
+
if (!decl.body) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
this.pendingBodies.set(decl, () => this.checkFunctionBody(decl, owner));
|
|
839
|
+
}
|
|
840
|
+
/** Check a function body now (used when a caller needs its inferred return type). */
|
|
841
|
+
ensureBodyChecked(decl) {
|
|
842
|
+
const job = this.pendingBodies.get(decl);
|
|
843
|
+
if (!job || this.checking.has(decl)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
this.pendingBodies.delete(decl);
|
|
847
|
+
this.checking.add(decl);
|
|
848
|
+
const savedScope = this.scope;
|
|
849
|
+
const savedFn = this.fn;
|
|
850
|
+
const savedLoop = this.loopDepth;
|
|
851
|
+
const savedSwitch = this.switchDepth;
|
|
852
|
+
this.scope = this.fileScope;
|
|
853
|
+
this.fn = null;
|
|
854
|
+
this.loopDepth = 0;
|
|
855
|
+
this.switchDepth = 0;
|
|
856
|
+
try {
|
|
857
|
+
job();
|
|
858
|
+
}
|
|
859
|
+
finally {
|
|
860
|
+
this.scope = savedScope;
|
|
861
|
+
this.fn = savedFn;
|
|
862
|
+
this.loopDepth = savedLoop;
|
|
863
|
+
this.switchDepth = savedSwitch;
|
|
864
|
+
this.checking.delete(decl);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
isChecking(decl) {
|
|
868
|
+
return this.checking.has(decl);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Callers before callees: a call site records its argument types into the callee's unannotated
|
|
872
|
+
* parameters (see calls.ts), so a body is checked once no pending function still calls it.
|
|
873
|
+
*/
|
|
874
|
+
drainBodies() {
|
|
875
|
+
const callees = new Map();
|
|
876
|
+
for (const decl of this.pendingBodies.keys()) {
|
|
877
|
+
callees.set(decl, calledNames(decl.body || []));
|
|
878
|
+
}
|
|
879
|
+
while (this.pendingBodies.size) {
|
|
880
|
+
const pending = [...this.pendingBodies.keys()];
|
|
881
|
+
const next = pending.find((d) => !pending.some((other) => other !== d && callees.get(other)?.has(d.name))) ?? pending[0];
|
|
882
|
+
this.ensureBodyChecked(next);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
checkFunctionBody(decl, owner) {
|
|
886
|
+
const sym = this.funcSyms.get(decl) ?? null;
|
|
887
|
+
const fnType = (sym?.type ?? this.signatureOf(decl, null));
|
|
888
|
+
this.pushTypeParams([...(owner?.typeParams ?? []), ...decl.typeParams]);
|
|
889
|
+
const isMethod = Boolean(owner) && !decl.isStatic;
|
|
890
|
+
const parallel = (decl.attributes || []).some((a) => a.name === "parallel");
|
|
891
|
+
const declaredRet = decl.returnType ? (decl.isAsync ? (0, types_js_1.prune)(fnType.ret).result : fnType.ret) : null;
|
|
892
|
+
const ctx = {
|
|
893
|
+
decl,
|
|
894
|
+
declaredRet,
|
|
895
|
+
returns: [],
|
|
896
|
+
depth: 1,
|
|
897
|
+
isMethod,
|
|
898
|
+
structId: owner?.id ?? null,
|
|
899
|
+
selfSym: null,
|
|
900
|
+
isAsync: decl.isAsync,
|
|
901
|
+
yields: false,
|
|
902
|
+
mutatesSelf: false,
|
|
903
|
+
contextOnly: sym?.contextOnly,
|
|
904
|
+
isParallel: parallel,
|
|
905
|
+
sym,
|
|
906
|
+
};
|
|
907
|
+
this.fn = ctx;
|
|
908
|
+
this.pushScope("function");
|
|
909
|
+
if (isMethod && owner) {
|
|
910
|
+
const selfType = { k: "struct", id: owner.id, name: owner.name, args: owner.typeParams.map((p) => ({ k: "param", name: p })) };
|
|
911
|
+
ctx.selfSym = this.declare("self", "self", selfType, decl.nameSpan, true);
|
|
912
|
+
ctx.selfSym.used = true;
|
|
913
|
+
}
|
|
914
|
+
this.declareParams(decl.params);
|
|
915
|
+
for (const stmt of decl.body) {
|
|
916
|
+
(0, statements_js_1.checkStatement)(this, stmt);
|
|
917
|
+
}
|
|
918
|
+
this.finishFunction(ctx, decl.body, decl.nameSpan, decl.name);
|
|
919
|
+
this.popScope();
|
|
920
|
+
this.popTypeParams();
|
|
921
|
+
if (sym) {
|
|
922
|
+
sym.type = fnType;
|
|
923
|
+
}
|
|
924
|
+
if (owner && isMethod) {
|
|
925
|
+
const m = owner.methods.get(decl.name);
|
|
926
|
+
if (m && m.decl === decl) {
|
|
927
|
+
m.mutatesSelf = ctx.mutatesSelf;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
(decl.fn ||= {}).mayYield = ctx.yields;
|
|
931
|
+
decl.fn.isMethod = isMethod;
|
|
932
|
+
decl.fn.ownerStruct = owner?.id;
|
|
933
|
+
decl.fn.selfMutated = ctx.mutatesSelf;
|
|
934
|
+
this.fn = null;
|
|
935
|
+
if (!decl.returnType) {
|
|
936
|
+
const inferred = ctx.returns.length ? ctx.returns.reduce((a, b) => (0, typeops_js_1.join)(this, a, b)) : types_js_1.T.void;
|
|
937
|
+
const retSlot = decl.isAsync ? (0, types_js_1.prune)(fnType.ret).result : fnType.ret;
|
|
938
|
+
if (retSlot.k === "infer" && !retSlot.resolved) {
|
|
939
|
+
retSlot.resolved = inferred;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
declareParams(params) {
|
|
944
|
+
for (const p of params) {
|
|
945
|
+
const t = p.ty ?? (p.type ? this.resolveType(p.type) : types_js_1.T.any);
|
|
946
|
+
const sym = this.declare(p.name, "param", t, p.span, true);
|
|
947
|
+
sym.used = true;
|
|
948
|
+
p.sym = sym;
|
|
949
|
+
if (p.defaultValue) {
|
|
950
|
+
const dt = (0, exprs_js_1.checkExpr)(this, p.defaultValue, (0, types_js_1.prune)(t).k === "infer" ? undefined : t);
|
|
951
|
+
if ((0, types_js_1.prune)(t).k === "infer" && !(0, types_js_1.prune)(t).resolved) {
|
|
952
|
+
(0, types_js_1.prune)(t).resolved = dt.k === "null" ? types_js_1.T.any : widenLiteral(dt);
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
this.expectAssignable(t, dt, p.defaultValue.span, `default value of '${p.name}'`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/** Missing-return and unreachable-code checks shared by functions and lambdas. */
|
|
961
|
+
finishFunction(ctx, body, nameSpan, name) {
|
|
962
|
+
const ret = ctx.declaredRet ? (0, types_js_1.prune)(ctx.declaredRet) : null;
|
|
963
|
+
if (ret && ret.k !== "void" && ret.k !== "any" && !(ret.k === "option") && !(0, flow_js_1.alwaysExits)(this, body)) {
|
|
964
|
+
this.error("CL4001", nameSpan, `missing return: '${name}' must return ${(0, types_js_1.typeToString)(ret)} on every path`);
|
|
965
|
+
}
|
|
966
|
+
else if (ret && ret.k === "option" && !(0, flow_js_1.alwaysExits)(this, body)) {
|
|
967
|
+
// Option<T> may fall through to null; mention it in strict mode only.
|
|
968
|
+
if (this.strict) {
|
|
969
|
+
this.warn("CL4002", nameSpan, `'${name}' can reach the end without returning; it returns null there`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// ---------- shared checks ----------
|
|
974
|
+
expectAssignable(to, from, span, what) {
|
|
975
|
+
const r = (0, typeops_js_1.assignable)(this, to, from);
|
|
976
|
+
if (r === "ok") {
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
if (r === "nullable") {
|
|
980
|
+
this.strictError("CL3011", span, `${what}: ${(0, types_js_1.typeToString)(from)} may be null, but ${(0, types_js_1.typeToString)(to)} is expected`, [
|
|
981
|
+
"check it first (`if (x != null) { … }`) or declare the target as optional (`" + (0, types_js_1.typeToString)(to) + "?`)",
|
|
982
|
+
]);
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
if (r === "narrowing") {
|
|
986
|
+
this.strictError("CL3012", span, `${what}: ${(0, types_js_1.typeToString)(from)} does not fit in ${(0, types_js_1.typeToString)(to)} without losing the fraction`, [
|
|
987
|
+
"convert explicitly with `int(x)` (truncates) or `Axiom.Round(x)`",
|
|
988
|
+
]);
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
this.error("CL3010", span, `${what}: expected ${(0, types_js_1.typeToString)(to)}, got ${(0, types_js_1.typeToString)(from)}`);
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
/** Mark a value-table read as copied when it flows into a new owner (CL++ value semantics). */
|
|
995
|
+
markCopy(expr) {
|
|
996
|
+
if (!(0, types_js_1.isValueTable)(expr.ty ?? types_js_1.T.unknown)) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
switch (expr.kind) {
|
|
1000
|
+
case "Name":
|
|
1001
|
+
case "Member":
|
|
1002
|
+
case "Index":
|
|
1003
|
+
case "SelfField":
|
|
1004
|
+
expr.copy = true;
|
|
1005
|
+
return;
|
|
1006
|
+
default:
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
noteYield() {
|
|
1011
|
+
if (this.fn) {
|
|
1012
|
+
this.fn.yields = true;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
effectiveContext() {
|
|
1016
|
+
if (this.fn?.contextOnly) {
|
|
1017
|
+
return this.fn.contextOnly;
|
|
1018
|
+
}
|
|
1019
|
+
return this.input.context;
|
|
1020
|
+
}
|
|
1021
|
+
/** Enforce a server/client rule at a use site. */
|
|
1022
|
+
checkContext(only, span, what, why, severity = "error") {
|
|
1023
|
+
const here = this.effectiveContext();
|
|
1024
|
+
if (here === "shared" || here === "plugin" || here === only) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const message = `${what} cannot be used in ${(0, context_js_1.contextLabel)(here)}: ${why}`;
|
|
1028
|
+
const notes = [only === "server" ? "move this code to a .server.clp script or ask the server through a RemoteEvent" : "move this code to a .client.clp LocalScript"];
|
|
1029
|
+
if (severity === "error") {
|
|
1030
|
+
this.error("CTX6002", span, message, notes);
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
this.warn("CTX6002", span, message, notes);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
useService(name, span) {
|
|
1037
|
+
this.requireRoblox(span, name);
|
|
1038
|
+
this.services.add(name);
|
|
1039
|
+
}
|
|
1040
|
+
// ---------- pass 4 ----------
|
|
1041
|
+
finalize() {
|
|
1042
|
+
this.reportUnused(this.fileScope);
|
|
1043
|
+
for (const call of this.mutatingCalls) {
|
|
1044
|
+
if (!call.method.mutatesSelf) {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
const root = call.root;
|
|
1048
|
+
root.mutatedFields = true;
|
|
1049
|
+
if ((root.kind === "var" || root.kind === "param") && !root.mutable) {
|
|
1050
|
+
this.error("CL2047", call.span, `${call.method.name}() changes '${root.name}', which is declared with let`, ["declare it with `let mut`"]);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
for (const { arg, decl, index } of this.deferredCopies) {
|
|
1054
|
+
const param = decl.params[index]?.sym;
|
|
1055
|
+
if (!param || param.mutatedFields || param.assigned || param.captured) {
|
|
1056
|
+
this.markCopy(arg);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
for (const sym of this.fileScope.values.values()) {
|
|
1060
|
+
switch (sym.kind) {
|
|
1061
|
+
case "func":
|
|
1062
|
+
if (sym.decl && !sym.decl.isExtern) {
|
|
1063
|
+
this.exports.funcs.set(sym.name, {
|
|
1064
|
+
name: sym.name,
|
|
1065
|
+
type: sym.type,
|
|
1066
|
+
contextOnly: sym.contextOnly,
|
|
1067
|
+
yields: sym.decl.fn?.mayYield,
|
|
1068
|
+
doc: sym.doc,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
break;
|
|
1072
|
+
case "const":
|
|
1073
|
+
if (sym.constValue !== undefined) {
|
|
1074
|
+
this.exports.consts.set(sym.name, { type: sym.type, value: sym.constValue });
|
|
1075
|
+
}
|
|
1076
|
+
break;
|
|
1077
|
+
default:
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
for (const s of this.ownStructs) {
|
|
1082
|
+
this.exports.structs.set(s.name, s);
|
|
1083
|
+
}
|
|
1084
|
+
for (const e of this.ownEnums) {
|
|
1085
|
+
this.exports.enums.set(e.name, e);
|
|
1086
|
+
}
|
|
1087
|
+
for (const [, v] of this.variants) {
|
|
1088
|
+
if (v.id.startsWith(`${this.input.moduleId}::`)) {
|
|
1089
|
+
this.exports.variants.set(v.name, v);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
for (const [name, sym] of this.fileScope.types) {
|
|
1093
|
+
if (sym.kind === "alias" && sym.file === this.input.moduleId) {
|
|
1094
|
+
this.exports.aliases.set(name, sym.type);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (this.input.kind === "script" && this.input.context !== "shared") {
|
|
1098
|
+
const init = this.fileScope.values.get("init");
|
|
1099
|
+
if (init && init.kind === "func" && !init.used && init.decl && init.decl.params.length === 0) {
|
|
1100
|
+
this.warn("CL4010", init.span, "function init() is never called", [
|
|
1101
|
+
"CL++ 0.9 has no entry function: top-level statements run when the script starts",
|
|
1102
|
+
"call `init();` at the end of the file or move its body to the top level",
|
|
1103
|
+
]);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
exports.Checker = Checker;
|
|
1109
|
+
function widenLiteral(t) {
|
|
1110
|
+
const p = (0, types_js_1.prune)(t);
|
|
1111
|
+
if (p.k === "lit") {
|
|
1112
|
+
return typeof p.value === "string" ? types_js_1.T.string : typeof p.value === "boolean" ? types_js_1.T.bool : Number.isInteger(p.value) ? types_js_1.T.int : types_js_1.T.float;
|
|
1113
|
+
}
|
|
1114
|
+
return p;
|
|
1115
|
+
}
|
|
1116
|
+
/** Names called directly (`f(…)`, `obj.f(…)`) anywhere in a body — a cheap syntactic call graph. */
|
|
1117
|
+
function calledNames(body) {
|
|
1118
|
+
const out = new Set();
|
|
1119
|
+
const visit = (node) => {
|
|
1120
|
+
if (!node || typeof node !== "object") {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (Array.isArray(node)) {
|
|
1124
|
+
for (const n of node) {
|
|
1125
|
+
visit(n);
|
|
1126
|
+
}
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const rec = node;
|
|
1130
|
+
if (rec.kind === "Call" && rec.callee && (rec.callee.kind === "Name" || rec.callee.kind === "Member") && rec.callee.name) {
|
|
1131
|
+
out.add(rec.callee.name);
|
|
1132
|
+
}
|
|
1133
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1134
|
+
if (key !== "ty" && key !== "res" && key !== "span" && value && typeof value === "object") {
|
|
1135
|
+
visit(value);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
visit(body);
|
|
1140
|
+
return out;
|
|
1141
|
+
}
|
|
1142
|
+
function isModuleLevelStatement(stmt) {
|
|
1143
|
+
switch (stmt.kind) {
|
|
1144
|
+
case "Var":
|
|
1145
|
+
case "Signal":
|
|
1146
|
+
case "Using":
|
|
1147
|
+
case "Decl":
|
|
1148
|
+
return true;
|
|
1149
|
+
case "Connect":
|
|
1150
|
+
return false;
|
|
1151
|
+
default:
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function contextAttribute(attrs) {
|
|
1156
|
+
for (const a of attrs) {
|
|
1157
|
+
if (a.name === "server" || a.name === "client") {
|
|
1158
|
+
return a.name;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return undefined;
|
|
1162
|
+
}
|
|
1163
|
+
/** Compile-time value of a literal/constant expression (`60 * 60`, `"a" .: "b"`). */
|
|
1164
|
+
function foldConstant(expr) {
|
|
1165
|
+
switch (expr.kind) {
|
|
1166
|
+
case "Number":
|
|
1167
|
+
return expr.value;
|
|
1168
|
+
case "String":
|
|
1169
|
+
return expr.value;
|
|
1170
|
+
case "Bool":
|
|
1171
|
+
return expr.value;
|
|
1172
|
+
case "Null":
|
|
1173
|
+
return null;
|
|
1174
|
+
case "Unary": {
|
|
1175
|
+
const v = foldConstant(expr.operand);
|
|
1176
|
+
if (expr.op === "-" && typeof v === "number") {
|
|
1177
|
+
return -v;
|
|
1178
|
+
}
|
|
1179
|
+
if (expr.op === "not" && typeof v === "boolean") {
|
|
1180
|
+
return !v;
|
|
1181
|
+
}
|
|
1182
|
+
return undefined;
|
|
1183
|
+
}
|
|
1184
|
+
case "Binary": {
|
|
1185
|
+
const l = foldConstant(expr.left);
|
|
1186
|
+
const r = foldConstant(expr.right);
|
|
1187
|
+
if (typeof l === "number" && typeof r === "number") {
|
|
1188
|
+
const isInt = Number.isInteger(l) && Number.isInteger(r) && !isFloatLiteral(expr.left) && !isFloatLiteral(expr.right);
|
|
1189
|
+
switch (expr.op) {
|
|
1190
|
+
case "+":
|
|
1191
|
+
return l + r;
|
|
1192
|
+
case "-":
|
|
1193
|
+
return l - r;
|
|
1194
|
+
case "*":
|
|
1195
|
+
return l * r;
|
|
1196
|
+
case "/":
|
|
1197
|
+
return r === 0 ? undefined : isInt ? Math.trunc(l / r) : l / r;
|
|
1198
|
+
case "%":
|
|
1199
|
+
return r === 0 ? undefined : l % r;
|
|
1200
|
+
default:
|
|
1201
|
+
return undefined;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (expr.op === ".:" && (typeof l === "string" || typeof l === "number") && (typeof r === "string" || typeof r === "number")) {
|
|
1205
|
+
return `${l}${r}`;
|
|
1206
|
+
}
|
|
1207
|
+
return undefined;
|
|
1208
|
+
}
|
|
1209
|
+
default:
|
|
1210
|
+
return undefined;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function isFloatLiteral(e) {
|
|
1214
|
+
return (e.kind === "Number" && e.isFloat) || (e.kind === "Binary" && (isFloatLiteral(e.left) || isFloatLiteral(e.right)));
|
|
1215
|
+
}
|