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