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,1943 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CL++ 0.9 parser — grammar and precedence of KartzRbx/CLPP `parser.cpp` (docs/ebnf.md),
|
|
3
|
+
* extended for the Roblox host: any keyword after `.`, calls on any expression, generic
|
|
4
|
+
* member calls (`x.WaitForChild<Model>("Map")`), `expr as Type`, typed `for (Player p in …)`,
|
|
5
|
+
* anonymous `func (…) { }` values, `expr ~> handler;` on Roblox events, `T?` and `func(A) -> R` types.
|
|
6
|
+
*
|
|
7
|
+
* CL++ precedence is kept exactly: unary `-`/`not`/`~` sit between `shl/shr` and comparison, so
|
|
8
|
+
* `-a + b` is `-(a + b)` and `not a == b` is `not (a == b)`. A unary operator in operand position
|
|
9
|
+
* (`a * -b`, `x == -1`), which CLPP rejects, binds tightly to its operand.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
Arg,
|
|
14
|
+
Attribute,
|
|
15
|
+
BinaryOp,
|
|
16
|
+
Decl,
|
|
17
|
+
DeclKind,
|
|
18
|
+
EnumDecl,
|
|
19
|
+
Expr,
|
|
20
|
+
FieldDecl,
|
|
21
|
+
FuncDecl,
|
|
22
|
+
LinkDecl,
|
|
23
|
+
MatchArm,
|
|
24
|
+
NamespaceDecl,
|
|
25
|
+
Param,
|
|
26
|
+
Pattern,
|
|
27
|
+
Program,
|
|
28
|
+
Stmt,
|
|
29
|
+
StructDecl,
|
|
30
|
+
SwitchCase,
|
|
31
|
+
TypeAliasDecl,
|
|
32
|
+
TypeNode,
|
|
33
|
+
VariantDecl,
|
|
34
|
+
VarName,
|
|
35
|
+
} from "./ast.js";
|
|
36
|
+
import type { DiagnosticBag } from "./diagnostics.js";
|
|
37
|
+
import { KEYWORDS, tokenize, VALUE_KEYWORDS, type Token } from "./lexer.js";
|
|
38
|
+
import type { SourceFile, Span } from "./source.js";
|
|
39
|
+
|
|
40
|
+
const TYPE_KEYWORDS = new Set(["int", "float", "double", "bool", "string", "task", "buffer", "Vector2", "Vector3", "Vector4", "void"]);
|
|
41
|
+
const ASSIGN_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%="]);
|
|
42
|
+
const LEGACY_SIGNAL_METHODS = new Set(["Connect", "Once", "ConnectParallel", "Wait"]);
|
|
43
|
+
const STATEMENT_KEYWORDS = new Set([
|
|
44
|
+
"let", "const", "constexpr", "auto", "if", "while", "for", "return", "break", "continue", "switch", "match", "try",
|
|
45
|
+
"post", "warn", "report", "cout", "func", "struct", "enum", "variant", "link", "import", "signal", "observable",
|
|
46
|
+
"atomic", "namespace", "abstract", "final", "async", "extern", "static", "void", "using",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
export function defaultAlias(path: string): string {
|
|
50
|
+
let base = path.replace(/^@/, "").replace(/\\/g, "/");
|
|
51
|
+
if (base.includes("/")) {
|
|
52
|
+
base = base.slice(base.lastIndexOf("/") + 1);
|
|
53
|
+
base = base.replace(/\.(server|client)?\.?clp$/i, "").replace(/\.clp$/i, "");
|
|
54
|
+
} else {
|
|
55
|
+
base = base.slice(base.lastIndexOf(".") + 1);
|
|
56
|
+
}
|
|
57
|
+
base = base.replace(/\.[^.]*$/, "");
|
|
58
|
+
if (!base) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
return base[0].toUpperCase() + base.slice(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class ParseAbort extends Error {}
|
|
65
|
+
|
|
66
|
+
export class Parser {
|
|
67
|
+
private tokens: Token[];
|
|
68
|
+
private i = 0;
|
|
69
|
+
private pendingDoc: string | undefined;
|
|
70
|
+
private depth = 0;
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
private readonly source: SourceFile,
|
|
74
|
+
private readonly diags: DiagnosticBag,
|
|
75
|
+
tokens?: Token[],
|
|
76
|
+
) {
|
|
77
|
+
this.tokens = tokens ?? tokenize(source.text, diags);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------- token helpers ----------
|
|
81
|
+
|
|
82
|
+
private peek(n = 0): Token {
|
|
83
|
+
return this.tokens[Math.min(this.i + n, this.tokens.length - 1)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private get tok(): Token {
|
|
87
|
+
return this.tokens[this.i];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private advance(): Token {
|
|
91
|
+
const t = this.tokens[this.i];
|
|
92
|
+
if (this.i < this.tokens.length - 1) {
|
|
93
|
+
this.i++;
|
|
94
|
+
}
|
|
95
|
+
return t;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private atEnd(): boolean {
|
|
99
|
+
return this.tok.kind === "eof";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private isOp(text: string, n = 0): boolean {
|
|
103
|
+
const t = this.peek(n);
|
|
104
|
+
return t.kind === "op" && t.text === text;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private isKw(text: string, n = 0): boolean {
|
|
108
|
+
const t = this.peek(n);
|
|
109
|
+
return t.kind === "keyword" && t.text === text;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private isIdent(n = 0, text?: string): boolean {
|
|
113
|
+
const t = this.peek(n);
|
|
114
|
+
return t.kind === "ident" && (text === undefined || t.text === text);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private eatOp(text: string): boolean {
|
|
118
|
+
if (this.isOp(text)) {
|
|
119
|
+
this.advance();
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private eatKw(text: string): boolean {
|
|
126
|
+
if (this.isKw(text)) {
|
|
127
|
+
this.advance();
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private prevEnd(): number {
|
|
134
|
+
return this.tokens[Math.max(0, this.i - 1)].end;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private spanFrom(start: number): Span {
|
|
138
|
+
return { start, end: Math.max(start, this.prevEnd()) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private errorAt(span: Span, message: string, notes?: string[], code = "CL1100"): void {
|
|
142
|
+
this.diags.error(code, span, message, notes);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private expectOp(text: string, context?: string): Token {
|
|
146
|
+
if (this.isOp(text)) {
|
|
147
|
+
return this.advance();
|
|
148
|
+
}
|
|
149
|
+
const t = this.tok;
|
|
150
|
+
const where = t.kind === "eof" ? "end of file" : `'${t.text}'`;
|
|
151
|
+
const span = text === ";" && this.i > 0 ? { start: this.prevEnd(), end: this.prevEnd() + 1 } : { start: t.start, end: Math.max(t.end, t.start + 1) };
|
|
152
|
+
this.errorAt(span, `expected '${text}'${context ? ` ${context}` : ""}, found ${where}`, undefined, text === ";" ? "CL1101" : "CL1102");
|
|
153
|
+
throw new ParseAbort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private expectName(what = "name"): Token {
|
|
157
|
+
if (this.tok.kind === "ident") {
|
|
158
|
+
return this.advance();
|
|
159
|
+
}
|
|
160
|
+
const t = this.tok;
|
|
161
|
+
if (t.kind === "keyword") {
|
|
162
|
+
this.errorAt({ start: t.start, end: t.end }, `'${t.text}' is a reserved word and cannot be used as a ${what}`, undefined, "CL1103");
|
|
163
|
+
} else {
|
|
164
|
+
this.errorAt({ start: t.start, end: Math.max(t.end, t.start + 1) }, `expected ${what}`, undefined, "CL1104");
|
|
165
|
+
}
|
|
166
|
+
throw new ParseAbort();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Name after `.` / `::` — keywords are fine there (`Instance.new`, `x.type`). */
|
|
170
|
+
private expectMemberName(): Token {
|
|
171
|
+
const t = this.tok;
|
|
172
|
+
if (t.kind === "ident" || t.kind === "keyword") {
|
|
173
|
+
return this.advance();
|
|
174
|
+
}
|
|
175
|
+
this.errorAt({ start: t.start, end: Math.max(t.end, t.start + 1) }, "expected member name", undefined, "CL1105");
|
|
176
|
+
throw new ParseAbort();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private synchronize(): void {
|
|
180
|
+
let braces = 0;
|
|
181
|
+
while (!this.atEnd()) {
|
|
182
|
+
const t = this.tok;
|
|
183
|
+
if (t.kind === "op") {
|
|
184
|
+
if (t.text === ";" && braces === 0) {
|
|
185
|
+
this.advance();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (t.text === "{") {
|
|
189
|
+
braces++;
|
|
190
|
+
} else if (t.text === "}") {
|
|
191
|
+
if (braces === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
braces--;
|
|
195
|
+
if (braces === 0) {
|
|
196
|
+
this.advance();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else if (braces === 0 && t.newlineBefore && t.kind === "keyword" && STATEMENT_KEYWORDS.has(t.text) && this.i > 0) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.advance();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------- program ----------
|
|
208
|
+
|
|
209
|
+
parseProgram(): Program {
|
|
210
|
+
const program: Program = { file: this.source.path, mode: "nonstrict", directives: [], body: [] };
|
|
211
|
+
let sawCode = false;
|
|
212
|
+
while (!this.atEnd()) {
|
|
213
|
+
const t = this.tok;
|
|
214
|
+
if (t.kind === "directive") {
|
|
215
|
+
this.advance();
|
|
216
|
+
const word = String(t.value || "");
|
|
217
|
+
if (sawCode) {
|
|
218
|
+
this.diags.warning("CL1201", { start: t.start, end: t.end }, `directive '<<!${word}' must be at the top of the file; it is ignored here`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (word === "strict" || word === "nocheck" || word === "nonstrict") {
|
|
222
|
+
program.mode = word;
|
|
223
|
+
} else if (word === "native" || word === "optimize" || word === "nostrict" || word === "pure" || word === "nolint") {
|
|
224
|
+
program.directives.push(word);
|
|
225
|
+
} else {
|
|
226
|
+
this.diags.warning("CL1202", { start: t.start, end: t.end }, `unknown directive '<<!${word}'`, ["known: strict, nonstrict, nocheck, native, pure, nolint"]);
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
sawCode = true;
|
|
231
|
+
const before = this.i;
|
|
232
|
+
const stmt = this.statementSafe(true);
|
|
233
|
+
if (stmt) {
|
|
234
|
+
program.body.push(stmt);
|
|
235
|
+
} else if (this.i === before) {
|
|
236
|
+
this.advance();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return program;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private statementSafe(top: boolean): Stmt | null {
|
|
243
|
+
const start = this.i;
|
|
244
|
+
try {
|
|
245
|
+
return this.statement(top);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (!(err instanceof ParseAbort)) {
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
if (this.i === start) {
|
|
251
|
+
this.advance();
|
|
252
|
+
}
|
|
253
|
+
this.synchronize();
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private attributes(): Attribute[] {
|
|
259
|
+
const attrs: Attribute[] = [];
|
|
260
|
+
while (this.isOp("[") && this.isOp("[", 1)) {
|
|
261
|
+
const start = this.tok.start;
|
|
262
|
+
this.advance();
|
|
263
|
+
this.advance();
|
|
264
|
+
do {
|
|
265
|
+
const nameTok = this.tok;
|
|
266
|
+
if (nameTok.kind !== "ident" && nameTok.kind !== "keyword") {
|
|
267
|
+
this.errorAt({ start: nameTok.start, end: nameTok.end }, "expected attribute name", undefined, "CL1106");
|
|
268
|
+
throw new ParseAbort();
|
|
269
|
+
}
|
|
270
|
+
this.advance();
|
|
271
|
+
const args: string[] = [];
|
|
272
|
+
if (this.eatOp("(")) {
|
|
273
|
+
while (!this.isOp(")") && !this.atEnd()) {
|
|
274
|
+
const a = this.advance();
|
|
275
|
+
if (a.text !== ",") {
|
|
276
|
+
args.push(a.kind === "string" ? String(a.value) : a.text);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.expectOp(")");
|
|
280
|
+
}
|
|
281
|
+
attrs.push({ name: nameTok.text, args, span: { start: nameTok.start, end: this.prevEnd() } });
|
|
282
|
+
} while (this.eatOp(","));
|
|
283
|
+
this.expectOp("]", "to close the attribute");
|
|
284
|
+
this.expectOp("]", "to close the attribute");
|
|
285
|
+
if (attrs.length && attrs[attrs.length - 1].span.start < start) {
|
|
286
|
+
attrs[attrs.length - 1].span.start = start;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return attrs;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private takeDoc(): string | undefined {
|
|
293
|
+
const doc = this.pendingDoc;
|
|
294
|
+
this.pendingDoc = undefined;
|
|
295
|
+
return doc;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------- statements ----------
|
|
299
|
+
|
|
300
|
+
private block(): Stmt[] {
|
|
301
|
+
this.expectOp("{", "to open a block");
|
|
302
|
+
this.depth++;
|
|
303
|
+
const body: Stmt[] = [];
|
|
304
|
+
while (!this.isOp("}") && !this.atEnd()) {
|
|
305
|
+
const before = this.i;
|
|
306
|
+
const stmt = this.statementSafe(false);
|
|
307
|
+
if (stmt) {
|
|
308
|
+
body.push(stmt);
|
|
309
|
+
} else if (this.i === before) {
|
|
310
|
+
this.advance();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
this.depth--;
|
|
314
|
+
this.expectOp("}", "to close the block");
|
|
315
|
+
return body;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private statement(top: boolean): Stmt | null {
|
|
319
|
+
while (this.tok.kind === "doc") {
|
|
320
|
+
this.pendingDoc = String(this.tok.value || "");
|
|
321
|
+
this.advance();
|
|
322
|
+
}
|
|
323
|
+
if (this.tok.kind === "directive") {
|
|
324
|
+
const t = this.advance();
|
|
325
|
+
this.diags.warning("CL1201", { start: t.start, end: t.end }, `directive '<<!${t.value}' must be at the top of the file; it is ignored here`);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const start = this.tok.start;
|
|
329
|
+
const attrs = this.attributes();
|
|
330
|
+
const stmt = this.statementInner(top, attrs);
|
|
331
|
+
if (stmt && attrs.length) {
|
|
332
|
+
stmt.attributes = attrs;
|
|
333
|
+
}
|
|
334
|
+
if (stmt && stmt.span.start > start) {
|
|
335
|
+
stmt.span = { start, end: stmt.span.end };
|
|
336
|
+
}
|
|
337
|
+
return stmt;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private declStmt(decl: Decl): Stmt {
|
|
341
|
+
return { kind: "Decl", decl, span: decl.span };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private statementInner(top: boolean, attrs: Attribute[]): Stmt | null {
|
|
345
|
+
const t = this.tok;
|
|
346
|
+
const start = t.start;
|
|
347
|
+
|
|
348
|
+
if (t.kind === "op" && t.text === "#") {
|
|
349
|
+
const lineEnd = this.source.text.indexOf("\n", t.start);
|
|
350
|
+
const end = lineEnd < 0 ? this.source.text.length : lineEnd;
|
|
351
|
+
const text = this.source.text.slice(t.start, end).trim();
|
|
352
|
+
this.errorAt({ start: t.start, end }, `'${text}' is C preprocessor syntax; CL++ 0.9 has no headers or #include`, [
|
|
353
|
+
"import a module with `link \"./file.clp\" as Name;`",
|
|
354
|
+
"Roblox types and services come from `link @clpp.roblox;`",
|
|
355
|
+
], "CL1107");
|
|
356
|
+
while (!this.atEnd() && this.tok.start < end) {
|
|
357
|
+
this.advance();
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (t.kind === "op" && t.text === ";") {
|
|
363
|
+
this.advance();
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (t.kind === "keyword") {
|
|
368
|
+
switch (t.text) {
|
|
369
|
+
case "static":
|
|
370
|
+
if (["func", "void", "async", "struct", "enum"].includes(this.peek(1).text)) {
|
|
371
|
+
this.advance();
|
|
372
|
+
const inner = this.statementInner(top, attrs);
|
|
373
|
+
if (inner && inner.kind === "Decl" && inner.decl.kind === "Func") {
|
|
374
|
+
inner.decl.isStatic = true;
|
|
375
|
+
}
|
|
376
|
+
return inner;
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
case "link":
|
|
380
|
+
case "import":
|
|
381
|
+
case "from":
|
|
382
|
+
return this.declStmt(this.link(attrs));
|
|
383
|
+
case "struct":
|
|
384
|
+
return this.declStmt(this.struct(attrs, false, false));
|
|
385
|
+
case "abstract":
|
|
386
|
+
case "final":
|
|
387
|
+
if (this.isKw("struct", 1)) {
|
|
388
|
+
this.advance();
|
|
389
|
+
return this.declStmt(this.struct(attrs, t.text === "abstract", t.text === "final"));
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
case "enum":
|
|
393
|
+
return this.declStmt(this.enumDecl(attrs));
|
|
394
|
+
case "variant":
|
|
395
|
+
return this.declStmt(this.variant(attrs));
|
|
396
|
+
case "type":
|
|
397
|
+
if (this.isIdent(1) && this.isOp("=", 2)) {
|
|
398
|
+
return this.declStmt(this.typeAlias(attrs));
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
case "namespace":
|
|
402
|
+
return this.declStmt(this.namespace(attrs));
|
|
403
|
+
case "func":
|
|
404
|
+
if (!this.isOp("(", 1)) {
|
|
405
|
+
return this.declStmt(this.func(attrs, {}));
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
case "async":
|
|
409
|
+
if (this.isKw("func", 1)) {
|
|
410
|
+
this.advance();
|
|
411
|
+
return this.declStmt(this.func(attrs, { isAsync: true }));
|
|
412
|
+
}
|
|
413
|
+
break;
|
|
414
|
+
case "extern":
|
|
415
|
+
this.advance();
|
|
416
|
+
if (!this.isKw("func")) {
|
|
417
|
+
this.errorAt({ start: this.tok.start, end: this.tok.end }, "expected 'func' after 'extern'");
|
|
418
|
+
throw new ParseAbort();
|
|
419
|
+
}
|
|
420
|
+
return this.declStmt(this.func(attrs, { isExtern: true }));
|
|
421
|
+
case "void":
|
|
422
|
+
if (this.isIdent(1) && this.isOp("(", 2)) {
|
|
423
|
+
return this.declStmt(this.voidFunc(attrs));
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
case "signal":
|
|
427
|
+
return this.signalDecl();
|
|
428
|
+
case "post":
|
|
429
|
+
case "cout":
|
|
430
|
+
case "warn":
|
|
431
|
+
case "report":
|
|
432
|
+
if (this.isOp("(", 1)) {
|
|
433
|
+
return this.output();
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
case "let":
|
|
437
|
+
return this.letStmt();
|
|
438
|
+
case "const":
|
|
439
|
+
case "constexpr":
|
|
440
|
+
case "auto":
|
|
441
|
+
case "atomic":
|
|
442
|
+
return this.keywordDecl(t.text as DeclKind);
|
|
443
|
+
case "observable":
|
|
444
|
+
return this.observableDecl();
|
|
445
|
+
case "decltype":
|
|
446
|
+
return this.decltypeDecl();
|
|
447
|
+
case "if":
|
|
448
|
+
return this.ifStmt();
|
|
449
|
+
case "while":
|
|
450
|
+
return this.whileStmt();
|
|
451
|
+
case "for":
|
|
452
|
+
return this.forStmt();
|
|
453
|
+
case "switch":
|
|
454
|
+
return this.switchStmt();
|
|
455
|
+
case "match":
|
|
456
|
+
return this.matchStmt();
|
|
457
|
+
case "try":
|
|
458
|
+
return this.tryStmt();
|
|
459
|
+
case "return": {
|
|
460
|
+
this.advance();
|
|
461
|
+
let value: Expr | null = null;
|
|
462
|
+
if (!this.isOp(";")) {
|
|
463
|
+
value = this.expression();
|
|
464
|
+
}
|
|
465
|
+
this.expectOp(";", "after return");
|
|
466
|
+
return { kind: "Return", value, span: this.spanFrom(start) };
|
|
467
|
+
}
|
|
468
|
+
case "break":
|
|
469
|
+
case "continue":
|
|
470
|
+
this.advance();
|
|
471
|
+
this.expectOp(";", `after ${t.text}`);
|
|
472
|
+
return { kind: t.text === "break" ? "Break" : "Continue", span: this.spanFrom(start) };
|
|
473
|
+
case "using":
|
|
474
|
+
return this.usingStmt();
|
|
475
|
+
default:
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (this.isOp("{")) {
|
|
481
|
+
const body = this.block();
|
|
482
|
+
return { kind: "Block", body, span: this.spanFrom(start) };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// `Type name = expr;`
|
|
486
|
+
const typed = this.tryTypedDecl();
|
|
487
|
+
if (typed) {
|
|
488
|
+
return typed;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return this.exprStatement();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private exprStatement(): Stmt {
|
|
495
|
+
const start = this.tok.start;
|
|
496
|
+
const expr = this.expression();
|
|
497
|
+
if (this.tok.kind === "op" && ASSIGN_OPS.has(this.tok.text)) {
|
|
498
|
+
const op = this.advance().text as "=";
|
|
499
|
+
const value = this.expression();
|
|
500
|
+
this.expectOp(";", "after assignment");
|
|
501
|
+
if (!isPlace(expr)) {
|
|
502
|
+
this.errorAt(expr.span, "cannot assign to this expression", ["assign to a variable, field (`p.hp`), element (`xs[i]`) or `@field`"], "CL1108");
|
|
503
|
+
}
|
|
504
|
+
return { kind: "Assign", target: expr, op, value, span: this.spanFrom(start) };
|
|
505
|
+
}
|
|
506
|
+
if (this.isOp("~>")) {
|
|
507
|
+
this.advance();
|
|
508
|
+
const handler = this.expression();
|
|
509
|
+
this.expectOp(";", "after connection");
|
|
510
|
+
return { kind: "Connect", signal: expr, handler, span: this.spanFrom(start) };
|
|
511
|
+
}
|
|
512
|
+
if (!this.isOp(";")) {
|
|
513
|
+
const t = this.tok;
|
|
514
|
+
if (t.newlineBefore && !this.atEnd()) {
|
|
515
|
+
this.errorAt({ start: this.prevEnd(), end: this.prevEnd() + 1 }, "expected ';' at the end of the statement", undefined, "CL1101");
|
|
516
|
+
return { kind: "Expr", expr, span: this.spanFrom(start) };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
this.expectOp(";", "after expression");
|
|
520
|
+
return { kind: "Expr", expr, span: this.spanFrom(start) };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private output(): Stmt {
|
|
524
|
+
const start = this.tok.start;
|
|
525
|
+
const channel = this.advance().text as "post" | "cout" | "warn" | "report";
|
|
526
|
+
this.expectOp("(");
|
|
527
|
+
const values: Expr[] = [];
|
|
528
|
+
if (!this.isOp(")")) {
|
|
529
|
+
values.push(this.expression());
|
|
530
|
+
while (this.eatOp(",")) {
|
|
531
|
+
values.push(this.expression());
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
this.expectOp(")");
|
|
535
|
+
this.expectOp(";", `after ${channel}(...)`);
|
|
536
|
+
const span = this.spanFrom(start);
|
|
537
|
+
if (values.length === 0) {
|
|
538
|
+
values.push({ kind: "String", value: "", span });
|
|
539
|
+
}
|
|
540
|
+
if (values.length === 1) {
|
|
541
|
+
return { kind: "Output", channel, value: values[0], span };
|
|
542
|
+
}
|
|
543
|
+
// post(a, b) → print(a, b) on Roblox: keep every argument.
|
|
544
|
+
const call: Expr = { kind: "Call", callee: { kind: "Name", name: channel === "warn" ? "warn" : "print", typeArgs: null, span }, args: values.map((v) => ({ name: null, value: v })), span };
|
|
545
|
+
if (channel === "report") {
|
|
546
|
+
return { kind: "Output", channel, value: values[0], span };
|
|
547
|
+
}
|
|
548
|
+
return { kind: "Expr", expr: call, span };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private letStmt(): Stmt {
|
|
552
|
+
const start = this.tok.start;
|
|
553
|
+
this.advance();
|
|
554
|
+
const mutable = this.eatKw("mut");
|
|
555
|
+
if (this.isOp("(")) {
|
|
556
|
+
this.advance();
|
|
557
|
+
const names: VarName[] = [];
|
|
558
|
+
do {
|
|
559
|
+
const n = this.expectName("variable name");
|
|
560
|
+
names.push({ name: n.text, span: { start: n.start, end: n.end } });
|
|
561
|
+
} while (this.eatOp(","));
|
|
562
|
+
this.expectOp(")");
|
|
563
|
+
this.expectOp("=");
|
|
564
|
+
const init = this.expression();
|
|
565
|
+
this.expectOp(";", "after declaration");
|
|
566
|
+
if (names.length < 2) {
|
|
567
|
+
this.errorAt(this.spanFrom(start), "destructuring needs at least two names: `let (a, b) = …;`", undefined, "CL1109");
|
|
568
|
+
}
|
|
569
|
+
return { kind: "Var", declKind: mutable ? "let mut" : "let", names, unpack: true, declaredType: null, decltypeOf: null, init, span: this.spanFrom(start) };
|
|
570
|
+
}
|
|
571
|
+
return this.finishVar(start, mutable ? "let mut" : "let", null, null);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private finishVar(start: number, declKind: DeclKind, declaredType: TypeNode | null, decltypeOf: Expr | null): Stmt {
|
|
575
|
+
const n = this.expectName("variable name");
|
|
576
|
+
let type = declaredType;
|
|
577
|
+
if (!type && this.eatOp(":")) {
|
|
578
|
+
type = this.type();
|
|
579
|
+
}
|
|
580
|
+
let init: Expr | null = null;
|
|
581
|
+
if (this.eatOp("=")) {
|
|
582
|
+
init = this.expression();
|
|
583
|
+
} else if (!this.isOp(";")) {
|
|
584
|
+
this.expectOp("=", "in declaration");
|
|
585
|
+
}
|
|
586
|
+
this.expectOp(";", "after declaration");
|
|
587
|
+
return {
|
|
588
|
+
kind: "Var",
|
|
589
|
+
declKind,
|
|
590
|
+
names: [{ name: n.text, span: { start: n.start, end: n.end } }],
|
|
591
|
+
unpack: false,
|
|
592
|
+
declaredType: type,
|
|
593
|
+
decltypeOf,
|
|
594
|
+
init,
|
|
595
|
+
span: this.spanFrom(start),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private keywordDecl(kind: DeclKind): Stmt {
|
|
600
|
+
const start = this.tok.start;
|
|
601
|
+
this.advance();
|
|
602
|
+
return this.finishVar(start, kind, null, null);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private observableDecl(): Stmt {
|
|
606
|
+
const start = this.tok.start;
|
|
607
|
+
this.advance();
|
|
608
|
+
let type: TypeNode | null = null;
|
|
609
|
+
const mark = this.i;
|
|
610
|
+
const maybe = this.tryType();
|
|
611
|
+
if (maybe && this.isIdent()) {
|
|
612
|
+
type = maybe;
|
|
613
|
+
} else {
|
|
614
|
+
this.i = mark;
|
|
615
|
+
}
|
|
616
|
+
return this.finishVar(start, "observable", type, null);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private decltypeDecl(): Stmt {
|
|
620
|
+
const start = this.tok.start;
|
|
621
|
+
this.advance();
|
|
622
|
+
this.expectOp("(");
|
|
623
|
+
const of = this.expression();
|
|
624
|
+
this.expectOp(")");
|
|
625
|
+
return this.finishVar(start, "decltype", null, of);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private tryTypedDecl(): Stmt | null {
|
|
629
|
+
const t = this.tok;
|
|
630
|
+
if (t.kind !== "ident" && !(t.kind === "keyword" && TYPE_KEYWORDS.has(t.text) && t.text !== "void")) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
const mark = this.i;
|
|
634
|
+
const type = this.tryType();
|
|
635
|
+
if (type && this.isIdent() && (this.isOp("=", 1) || this.isOp(";", 1))) {
|
|
636
|
+
return this.finishVar(t.start, "typed", type, null);
|
|
637
|
+
}
|
|
638
|
+
this.i = mark;
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private ifStmt(): Stmt {
|
|
643
|
+
const start = this.tok.start;
|
|
644
|
+
this.advance();
|
|
645
|
+
this.expectOp("(", "after 'if'");
|
|
646
|
+
let letName: VarName | null = null;
|
|
647
|
+
if (this.eatKw("let")) {
|
|
648
|
+
const n = this.expectName("variable name");
|
|
649
|
+
letName = { name: n.text, span: { start: n.start, end: n.end } };
|
|
650
|
+
this.expectOp("=");
|
|
651
|
+
}
|
|
652
|
+
const cond = this.expression();
|
|
653
|
+
this.expectOp(")", "to close the condition");
|
|
654
|
+
const then = this.block();
|
|
655
|
+
let elseBody: Stmt[] | null = null;
|
|
656
|
+
if (this.eatKw("else")) {
|
|
657
|
+
if (this.isKw("if")) {
|
|
658
|
+
elseBody = [this.ifStmt()];
|
|
659
|
+
} else {
|
|
660
|
+
elseBody = this.block();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return { kind: "If", cond, letName, then, else: elseBody, span: this.spanFrom(start) };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private whileStmt(): Stmt {
|
|
667
|
+
const start = this.tok.start;
|
|
668
|
+
this.advance();
|
|
669
|
+
this.expectOp("(", "after 'while'");
|
|
670
|
+
const cond = this.expression();
|
|
671
|
+
this.expectOp(")", "to close the condition");
|
|
672
|
+
const body = this.block();
|
|
673
|
+
return { kind: "While", cond, body, span: this.spanFrom(start) };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private forStmt(): Stmt {
|
|
677
|
+
const start = this.tok.start;
|
|
678
|
+
this.advance();
|
|
679
|
+
this.expectOp("(", "after 'for'");
|
|
680
|
+
// for (let [mut] x in …)
|
|
681
|
+
if (this.isKw("let")) {
|
|
682
|
+
const nameAt = this.isKw("mut", 1) ? 2 : 1;
|
|
683
|
+
if (this.peek(nameAt).kind === "ident" && this.isKw("in", nameAt + 1)) {
|
|
684
|
+
this.advance();
|
|
685
|
+
const mutable = this.eatKw("mut");
|
|
686
|
+
const n = this.expectName("loop variable");
|
|
687
|
+
this.advance(); // in
|
|
688
|
+
const iterable = this.expression();
|
|
689
|
+
this.expectOp(")");
|
|
690
|
+
const body = this.block();
|
|
691
|
+
return { kind: "ForIn", name: { name: n.text, span: { start: n.start, end: n.end } }, mutable, iterable, body, span: this.spanFrom(start) };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// for (Player p in …) — typed loop variable (Cluaupp)
|
|
695
|
+
{
|
|
696
|
+
const mark = this.i;
|
|
697
|
+
const type = this.tryType();
|
|
698
|
+
if (type && this.isIdent() && this.isKw("in", 1)) {
|
|
699
|
+
const n = this.advance();
|
|
700
|
+
this.advance();
|
|
701
|
+
const iterable = this.expression();
|
|
702
|
+
this.expectOp(")");
|
|
703
|
+
const body = this.block();
|
|
704
|
+
return { kind: "ForIn", name: { name: n.text, span: { start: n.start, end: n.end } }, mutable: false, iterable, body, declaredType: type, span: this.spanFrom(start) };
|
|
705
|
+
}
|
|
706
|
+
this.i = mark;
|
|
707
|
+
}
|
|
708
|
+
let init: Stmt | null = null;
|
|
709
|
+
if (this.isOp(";")) {
|
|
710
|
+
this.advance();
|
|
711
|
+
} else if (this.isKw("let")) {
|
|
712
|
+
init = this.letStmt();
|
|
713
|
+
} else {
|
|
714
|
+
const typed = this.tryTypedDecl();
|
|
715
|
+
init = typed ?? this.simpleAssign(true);
|
|
716
|
+
}
|
|
717
|
+
let cond: Expr | null = null;
|
|
718
|
+
if (!this.isOp(";")) {
|
|
719
|
+
cond = this.expression();
|
|
720
|
+
}
|
|
721
|
+
this.expectOp(";", "after the loop condition");
|
|
722
|
+
let step: Stmt | null = null;
|
|
723
|
+
if (!this.isOp(")")) {
|
|
724
|
+
step = this.simpleAssign(false);
|
|
725
|
+
}
|
|
726
|
+
this.expectOp(")", "to close the loop header");
|
|
727
|
+
const body = this.block();
|
|
728
|
+
return { kind: "For", init, cond, step, body, span: this.spanFrom(start) };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** `x = e` / `x += e` / `x++` with or without the trailing `;`. */
|
|
732
|
+
private simpleAssign(requireSemicolon: boolean): Stmt {
|
|
733
|
+
const start = this.tok.start;
|
|
734
|
+
const target = this.expression();
|
|
735
|
+
let stmt: Stmt;
|
|
736
|
+
if (this.tok.kind === "op" && ASSIGN_OPS.has(this.tok.text)) {
|
|
737
|
+
const op = this.advance().text as "=";
|
|
738
|
+
const value = this.expression();
|
|
739
|
+
stmt = { kind: "Assign", target, op, value, span: this.spanFrom(start) };
|
|
740
|
+
} else {
|
|
741
|
+
stmt = { kind: "Expr", expr: target, span: this.spanFrom(start) };
|
|
742
|
+
}
|
|
743
|
+
if (requireSemicolon) {
|
|
744
|
+
this.expectOp(";");
|
|
745
|
+
}
|
|
746
|
+
return stmt;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private switchStmt(): Stmt {
|
|
750
|
+
const start = this.tok.start;
|
|
751
|
+
this.advance();
|
|
752
|
+
this.expectOp("(", "after 'switch'");
|
|
753
|
+
const subject = this.expression();
|
|
754
|
+
this.expectOp(")");
|
|
755
|
+
this.expectOp("{");
|
|
756
|
+
const cases: SwitchCase[] = [];
|
|
757
|
+
let sawDefault = false;
|
|
758
|
+
while (!this.isOp("}") && !this.atEnd()) {
|
|
759
|
+
const caseStart = this.tok.start;
|
|
760
|
+
let test: Expr | null = null;
|
|
761
|
+
if (this.eatKw("case")) {
|
|
762
|
+
test = this.expression();
|
|
763
|
+
} else if (this.eatKw("default")) {
|
|
764
|
+
if (sawDefault) {
|
|
765
|
+
this.errorAt(this.spanFrom(caseStart), "switch already has a default case", undefined, "CL1110");
|
|
766
|
+
}
|
|
767
|
+
sawDefault = true;
|
|
768
|
+
} else {
|
|
769
|
+
this.errorAt({ start: this.tok.start, end: this.tok.end }, "expected 'case' or 'default'", undefined, "CL1111");
|
|
770
|
+
throw new ParseAbort();
|
|
771
|
+
}
|
|
772
|
+
this.expectOp(":", "after the case label");
|
|
773
|
+
const body: Stmt[] = [];
|
|
774
|
+
while (!this.isKw("case") && !this.isKw("default") && !this.isOp("}") && !this.atEnd()) {
|
|
775
|
+
const before = this.i;
|
|
776
|
+
const stmt = this.statementSafe(false);
|
|
777
|
+
if (stmt) {
|
|
778
|
+
body.push(stmt);
|
|
779
|
+
} else if (this.i === before) {
|
|
780
|
+
this.advance();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
cases.push({ test, body, span: this.spanFrom(caseStart) });
|
|
784
|
+
}
|
|
785
|
+
this.expectOp("}");
|
|
786
|
+
return { kind: "Switch", subject, cases, span: this.spanFrom(start) };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private matchStmt(): Stmt {
|
|
790
|
+
const start = this.tok.start;
|
|
791
|
+
this.advance();
|
|
792
|
+
this.expectOp("(", "after 'match'");
|
|
793
|
+
const subject = this.expression();
|
|
794
|
+
this.expectOp(")");
|
|
795
|
+
this.expectOp("{");
|
|
796
|
+
const arms: MatchArm[] = [];
|
|
797
|
+
while (!this.isOp("}") && !this.atEnd()) {
|
|
798
|
+
const armStart = this.tok.start;
|
|
799
|
+
try {
|
|
800
|
+
const pattern = this.pattern();
|
|
801
|
+
let guard: Expr | null = null;
|
|
802
|
+
if (this.eatKw("guard")) {
|
|
803
|
+
guard = this.expression();
|
|
804
|
+
}
|
|
805
|
+
this.expectOp("~>", "after the match pattern");
|
|
806
|
+
let body: Stmt[];
|
|
807
|
+
if (this.isOp("{")) {
|
|
808
|
+
body = this.block();
|
|
809
|
+
} else {
|
|
810
|
+
const stmt = this.statement(false);
|
|
811
|
+
body = stmt ? [stmt] : [];
|
|
812
|
+
}
|
|
813
|
+
arms.push({ pattern, guard, body, span: this.spanFrom(armStart) });
|
|
814
|
+
} catch (err) {
|
|
815
|
+
if (!(err instanceof ParseAbort)) {
|
|
816
|
+
throw err;
|
|
817
|
+
}
|
|
818
|
+
this.synchronize();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
this.expectOp("}");
|
|
822
|
+
return { kind: "Match", subject, arms, span: this.spanFrom(start) };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private pattern(): Pattern {
|
|
826
|
+
const t = this.tok;
|
|
827
|
+
if (t.kind === "ident" && t.text === "_" && (this.isOp("~>", 1) || this.isKw("guard", 1))) {
|
|
828
|
+
this.advance();
|
|
829
|
+
return { kind: "Wildcard", span: { start: t.start, end: t.end } };
|
|
830
|
+
}
|
|
831
|
+
const expr = this.orExpr();
|
|
832
|
+
const span = expr.span;
|
|
833
|
+
if (expr.kind === "Name" && !expr.typeArgs) {
|
|
834
|
+
return { kind: "Case", name: expr.name, qualifier: null, binding: null, span };
|
|
835
|
+
}
|
|
836
|
+
if (expr.kind === "Call" && expr.args.length === 1 && expr.args[0].name === null && expr.args[0].value.kind === "Name") {
|
|
837
|
+
const arg = expr.args[0].value;
|
|
838
|
+
const binding: VarName = { name: arg.name, span: arg.span };
|
|
839
|
+
if (expr.callee.kind === "Name") {
|
|
840
|
+
return { kind: "Case", name: expr.callee.name, qualifier: null, binding, span };
|
|
841
|
+
}
|
|
842
|
+
if (expr.callee.kind === "Member") {
|
|
843
|
+
return { kind: "Case", name: expr.callee.name, qualifier: expr.callee.object, binding, span };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return { kind: "Value", expr, span };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private tryStmt(): Stmt {
|
|
850
|
+
const start = this.tok.start;
|
|
851
|
+
this.advance();
|
|
852
|
+
const body = this.block();
|
|
853
|
+
if (!this.eatKw("catch")) {
|
|
854
|
+
this.errorAt({ start: this.tok.start, end: this.tok.end }, "expected 'catch' after the try block", undefined, "CL1112");
|
|
855
|
+
throw new ParseAbort();
|
|
856
|
+
}
|
|
857
|
+
let catchName: VarName | null = null;
|
|
858
|
+
if (this.eatOp("(")) {
|
|
859
|
+
if (this.isIdent()) {
|
|
860
|
+
const n = this.advance();
|
|
861
|
+
catchName = { name: n.text, span: { start: n.start, end: n.end } };
|
|
862
|
+
}
|
|
863
|
+
this.expectOp(")");
|
|
864
|
+
}
|
|
865
|
+
const handler = this.block();
|
|
866
|
+
return { kind: "Try", body, catchName, handler, span: this.spanFrom(start) };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private usingStmt(): Stmt {
|
|
870
|
+
const start = this.tok.start;
|
|
871
|
+
this.advance();
|
|
872
|
+
const mod = this.expectName("module alias");
|
|
873
|
+
this.expectOp(".");
|
|
874
|
+
const member = this.expectMemberName();
|
|
875
|
+
this.expectOp(";");
|
|
876
|
+
return { kind: "Using", module: mod.text, member: member.text, memberSpan: { start: member.start, end: member.end }, span: this.spanFrom(start) };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
private signalDecl(): Stmt {
|
|
880
|
+
const start = this.tok.start;
|
|
881
|
+
this.advance();
|
|
882
|
+
const n = this.expectName("signal name");
|
|
883
|
+
const params: TypeNode[] = [];
|
|
884
|
+
if (this.eatOp("(")) {
|
|
885
|
+
if (!this.isOp(")")) {
|
|
886
|
+
do {
|
|
887
|
+
params.push(this.type());
|
|
888
|
+
if (this.isIdent()) {
|
|
889
|
+
this.advance();
|
|
890
|
+
}
|
|
891
|
+
} while (this.eatOp(","));
|
|
892
|
+
}
|
|
893
|
+
this.expectOp(")");
|
|
894
|
+
}
|
|
895
|
+
this.expectOp(";", "after signal declaration");
|
|
896
|
+
return { kind: "Signal", name: { name: n.text, span: { start: n.start, end: n.end } }, params, span: this.spanFrom(start) };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ---------- declarations ----------
|
|
900
|
+
|
|
901
|
+
private link(attrs: Attribute[]): LinkDecl {
|
|
902
|
+
const start = this.tok.start;
|
|
903
|
+
this.advance();
|
|
904
|
+
let path = "";
|
|
905
|
+
let isPackage = false;
|
|
906
|
+
let pathSpan: Span;
|
|
907
|
+
if (this.isOp("@")) {
|
|
908
|
+
const at = this.advance();
|
|
909
|
+
const first = this.expectMemberName();
|
|
910
|
+
path = `@${first.text}`;
|
|
911
|
+
while (this.eatOp(".")) {
|
|
912
|
+
path += `.${this.expectMemberName().text}`;
|
|
913
|
+
}
|
|
914
|
+
isPackage = true;
|
|
915
|
+
pathSpan = { start: at.start, end: this.prevEnd() };
|
|
916
|
+
} else if (this.tok.kind === "string") {
|
|
917
|
+
const s = this.advance();
|
|
918
|
+
path = String(s.value);
|
|
919
|
+
pathSpan = { start: s.start, end: s.end };
|
|
920
|
+
} else if (this.isOp("{")) {
|
|
921
|
+
this.errorAt({ start: this.tok.start, end: this.tok.end }, "named imports `import { A } from \"…\"` were removed in CL++ 0.9", ["use `link \"./file.clp\" as Name;` and refer to `Name.A`"], "CL1113");
|
|
922
|
+
throw new ParseAbort();
|
|
923
|
+
} else {
|
|
924
|
+
this.errorAt({ start: this.tok.start, end: this.tok.end }, "expected a module: `@clpp.roblox` or \"./file.clp\"", undefined, "CL1114");
|
|
925
|
+
throw new ParseAbort();
|
|
926
|
+
}
|
|
927
|
+
let alias = "";
|
|
928
|
+
let aliasExplicit = false;
|
|
929
|
+
let aliasSpan = pathSpan;
|
|
930
|
+
if (this.eatKw("as")) {
|
|
931
|
+
const a = this.expectName("module alias");
|
|
932
|
+
alias = a.text;
|
|
933
|
+
aliasExplicit = true;
|
|
934
|
+
aliasSpan = { start: a.start, end: a.end };
|
|
935
|
+
} else {
|
|
936
|
+
alias = defaultAlias(path);
|
|
937
|
+
}
|
|
938
|
+
this.expectOp(";", "after link");
|
|
939
|
+
if (!alias) {
|
|
940
|
+
this.errorAt(pathSpan, "cannot name this module; add `as Name`", undefined, "CL1115");
|
|
941
|
+
}
|
|
942
|
+
return { kind: "Link", path, isPackage, alias, aliasExplicit, pathSpan, aliasSpan, span: this.spanFrom(start), attributes: attrs };
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
private typeParams(): string[] {
|
|
946
|
+
const out: string[] = [];
|
|
947
|
+
if (this.eatOp("<")) {
|
|
948
|
+
do {
|
|
949
|
+
out.push(this.expectName("type parameter").text);
|
|
950
|
+
} while (this.eatOp(","));
|
|
951
|
+
this.expectOp(">");
|
|
952
|
+
}
|
|
953
|
+
return out;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private struct(attrs: Attribute[], isAbstract: boolean, isFinal: boolean): StructDecl {
|
|
957
|
+
const start = this.tok.start;
|
|
958
|
+
const doc = this.takeDoc();
|
|
959
|
+
this.advance();
|
|
960
|
+
const n = this.expectName("struct name");
|
|
961
|
+
const typeParams = this.typeParams();
|
|
962
|
+
const bases: TypeNode[] = [];
|
|
963
|
+
if (this.eatOp(":")) {
|
|
964
|
+
do {
|
|
965
|
+
bases.push(this.type());
|
|
966
|
+
} while (this.eatOp(","));
|
|
967
|
+
}
|
|
968
|
+
this.expectOp("{", "to open the struct body");
|
|
969
|
+
const decl: StructDecl = {
|
|
970
|
+
kind: "Struct",
|
|
971
|
+
name: n.text,
|
|
972
|
+
nameSpan: { start: n.start, end: n.end },
|
|
973
|
+
span: { start, end: start },
|
|
974
|
+
typeParams,
|
|
975
|
+
bases,
|
|
976
|
+
fields: [],
|
|
977
|
+
methods: [],
|
|
978
|
+
isAbstract,
|
|
979
|
+
isFinal,
|
|
980
|
+
attributes: attrs,
|
|
981
|
+
doc,
|
|
982
|
+
};
|
|
983
|
+
while (!this.isOp("}") && !this.atEnd()) {
|
|
984
|
+
try {
|
|
985
|
+
this.member(decl);
|
|
986
|
+
} catch (err) {
|
|
987
|
+
if (!(err instanceof ParseAbort)) {
|
|
988
|
+
throw err;
|
|
989
|
+
}
|
|
990
|
+
this.synchronize();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
this.expectOp("}", "to close the struct");
|
|
994
|
+
this.eatOp(";");
|
|
995
|
+
decl.span = this.spanFrom(start);
|
|
996
|
+
return decl;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private member(decl: StructDecl): void {
|
|
1000
|
+
while (this.tok.kind === "doc") {
|
|
1001
|
+
this.pendingDoc = String(this.tok.value || "");
|
|
1002
|
+
this.advance();
|
|
1003
|
+
}
|
|
1004
|
+
const attrs = this.attributes();
|
|
1005
|
+
const mods = { isPrivate: false, isOverride: false, isAbstract: false, isFinal: false, isStatic: false, isAsync: false };
|
|
1006
|
+
for (;;) {
|
|
1007
|
+
if (this.eatKw("private")) {
|
|
1008
|
+
mods.isPrivate = true;
|
|
1009
|
+
} else if (this.eatKw("public")) {
|
|
1010
|
+
mods.isPrivate = false;
|
|
1011
|
+
} else if (this.eatKw("override")) {
|
|
1012
|
+
mods.isOverride = true;
|
|
1013
|
+
} else if (this.eatKw("abstract")) {
|
|
1014
|
+
mods.isAbstract = true;
|
|
1015
|
+
} else if (this.eatKw("final")) {
|
|
1016
|
+
mods.isFinal = true;
|
|
1017
|
+
} else if (this.eatKw("static")) {
|
|
1018
|
+
mods.isStatic = true;
|
|
1019
|
+
} else if (this.isKw("async") && this.isKw("func", 1)) {
|
|
1020
|
+
this.advance();
|
|
1021
|
+
mods.isAsync = true;
|
|
1022
|
+
} else {
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (this.isKw("func")) {
|
|
1027
|
+
const fn = this.func(attrs, mods);
|
|
1028
|
+
if (decl.methods.some((m) => m.name === fn.name)) {
|
|
1029
|
+
this.errorAt(fn.nameSpan, `method '${fn.name}' is already declared in struct ${decl.name}`, undefined, "CL2001");
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
decl.methods.push(fn);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (this.isKw("void") && this.isIdent(1) && this.isOp("(", 2)) {
|
|
1036
|
+
const fn = this.voidFunc(attrs);
|
|
1037
|
+
Object.assign(fn, mods);
|
|
1038
|
+
decl.methods.push(fn);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const start = this.tok.start;
|
|
1042
|
+
const doc = this.takeDoc();
|
|
1043
|
+
const type = this.type();
|
|
1044
|
+
const n = this.expectName("field name");
|
|
1045
|
+
let defaultValue: Expr | null = null;
|
|
1046
|
+
if (this.eatOp("=")) {
|
|
1047
|
+
defaultValue = this.expression();
|
|
1048
|
+
}
|
|
1049
|
+
this.expectOp(";", "after field");
|
|
1050
|
+
if (decl.fields.some((f) => f.name === n.text)) {
|
|
1051
|
+
this.errorAt({ start: n.start, end: n.end }, `field '${n.text}' is already declared in struct ${decl.name}`, undefined, "CL2002");
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const field: FieldDecl = { name: n.text, nameSpan: { start: n.start, end: n.end }, type, isPrivate: mods.isPrivate, span: this.spanFrom(start), doc };
|
|
1055
|
+
if (defaultValue) {
|
|
1056
|
+
field.defaultValue = defaultValue;
|
|
1057
|
+
}
|
|
1058
|
+
decl.fields.push(field);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private enumDecl(attrs: Attribute[]): EnumDecl {
|
|
1062
|
+
const start = this.tok.start;
|
|
1063
|
+
const doc = this.takeDoc();
|
|
1064
|
+
this.advance();
|
|
1065
|
+
const n = this.expectName("enum name");
|
|
1066
|
+
this.expectOp("{");
|
|
1067
|
+
const cases: EnumDecl["cases"] = [];
|
|
1068
|
+
while (!this.isOp("}") && !this.atEnd()) {
|
|
1069
|
+
const c = this.expectName("enum case");
|
|
1070
|
+
let payload: TypeNode | null = null;
|
|
1071
|
+
if (this.eatOp("(")) {
|
|
1072
|
+
payload = this.type();
|
|
1073
|
+
this.expectOp(")");
|
|
1074
|
+
}
|
|
1075
|
+
if (cases.some((x) => x.name === c.text)) {
|
|
1076
|
+
this.errorAt({ start: c.start, end: c.end }, `case '${c.text}' is already declared in enum ${n.text}`, undefined, "CL2003");
|
|
1077
|
+
} else {
|
|
1078
|
+
cases.push({ name: c.text, span: { start: c.start, end: c.end }, payload });
|
|
1079
|
+
}
|
|
1080
|
+
if (!this.eatOp(",")) {
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
this.expectOp("}", "to close the enum");
|
|
1085
|
+
this.eatOp(";");
|
|
1086
|
+
return { kind: "Enum", name: n.text, nameSpan: { start: n.start, end: n.end }, span: this.spanFrom(start), cases, attributes: attrs, doc };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private variant(attrs: Attribute[]): VariantDecl {
|
|
1090
|
+
const start = this.tok.start;
|
|
1091
|
+
this.advance();
|
|
1092
|
+
const n = this.expectName("variant name");
|
|
1093
|
+
this.expectOp("{");
|
|
1094
|
+
const alternatives: TypeNode[] = [];
|
|
1095
|
+
while (!this.isOp("}") && !this.atEnd()) {
|
|
1096
|
+
alternatives.push(this.type());
|
|
1097
|
+
if (!this.eatOp(",")) {
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
this.expectOp("}");
|
|
1102
|
+
this.eatOp(";");
|
|
1103
|
+
if (alternatives.length === 0) {
|
|
1104
|
+
this.errorAt(this.spanFrom(start), "a variant needs at least one type", undefined, "CL1116");
|
|
1105
|
+
}
|
|
1106
|
+
return { kind: "Variant", name: n.text, nameSpan: { start: n.start, end: n.end }, span: this.spanFrom(start), alternatives, attributes: attrs };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
private typeAlias(attrs: Attribute[]): TypeAliasDecl {
|
|
1110
|
+
const start = this.tok.start;
|
|
1111
|
+
this.advance();
|
|
1112
|
+
const n = this.expectName("type name");
|
|
1113
|
+
this.expectOp("=");
|
|
1114
|
+
const type = this.type();
|
|
1115
|
+
this.expectOp(";", "after type alias");
|
|
1116
|
+
return { kind: "TypeAlias", name: n.text, nameSpan: { start: n.start, end: n.end }, span: this.spanFrom(start), type, attributes: attrs };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private namespace(attrs: Attribute[]): NamespaceDecl {
|
|
1120
|
+
const start = this.tok.start;
|
|
1121
|
+
this.advance();
|
|
1122
|
+
const n = this.expectName("namespace name");
|
|
1123
|
+
this.expectOp("{");
|
|
1124
|
+
const funcs: FuncDecl[] = [];
|
|
1125
|
+
while (!this.isOp("}") && !this.atEnd()) {
|
|
1126
|
+
try {
|
|
1127
|
+
const fattrs = this.attributes();
|
|
1128
|
+
const isStatic = this.eatKw("static");
|
|
1129
|
+
const isAsync = this.isKw("async") && this.isKw("func", 1) ? (this.advance(), true) : false;
|
|
1130
|
+
if (this.isKw("func")) {
|
|
1131
|
+
funcs.push(this.func(fattrs, { isAsync, isStatic }));
|
|
1132
|
+
} else if (this.isKw("void") && this.isIdent(1)) {
|
|
1133
|
+
funcs.push(this.voidFunc(fattrs));
|
|
1134
|
+
} else {
|
|
1135
|
+
this.errorAt({ start: this.tok.start, end: this.tok.end }, "a namespace can only contain functions", undefined, "CL1117");
|
|
1136
|
+
throw new ParseAbort();
|
|
1137
|
+
}
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
if (!(err instanceof ParseAbort)) {
|
|
1140
|
+
throw err;
|
|
1141
|
+
}
|
|
1142
|
+
this.synchronize();
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
this.expectOp("}");
|
|
1146
|
+
return { kind: "Namespace", name: n.text, nameSpan: { start: n.start, end: n.end }, span: this.spanFrom(start), funcs, attributes: attrs };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
private func(attrs: Attribute[], mods: Partial<FuncDecl>): FuncDecl {
|
|
1150
|
+
const start = this.tok.start;
|
|
1151
|
+
const doc = this.takeDoc();
|
|
1152
|
+
this.advance(); // func
|
|
1153
|
+
let name: string;
|
|
1154
|
+
let nameSpan: Span;
|
|
1155
|
+
let operator: FuncDecl["operator"] = null;
|
|
1156
|
+
if (this.eatKw("operator")) {
|
|
1157
|
+
const sig = this.tok;
|
|
1158
|
+
if (this.eatOp("+")) {
|
|
1159
|
+
operator = "+";
|
|
1160
|
+
} else if (this.eatOp("-")) {
|
|
1161
|
+
operator = "-";
|
|
1162
|
+
} else if (this.eatOp("[")) {
|
|
1163
|
+
this.expectOp("]");
|
|
1164
|
+
operator = "[]";
|
|
1165
|
+
} else {
|
|
1166
|
+
this.errorAt({ start: sig.start, end: sig.end }, "expected '+', '-' or '[]' after 'operator'", undefined, "CL1118");
|
|
1167
|
+
throw new ParseAbort();
|
|
1168
|
+
}
|
|
1169
|
+
name = `operator${operator}`;
|
|
1170
|
+
nameSpan = { start: sig.start, end: this.prevEnd() };
|
|
1171
|
+
} else {
|
|
1172
|
+
const n = this.expectName("function name");
|
|
1173
|
+
name = n.text;
|
|
1174
|
+
nameSpan = { start: n.start, end: n.end };
|
|
1175
|
+
}
|
|
1176
|
+
const typeParams = this.typeParams();
|
|
1177
|
+
const params = this.params();
|
|
1178
|
+
let returnType: TypeNode | null = null;
|
|
1179
|
+
if (this.eatOp("->")) {
|
|
1180
|
+
returnType = this.type();
|
|
1181
|
+
}
|
|
1182
|
+
const where: FuncDecl["where"] = [];
|
|
1183
|
+
if (this.eatKw("where")) {
|
|
1184
|
+
do {
|
|
1185
|
+
const w = this.expectName("type parameter");
|
|
1186
|
+
this.expectOp(":");
|
|
1187
|
+
where.push({ name: w.text, type: this.type() });
|
|
1188
|
+
} while (this.eatOp(","));
|
|
1189
|
+
}
|
|
1190
|
+
let body: Stmt[] | null = null;
|
|
1191
|
+
if (this.isOp("{")) {
|
|
1192
|
+
body = this.block();
|
|
1193
|
+
} else {
|
|
1194
|
+
this.expectOp(";", "or a body `{ … }` after the function signature");
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
kind: "Func",
|
|
1198
|
+
name,
|
|
1199
|
+
nameSpan,
|
|
1200
|
+
span: this.spanFrom(start),
|
|
1201
|
+
typeParams,
|
|
1202
|
+
params,
|
|
1203
|
+
returnType,
|
|
1204
|
+
body,
|
|
1205
|
+
isAsync: false,
|
|
1206
|
+
isExtern: false,
|
|
1207
|
+
isStatic: false,
|
|
1208
|
+
isAbstract: false,
|
|
1209
|
+
isOverride: false,
|
|
1210
|
+
isFinal: false,
|
|
1211
|
+
isPrivate: false,
|
|
1212
|
+
operator,
|
|
1213
|
+
where,
|
|
1214
|
+
attributes: attrs,
|
|
1215
|
+
doc,
|
|
1216
|
+
...mods,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private voidFunc(attrs: Attribute[]): FuncDecl {
|
|
1221
|
+
const start = this.tok.start;
|
|
1222
|
+
const doc = this.takeDoc();
|
|
1223
|
+
this.advance(); // void
|
|
1224
|
+
const n = this.expectName("function name");
|
|
1225
|
+
const params = this.params();
|
|
1226
|
+
const body = this.block();
|
|
1227
|
+
return {
|
|
1228
|
+
kind: "Func",
|
|
1229
|
+
name: n.text,
|
|
1230
|
+
nameSpan: { start: n.start, end: n.end },
|
|
1231
|
+
span: this.spanFrom(start),
|
|
1232
|
+
typeParams: [],
|
|
1233
|
+
params,
|
|
1234
|
+
returnType: { kind: "Named", name: "void", args: [], span: { start, end: start + 4 } },
|
|
1235
|
+
body,
|
|
1236
|
+
isAsync: false,
|
|
1237
|
+
isExtern: false,
|
|
1238
|
+
isStatic: false,
|
|
1239
|
+
isAbstract: false,
|
|
1240
|
+
isOverride: false,
|
|
1241
|
+
isFinal: false,
|
|
1242
|
+
isPrivate: false,
|
|
1243
|
+
operator: null,
|
|
1244
|
+
where: [],
|
|
1245
|
+
attributes: attrs,
|
|
1246
|
+
doc,
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
private params(): Param[] {
|
|
1251
|
+
this.expectOp("(", "to open the parameter list");
|
|
1252
|
+
const params: Param[] = [];
|
|
1253
|
+
if (!this.isOp(")")) {
|
|
1254
|
+
do {
|
|
1255
|
+
const pStart = this.tok.start;
|
|
1256
|
+
let type: TypeNode | null = null;
|
|
1257
|
+
const mark = this.i;
|
|
1258
|
+
const maybe = this.tryType();
|
|
1259
|
+
if (maybe && (this.isIdent() || this.isOp("..."))) {
|
|
1260
|
+
type = maybe;
|
|
1261
|
+
} else {
|
|
1262
|
+
this.i = mark;
|
|
1263
|
+
}
|
|
1264
|
+
const variadic = this.eatOp("...");
|
|
1265
|
+
const n = this.expectName("parameter name");
|
|
1266
|
+
let defaultValue: Expr | null = null;
|
|
1267
|
+
if (this.eatOp("=")) {
|
|
1268
|
+
defaultValue = this.expression();
|
|
1269
|
+
}
|
|
1270
|
+
params.push({ name: n.text, span: { start: n.start, end: n.end }, type, variadic, defaultValue });
|
|
1271
|
+
if (variadic && this.isOp(",")) {
|
|
1272
|
+
this.errorAt(this.spanFrom(pStart), "the variadic parameter must be the last one", undefined, "CL1119");
|
|
1273
|
+
}
|
|
1274
|
+
} while (this.eatOp(","));
|
|
1275
|
+
}
|
|
1276
|
+
this.expectOp(")", "to close the parameter list");
|
|
1277
|
+
return params;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ---------- types ----------
|
|
1281
|
+
|
|
1282
|
+
/** Type or an error. */
|
|
1283
|
+
private type(): TypeNode {
|
|
1284
|
+
const t = this.tok;
|
|
1285
|
+
const type = this.tryType();
|
|
1286
|
+
if (!type) {
|
|
1287
|
+
this.errorAt({ start: t.start, end: Math.max(t.end, t.start + 1) }, "expected a type", undefined, "CL1120");
|
|
1288
|
+
throw new ParseAbort();
|
|
1289
|
+
}
|
|
1290
|
+
return type;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/** Speculative type parse: returns null (and restores nothing) on failure. Never reports. */
|
|
1294
|
+
private tryType(): TypeNode | null {
|
|
1295
|
+
const start = this.tok.start;
|
|
1296
|
+
const first = this.typeAtom();
|
|
1297
|
+
if (!first) {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
if (this.isOp("|") || this.isOp("&")) {
|
|
1301
|
+
const op = this.tok.text;
|
|
1302
|
+
const members = [first];
|
|
1303
|
+
while (this.isOp(op)) {
|
|
1304
|
+
const mark = this.i;
|
|
1305
|
+
this.advance();
|
|
1306
|
+
const next = this.typeAtom();
|
|
1307
|
+
if (!next) {
|
|
1308
|
+
this.i = mark;
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
members.push(next);
|
|
1312
|
+
}
|
|
1313
|
+
if (members.length > 1) {
|
|
1314
|
+
return { kind: op === "|" ? "Union" : "Intersection", members, span: this.spanFrom(start) };
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return first;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
private typeAtom(): TypeNode | null {
|
|
1321
|
+
const t = this.tok;
|
|
1322
|
+
const start = t.start;
|
|
1323
|
+
let node: TypeNode | null = null;
|
|
1324
|
+
if (t.kind === "string") {
|
|
1325
|
+
this.advance();
|
|
1326
|
+
node = { kind: "Literal", value: String(t.value), span: { start, end: t.end } };
|
|
1327
|
+
} else if (t.kind === "int" || t.kind === "float") {
|
|
1328
|
+
this.advance();
|
|
1329
|
+
node = { kind: "Literal", value: Number(t.value), span: { start, end: t.end } };
|
|
1330
|
+
} else if (t.kind === "keyword" && (t.text === "true" || t.text === "false")) {
|
|
1331
|
+
this.advance();
|
|
1332
|
+
node = { kind: "Literal", value: t.text === "true", span: { start, end: t.end } };
|
|
1333
|
+
} else if (t.kind === "keyword" && t.text === "null") {
|
|
1334
|
+
this.advance();
|
|
1335
|
+
node = { kind: "Named", name: "null", args: [], span: { start, end: t.end } };
|
|
1336
|
+
} else if (t.kind === "keyword" && t.text === "func" && this.isOp("(", 1)) {
|
|
1337
|
+
this.advance();
|
|
1338
|
+
this.advance();
|
|
1339
|
+
const params: TypeNode[] = [];
|
|
1340
|
+
if (!this.isOp(")")) {
|
|
1341
|
+
do {
|
|
1342
|
+
const p = this.tryType();
|
|
1343
|
+
if (!p) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
params.push(p);
|
|
1347
|
+
if (this.isIdent()) {
|
|
1348
|
+
this.advance();
|
|
1349
|
+
}
|
|
1350
|
+
} while (this.eatOp(","));
|
|
1351
|
+
}
|
|
1352
|
+
if (!this.eatOp(")")) {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
let ret: TypeNode | null = null;
|
|
1356
|
+
if (this.eatOp("->")) {
|
|
1357
|
+
ret = this.typeAtom();
|
|
1358
|
+
if (!ret) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
node = { kind: "Func", params, ret, span: this.spanFrom(start) };
|
|
1363
|
+
} else if (t.kind === "op" && t.text === "(") {
|
|
1364
|
+
const mark = this.i;
|
|
1365
|
+
this.advance();
|
|
1366
|
+
const inner = this.tryType();
|
|
1367
|
+
if (!inner || !this.eatOp(")")) {
|
|
1368
|
+
this.i = mark;
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
node = inner;
|
|
1372
|
+
} else if (t.kind === "ident" || (t.kind === "keyword" && TYPE_KEYWORDS.has(t.text))) {
|
|
1373
|
+
this.advance();
|
|
1374
|
+
let name = t.text;
|
|
1375
|
+
let qualifier: string | undefined;
|
|
1376
|
+
while (this.isOp(".") && (this.peek(1).kind === "ident" || this.peek(1).kind === "keyword")) {
|
|
1377
|
+
// `Combat.Fighter`, `Enum.Material`
|
|
1378
|
+
if (this.peek(1).kind === "keyword" && !TYPE_KEYWORDS.has(this.peek(1).text)) {
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1381
|
+
this.advance();
|
|
1382
|
+
qualifier = qualifier ? `${qualifier}.${name}` : name;
|
|
1383
|
+
name = this.advance().text;
|
|
1384
|
+
}
|
|
1385
|
+
const args: TypeNode[] = [];
|
|
1386
|
+
if (this.isOp("<")) {
|
|
1387
|
+
const mark = this.i;
|
|
1388
|
+
this.advance();
|
|
1389
|
+
let ok = true;
|
|
1390
|
+
if (!this.isOp(">")) {
|
|
1391
|
+
do {
|
|
1392
|
+
const arg = this.tryType();
|
|
1393
|
+
if (!arg) {
|
|
1394
|
+
ok = false;
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
args.push(arg);
|
|
1398
|
+
} while (this.eatOp(","));
|
|
1399
|
+
}
|
|
1400
|
+
if (!ok || !this.eatOp(">")) {
|
|
1401
|
+
this.i = mark;
|
|
1402
|
+
args.length = 0;
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
node = { kind: "Named", name, args, span: this.spanFrom(start) };
|
|
1407
|
+
if (qualifier) {
|
|
1408
|
+
node.qualifier = qualifier;
|
|
1409
|
+
}
|
|
1410
|
+
} else {
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
while (this.isOp("?")) {
|
|
1414
|
+
// `Part?` — but not the ternary `cond ? a : b` (type contexts never see that).
|
|
1415
|
+
this.advance();
|
|
1416
|
+
node = { kind: "Optional", inner: node, span: this.spanFrom(start) };
|
|
1417
|
+
}
|
|
1418
|
+
return node;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ---------- expressions ----------
|
|
1422
|
+
|
|
1423
|
+
expression(): Expr {
|
|
1424
|
+
const cond = this.orExpr();
|
|
1425
|
+
if (this.isOp("?")) {
|
|
1426
|
+
this.advance();
|
|
1427
|
+
const then = this.expression();
|
|
1428
|
+
this.expectOp(":", "in the ternary expression");
|
|
1429
|
+
const otherwise = this.expression();
|
|
1430
|
+
return { kind: "Ternary", cond, then, else: otherwise, span: { start: cond.span.start, end: otherwise.span.end } };
|
|
1431
|
+
}
|
|
1432
|
+
return cond;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
private binary(op: BinaryOp, left: Expr, right: Expr, opTok: Token): Expr {
|
|
1436
|
+
return { kind: "Binary", op, left, right, opSpan: { start: opTok.start, end: opTok.end }, span: { start: left.span.start, end: right.span.end } };
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
private orExpr(): Expr {
|
|
1440
|
+
let left = this.andExpr();
|
|
1441
|
+
while (this.isKw("or") || this.isOp("||")) {
|
|
1442
|
+
const op = this.advance();
|
|
1443
|
+
left = this.binary("or", left, this.andExpr(), op);
|
|
1444
|
+
}
|
|
1445
|
+
return left;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
private andExpr(): Expr {
|
|
1449
|
+
let left = this.bitOr();
|
|
1450
|
+
while (this.isKw("and") || this.isOp("&&")) {
|
|
1451
|
+
const op = this.advance();
|
|
1452
|
+
left = this.binary("and", left, this.bitOr(), op);
|
|
1453
|
+
}
|
|
1454
|
+
return left;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
private bitOr(): Expr {
|
|
1458
|
+
let left = this.bitXor();
|
|
1459
|
+
while (this.isOp("|")) {
|
|
1460
|
+
const op = this.advance();
|
|
1461
|
+
left = this.binary("|", left, this.bitXor(), op);
|
|
1462
|
+
}
|
|
1463
|
+
return left;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private bitXor(): Expr {
|
|
1467
|
+
let left = this.bitAnd();
|
|
1468
|
+
while (this.isOp("^")) {
|
|
1469
|
+
const op = this.advance();
|
|
1470
|
+
left = this.binary("^", left, this.bitAnd(), op);
|
|
1471
|
+
}
|
|
1472
|
+
return left;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
private bitAnd(): Expr {
|
|
1476
|
+
let left = this.shift();
|
|
1477
|
+
while (this.isOp("&")) {
|
|
1478
|
+
const op = this.advance();
|
|
1479
|
+
left = this.binary("&", left, this.shift(), op);
|
|
1480
|
+
}
|
|
1481
|
+
return left;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
private shift(): Expr {
|
|
1485
|
+
let left = this.unary();
|
|
1486
|
+
while (this.isKw("shl") || this.isKw("shr")) {
|
|
1487
|
+
const op = this.advance();
|
|
1488
|
+
left = this.binary(op.text as "shl", left, this.unary(), op);
|
|
1489
|
+
}
|
|
1490
|
+
return left;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/** CL++ unary level: the operand is everything up to `shl`/`shr`. */
|
|
1494
|
+
private unary(): Expr {
|
|
1495
|
+
const t = this.tok;
|
|
1496
|
+
const start = t.start;
|
|
1497
|
+
if (t.kind === "op" && (t.text === "-" || t.text === "~" || t.text === "!")) {
|
|
1498
|
+
this.advance();
|
|
1499
|
+
const operand = this.unary();
|
|
1500
|
+
const op = t.text === "!" ? "not" : (t.text as "-" | "~");
|
|
1501
|
+
return { kind: "Unary", op, operand, loose: true, span: { start, end: operand.span.end } };
|
|
1502
|
+
}
|
|
1503
|
+
if (t.kind === "keyword") {
|
|
1504
|
+
switch (t.text) {
|
|
1505
|
+
case "not": {
|
|
1506
|
+
this.advance();
|
|
1507
|
+
const operand = this.unary();
|
|
1508
|
+
return { kind: "Unary", op: "not", operand, loose: true, span: { start, end: operand.span.end } };
|
|
1509
|
+
}
|
|
1510
|
+
case "spawn": {
|
|
1511
|
+
this.advance();
|
|
1512
|
+
const call = this.unary();
|
|
1513
|
+
return { kind: "Spawn", call, span: { start, end: call.span.end } };
|
|
1514
|
+
}
|
|
1515
|
+
case "await": {
|
|
1516
|
+
this.advance();
|
|
1517
|
+
const operand = this.unary();
|
|
1518
|
+
return { kind: "Await", operand, span: { start, end: operand.span.end } };
|
|
1519
|
+
}
|
|
1520
|
+
case "thread": {
|
|
1521
|
+
if (this.isOp(".", 1) || this.isOp("::", 1)) {
|
|
1522
|
+
break;
|
|
1523
|
+
}
|
|
1524
|
+
this.advance();
|
|
1525
|
+
const call = this.unary();
|
|
1526
|
+
return { kind: "Thread", call, span: { start, end: call.span.end } };
|
|
1527
|
+
}
|
|
1528
|
+
case "new":
|
|
1529
|
+
return this.newExpr();
|
|
1530
|
+
default:
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (t.kind === "op" && (t.text === "++" || t.text === "--")) {
|
|
1535
|
+
this.advance();
|
|
1536
|
+
const target = this.unary();
|
|
1537
|
+
return { kind: "Update", target, delta: t.text === "++" ? 1 : -1, prefix: true, span: { start, end: target.span.end } };
|
|
1538
|
+
}
|
|
1539
|
+
return this.comparison();
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
private newExpr(): Expr {
|
|
1543
|
+
const start = this.tok.start;
|
|
1544
|
+
this.advance();
|
|
1545
|
+
const target = this.primary(true);
|
|
1546
|
+
if (this.isOp("(")) {
|
|
1547
|
+
const args = this.args();
|
|
1548
|
+
return this.postfix({ kind: "New", target, args, hasParens: true, span: this.spanFrom(start) });
|
|
1549
|
+
}
|
|
1550
|
+
return { kind: "New", target, args: [], hasParens: false, span: this.spanFrom(start) };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
private comparison(): Expr {
|
|
1554
|
+
let left = this.additive();
|
|
1555
|
+
for (;;) {
|
|
1556
|
+
const t = this.tok;
|
|
1557
|
+
if (t.kind === "op" && (t.text === "==" || t.text === "!=" || t.text === "<" || t.text === "<=" || t.text === ">" || t.text === ">=")) {
|
|
1558
|
+
this.advance();
|
|
1559
|
+
left = this.binary(t.text as BinaryOp, left, this.additive(), t);
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
return left;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
private additive(): Expr {
|
|
1567
|
+
let left = this.multiplicative();
|
|
1568
|
+
for (;;) {
|
|
1569
|
+
const t = this.tok;
|
|
1570
|
+
if (t.kind === "op" && (t.text === "+" || t.text === "-" || t.text === ".:" || t.text === "..")) {
|
|
1571
|
+
this.advance();
|
|
1572
|
+
left = this.binary(t.text as BinaryOp, left, this.multiplicative(), t);
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
return left;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
private multiplicative(): Expr {
|
|
1580
|
+
let left = this.operand();
|
|
1581
|
+
for (;;) {
|
|
1582
|
+
const t = this.tok;
|
|
1583
|
+
if (t.kind === "op" && (t.text === "*" || t.text === "/" || t.text === "%")) {
|
|
1584
|
+
this.advance();
|
|
1585
|
+
left = this.binary(t.text as BinaryOp, left, this.operand(), t);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
return left;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** Operand of `*`, `+`, comparisons: a postfix expression, or a tight prefix operator (Cluaupp). */
|
|
1593
|
+
private operand(): Expr {
|
|
1594
|
+
const t = this.tok;
|
|
1595
|
+
const start = t.start;
|
|
1596
|
+
if (t.kind === "op" && (t.text === "-" || t.text === "~" || t.text === "!")) {
|
|
1597
|
+
this.advance();
|
|
1598
|
+
const inner = this.operand();
|
|
1599
|
+
return { kind: "Unary", op: t.text === "!" ? "not" : (t.text as "-" | "~"), operand: inner, loose: false, span: { start, end: inner.span.end } };
|
|
1600
|
+
}
|
|
1601
|
+
if (t.kind === "keyword" && t.text === "not") {
|
|
1602
|
+
this.advance();
|
|
1603
|
+
const inner = this.operand();
|
|
1604
|
+
return { kind: "Unary", op: "not", operand: inner, loose: false, span: { start, end: inner.span.end } };
|
|
1605
|
+
}
|
|
1606
|
+
if (t.kind === "keyword" && (t.text === "await" || t.text === "spawn")) {
|
|
1607
|
+
this.advance();
|
|
1608
|
+
const inner = this.operand();
|
|
1609
|
+
return t.text === "await" ? { kind: "Await", operand: inner, span: { start, end: inner.span.end } } : { kind: "Spawn", call: inner, span: { start, end: inner.span.end } };
|
|
1610
|
+
}
|
|
1611
|
+
if (t.kind === "keyword" && t.text === "new") {
|
|
1612
|
+
return this.newExpr();
|
|
1613
|
+
}
|
|
1614
|
+
return this.postfix(this.primary(false));
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
private args(): Arg[] {
|
|
1618
|
+
this.expectOp("(");
|
|
1619
|
+
const args: Arg[] = [];
|
|
1620
|
+
if (!this.isOp(")")) {
|
|
1621
|
+
do {
|
|
1622
|
+
if (this.isOp(")")) {
|
|
1623
|
+
break; // trailing comma
|
|
1624
|
+
}
|
|
1625
|
+
if ((this.tok.kind === "ident" || this.tok.kind === "keyword") && this.isOp(":", 1) && !this.isOp("::", 1)) {
|
|
1626
|
+
const n = this.advance();
|
|
1627
|
+
this.advance();
|
|
1628
|
+
args.push({ name: n.text, nameSpan: { start: n.start, end: n.end }, value: this.expression() });
|
|
1629
|
+
} else {
|
|
1630
|
+
args.push({ name: null, value: this.expression() });
|
|
1631
|
+
}
|
|
1632
|
+
} while (this.eatOp(","));
|
|
1633
|
+
}
|
|
1634
|
+
this.expectOp(")", "to close the argument list");
|
|
1635
|
+
return args;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/** `<T, U>` immediately followed by `(` — a generic call. Restores position when it is not. */
|
|
1639
|
+
private tryCallTypeArgs(): TypeNode[] | null {
|
|
1640
|
+
if (!this.isOp("<")) {
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
const mark = this.i;
|
|
1644
|
+
this.advance();
|
|
1645
|
+
const args: TypeNode[] = [];
|
|
1646
|
+
if (!this.isOp(">")) {
|
|
1647
|
+
do {
|
|
1648
|
+
const t = this.tryType();
|
|
1649
|
+
if (!t) {
|
|
1650
|
+
this.i = mark;
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
args.push(t);
|
|
1654
|
+
} while (this.eatOp(","));
|
|
1655
|
+
}
|
|
1656
|
+
if (!this.eatOp(">") || !this.isOp("(")) {
|
|
1657
|
+
this.i = mark;
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
return args;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
private postfix(expr: Expr): Expr {
|
|
1664
|
+
for (;;) {
|
|
1665
|
+
const t = this.tok;
|
|
1666
|
+
if (t.kind === "op") {
|
|
1667
|
+
if (t.text === ".") {
|
|
1668
|
+
this.advance();
|
|
1669
|
+
const n = this.expectMemberName();
|
|
1670
|
+
const typeArgs = this.tryCallTypeArgs();
|
|
1671
|
+
expr = { kind: "Member", object: expr, name: n.text, nameSpan: { start: n.start, end: n.end }, typeArgs, span: { start: expr.span.start, end: n.end } };
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
if (t.text === "::") {
|
|
1675
|
+
this.advance();
|
|
1676
|
+
const n = this.expectMemberName();
|
|
1677
|
+
expr = { kind: "Scope", object: expr, name: n.text, nameSpan: { start: n.start, end: n.end }, span: { start: expr.span.start, end: n.end } };
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
if (t.text === "[") {
|
|
1681
|
+
if (this.isOp("[", 1) && expr.kind !== "Index") {
|
|
1682
|
+
// `[[attr]]` never follows an expression; treat as index anyway.
|
|
1683
|
+
}
|
|
1684
|
+
this.advance();
|
|
1685
|
+
const index = this.expression();
|
|
1686
|
+
this.expectOp("]", "to close the index");
|
|
1687
|
+
expr = { kind: "Index", object: expr, index, span: this.spanFrom(expr.span.start) };
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
if (t.text === "(") {
|
|
1691
|
+
const args = this.args();
|
|
1692
|
+
expr = { kind: "Call", callee: expr, args, span: this.spanFrom(expr.span.start) };
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (t.text === "++" || t.text === "--") {
|
|
1696
|
+
this.advance();
|
|
1697
|
+
expr = { kind: "Update", target: expr, delta: t.text === "++" ? 1 : -1, prefix: false, span: this.spanFrom(expr.span.start) };
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
if (t.text === "~>" && (this.peek(1).kind === "ident") && LEGACY_SIGNAL_METHODS.has(this.peek(1).text) && this.isOp("(", 2)) {
|
|
1701
|
+
// Cluaupp 1.x `signal~>Connect(fn)` → `signal.Connect(fn)`
|
|
1702
|
+
this.advance();
|
|
1703
|
+
const n = this.advance();
|
|
1704
|
+
const member: Expr = { kind: "Member", object: expr, name: n.text, nameSpan: { start: n.start, end: n.end }, typeArgs: null, span: { start: expr.span.start, end: n.end } };
|
|
1705
|
+
const args = this.args();
|
|
1706
|
+
expr = { kind: "Call", callee: member, args, legacyConnect: true, span: this.spanFrom(expr.span.start) };
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (t.kind === "keyword" && t.text === "as" && !t.newlineBefore) {
|
|
1711
|
+
this.advance();
|
|
1712
|
+
const type = this.type();
|
|
1713
|
+
return { kind: "Cast", operand: expr, type, span: this.spanFrom(expr.span.start) };
|
|
1714
|
+
}
|
|
1715
|
+
return expr;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
private primary(forNew: boolean): Expr {
|
|
1720
|
+
const t = this.tok;
|
|
1721
|
+
const start = t.start;
|
|
1722
|
+
const span = { start, end: t.end };
|
|
1723
|
+
switch (t.kind) {
|
|
1724
|
+
case "int":
|
|
1725
|
+
case "float":
|
|
1726
|
+
this.advance();
|
|
1727
|
+
return { kind: "Number", value: Number(t.value), isFloat: t.kind === "float", raw: t.text, span };
|
|
1728
|
+
case "string":
|
|
1729
|
+
this.advance();
|
|
1730
|
+
return { kind: "String", value: String(t.value ?? ""), span };
|
|
1731
|
+
case "template":
|
|
1732
|
+
this.advance();
|
|
1733
|
+
return this.template(t);
|
|
1734
|
+
case "ident": {
|
|
1735
|
+
if (this.isOp("=>", 1)) {
|
|
1736
|
+
return this.lambdaSingle();
|
|
1737
|
+
}
|
|
1738
|
+
this.advance();
|
|
1739
|
+
const typeArgs = this.tryCallTypeArgs();
|
|
1740
|
+
return { kind: "Name", name: t.text, typeArgs, span };
|
|
1741
|
+
}
|
|
1742
|
+
case "keyword":
|
|
1743
|
+
return this.keywordPrimary(t, forNew);
|
|
1744
|
+
case "op":
|
|
1745
|
+
if (t.text === "(") {
|
|
1746
|
+
const lambda = this.tryLambdaParens();
|
|
1747
|
+
if (lambda) {
|
|
1748
|
+
return lambda;
|
|
1749
|
+
}
|
|
1750
|
+
this.advance();
|
|
1751
|
+
const inner = this.expression();
|
|
1752
|
+
this.expectOp(")", "to close the parenthesis");
|
|
1753
|
+
return inner;
|
|
1754
|
+
}
|
|
1755
|
+
if (t.text === "@") {
|
|
1756
|
+
this.advance();
|
|
1757
|
+
let n = this.expectMemberName();
|
|
1758
|
+
if (n.text === "this") {
|
|
1759
|
+
if (!this.eatOp(".") && !this.eatOp("::")) {
|
|
1760
|
+
this.errorAt({ start: this.tok.start, end: this.tok.end }, "expected '.' after '@this'", undefined, "CL1121");
|
|
1761
|
+
throw new ParseAbort();
|
|
1762
|
+
}
|
|
1763
|
+
n = this.expectMemberName();
|
|
1764
|
+
}
|
|
1765
|
+
return { kind: "SelfField", name: n.text, nameSpan: { start: n.start, end: n.end }, span: { start, end: n.end } };
|
|
1766
|
+
}
|
|
1767
|
+
break;
|
|
1768
|
+
default:
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
const where = t.kind === "eof" ? "end of file" : `'${t.text}'`;
|
|
1772
|
+
this.errorAt({ start: t.start, end: Math.max(t.end, t.start + 1) }, `expected an expression, found ${where}`, undefined, "CL1122");
|
|
1773
|
+
throw new ParseAbort();
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
private keywordPrimary(t: Token, forNew: boolean): Expr {
|
|
1777
|
+
const start = t.start;
|
|
1778
|
+
const span = { start, end: t.end };
|
|
1779
|
+
switch (t.text) {
|
|
1780
|
+
case "true":
|
|
1781
|
+
case "false":
|
|
1782
|
+
this.advance();
|
|
1783
|
+
return { kind: "Bool", value: t.text === "true", span };
|
|
1784
|
+
case "null":
|
|
1785
|
+
this.advance();
|
|
1786
|
+
return { kind: "Null", span };
|
|
1787
|
+
case "endl":
|
|
1788
|
+
this.advance();
|
|
1789
|
+
return { kind: "String", value: "\n", span };
|
|
1790
|
+
case "func": {
|
|
1791
|
+
if (this.isOp("(", 1)) {
|
|
1792
|
+
this.advance();
|
|
1793
|
+
const params = this.params();
|
|
1794
|
+
let returnType: TypeNode | null = null;
|
|
1795
|
+
if (this.eatOp("->")) {
|
|
1796
|
+
returnType = this.type();
|
|
1797
|
+
}
|
|
1798
|
+
const body = this.block();
|
|
1799
|
+
return { kind: "Function", params, returnType, body, isLambda: false, span: this.spanFrom(start) };
|
|
1800
|
+
}
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
case "parallel": {
|
|
1804
|
+
this.advance();
|
|
1805
|
+
const args = this.args();
|
|
1806
|
+
if (args.length < 1) {
|
|
1807
|
+
this.errorAt(this.spanFrom(start), "parallel(...) needs at least one call", undefined, "CL1123");
|
|
1808
|
+
}
|
|
1809
|
+
return { kind: "Parallel", calls: args.map((a) => a.value), span: this.spanFrom(start) };
|
|
1810
|
+
}
|
|
1811
|
+
case "pcall":
|
|
1812
|
+
case "move":
|
|
1813
|
+
case "join": {
|
|
1814
|
+
this.advance();
|
|
1815
|
+
this.expectOp("(", `after '${t.text}'`);
|
|
1816
|
+
const operand = this.expression();
|
|
1817
|
+
this.expectOp(")");
|
|
1818
|
+
const s = this.spanFrom(start);
|
|
1819
|
+
if (t.text === "pcall") {
|
|
1820
|
+
return { kind: "PCall", operand, span: s };
|
|
1821
|
+
}
|
|
1822
|
+
if (t.text === "move") {
|
|
1823
|
+
return { kind: "Move", operand, span: s };
|
|
1824
|
+
}
|
|
1825
|
+
return { kind: "Join", operand, span: s };
|
|
1826
|
+
}
|
|
1827
|
+
default:
|
|
1828
|
+
break;
|
|
1829
|
+
}
|
|
1830
|
+
// Type keywords used as values: Vector3(…), int(x), string.format, task.wait, buffer::create, type(x)
|
|
1831
|
+
if (TYPE_KEYWORDS.has(t.text) || VALUE_KEYWORDS.has(t.text)) {
|
|
1832
|
+
if (forNew || this.isOp("(", 1) || this.isOp(".", 1) || this.isOp("::", 1) || this.isOp("<", 1)) {
|
|
1833
|
+
this.advance();
|
|
1834
|
+
const typeArgs = this.tryCallTypeArgs();
|
|
1835
|
+
return { kind: "Name", name: t.text, typeArgs, span };
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const message = KEYWORDS.has(t.text) ? `'${t.text}' cannot start an expression here` : `unexpected '${t.text}'`;
|
|
1839
|
+
this.errorAt(span, message, undefined, "CL1124");
|
|
1840
|
+
throw new ParseAbort();
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
private lambdaSingle(): Expr {
|
|
1844
|
+
const n = this.advance();
|
|
1845
|
+
this.advance(); // =>
|
|
1846
|
+
const params: Param[] = [{ name: n.text, span: { start: n.start, end: n.end }, type: null, variadic: false, defaultValue: null }];
|
|
1847
|
+
const body = this.isOp("{") ? this.block() : this.expression();
|
|
1848
|
+
return { kind: "Function", params, returnType: null, body, isLambda: true, span: this.spanFrom(n.start) };
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/** `(a, b) => expr` / `(int a) => expr` (Cluaupp). */
|
|
1852
|
+
private tryLambdaParens(): Expr | null {
|
|
1853
|
+
const mark = this.i;
|
|
1854
|
+
const start = this.tok.start;
|
|
1855
|
+
// fast reject: scan for `) =>` at depth 0
|
|
1856
|
+
let depth = 0;
|
|
1857
|
+
let j = 0;
|
|
1858
|
+
for (;; j++) {
|
|
1859
|
+
const t = this.peek(j);
|
|
1860
|
+
if (t.kind === "eof") {
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
if (t.kind === "op") {
|
|
1864
|
+
if (t.text === "(") {
|
|
1865
|
+
depth++;
|
|
1866
|
+
} else if (t.text === ")") {
|
|
1867
|
+
depth--;
|
|
1868
|
+
if (depth === 0) {
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
} else if (t.text === ";" || t.text === "{" || t.text === "}") {
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (!this.isOp("=>", j + 1)) {
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
try {
|
|
1880
|
+
const params = this.params();
|
|
1881
|
+
this.expectOp("=>");
|
|
1882
|
+
const body = this.isOp("{") ? this.block() : this.expression();
|
|
1883
|
+
return { kind: "Function", params, returnType: null, body, isLambda: true, span: this.spanFrom(start) };
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
if (!(err instanceof ParseAbort)) {
|
|
1886
|
+
throw err;
|
|
1887
|
+
}
|
|
1888
|
+
this.i = mark;
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
private template(t: Token): Expr {
|
|
1894
|
+
const parts: Array<string | Expr> = [];
|
|
1895
|
+
for (const part of t.parts || []) {
|
|
1896
|
+
if (part.text !== undefined) {
|
|
1897
|
+
parts.push(part.text);
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
if (!part.hole) {
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
const hole = part.hole;
|
|
1904
|
+
if (this.source.text.slice(hole.start, hole.end).trim() === "") {
|
|
1905
|
+
this.errorAt({ start: hole.start - 2, end: hole.end + 1 }, "empty interpolation `${}`", undefined, "CL1125");
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
const tokens = tokenize(this.source.text, this.diags, hole.start, hole.end);
|
|
1909
|
+
const sub = new Parser(this.source, this.diags, tokens);
|
|
1910
|
+
try {
|
|
1911
|
+
const expr = sub.expression();
|
|
1912
|
+
if (!sub.atEnd()) {
|
|
1913
|
+
this.errorAt({ start: sub.tok.start, end: sub.tok.end }, "unexpected text in interpolation", undefined, "CL1126");
|
|
1914
|
+
}
|
|
1915
|
+
parts.push(expr);
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
if (!(err instanceof ParseAbort)) {
|
|
1918
|
+
throw err;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
return { kind: "Template", parts, span: { start: t.start, end: t.end } };
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
export function isPlace(expr: Expr): boolean {
|
|
1927
|
+
switch (expr.kind) {
|
|
1928
|
+
case "Name":
|
|
1929
|
+
return !expr.typeArgs;
|
|
1930
|
+
case "SelfField":
|
|
1931
|
+
return true;
|
|
1932
|
+
case "Member":
|
|
1933
|
+
return !expr.typeArgs;
|
|
1934
|
+
case "Index":
|
|
1935
|
+
return true;
|
|
1936
|
+
default:
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
export function parse(source: SourceFile, diags: DiagnosticBag): Program {
|
|
1942
|
+
return new Parser(source, diags).parseProgram();
|
|
1943
|
+
}
|