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