@velarscript/compiler 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +70 -0
  3. package/dist/analyzer.d.ts +1175 -0
  4. package/dist/analyzer.d.ts.map +1 -0
  5. package/dist/analyzer.js +12130 -0
  6. package/dist/analyzer.js.map +1 -0
  7. package/dist/application-package-host.d.ts +28 -0
  8. package/dist/application-package-host.d.ts.map +1 -0
  9. package/dist/application-package-host.js +2 -0
  10. package/dist/application-package-host.js.map +1 -0
  11. package/dist/ast.d.ts +853 -0
  12. package/dist/ast.d.ts.map +1 -0
  13. package/dist/ast.js +245 -0
  14. package/dist/ast.js.map +1 -0
  15. package/dist/class-runtime.d.ts +6 -0
  16. package/dist/class-runtime.d.ts.map +1 -0
  17. package/dist/class-runtime.js +45 -0
  18. package/dist/class-runtime.js.map +1 -0
  19. package/dist/collection-lowering-runtime.d.ts +7 -0
  20. package/dist/collection-lowering-runtime.d.ts.map +1 -0
  21. package/dist/collection-lowering-runtime.js +952 -0
  22. package/dist/collection-lowering-runtime.js.map +1 -0
  23. package/dist/collection-runtime.d.ts +16 -0
  24. package/dist/collection-runtime.d.ts.map +1 -0
  25. package/dist/collection-runtime.js +231 -0
  26. package/dist/collection-runtime.js.map +1 -0
  27. package/dist/core-vocabulary.d.ts +141 -0
  28. package/dist/core-vocabulary.d.ts.map +1 -0
  29. package/dist/core-vocabulary.js +88 -0
  30. package/dist/core-vocabulary.js.map +1 -0
  31. package/dist/diagnostic.d.ts +42 -0
  32. package/dist/diagnostic.d.ts.map +1 -0
  33. package/dist/diagnostic.js +66 -0
  34. package/dist/diagnostic.js.map +1 -0
  35. package/dist/embedded-javascript.d.ts +74 -0
  36. package/dist/embedded-javascript.d.ts.map +1 -0
  37. package/dist/embedded-javascript.js +341 -0
  38. package/dist/embedded-javascript.js.map +1 -0
  39. package/dist/embedded-module.d.ts +11 -0
  40. package/dist/embedded-module.d.ts.map +1 -0
  41. package/dist/embedded-module.js +67 -0
  42. package/dist/embedded-module.js.map +1 -0
  43. package/dist/embedded-source.d.ts +19 -0
  44. package/dist/embedded-source.d.ts.map +1 -0
  45. package/dist/embedded-source.js +61 -0
  46. package/dist/embedded-source.js.map +1 -0
  47. package/dist/emitter.d.ts +188 -0
  48. package/dist/emitter.d.ts.map +1 -0
  49. package/dist/emitter.js +3254 -0
  50. package/dist/emitter.js.map +1 -0
  51. package/dist/error-runtime.d.ts +17 -0
  52. package/dist/error-runtime.d.ts.map +1 -0
  53. package/dist/error-runtime.js +109 -0
  54. package/dist/error-runtime.js.map +1 -0
  55. package/dist/extension.d.ts +373 -0
  56. package/dist/extension.d.ts.map +1 -0
  57. package/dist/extension.js +25 -0
  58. package/dist/extension.js.map +1 -0
  59. package/dist/formatter.d.ts +13 -0
  60. package/dist/formatter.d.ts.map +1 -0
  61. package/dist/formatter.js +1440 -0
  62. package/dist/formatter.js.map +1 -0
  63. package/dist/framework-host.d.ts +87 -0
  64. package/dist/framework-host.d.ts.map +1 -0
  65. package/dist/framework-host.js +2 -0
  66. package/dist/framework-host.js.map +1 -0
  67. package/dist/index.d.ts +75 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +932 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/interpolated-string.d.ts +47 -0
  72. package/dist/interpolated-string.d.ts.map +1 -0
  73. package/dist/interpolated-string.js +344 -0
  74. package/dist/interpolated-string.js.map +1 -0
  75. package/dist/json-runtime.d.ts +2 -0
  76. package/dist/json-runtime.d.ts.map +1 -0
  77. package/dist/json-runtime.js +193 -0
  78. package/dist/json-runtime.js.map +1 -0
  79. package/dist/language-guidance.d.ts +31 -0
  80. package/dist/language-guidance.d.ts.map +1 -0
  81. package/dist/language-guidance.js +187 -0
  82. package/dist/language-guidance.js.map +1 -0
  83. package/dist/lexer.d.ts +85 -0
  84. package/dist/lexer.d.ts.map +1 -0
  85. package/dist/lexer.js +1004 -0
  86. package/dist/lexer.js.map +1 -0
  87. package/dist/limits.d.ts +2 -0
  88. package/dist/limits.d.ts.map +1 -0
  89. package/dist/limits.js +2 -0
  90. package/dist/limits.js.map +1 -0
  91. package/dist/mechanical-fix.d.ts +26 -0
  92. package/dist/mechanical-fix.d.ts.map +1 -0
  93. package/dist/mechanical-fix.js +61 -0
  94. package/dist/mechanical-fix.js.map +1 -0
  95. package/dist/narrowing-runtime.d.ts +5 -0
  96. package/dist/narrowing-runtime.d.ts.map +1 -0
  97. package/dist/narrowing-runtime.js +27 -0
  98. package/dist/narrowing-runtime.js.map +1 -0
  99. package/dist/number-runtime.d.ts +3 -0
  100. package/dist/number-runtime.d.ts.map +1 -0
  101. package/dist/number-runtime.js +37 -0
  102. package/dist/number-runtime.js.map +1 -0
  103. package/dist/parser.d.ts +245 -0
  104. package/dist/parser.d.ts.map +1 -0
  105. package/dist/parser.js +3582 -0
  106. package/dist/parser.js.map +1 -0
  107. package/dist/primitive-runtime.d.ts +4 -0
  108. package/dist/primitive-runtime.d.ts.map +1 -0
  109. package/dist/primitive-runtime.js +39 -0
  110. package/dist/primitive-runtime.js.map +1 -0
  111. package/dist/promise-runtime.d.ts +6 -0
  112. package/dist/promise-runtime.d.ts.map +1 -0
  113. package/dist/promise-runtime.js +86 -0
  114. package/dist/promise-runtime.js.map +1 -0
  115. package/dist/reactive-bridge-runtime.d.ts +7 -0
  116. package/dist/reactive-bridge-runtime.d.ts.map +1 -0
  117. package/dist/reactive-bridge-runtime.js +112 -0
  118. package/dist/reactive-bridge-runtime.js.map +1 -0
  119. package/dist/runtime-abi.d.ts +24 -0
  120. package/dist/runtime-abi.d.ts.map +1 -0
  121. package/dist/runtime-abi.js +24 -0
  122. package/dist/runtime-abi.js.map +1 -0
  123. package/dist/semantic.d.ts +142 -0
  124. package/dist/semantic.d.ts.map +1 -0
  125. package/dist/semantic.js +845 -0
  126. package/dist/semantic.js.map +1 -0
  127. package/dist/source-names.d.ts +32 -0
  128. package/dist/source-names.d.ts.map +1 -0
  129. package/dist/source-names.js +101 -0
  130. package/dist/source-names.js.map +1 -0
  131. package/dist/source.d.ts +19 -0
  132. package/dist/source.d.ts.map +1 -0
  133. package/dist/source.js +64 -0
  134. package/dist/source.js.map +1 -0
  135. package/dist/text-runtime.d.ts +2 -0
  136. package/dist/text-runtime.d.ts.map +1 -0
  137. package/dist/text-runtime.js +285 -0
  138. package/dist/text-runtime.js.map +1 -0
  139. package/dist/token.d.ts +19 -0
  140. package/dist/token.d.ts.map +1 -0
  141. package/dist/token.js +51 -0
  142. package/dist/token.js.map +1 -0
  143. package/dist/type-registry-runtime.d.ts +7 -0
  144. package/dist/type-registry-runtime.d.ts.map +1 -0
  145. package/dist/type-registry-runtime.js +51 -0
  146. package/dist/type-registry-runtime.js.map +1 -0
  147. package/dist/type-validation-runtime.d.ts +17 -0
  148. package/dist/type-validation-runtime.d.ts.map +1 -0
  149. package/dist/type-validation-runtime.js +150 -0
  150. package/dist/type-validation-runtime.js.map +1 -0
  151. package/dist/types.d.ts +356 -0
  152. package/dist/types.d.ts.map +1 -0
  153. package/dist/types.js +1170 -0
  154. package/dist/types.js.map +1 -0
  155. package/dist/utf8-runtime.d.ts +7 -0
  156. package/dist/utf8-runtime.d.ts.map +1 -0
  157. package/dist/utf8-runtime.js +43 -0
  158. package/dist/utf8-runtime.js.map +1 -0
  159. package/package.json +48 -0
package/dist/parser.js ADDED
@@ -0,0 +1,3582 @@
1
+ import { CORE_STATEMENT_HEAD_KEYWORDS, CORE_WORDS, TYPE_PARAMETER_DECLARATION_FORMS, typeParameterDeclarationFormsPhrase } from "./core-vocabulary.js";
2
+ import { diagnostic, mechanicalEdits, mechanicalFix, recoveredDiagnostic } from "./diagnostic.js";
3
+ import { inspectEmbeddedJavaScript, isEmbeddedJavaScriptTokenPayload } from "./embedded-javascript.js";
4
+ import { findInterpolatedExpressionEnd, scanStringEscape, scanStringLiteral } from "./interpolated-string.js";
5
+ import { declarationKeywordGuidance, sourceTypeNameGuidance, REST_PARAMETER_ELEMENT_TYPE_MESSAGE } from "./language-guidance.js";
6
+ import { Lexer } from "./lexer.js";
7
+ import { span } from "./source.js";
8
+ import { keywordKinds } from "./token.js";
9
+ import { formatTypeSyntax } from "./types.js";
10
+ const memberNameKinds = new Set(["identifier", ...Object.values(keywordKinds)]);
11
+ // Token kinds that begin a statement but can never begin a record field or
12
+ // appear inside a record literal's field list. A keyword followed by ':' is a
13
+ // keyword-named field, so it never counts as statement evidence.
14
+ const statementStarterKinds = new Set([
15
+ "const", "let", "def", "return", "throw", "assert", "if", "for", "while", "break", "continue", "try", "pass",
16
+ ]);
17
+ // D30 item 16: `match` and `case` are contextual keywords, so statement
18
+ // evidence for them is carried by the word rather than by a token kind. D62
19
+ // rule 157: the pair is derived from Core's roster — the words whose statement
20
+ // shape takes a subject — rather than kept as a second one-word copy of it.
21
+ const statementStarterWords = new Set(CORE_STATEMENT_HEAD_KEYWORDS);
22
+ // Token kinds that legally appear at the top level of a record literal's
23
+ // field list: field names, shorthand entries, and their separators.
24
+ const recordFieldLevelKinds = new Set(["identifier", "string", "comma", ...Object.values(keywordKinds)]);
25
+ const MAX_PARSE_DEPTH = 512;
26
+ const PARSER_COMPLEXITY_FAILURE = Object.freeze({ kind: "VelarParserComplexityFailure" });
27
+ export function isParserComplexityFailure(value) {
28
+ return value === PARSER_COMPLEXITY_FAILURE;
29
+ }
30
+ const binaryPrecedence = {
31
+ nullish: 1,
32
+ or: 2,
33
+ and: 3,
34
+ pipe: 4,
35
+ caret: 5,
36
+ amp: 6,
37
+ equal: 7,
38
+ notEqual: 7,
39
+ is: 7,
40
+ in: 7,
41
+ less: 7,
42
+ lessEqual: 7,
43
+ greater: 7,
44
+ greaterEqual: 7,
45
+ leftShift: 8,
46
+ rightShift: 8,
47
+ unsignedRightShift: 8,
48
+ plus: 9,
49
+ minus: 9,
50
+ star: 10,
51
+ slash: 10,
52
+ percent: 10,
53
+ };
54
+ const comparisonOperators = {
55
+ equal: "==",
56
+ notEqual: "!=",
57
+ less: "<",
58
+ lessEqual: "<=",
59
+ greater: ">",
60
+ greaterEqual: ">=",
61
+ };
62
+ // An extension's contextual keyword directly followed by ':' opens an
63
+ // indentation-owned extension block whose capture depends on physical lines; a
64
+ // bracket fragment containing one keeps line-sensitive form.
65
+ function containsExtensionBlockStart(tokens, words) {
66
+ return tokens.some((token, index) => token.kind === "identifier" && words.has(token.value) && tokens[index + 1]?.kind === "colon");
67
+ }
68
+ // Statement-boundary guidance names the leftover token as the author typed it.
69
+ // Long literals are clipped so one runaway string cannot swamp the message.
70
+ function describeStatementToken(token) {
71
+ const text = token.value;
72
+ if (!text)
73
+ return token.kind;
74
+ return text.length > 24 ? `${text.slice(0, 24)}…` : text;
75
+ }
76
+ const assignmentOperators = {
77
+ assign: "=",
78
+ plusAssign: "+=",
79
+ minusAssign: "-=",
80
+ starAssign: "*=",
81
+ slashAssign: "/=",
82
+ percentAssign: "%=",
83
+ bitOrAssign: "|=",
84
+ bitAndAssign: "&=",
85
+ bitXorAssign: "^=",
86
+ leftShiftAssign: "<<=",
87
+ rightShiftAssign: ">>=",
88
+ unsignedRightShiftAssign: ">>>=",
89
+ };
90
+ export class Parser {
91
+ // Keep a private mutable token view so a run of adjacent `>` characters can
92
+ // be consumed one close at a time while parsing nested generic types. The
93
+ // lexer must still emit `>>` and `>>>` as shift operators in expressions;
94
+ // splitting them here is contextual and therefore preserves both grammars.
95
+ tokens;
96
+ lexicalExtensions;
97
+ diagnostics = [];
98
+ genericCallableNames = new Set();
99
+ /** Extension-owned contextual keywords: names until a shape claims them. */
100
+ contextualKeywords;
101
+ index = 0;
102
+ parseDepth = 0;
103
+ recoveredImportDelimiterBoundary = false;
104
+ /**
105
+ * D65 rule 170: non-zero while `parseParameters` is reading an arrow's
106
+ * parameter list — the one list a contextual function type can still type.
107
+ * It is a field rather than an argument because `packages/web` overrides
108
+ * `parseParameters()` and forwards to `super.parseParameters()`, so an added
109
+ * argument would be silently dropped for every arrow inside a web module.
110
+ */
111
+ contextualParameterDepth = 0;
112
+ constructor(tokens, lexicalExtensions = []) {
113
+ this.tokens = [...tokens];
114
+ this.lexicalExtensions = lexicalExtensions;
115
+ this.contextualKeywords = new Set(lexicalExtensions.flatMap((extension) => [...extension.contextualKeywords ?? []]));
116
+ for (let index = 0; index + 2 < tokens.length; index += 1) {
117
+ if (tokens[index]?.kind === "def" && tokens[index + 1]?.kind === "identifier" && tokens[index + 2]?.kind === "less") {
118
+ this.genericCallableNames.add(tokens[index + 1].value);
119
+ }
120
+ }
121
+ }
122
+ parse() {
123
+ const body = [];
124
+ this.consumeNewlines();
125
+ while (!this.check("eof")) {
126
+ if (this.match("dedent")) {
127
+ this.diagnostics.push(diagnostic("VEL2004", "Unexpected end of an indented block", this.previous().span));
128
+ continue;
129
+ }
130
+ const statement = this.parseStatement();
131
+ if (statement) {
132
+ body.push(statement);
133
+ }
134
+ else {
135
+ this.synchronize();
136
+ }
137
+ this.finishStatementBoundary();
138
+ this.consumeNewlines();
139
+ }
140
+ const end = this.current().span.end;
141
+ return {
142
+ program: { kind: "Program", body, span: span(0, end) },
143
+ diagnostics: this.diagnostics,
144
+ };
145
+ }
146
+ parseExpressionFragment() {
147
+ this.consumeNewlines();
148
+ const expression = this.recoverExpressionAssignment(this.parseExpression());
149
+ this.consumeNewlines();
150
+ if (!this.check("eof")) {
151
+ this.diagnostics.push(diagnostic("VEL2006", "Unexpected tokens in interpolated expression", this.current().span));
152
+ }
153
+ return { expression, diagnostics: this.diagnostics };
154
+ }
155
+ // An assignment written where only an expression is valid (an interpolated
156
+ // fragment or an arrow body) receives directive guidance and recovers as an
157
+ // AssignmentExpression node, so later stages can report their own guidance
158
+ // for the same code instead of a bare unexpected-token cascade.
159
+ recoverExpressionAssignment(expression) {
160
+ const operator = assignmentOperators[this.current().kind];
161
+ if (!operator)
162
+ return expression;
163
+ const operatorToken = this.advance();
164
+ this.diagnostics.push(recoveredDiagnostic("VEL2028", "Assignment is a statement, not an expression; write it on its own line inside a function, action, or handler body", operatorToken.span));
165
+ if (expression.kind !== "IdentifierExpression" && expression.kind !== "MemberExpression" && expression.kind !== "IndexExpression") {
166
+ this.diagnostics.push(diagnostic("VEL2005", "Assignment target must be a name, member, or index", expression.span));
167
+ }
168
+ const value = this.recoverExpressionAssignment(this.parseExpression());
169
+ return { kind: "AssignmentExpression", target: expression, operator, value, span: span(expression.span.start, value.span.end) };
170
+ }
171
+ parseStatement() {
172
+ return this.withParseDepth(() => this.parseStatementBody());
173
+ }
174
+ parseStatementBody() {
175
+ const start = this.current().span.start;
176
+ if (this.check("import") && this.tokens[this.index + 1]?.kind !== "leftParen") {
177
+ this.advance();
178
+ const extensionImport = this.parseExtensionImport(start);
179
+ if (extensionImport !== undefined)
180
+ return extensionImport;
181
+ return this.parseImport(start);
182
+ }
183
+ if (this.match("extern")) {
184
+ if (this.match("js"))
185
+ return this.parseEmbeddedJavaScript(start, false);
186
+ return this.parseExternModule(start);
187
+ }
188
+ if (this.match("unsafe")) {
189
+ const extensionStatement = this.parseUnsafeExtensionStatement(start);
190
+ if (extensionStatement !== undefined)
191
+ return extensionStatement;
192
+ this.expect("js", "Expected 'js' after 'unsafe', or an unsafe block owned by an installed extension");
193
+ return this.parseEmbeddedJavaScript(start, true);
194
+ }
195
+ const exported = this.match("export");
196
+ // D50 rule 100: `export type {Name} from "..."` is the re-export half of the
197
+ // TypeScript habit, recognized so it can be taught; `export type Name:` is
198
+ // still an ordinary exported type declaration.
199
+ if (exported && (this.check("leftBrace") || this.check("star")
200
+ || (this.checkWord(CORE_WORDS.type) && (this.peekKind(1) === "leftBrace" || this.peekKind(1) === "star")))) {
201
+ return this.parseReExport(start);
202
+ }
203
+ // MOD-U2: `export default` is the JavaScript habit; VelarScript modules
204
+ // have no default export, so the spelling gets one directed answer
205
+ // instead of the generic export cascade.
206
+ if (exported && this.check("identifier") && this.current().value === "default") {
207
+ this.diagnostics.push(diagnostic("VEL2001", "VelarScript modules have no default export; export the declaration by name — export const name = ..., export def name(...)", this.current().span));
208
+ this.skipMistypedDeclaration();
209
+ return { kind: "PassStatement", span: this.previous().span };
210
+ }
211
+ const abstract = this.match("abstract");
212
+ const asynchronous = this.match("async");
213
+ const extensionStatement = this.parseExtensionStatement(start, { exported, abstract, asynchronous });
214
+ if (extensionStatement !== undefined)
215
+ return extensionStatement;
216
+ if (this.match("def")) {
217
+ if (abstract)
218
+ this.diagnostics.push(diagnostic("VEL2013", "Only class methods can be declared with 'abstract'", this.previous().span));
219
+ return this.parseFunction(start, exported, asynchronous);
220
+ }
221
+ if (asynchronous && this.match("for")) {
222
+ if (exported)
223
+ this.diagnostics.push(diagnostic("VEL2001", "An async for loop cannot be exported", this.previous().span));
224
+ if (abstract)
225
+ this.diagnostics.push(diagnostic("VEL2001", "'abstract' cannot prefix an async for loop", this.previous().span));
226
+ return this.parseForStatement(start, true);
227
+ }
228
+ if (asynchronous) {
229
+ // The detached-execution statement: 'async <expression>'. 'def' and
230
+ // 'for' consumed their spellings above, so any remaining continuation
231
+ // is the expression form; the analyzer requires its checked type to be
232
+ // Promise<null>.
233
+ if (exported)
234
+ this.diagnostics.push(diagnostic("VEL2001", "An 'async' statement cannot be exported", this.previous().span));
235
+ if (abstract)
236
+ this.diagnostics.push(diagnostic("VEL2001", "'abstract' cannot prefix an 'async' statement", this.previous().span));
237
+ if (this.check("newline") || this.check("dedent") || this.check("eof")) {
238
+ this.diagnostics.push(diagnostic("VEL2001", "'async' must be followed by 'def', 'for', or a call to run detached", this.previous().span));
239
+ return null;
240
+ }
241
+ const expression = this.parseExpression();
242
+ return { kind: "AsyncStatement", expression, span: span(start, expression.span.end) };
243
+ }
244
+ if (abstract && !this.check("class")) {
245
+ this.diagnostics.push(diagnostic("VEL2001", "'abstract' must be followed by 'class'", this.previous().span));
246
+ return null;
247
+ }
248
+ if (this.typeDeclarationAhead()) {
249
+ this.advance();
250
+ return this.parseTypeDefinition(start, exported);
251
+ }
252
+ if (this.match("enum")) {
253
+ return this.parseEnumDeclaration(start, exported);
254
+ }
255
+ if (this.match("class")) {
256
+ return this.parseClassDeclaration(start, exported, abstract);
257
+ }
258
+ if (this.check("const") || this.check("let")) {
259
+ return this.parseVariable(start, exported);
260
+ }
261
+ // D30 item 16: `match` is a contextual keyword, recognized only by the
262
+ // shape no expression can have — a header line ending in ':' whose
263
+ // indented block opens with `case`. Every other `match` is a name, so
264
+ // `match(value)` calls a function and `match = 1` assigns a binding. The
265
+ // decision happens here, ahead of the unknown-declaration guidance, because
266
+ // `match value:` is otherwise two adjacent identifiers.
267
+ if (this.matchStatementAhead()) {
268
+ if (exported)
269
+ this.diagnostics.push(diagnostic("VEL2001", "A match statement cannot be exported", this.current().span));
270
+ this.advance();
271
+ return this.parseMatch(start);
272
+ }
273
+ // D39 item 53: `test "name":` is a contextual keyword — statement head, a
274
+ // string literal, then a block. `test` stays an ordinary name everywhere
275
+ // else, including `test(...)` and `const test = ...`.
276
+ if (this.checkWord(CORE_WORDS.test)
277
+ && this.peekKind(1) === "string" && this.peekKind(2) === "colon") {
278
+ const keyword = this.advance();
279
+ if (exported)
280
+ this.diagnostics.push(diagnostic("VEL2001", "A test is discovered by the runner and is not exported", keyword.span));
281
+ const title = this.advance();
282
+ const body = this.parseBlock();
283
+ return {
284
+ kind: "TestDeclaration",
285
+ title: title.value,
286
+ titleSpan: title.span,
287
+ body,
288
+ span: span(start, body.at(-1)?.span.end ?? title.span.end),
289
+ };
290
+ }
291
+ // D43 item 69: `using` is a contextual keyword — statement head, an
292
+ // identifier, then `=`. Everywhere else `using` stays an ordinary name.
293
+ if (this.checkWord(CORE_WORDS.using) && this.peekKind(1) === "identifier") {
294
+ if (this.peekKind(2) === "assign") {
295
+ const keyword = this.advance();
296
+ if (exported)
297
+ this.diagnostics.push(diagnostic("VEL2001", "A 'using' binding cannot be exported; it is released when its scope ends", keyword.span));
298
+ return this.parseUsing(keyword.span.start);
299
+ }
300
+ if (this.peekKind(2) === "colon") {
301
+ const keyword = this.current();
302
+ const end = this.tokens[this.index + 2]?.span.end ?? keyword.span.end;
303
+ this.diagnostics.push(diagnostic("VEL2036", "A 'using' binding takes its type from the initializer; write 'using name = expression'", span(keyword.span.start, end)));
304
+ this.synchronize();
305
+ return { kind: "PassStatement", span: keyword.span };
306
+ }
307
+ }
308
+ // MIG-4: 'invert x' was the removed toggle statement, and its leftover
309
+ // operand otherwise falls into the generic statement-boundary message,
310
+ // which never names the assignment that replaced it. 'invert' stayed an
311
+ // ordinary identifier, so only the removed statement's own shape — the
312
+ // bare word followed by a value name — is claimed here; 'invert(...)',
313
+ // 'invert.field', and 'invert = ...' keep their ordinary meanings.
314
+ if (this.check("identifier") && this.current().value === "invert" && this.peekKind(1) === "identifier") {
315
+ const keyword = this.current();
316
+ const target = this.describeInvertTarget(this.index + 1);
317
+ this.synchronize();
318
+ const removed = span(keyword.span.start, this.previous().span.end);
319
+ this.diagnostics.push(diagnostic("VEL2033", `Use '${target} = not ${target}'; the 'invert' statement was removed`, removed));
320
+ return { kind: "PassStatement", span: removed };
321
+ }
322
+ if (this.check("identifier") && this.peekKind(1) === "identifier") {
323
+ const first = this.current();
324
+ const guidance = declarationKeywordGuidance(first.value);
325
+ if (guidance) {
326
+ // Parse the remainder as the guided declaration whenever it has the
327
+ // guided shape, so body-level and semantic-level guidance surfaces in
328
+ // the same compile instead of hiding behind the skipped block.
329
+ const shape = this.peekKind(2);
330
+ if (guidance.keyword === "def" && (shape === "leftParen" || shape === "less")) {
331
+ this.diagnostics.push(recoveredDiagnostic("VEL2026", guidance.message, first.span, mechanicalFix(first.span, guidance.keyword, `Use '${guidance.keyword}'`)));
332
+ this.advance();
333
+ return this.parseFunction(start, exported, asynchronous);
334
+ }
335
+ if (guidance.keyword === "type" && (shape === "colon" || shape === "assign")) {
336
+ this.diagnostics.push(recoveredDiagnostic("VEL2026", guidance.message, first.span, mechanicalFix(first.span, guidance.keyword, `Use '${guidance.keyword}'`)));
337
+ this.advance();
338
+ return this.parseTypeDefinition(start, exported);
339
+ }
340
+ this.diagnostics.push(diagnostic("VEL2026", guidance.message, first.span));
341
+ this.skipMistypedDeclaration();
342
+ return { kind: "PassStatement", span: first.span };
343
+ }
344
+ const following = this.peekKind(2);
345
+ if (following === "leftParen" || following === "colon") {
346
+ // D51 (audit 12): `test` is a real declaration head, so calling it
347
+ // unknown was false. What is wrong is the name's shape: a test name is
348
+ // the sentence a report prints, so it is a string, not an identifier.
349
+ this.diagnostics.push(diagnostic("VEL2026", first.value === CORE_WORDS.test && following === "colon"
350
+ ? `A test name is the sentence a report prints, so it is written as a string — 'test "${this.peekValue(1)}":'`
351
+ : `Unknown declaration keyword '${first.value}'; VelarScript declarations start with 'def', 'type', 'enum', 'class', 'const', or 'let'`, first.span));
352
+ this.skipMistypedDeclaration();
353
+ return { kind: "PassStatement", span: first.span };
354
+ }
355
+ }
356
+ if (exported) {
357
+ this.diagnostics.push(diagnostic("VEL2001", "'export' must be followed by a declaration", this.previous().span));
358
+ return null;
359
+ }
360
+ if (this.match("return")) {
361
+ const keyword = this.previous();
362
+ const value = this.atStatementEnd() ? null : this.parseExpression();
363
+ return { kind: "ReturnStatement", value, span: span(keyword.span.start, value?.span.end ?? keyword.span.end) };
364
+ }
365
+ if (this.match("throw")) {
366
+ const keyword = this.previous();
367
+ if (this.atStatementEnd()) {
368
+ this.diagnostics.push(diagnostic("VEL2009", "'throw' requires an Error value", keyword.span));
369
+ return null;
370
+ }
371
+ const value = this.parseExpression();
372
+ return { kind: "ThrowStatement", value, span: span(keyword.span.start, value.span.end) };
373
+ }
374
+ if (this.match("assert")) {
375
+ const keyword = this.previous();
376
+ if (this.atStatementEnd()) {
377
+ this.diagnostics.push(diagnostic("VEL2017", "'assert' requires a condition", keyword.span));
378
+ return null;
379
+ }
380
+ const condition = this.parseExpression();
381
+ let message = null;
382
+ if (this.match("else")) {
383
+ if (this.atStatementEnd()) {
384
+ this.diagnostics.push(diagnostic("VEL2017", "'assert' requires a message after 'else'", this.previous().span));
385
+ }
386
+ else {
387
+ message = this.parseExpression();
388
+ }
389
+ }
390
+ else if (this.match("comma")) {
391
+ const separator = this.previous();
392
+ this.diagnostics.push(recoveredDiagnostic("VEL2017", "Use 'assert condition else message'; an assertion message belongs to the failing branch", separator.span,
393
+ // The next token's start absorbs whatever spacing followed the comma.
394
+ this.atStatementEnd() ? undefined : mechanicalFix(span(separator.span.start, this.current().span.start), " else ", "Use 'assert condition else message'")));
395
+ if (this.atStatementEnd()) {
396
+ this.diagnostics.push(diagnostic("VEL2017", "'assert' requires a message after 'else'", separator.span));
397
+ }
398
+ else {
399
+ message = this.parseExpression();
400
+ }
401
+ }
402
+ return { kind: "AssertStatement", condition, message, span: span(keyword.span.start, message?.span.end ?? condition.span.end) };
403
+ }
404
+ if (this.match("if")) {
405
+ return this.parseIf(start);
406
+ }
407
+ if (this.match("for")) {
408
+ let asynchronousLoop = false;
409
+ const forKeyword = this.previous();
410
+ if (this.match("await")) {
411
+ asynchronousLoop = true;
412
+ this.diagnostics.push(recoveredDiagnostic("VEL2017", "Use 'async for value in source'; the async marker precedes the loop", this.previous().span, mechanicalEdits([
413
+ { span: span(forKeyword.span.start, forKeyword.span.start), text: "async " },
414
+ { span: span(this.previous().span.start, this.current().span.start), text: "" },
415
+ ], "Use 'async for value in source'")));
416
+ }
417
+ return this.parseForStatement(start, asynchronousLoop);
418
+ }
419
+ if (this.match("while")) {
420
+ const condition = this.parseExpression();
421
+ const body = this.parseBlock();
422
+ return { kind: "WhileStatement", condition, body, span: span(start, body.at(-1)?.span.end ?? condition.span.end) };
423
+ }
424
+ if (this.match("break")) {
425
+ return { kind: "BreakStatement", span: this.previous().span };
426
+ }
427
+ if (this.match("continue")) {
428
+ return { kind: "ContinueStatement", span: this.previous().span };
429
+ }
430
+ // D39 item 51: the statement-head `try` is the block form only when a
431
+ // block follows it. Anything else is the expression form, which reaches
432
+ // the expression parser below and must be consumed by something.
433
+ if (this.check("try") && this.peekKind(1) === "colon") {
434
+ this.advance();
435
+ return this.parseTry(start);
436
+ }
437
+ if (this.match("pass")) {
438
+ return { kind: "PassStatement", span: this.previous().span };
439
+ }
440
+ if (this.check("else") || this.check("catch") || this.check("finally") || this.orphanCaseClauseAhead()) {
441
+ this.diagnostics.push(diagnostic("VEL2001", `'${this.current().value}' does not follow a matching block`, this.current().span));
442
+ return null;
443
+ }
444
+ const expression = this.parseExpression();
445
+ const operator = assignmentOperators[this.current().kind];
446
+ if (operator) {
447
+ this.advance();
448
+ if (expression.kind !== "IdentifierExpression" && expression.kind !== "MemberExpression" && expression.kind !== "IndexExpression") {
449
+ this.diagnostics.push(diagnostic("VEL2005", "Assignment target must be a name, member, or index", expression.span));
450
+ }
451
+ const value = this.parseExpression();
452
+ return {
453
+ kind: "AssignmentStatement",
454
+ target: expression,
455
+ operator,
456
+ value,
457
+ span: span(expression.span.start, value.span.end),
458
+ };
459
+ }
460
+ return { kind: "ExpressionStatement", expression, span: expression.span };
461
+ }
462
+ /**
463
+ * D30 item 16: `type` opens a declaration only in its declaration shape —
464
+ * the word, a name, and then ':' for a record body, '=' for an alias, or '<'
465
+ * for the rejected type-parameter spelling. `type = payload.type`,
466
+ * `type(value)`, and `type.field` all keep the identifier reading.
467
+ */
468
+ typeDeclarationAhead() {
469
+ if (!this.checkWord(CORE_WORDS.type) || this.peekKind(1) !== "identifier")
470
+ return false;
471
+ const shape = this.peekKind(2);
472
+ return shape === "colon" || shape === "assign" || shape === "less";
473
+ }
474
+ /**
475
+ * A match statement is the one statement whose header ends in ':' and opens
476
+ * an indented block. No expression statement can end in ':' — a call is
477
+ * `match(value)` and an assignment is `match = value` — so the two-line
478
+ * lookahead is exact. D30 item 16 named `case` as the block's first token;
479
+ * the block's contents are deliberately not inspected, so a malformed first
480
+ * branch and the `else:` recovery still reach the match block's own
481
+ * teaching instead of falling back to a bare-name reading.
482
+ */
483
+ matchStatementAhead() {
484
+ if (!this.checkWord(CORE_WORDS.match))
485
+ return false;
486
+ let depth = 0;
487
+ let offset = 1;
488
+ for (; this.index + offset < this.tokens.length; offset += 1) {
489
+ const kind = this.tokens[this.index + offset].kind;
490
+ if (kind === "leftParen" || kind === "leftBracket" || kind === "leftBrace")
491
+ depth += 1;
492
+ else if (kind === "rightParen" || kind === "rightBracket" || kind === "rightBrace")
493
+ depth -= 1;
494
+ else if (depth === 0 && (kind === "newline" || kind === "dedent" || kind === "eof"))
495
+ break;
496
+ }
497
+ if (offset < 3 || this.tokens[this.index + offset - 1]?.kind !== "colon")
498
+ return false;
499
+ while (this.peekKind(offset) === "newline")
500
+ offset += 1;
501
+ return this.peekKind(offset) === "indent";
502
+ }
503
+ /**
504
+ * `case` outside a match block is an ordinary name, but a `case ...:` line
505
+ * standing alone is the author reaching for a branch that has no header. The
506
+ * branch shape — the word followed by a pattern and a line-ending ':' — keeps
507
+ * the existing directed message without claiming the word anywhere else.
508
+ */
509
+ orphanCaseClauseAhead() {
510
+ if (!this.checkWord(CORE_WORDS.case) || this.peekKind(1) === "colon")
511
+ return false;
512
+ let depth = 0;
513
+ for (let offset = 1; this.index + offset < this.tokens.length; offset += 1) {
514
+ const kind = this.tokens[this.index + offset].kind;
515
+ if (kind === "leftParen" || kind === "leftBracket" || kind === "leftBrace")
516
+ depth += 1;
517
+ else if (kind === "rightParen" || kind === "rightBracket" || kind === "rightBrace")
518
+ depth -= 1;
519
+ else if (depth === 0 && (kind === "newline" || kind === "dedent" || kind === "eof")) {
520
+ return this.tokens[this.index + offset - 1]?.kind === "colon";
521
+ }
522
+ }
523
+ return false;
524
+ }
525
+ parseForStatement(start, asynchronous) {
526
+ const pattern = this.parseBindingPattern();
527
+ const secondPattern = this.match("comma") ? this.parseBindingPattern() : null;
528
+ if (secondPattern && this.match("comma")) {
529
+ const third = this.parseBindingPattern();
530
+ this.diagnostics.push(diagnostic("VEL2017", "A for loop accepts one binding or two slots; use 'for [a, b] in ...' to destructure one item", third.span));
531
+ }
532
+ this.expect("in", "Expected 'in' after loop binding");
533
+ const iterable = this.parseExpression();
534
+ const body = this.parseBlock();
535
+ return { kind: "ForStatement", asynchronous, pattern, secondPattern, iterable, body, span: span(start, body.at(-1)?.span.end ?? iterable.span.end) };
536
+ }
537
+ /**
538
+ * D50 rule 100: `type` is a contextual keyword, so `import type {User} from`
539
+ * is the TypeScript habit while `import type from "./x.vel"` still reads as a
540
+ * default import named `type`. The habit is recognized by what follows the
541
+ * word — a brace list, a namespace star, or a name that is not `from` — so
542
+ * that it can be taught rather than met with a generic parse error.
543
+ */
544
+ typeImportMarkerAhead() {
545
+ if (!this.checkWord(CORE_WORDS.type))
546
+ return false;
547
+ const next = this.tokens[this.index + 1];
548
+ if (!next)
549
+ return false;
550
+ return next.kind === "leftBrace" || next.kind === "star"
551
+ || (next.kind === "identifier" && next.value !== CORE_WORDS.from);
552
+ }
553
+ /**
554
+ * D50 rule 100 (retiring D38 rule 49): TypeScript needs `import type` because
555
+ * TypeScript erases types, so a type import can carry no module edge.
556
+ * VelarScript does not erase: every named type carries its runtime validator,
557
+ * an enum is a runtime value, and a class is a runtime value — so a type
558
+ * import is an ordinary import and the marker has nothing left to mean.
559
+ * Dropping the word is the whole rewrite, which is why `velar fix` applies it.
560
+ */
561
+ rejectTypeImportMarker(marker, form) {
562
+ this.diagnostics.push(recoveredDiagnostic("VEL2029", "VelarScript does not erase types: a type carries its runtime validator, so a type import is an ordinary import"
563
+ + ` — drop 'type' and write ${form} {Name} from "..."`, marker.span, mechanicalFix({ start: marker.span.start, end: marker.span.end + 1 }, "", "Drop 'type' from the import")));
564
+ }
565
+ parseImport(start) {
566
+ const javascript = this.match("js");
567
+ const unsafe = javascript && this.match("unsafe");
568
+ if (this.typeImportMarkerAhead())
569
+ this.rejectTypeImportMarker(this.advance(), "import");
570
+ const specifiers = [];
571
+ // MOD-I2 / D50 rule 99: a side-effect import is invisible action. The
572
+ // reader sees the line and cannot tell what happened, which is the same
573
+ // reason D43 rule 68 excludes user-defined decorators: no mechanism may
574
+ // hide behavior from the owner of the code. Both parents spell this, and
575
+ // that has never been sufficient on its own — Vel already removed
576
+ // truthiness, coercive equality, and `switch`. Both spellings are refused,
577
+ // and both get the one message that names the visible form.
578
+ if (this.check("string")) {
579
+ const source = this.advance();
580
+ if (source.value === "") {
581
+ this.diagnostics.push(diagnostic("VEL2001", "A module path cannot be empty", source.span));
582
+ return null;
583
+ }
584
+ this.reportSideEffectImport(span(start, source.span.end), source.value);
585
+ return null;
586
+ }
587
+ let emptyBraces = null;
588
+ if (this.match("star")) {
589
+ const star = this.previous();
590
+ this.expectWord(CORE_WORDS.as, "Expected 'as' after namespace import");
591
+ const local = this.expect("identifier", "Expected a namespace name");
592
+ specifiers.push({ imported: "*", local: local.value, namespace: true, span: span(star.span.start, local.span.end) });
593
+ }
594
+ else if (this.match("leftBrace")) {
595
+ const brace = this.previous();
596
+ if (!this.check("rightBrace")) {
597
+ do {
598
+ // The inline marker is the same habit written per name.
599
+ if (this.checkWord(CORE_WORDS.type) && this.peekKind(1) === "identifier") {
600
+ this.rejectTypeImportMarker(this.advance(), "import");
601
+ }
602
+ const imported = this.expect("identifier", "Expected an imported name");
603
+ const local = this.matchWord(CORE_WORDS.as) ? this.expect("identifier", "Expected a local import name") : imported;
604
+ specifiers.push({ imported: imported.value, local: local.value, namespace: false, span: span(imported.span.start, local.span.end) });
605
+ } while (this.match("comma") && !this.check("rightBrace"));
606
+ }
607
+ else {
608
+ emptyBraces = span(brace.span.start, this.current().span.end);
609
+ }
610
+ const closed = this.check("rightBrace");
611
+ this.expect("rightBrace", "Expected '}' after imports");
612
+ // An unmatched `{` makes the lexer suppress the physical newline. Once
613
+ // the parser has recovered the rest of the import, the next statement
614
+ // is nevertheless already on its own source line; do not tell the
615
+ // author to move it there again.
616
+ if (!closed)
617
+ this.recoveredImportDelimiterBoundary = true;
618
+ }
619
+ else {
620
+ const local = this.expect("identifier", "Expected a default import name");
621
+ specifiers.push({ imported: "default", local: local.value, namespace: false, span: local.span });
622
+ }
623
+ this.expectWord(CORE_WORDS.from, "Expected 'from' after imports");
624
+ // MOD-I1 / BRG-D1: a recovered import must never fabricate a dependency.
625
+ // The synthesized empty-source token used to flow into module resolution
626
+ // as `''`, whose nonsense "invalid package name" failure buried this
627
+ // parser's own diagnostics for the same line.
628
+ const hadSource = this.check("string");
629
+ const source = this.expect("string", "Expected a module path string");
630
+ if (!hadSource) {
631
+ this.synchronize();
632
+ return null;
633
+ }
634
+ if (source.value === "") {
635
+ this.diagnostics.push(diagnostic("VEL2001", "A module path cannot be empty", source.span));
636
+ return null;
637
+ }
638
+ // Empty braces bind no names either, so this is the same side-effect
639
+ // import wearing a binding list. One rule, one message, both spellings.
640
+ if (emptyBraces) {
641
+ this.reportSideEffectImport(span(start, source.span.end), source.value);
642
+ return null;
643
+ }
644
+ this.reportInlineDataJavaScriptMigration(start, source, javascript, unsafe, specifiers);
645
+ return { kind: "ImportDeclaration", source: source.value, sourceSpan: source.span, javascript, unsafe, specifiers, span: span(start, source.span.end) };
646
+ }
647
+ /** D53 rule 117: only an export-exact data URL has a semantics-preserving block rewrite. */
648
+ reportInlineDataJavaScriptMigration(start, source, javascript, unsafe, specifiers) {
649
+ const prefix = "data:text/javascript,";
650
+ if (!javascript || !unsafe || !source.value.startsWith(prefix))
651
+ return;
652
+ if (specifiers.some((specifier) => specifier.namespace || specifier.imported === "default" || specifier.imported !== specifier.local))
653
+ return;
654
+ let embedded;
655
+ try {
656
+ embedded = decodeURIComponent(source.value.slice(prefix.length));
657
+ }
658
+ catch {
659
+ return;
660
+ }
661
+ const inspected = inspectEmbeddedJavaScript(embedded, 0, false);
662
+ if (inspected.issues.length > 0)
663
+ return;
664
+ const imported = [...new Set(specifiers.map((specifier) => specifier.imported))].sort();
665
+ const exported = [...new Set(inspected.exports.map((item) => item.name))].sort();
666
+ if (imported.length !== exported.length || imported.some((name, index) => name !== exported[index]))
667
+ return;
668
+ // A source line indistinguishable from the new structural delimiter cannot
669
+ // be rewritten without inventing an escape rule D53 deliberately lacks.
670
+ if (embedded.split(/\r\n|\r|\n/u).some((line) => /^`[ \t]*$/u.test(line)))
671
+ return;
672
+ const replacement = `unsafe js\`\n${embedded}${embedded.endsWith("\n") || embedded.endsWith("\r") ? "" : "\n"}\``;
673
+ this.diagnostics.push(recoveredDiagnostic("VEL2029", "Inline JavaScript data URLs have a source-mapped block spelling; move the exact module source into 'unsafe js`...`'", span(start, source.span.end), mechanicalFix(span(start, source.span.end), replacement, "Rewrite the export-exact data URL as an unsafe JavaScript block")));
674
+ }
675
+ /**
676
+ * D50 rule 99: the effects of a module have to be visible at the place they
677
+ * happen. There is no mechanical rewrite here — naming the function to
678
+ * export and call is the author's decision, not a spelling change.
679
+ */
680
+ reportSideEffectImport(importSpan, source) {
681
+ this.diagnostics.push(diagnostic("VEL2029", `A module's effects must be visible where they happen; export a function and call it — import {install} from ${JSON.stringify(source)}, then install()`, importSpan));
682
+ }
683
+ parseReExport(start) {
684
+ if (this.checkWord(CORE_WORDS.type))
685
+ this.rejectTypeImportMarker(this.advance(), "export");
686
+ if (this.match("star")) {
687
+ const star = this.previous();
688
+ if (this.matchWord(CORE_WORDS.as))
689
+ this.match("identifier");
690
+ if (this.matchWord(CORE_WORDS.from))
691
+ this.match("string");
692
+ this.diagnostics.push(diagnostic("VEL2029", "Namespace re-export 'export * from' is not supported; re-export each name explicitly with export {name, other as alias} from \"./module.vel\"", star.span));
693
+ return null;
694
+ }
695
+ this.expect("leftBrace", "Expected '{' after 'export'");
696
+ const specifiers = [];
697
+ if (!this.check("rightBrace")) {
698
+ do {
699
+ if (this.checkWord(CORE_WORDS.type) && this.peekKind(1) === "identifier") {
700
+ this.rejectTypeImportMarker(this.advance(), "export");
701
+ }
702
+ const imported = this.expect("identifier", "Expected a re-exported name");
703
+ const alias = this.matchWord(CORE_WORDS.as) ? this.expect("identifier", "Expected a re-export alias") : imported;
704
+ specifiers.push({ imported: imported.value, exported: alias.value, span: span(imported.span.start, alias.span.end) });
705
+ } while (this.match("comma") && !this.check("rightBrace"));
706
+ }
707
+ this.expect("rightBrace", "Expected '}' after re-exported names");
708
+ this.expectWord(CORE_WORDS.from, "Expected 'from' after re-exported names; VelarScript modules export declarations directly and re-export other modules' names with export {name} from \"./module.vel\"");
709
+ // MOD-I1 / BRG-D1: like parseImport, a recovered re-export never
710
+ // fabricates an empty-source dependency.
711
+ const hadSource = this.check("string");
712
+ const source = this.expect("string", "Expected a module path string");
713
+ if (!hadSource) {
714
+ this.synchronize();
715
+ return null;
716
+ }
717
+ if (source.value === "") {
718
+ this.diagnostics.push(diagnostic("VEL2001", "A module path cannot be empty", source.span));
719
+ return null;
720
+ }
721
+ if (specifiers.length === 0) {
722
+ this.diagnostics.push(diagnostic("VEL2029", "A re-export must name at least one export", span(start, source.span.end)));
723
+ }
724
+ return { kind: "ReExportDeclaration", source: source.value, sourceSpan: source.span, specifiers, span: span(start, source.span.end) };
725
+ }
726
+ parseExternModule(start) {
727
+ this.expect("module", "Expected 'module' after 'extern'");
728
+ const source = this.expect("string", "Expected a module name string");
729
+ this.expect("colon", "Expected ':' after extern module name");
730
+ const contract = this.parseExternContract(source.span.end);
731
+ return {
732
+ kind: "ExternModuleDeclaration",
733
+ source: source.value,
734
+ functions: contract.functions,
735
+ constants: contract.constants,
736
+ classes: contract.classes,
737
+ span: span(start, contract.span.end),
738
+ };
739
+ }
740
+ parseEmbeddedJavaScript(start, unsafe) {
741
+ const captures = unsafe ? [] : this.parseEmbeddedJavaScriptCaptures();
742
+ const sourceToken = this.expect("string", "Expected a multiline raw JavaScript source block after 'js'");
743
+ const payload = isEmbeddedJavaScriptTokenPayload(sourceToken.payload) ? sourceToken.payload : null;
744
+ if (!payload) {
745
+ this.diagnostics.push(diagnostic("VEL2037", "Inline JavaScript source uses a multiline raw backtick block whose closing backtick is alone at the declaration's indentation", sourceToken.span));
746
+ }
747
+ const sourceSpan = payload?.sourceSpan ?? sourceToken.span;
748
+ const source = payload ? sourceToken.value : "";
749
+ const inspected = inspectEmbeddedJavaScript(source, sourceSpan.start, !unsafe);
750
+ for (const issue of inspected.issues)
751
+ this.diagnostics.push(diagnostic("VEL2037", issue.message, issue.span));
752
+ const capturesByName = new Map(captures.map((capture) => [capture.name, capture]));
753
+ for (const binding of inspected.bindings) {
754
+ const capture = capturesByName.get(binding.name);
755
+ if (!capture)
756
+ continue;
757
+ this.diagnostics.push(diagnostic("VEL2037", `Capture '${capture.name}' conflicts with a top-level JavaScript binding of the same name; rename one so the factory parameter cannot shadow module state`, binding.nameSpan));
758
+ }
759
+ for (const exported of inspected.exports) {
760
+ const capture = capturesByName.get(exported.name);
761
+ if (!capture)
762
+ continue;
763
+ this.diagnostics.push(diagnostic("VEL2037", `Capture '${capture.name}' conflicts with a JavaScript export of the same name; rename one so the generated VelarScript binding cannot hide the captured value`, exported.nameSpan));
764
+ }
765
+ const contract = unsafe ? null : (() => {
766
+ this.expect("colon", "Expected ':' after a checked inline JavaScript source block");
767
+ return this.parseExternContract(sourceToken.span.end);
768
+ })();
769
+ if (contract)
770
+ this.reportEmbeddedJavaScriptContract(inspected.exports, contract);
771
+ return {
772
+ kind: "EmbeddedJavaScriptDeclaration",
773
+ form: unsafe ? "unsafe" : "checked",
774
+ unsafe,
775
+ captures,
776
+ source,
777
+ sourceSpan,
778
+ exports: inspected.exports,
779
+ imports: inspected.imports,
780
+ dependencies: inspected.dependencies,
781
+ bindings: inspected.bindings,
782
+ factoryEdits: inspected.factoryEdits,
783
+ contract,
784
+ span: span(start, contract?.span.end ?? sourceToken.span.end),
785
+ };
786
+ }
787
+ parseEmbeddedJavaScriptCaptures() {
788
+ this.expect("leftParen", "Expected '(' after 'extern js'; checked inline JavaScript declares every captured value and its type");
789
+ const captures = [];
790
+ if (!this.check("rightParen")) {
791
+ do {
792
+ const start = this.current().span.start;
793
+ const rest = this.match("ellipsis");
794
+ if (rest) {
795
+ this.diagnostics.push(diagnostic("VEL2037", "Inline JavaScript captures are one value per named factory parameter; rest captures are not supported", this.previous().span));
796
+ }
797
+ const name = this.expectBindingName("Expected a capture name", "capture name");
798
+ let type;
799
+ if (this.match("colon")) {
800
+ type = this.parseTypeReference();
801
+ }
802
+ else {
803
+ this.diagnostics.push(diagnostic("VEL2037", `Capture '${name.value}' requires an explicit type; captured values cross the VelarScript/JavaScript boundary through this contract`, name.span));
804
+ type = { syntax: { kind: "NamedTypeSyntax", name: "unknown", span: name.span }, span: name.span };
805
+ }
806
+ let end = type.span.end;
807
+ if (this.match("assign")) {
808
+ const assign = this.previous();
809
+ const defaultValue = this.parseExpression();
810
+ end = defaultValue.span.end;
811
+ this.diagnostics.push(diagnostic("VEL2037", "Inline JavaScript captures cannot declare defaults; pass the value explicitly at the declaration site", span(assign.span.start, defaultValue.span.end)));
812
+ }
813
+ captures.push({ name: name.value, nameSpan: name.span, type, span: span(start, end) });
814
+ } while (this.match("comma") && !this.check("rightParen"));
815
+ }
816
+ this.expect("rightParen", "Expected ')' after inline JavaScript captures");
817
+ return captures;
818
+ }
819
+ reportEmbeddedJavaScriptContract(exports, contract) {
820
+ const exported = new Map(exports.map((item) => [item.name, item]));
821
+ const declared = new Map();
822
+ for (const declaration of [...contract.functions, ...contract.constants, ...contract.classes]) {
823
+ declared.set(declaration.name, declaration.span);
824
+ if (exported.has(declaration.name))
825
+ continue;
826
+ this.diagnostics.push(diagnostic("VEL2037", `Inline JavaScript contract declares '${declaration.name}', but the source has no named ESM export '${declaration.name}'`, declaration.span));
827
+ }
828
+ for (const item of exports) {
829
+ if (declared.has(item.name))
830
+ continue;
831
+ this.diagnostics.push(diagnostic("VEL2037", `JavaScript export '${item.name}' has no checked contract declaration; add an 'export def', 'export const', or 'export class' entry below the block`, item.nameSpan));
832
+ }
833
+ }
834
+ parseExternContract(start) {
835
+ this.expect("newline", "Expected a newline before extern declarations");
836
+ this.consumeNewlines();
837
+ this.expect("indent", "Expected indented extern declarations");
838
+ const functions = [];
839
+ const constants = [];
840
+ const classes = [];
841
+ this.consumeNewlines();
842
+ while (!this.check("dedent") && !this.check("eof")) {
843
+ const declarationStart = this.current().span.start;
844
+ if (!this.match("export")) {
845
+ this.diagnostics.push(diagnostic("VEL2010", "Extern declarations must be exported", this.current().span));
846
+ this.synchronize();
847
+ this.consumeNewlines();
848
+ continue;
849
+ }
850
+ if (this.match("class")) {
851
+ classes.push(this.parseExternClass(declarationStart));
852
+ this.consumeNewlines();
853
+ continue;
854
+ }
855
+ const asynchronous = this.match("async");
856
+ if (!asynchronous && this.match("const")) {
857
+ const name = this.expect("identifier", "Expected an extern constant name");
858
+ this.expect("colon", "Expected ':' after an extern constant name");
859
+ const type = this.parseTypeReference();
860
+ constants.push({ name: name.value, type, span: span(declarationStart, type.span.end) });
861
+ this.expectStatementEnd();
862
+ this.consumeNewlines();
863
+ continue;
864
+ }
865
+ if (!this.match("def")) {
866
+ // BRG-N2: the legal member list names all three forms.
867
+ this.diagnostics.push(diagnostic("VEL2010", "Extern modules declare functions with 'export def', read-only values with 'export const name: Type', or classes with 'export class Name:'", this.current().span));
868
+ this.synchronize();
869
+ this.consumeNewlines();
870
+ continue;
871
+ }
872
+ const name = this.expect("identifier", "Expected an extern function name");
873
+ const typeParameters = this.parseTypeParameters();
874
+ const parameters = this.parseParameters();
875
+ this.reportUntypedExternParameters(parameters);
876
+ const parameterListEnd = this.previous().span.end;
877
+ const returnType = this.match("arrow") ? this.parseTypeReference() : null;
878
+ functions.push({
879
+ asynchronous,
880
+ name: name.value,
881
+ ...(typeParameters ? { typeParameters } : {}),
882
+ parameters,
883
+ returnType,
884
+ signatureSpan: span(declarationStart, returnType?.span.end ?? parameterListEnd),
885
+ span: span(declarationStart, returnType?.span.end ?? this.previous().span.end),
886
+ });
887
+ if (this.reportExternDeclarationBody()) {
888
+ this.consumeNewlines();
889
+ continue;
890
+ }
891
+ this.expectStatementEnd();
892
+ this.consumeNewlines();
893
+ }
894
+ const close = this.expect("dedent", "Expected the end of extern declarations");
895
+ return {
896
+ functions,
897
+ constants,
898
+ classes,
899
+ span: span(start, Math.max(functions.at(-1)?.span.end ?? start, constants.at(-1)?.span.end ?? start, classes.at(-1)?.span.end ?? start, close.span.end)),
900
+ };
901
+ }
902
+ parseExternClass(start) {
903
+ const name = this.expect("identifier", "Expected an extern class name");
904
+ // BRG-U6: a generic extern class gets the same polite rejection as a
905
+ // source class instead of a bare parse cascade; generic extern `def`
906
+ // members remain the generic surface.
907
+ if (this.check("less")) {
908
+ this.diagnostics.push(diagnostic("VEL2025", `Extern class '${name.value}' cannot declare type parameters; declare the class without them and use generic 'def' members or 'unknown' where the type varies`, this.current().span));
909
+ this.parseTypeParameters();
910
+ }
911
+ let parameters = [];
912
+ if (this.check("leftParen")) {
913
+ this.diagnostics.push(diagnostic("VEL2022", `Extern class '${name.value}' declares its constructor in the class body with 'constructor(...)'`, this.current().span));
914
+ this.parseExternClassParameters();
915
+ }
916
+ const base = this.match("extends") ? this.expect("identifier", "Expected an extern base class name after 'extends'").value : null;
917
+ this.expect("colon", "Expected ':' before an extern class body");
918
+ this.expect("newline", "Expected a newline before an extern class body");
919
+ this.consumeNewlines();
920
+ this.expect("indent", "Expected an indented extern class body");
921
+ const fields = [];
922
+ const getters = [];
923
+ const methods = [];
924
+ let constructorSeen = false;
925
+ this.consumeNewlines();
926
+ while (!this.check("dedent") && !this.check("eof")) {
927
+ const memberStart = this.current().span.start;
928
+ if (this.match("pass")) {
929
+ this.expectStatementEnd();
930
+ this.consumeNewlines();
931
+ continue;
932
+ }
933
+ if (this.checkWord(CORE_WORDS.constructor)) {
934
+ this.advance();
935
+ const constructorParameters = this.parseParameters();
936
+ this.reportUntypedExternParameters(constructorParameters);
937
+ if (constructorSeen) {
938
+ this.diagnostics.push(diagnostic("VEL2022", `Extern class '${name.value}' has more than one constructor`, span(memberStart, this.previous().span.end)));
939
+ }
940
+ else {
941
+ parameters = constructorParameters.map((parameter) => ({ ...parameter, binding: null, private: false }));
942
+ constructorSeen = true;
943
+ }
944
+ this.expectStatementEnd();
945
+ this.consumeNewlines();
946
+ continue;
947
+ }
948
+ let static_ = false;
949
+ let asynchronous = false;
950
+ let scanningModifiers = true;
951
+ // CLS-I5: reported once the member kind is known — see the source class
952
+ // body; an extern getter or method has no read-only contract either.
953
+ let readonlyModifier = null;
954
+ while (scanningModifiers) {
955
+ if (this.match("static"))
956
+ static_ = true;
957
+ else if (this.match("async"))
958
+ asynchronous = true;
959
+ else if (this.checkWord(CORE_WORDS.readonly))
960
+ readonlyModifier = this.advance();
961
+ else
962
+ scanningModifiers = false;
963
+ }
964
+ const mutable = this.match("let");
965
+ const readonly = !mutable && this.match("const");
966
+ if (mutable || readonly) {
967
+ this.reportClassMemberReadonly(readonlyModifier, "field", "VEL2010");
968
+ if (asynchronous)
969
+ this.diagnostics.push(diagnostic("VEL2010", "Extern class fields cannot be async", this.previous().span));
970
+ const fieldName = this.expectMemberName("Expected an extern class field name");
971
+ this.expect("colon", "Expected ':' after an extern class field name");
972
+ const type = this.parseTypeReference();
973
+ fields.push({ static: static_, mutable, name: fieldName.value, type, span: span(memberStart, type.span.end) });
974
+ this.expectStatementEnd();
975
+ this.consumeNewlines();
976
+ continue;
977
+ }
978
+ if (this.checkWord(CORE_WORDS.get)) {
979
+ this.advance();
980
+ this.reportClassMemberReadonly(readonlyModifier, "executable", "VEL2010");
981
+ const getterName = this.expectMemberName("Expected an extern class getter name");
982
+ this.expect("leftParen", "Expected '(' after an extern getter name");
983
+ if (!this.check("rightParen")) {
984
+ this.diagnostics.push(diagnostic("VEL2023", "An extern getter cannot accept parameters", this.current().span));
985
+ while (!this.check("rightParen") && !this.check("newline") && !this.check("eof"))
986
+ this.advance();
987
+ }
988
+ this.expect("rightParen", "Expected ')' after an extern getter name");
989
+ let type;
990
+ if (this.match("arrow")) {
991
+ type = this.parseTypeReference();
992
+ }
993
+ else {
994
+ this.diagnostics.push(diagnostic("VEL4023", `Extern getter '${getterName.value}' requires an explicit result annotation`, getterName.span));
995
+ type = { syntax: { kind: "NamedTypeSyntax", name: "unknown", span: getterName.span }, span: getterName.span };
996
+ }
997
+ if (asynchronous)
998
+ this.diagnostics.push(diagnostic("VEL2023", "An extern getter cannot be async; expose an ordinary async method instead", span(memberStart, type.span.end)));
999
+ getters.push({ static: static_, name: getterName.value, type, span: span(memberStart, type.span.end) });
1000
+ this.expectStatementEnd();
1001
+ this.consumeNewlines();
1002
+ continue;
1003
+ }
1004
+ if (this.match("def")) {
1005
+ this.reportClassMemberReadonly(readonlyModifier, "executable", "VEL2010");
1006
+ const methodName = this.expectMemberName("Expected an extern class method name");
1007
+ const typeParameters = this.parseTypeParameters();
1008
+ const methodParameters = this.parseParameters();
1009
+ this.reportUntypedExternParameters(methodParameters);
1010
+ const parameterListEnd = this.previous().span.end;
1011
+ const returnType = this.match("arrow") ? this.parseTypeReference() : null;
1012
+ methods.push({
1013
+ static: static_,
1014
+ asynchronous,
1015
+ name: methodName.value,
1016
+ ...(typeParameters ? { typeParameters } : {}),
1017
+ parameters: methodParameters,
1018
+ returnType,
1019
+ signatureSpan: span(memberStart, returnType?.span.end ?? parameterListEnd),
1020
+ span: span(memberStart, returnType?.span.end ?? this.previous().span.end),
1021
+ });
1022
+ if (this.reportExternDeclarationBody()) {
1023
+ this.consumeNewlines();
1024
+ continue;
1025
+ }
1026
+ this.expectStatementEnd();
1027
+ this.consumeNewlines();
1028
+ continue;
1029
+ }
1030
+ this.diagnostics.push(diagnostic("VEL2010", "Extern class bodies declare fields with const/let, one constructor signature, getters with get, methods with def, or 'pass'", this.current().span));
1031
+ this.synchronize();
1032
+ this.consumeNewlines();
1033
+ }
1034
+ const close = this.expect("dedent", "Expected the end of an extern class body");
1035
+ return { name: name.value, parameters, base, fields, getters, methods, span: span(start, Math.max(fields.at(-1)?.span.end ?? start, getters.at(-1)?.span.end ?? start, methods.at(-1)?.span.end ?? start, close.span.end)) };
1036
+ }
1037
+ parseExtensionStatement(_start, _modifiers) {
1038
+ return undefined;
1039
+ }
1040
+ /** Lets an extension claim its own `unsafe <shape>` before Core requires `js`. */
1041
+ parseUnsafeExtensionStatement(_start) {
1042
+ return undefined;
1043
+ }
1044
+ parseExtensionImport(_start) {
1045
+ return undefined;
1046
+ }
1047
+ parseVariable(start, exported) {
1048
+ const bindingToken = this.advance();
1049
+ const pattern = this.parseBindingPattern();
1050
+ const type = this.match("colon") ? this.parseTypeReference() : null;
1051
+ this.expect("assign", "Expected '=' after binding pattern");
1052
+ const initializer = this.parseExpression();
1053
+ return {
1054
+ kind: "VariableDeclaration",
1055
+ binding: bindingToken.kind === "let" ? "let" : "const",
1056
+ exported,
1057
+ pattern,
1058
+ type,
1059
+ initializer,
1060
+ span: span(start, initializer.span.end),
1061
+ };
1062
+ }
1063
+ parseUsing(start) {
1064
+ const name = this.expect("identifier", "Expected a name after 'using'");
1065
+ this.expect("assign", "Expected '=' after a 'using' name");
1066
+ const initializer = this.parseExpression();
1067
+ return {
1068
+ kind: "UsingDeclaration",
1069
+ name: name.value,
1070
+ nameSpan: name.span,
1071
+ initializer,
1072
+ span: span(start, initializer.span.end),
1073
+ };
1074
+ }
1075
+ parseBindingPattern() {
1076
+ return this.withParseDepth(() => this.parseBindingPatternBody());
1077
+ }
1078
+ parseBindingPatternBody() {
1079
+ if (this.match("identifier")) {
1080
+ const name = this.previous();
1081
+ return { kind: "NameBindingPattern", name: name.value, span: name.span };
1082
+ }
1083
+ if (this.match("leftBrace")) {
1084
+ const open = this.previous();
1085
+ const entries = [];
1086
+ let rest = null;
1087
+ while (!this.check("rightBrace") && !this.check("eof")) {
1088
+ if (this.match("ellipsis")) {
1089
+ const name = this.expect("identifier", "Expected a name after '...'");
1090
+ rest = { kind: "NameBindingPattern", name: name.value, span: name.span };
1091
+ if (!this.check("rightBrace"))
1092
+ this.diagnostics.push(diagnostic("VEL2011", "A rest binding must be last", name.span));
1093
+ break;
1094
+ }
1095
+ const property = this.expectMemberName("Expected an object binding field name");
1096
+ const renamed = this.match("colon");
1097
+ if (!renamed && property.kind !== "identifier") {
1098
+ this.diagnostics.push(diagnostic("VEL2011", `Keyword-named field '${property.value}' requires ': name' in an object binding pattern`, property.span));
1099
+ }
1100
+ const pattern = renamed
1101
+ ? this.parseBindingPattern()
1102
+ : { kind: "NameBindingPattern", name: property.kind === "identifier" ? property.value : "_invalid", span: property.span };
1103
+ entries.push({ property: property.value, pattern, span: span(property.span.start, pattern.span.end) });
1104
+ if (!this.match("comma"))
1105
+ break;
1106
+ }
1107
+ const close = this.expect("rightBrace", "Expected '}' after object binding");
1108
+ return { kind: "ObjectBindingPattern", entries, rest, span: span(open.span.start, close.span.end) };
1109
+ }
1110
+ if (this.match("leftBracket")) {
1111
+ const open = this.previous();
1112
+ const elements = [];
1113
+ let rest = null;
1114
+ while (!this.check("rightBracket") && !this.check("eof")) {
1115
+ if (this.match("comma")) {
1116
+ elements.push(null);
1117
+ continue;
1118
+ }
1119
+ if (this.match("ellipsis")) {
1120
+ const name = this.expect("identifier", "Expected a name after '...'");
1121
+ rest = { kind: "NameBindingPattern", name: name.value, span: name.span };
1122
+ if (!this.check("rightBracket"))
1123
+ this.diagnostics.push(diagnostic("VEL2011", "A rest binding must be last", name.span));
1124
+ break;
1125
+ }
1126
+ elements.push(this.parseBindingPattern());
1127
+ if (!this.match("comma"))
1128
+ break;
1129
+ }
1130
+ const close = this.expect("rightBracket", "Expected ']' after list binding");
1131
+ return { kind: "ListBindingPattern", elements, rest, span: span(open.span.start, close.span.end) };
1132
+ }
1133
+ const token = this.current();
1134
+ this.diagnostics.push(diagnostic("VEL2011", this.reservedWordMessage("binding name") ?? "Expected a binding name or destructuring pattern", token.span));
1135
+ this.advance();
1136
+ return { kind: "NameBindingPattern", name: "_invalid", span: token.span };
1137
+ }
1138
+ parseFunction(start, exported, asynchronous) {
1139
+ const name = this.expect("identifier", "Expected a function name");
1140
+ const typeParameters = this.parseTypeParameters();
1141
+ const parameters = this.parseParameters();
1142
+ const parameterListEnd = this.previous().span.end;
1143
+ const returnType = this.match("arrow") ? this.parseTypeReference() : null;
1144
+ const body = this.parseBlock();
1145
+ const end = body.at(-1)?.span.end ?? returnType?.span.end ?? name.span.end;
1146
+ return {
1147
+ kind: "FunctionDeclaration",
1148
+ exported,
1149
+ asynchronous,
1150
+ name: name.value,
1151
+ ...(typeParameters ? { typeParameters } : {}),
1152
+ parameters,
1153
+ returnType,
1154
+ ...(returnType ? { resultAnnotationSpan: span(parameterListEnd, returnType.span.end) } : {}),
1155
+ signatureSpan: span(start, returnType?.span.end ?? parameterListEnd),
1156
+ body,
1157
+ span: span(start, end),
1158
+ };
1159
+ }
1160
+ parseTypeParameters() {
1161
+ if (!this.match("less"))
1162
+ return null;
1163
+ const open = this.previous();
1164
+ const parameters = [];
1165
+ if (!this.check("greater")) {
1166
+ do {
1167
+ const name = this.expect("identifier", "Expected a type parameter name");
1168
+ // D41 item 61: `<T: Bound>` names one word from the compiler's closed
1169
+ // bound vocabulary. The name is taken here and judged by the analyzer,
1170
+ // so an unknown word gets a directed diagnostic instead of a parse
1171
+ // cascade.
1172
+ const bound = this.match("colon")
1173
+ ? this.expect("identifier", "Expected a type parameter bound name after ':'")
1174
+ : null;
1175
+ if (name.value) {
1176
+ parameters.push({
1177
+ name: name.value,
1178
+ ...(bound?.value ? { bound: bound.value, boundSpan: bound.span } : {}),
1179
+ span: name.span,
1180
+ });
1181
+ }
1182
+ } while (this.match("comma") && !this.check("greater"));
1183
+ }
1184
+ const close = this.expect("greater", "Expected '>' after type parameters");
1185
+ if (parameters.length === 0) {
1186
+ this.diagnostics.push(diagnostic("VEL2025", "A type parameter list requires at least one name", span(open.span.start, close.span.end)));
1187
+ }
1188
+ return parameters;
1189
+ }
1190
+ parseParameters() {
1191
+ this.expect("leftParen", "Expected '('");
1192
+ const parameters = [];
1193
+ let sawRest = false;
1194
+ let sawDefault = false;
1195
+ if (!this.check("rightParen")) {
1196
+ do {
1197
+ const rest = this.match("ellipsis");
1198
+ const name = this.expectBindingName("Expected a parameter name", "parameter name");
1199
+ const type = this.match("colon") ? this.parseTypeReference() : null;
1200
+ const defaultValue = this.match("assign") ? this.parseExpression() : null;
1201
+ const parameterSpan = span(name.span.start, defaultValue?.span.end ?? type?.span.end ?? name.span.end);
1202
+ if (sawRest) {
1203
+ this.diagnostics.push(diagnostic("VEL2016", "A rest parameter must be the final parameter", parameterSpan));
1204
+ }
1205
+ // D65 rule 170: in a parameter list that can be contextually typed —
1206
+ // an arrow's — the missing element type may still arrive from the
1207
+ // contextual function type's own rest, exactly as a fixed parameter's
1208
+ // type does, so the refusal waits for the analyzer, which is the only
1209
+ // place that knows whether the context supplied one. Every other
1210
+ // parameter list is a declaration with no context by construction, and
1211
+ // is refused here where it always was.
1212
+ if (rest && !type && this.contextualParameterDepth === 0) {
1213
+ this.diagnostics.push(diagnostic("VEL2016", REST_PARAMETER_ELEMENT_TYPE_MESSAGE, parameterSpan));
1214
+ }
1215
+ if (rest && defaultValue) {
1216
+ this.diagnostics.push(diagnostic("VEL2016", "A rest parameter cannot have a default value", parameterSpan));
1217
+ }
1218
+ if (!rest && !defaultValue && sawDefault && !sawRest) {
1219
+ this.diagnostics.push(diagnostic("VEL2016", "A required parameter cannot follow a parameter with a default value", parameterSpan));
1220
+ }
1221
+ parameters.push({ name: name.value, type, defaultValue, rest, span: parameterSpan });
1222
+ if (!rest && defaultValue)
1223
+ sawDefault = true;
1224
+ sawRest ||= rest;
1225
+ } while (this.match("comma") && !this.check("rightParen"));
1226
+ }
1227
+ this.expect("rightParen", "Expected ')' after parameters");
1228
+ return parameters;
1229
+ }
1230
+ parseClassConstructorParameters() {
1231
+ this.expect("leftParen", "Expected '('");
1232
+ const parameters = [];
1233
+ let sawDefault = false;
1234
+ if (!this.check("rightParen")) {
1235
+ do {
1236
+ const rest = this.match("ellipsis");
1237
+ let private_ = this.match("private");
1238
+ const invalidStatic = this.match("static") ? this.previous() : null;
1239
+ if (!private_)
1240
+ private_ = this.match("private");
1241
+ const binding = this.match("const") ? "const" : this.match("let") ? "let" : null;
1242
+ const name = this.expectBindingName("Expected a constructor parameter name", "parameter name");
1243
+ const type = this.match("colon") ? this.parseTypeReference() : null;
1244
+ const defaultValue = this.match("assign") ? this.parseExpression() : null;
1245
+ const parameterSpan = span(name.span.start, defaultValue?.span.end ?? type?.span.end ?? name.span.end);
1246
+ if (invalidStatic) {
1247
+ this.diagnostics.push(diagnostic("VEL2021", "Constructor parameters cannot be static", invalidStatic.span));
1248
+ }
1249
+ if (private_ && !binding) {
1250
+ this.diagnostics.push(diagnostic("VEL2021", "A private constructor parameter must declare a field with 'const' or 'let'", parameterSpan));
1251
+ }
1252
+ if (binding && !type) {
1253
+ this.diagnostics.push(diagnostic("VEL2021", "Constructor parameter fields require an explicit type", parameterSpan));
1254
+ }
1255
+ // Source constructors lower to `constructor(...)` parameter lists that
1256
+ // the class shape counts as fixed arity, so a rest spelling is either
1257
+ // uncallable or silently wrong at runtime. Extern class declarations
1258
+ // keep rest support: they describe existing JavaScript constructors.
1259
+ if (rest) {
1260
+ this.diagnostics.push(diagnostic("VEL2016", "Class constructors do not support rest parameters", name.span));
1261
+ }
1262
+ if (!rest && !defaultValue && sawDefault) {
1263
+ this.diagnostics.push(diagnostic("VEL2016", "A required parameter cannot follow a parameter with a default value", parameterSpan));
1264
+ }
1265
+ parameters.push({
1266
+ name: name.value,
1267
+ type,
1268
+ defaultValue,
1269
+ rest,
1270
+ binding,
1271
+ private: private_ && binding !== null,
1272
+ span: parameterSpan,
1273
+ });
1274
+ if (!rest && defaultValue)
1275
+ sawDefault = true;
1276
+ } while (this.match("comma") && !this.check("rightParen"));
1277
+ }
1278
+ this.expect("rightParen", "Expected ')' after constructor parameters");
1279
+ return parameters;
1280
+ }
1281
+ parseExternClassParameters() {
1282
+ this.expect("leftParen", "Expected '('");
1283
+ const parameters = [];
1284
+ let sawRest = false;
1285
+ let sawDefault = false;
1286
+ if (!this.check("rightParen")) {
1287
+ do {
1288
+ const rest = this.match("ellipsis");
1289
+ const binding = this.match("const") ? "const" : this.match("let") ? "let" : null;
1290
+ const name = this.expectBindingName("Expected an extern class parameter name", "parameter name");
1291
+ const type = this.match("colon") ? this.parseTypeReference() : null;
1292
+ const defaultValue = this.match("assign") ? this.parseExpression() : null;
1293
+ const parameterSpan = span(name.span.start, defaultValue?.span.end ?? type?.span.end ?? name.span.end);
1294
+ if (sawRest)
1295
+ this.diagnostics.push(diagnostic("VEL2016", "A rest parameter must be the final parameter", parameterSpan));
1296
+ if (rest && !type)
1297
+ this.diagnostics.push(diagnostic("VEL2016", REST_PARAMETER_ELEMENT_TYPE_MESSAGE, parameterSpan));
1298
+ if (rest && defaultValue)
1299
+ this.diagnostics.push(diagnostic("VEL2016", "A rest parameter cannot have a default value", parameterSpan));
1300
+ if (rest && binding)
1301
+ this.diagnostics.push(diagnostic("VEL2016", "A rest parameter cannot declare a class field", parameterSpan));
1302
+ if (!rest && !defaultValue && sawDefault && !sawRest) {
1303
+ this.diagnostics.push(diagnostic("VEL2016", "A required parameter cannot follow a parameter with a default value", parameterSpan));
1304
+ }
1305
+ parameters.push({ name: name.value, binding, private: false, type, defaultValue, rest, span: parameterSpan });
1306
+ if (!rest && defaultValue)
1307
+ sawDefault = true;
1308
+ sawRest ||= rest;
1309
+ } while (this.match("comma") && !this.check("rightParen"));
1310
+ }
1311
+ this.expect("rightParen", "Expected ')' after extern class parameters");
1312
+ return parameters;
1313
+ }
1314
+ parseTypeDefinition(start, exported) {
1315
+ const name = this.expect("identifier", "Expected a type name");
1316
+ const typeParameters = this.parseTypeParameters();
1317
+ if (this.match("assign")) {
1318
+ const target = this.parseTypeReference();
1319
+ // D55 rule 120 admits the generic *record*. An alias names an
1320
+ // instantiation — `type Boxed = Box<string>` — which is rule 123's whole
1321
+ // idiom, so a parameter list here has nothing to bind and the refusal
1322
+ // says what to write instead.
1323
+ if (typeParameters) {
1324
+ this.diagnostics.push(diagnostic("VEL2025", `Type alias '${name.value}' cannot declare type parameters; an alias names one instantiation with concrete arguments, while '<T>' belongs to a '${TYPE_PARAMETER_DECLARATION_FORMS.join("' or a '")}' declaration`, name.span));
1325
+ }
1326
+ return { kind: "TypeAliasDeclaration", exported, name: name.value, target, span: span(start, target.span.end) };
1327
+ }
1328
+ this.expect("colon", "Expected ':' after type name");
1329
+ this.expect("newline", "Expected a newline before type fields");
1330
+ this.consumeNewlines();
1331
+ this.expect("indent", "Expected indented type fields");
1332
+ const fields = [];
1333
+ this.consumeNewlines();
1334
+ while (!this.check("dedent") && !this.check("eof")) {
1335
+ // D64 rule 164: `readonly` is a contextual keyword, so it is claimed by
1336
+ // its own shape and by nothing else. `readonly: number` is a field named
1337
+ // `readonly` — every other contextual keyword already reads that way in
1338
+ // this position, `{readonly: 1}` already builds that record on the value
1339
+ // side, and charter §3 promises the word is an ordinary name here.
1340
+ // `readonly readonly: number` still declares a read-only field of that
1341
+ // name, because the modifier shape is `readonly` followed by a name.
1342
+ const readonly = this.checkWord(CORE_WORDS.readonly) && this.peekKind(1) !== "colon";
1343
+ const fieldStart = readonly ? this.advance().span.start : this.current().span.start;
1344
+ const fieldName = this.expectMemberName("Expected a field name");
1345
+ this.expect("colon", "Expected ':' after field name");
1346
+ const type = this.parseTypeReference();
1347
+ if (this.check("assign")) {
1348
+ // ENM-U5: record fields carry no defaults — a record is data, so
1349
+ // every construction site states its values.
1350
+ this.diagnostics.push(diagnostic("VEL2017", `Record fields do not take default values; make the field optional ('${fieldName.value}: ...?') or set the value where the record is built`, this.current().span));
1351
+ this.advance();
1352
+ this.parseExpression();
1353
+ }
1354
+ fields.push({ readonly, name: fieldName.value, type, span: span(fieldStart, type.span.end) });
1355
+ this.expectStatementEnd();
1356
+ this.consumeNewlines();
1357
+ }
1358
+ const close = this.expect("dedent", "Expected the end of type fields");
1359
+ return { kind: "TypeDeclaration", exported, name: name.value, ...(typeParameters ? { typeParameters } : {}), fields, span: span(start, fields.at(-1)?.span.end ?? close.span.end) };
1360
+ }
1361
+ parseEnumDeclaration(start, exported) {
1362
+ const name = this.expect("identifier", "Expected an enum name");
1363
+ // D55 rule 127.1: `enum` was the one declaration in this family with no
1364
+ // `parseTypeParameters` call at all, so `enum Color<T>:` cascaded into six
1365
+ // parse errors instead of saying the one thing that is wrong.
1366
+ if (this.check("less")) {
1367
+ this.parseTypeParameters();
1368
+ this.diagnostics.push(diagnostic("VEL2025", `Enum '${name.value}' cannot declare type parameters; ${typeParameterDeclarationFormsPhrase()} take '<T>'`, name.span));
1369
+ }
1370
+ this.expect("colon", "Expected ':' after enum name");
1371
+ this.expect("newline", "Expected a newline before enum members");
1372
+ this.consumeNewlines();
1373
+ this.expect("indent", "Expected indented enum members");
1374
+ const members = [];
1375
+ this.consumeNewlines();
1376
+ while (!this.check("dedent") && !this.check("eof")) {
1377
+ // ENM-I8: a bare `pass` line is the placeholder statement here exactly
1378
+ // as in a class body — never a member named 'pass'. An enum whose body
1379
+ // is only `pass` then falls through to the existing "requires at least
1380
+ // one member" rule, and 'pass' becomes the one undeclarable member name.
1381
+ if (this.check("pass")) {
1382
+ const keyword = this.advance();
1383
+ if (this.check("assign")) {
1384
+ this.diagnostics.push(diagnostic("VEL2017", "'pass' is the placeholder line and cannot be declared as an enum member; pick another member name", keyword.span));
1385
+ this.synchronize();
1386
+ }
1387
+ else {
1388
+ this.expectStatementEnd();
1389
+ }
1390
+ this.consumeNewlines();
1391
+ continue;
1392
+ }
1393
+ const member = this.expectMemberName("Expected an enum member name");
1394
+ let value = member.value;
1395
+ let valueSpan;
1396
+ if (this.match("assign")) {
1397
+ if (this.check("string")) {
1398
+ const serialized = this.advance();
1399
+ const payload = serialized.payload;
1400
+ if (payload?.layout) {
1401
+ this.diagnostics.push(diagnostic("VEL2017", "An enum member value must be an inline string", serialized.span));
1402
+ }
1403
+ value = serialized.value;
1404
+ valueSpan = serialized.span;
1405
+ }
1406
+ else if (this.check("fstring")) {
1407
+ const serialized = this.advance();
1408
+ this.diagnostics.push(diagnostic("VEL2017", "An enum member value is static; use an inline quoted string without interpolation", serialized.span));
1409
+ valueSpan = serialized.span;
1410
+ }
1411
+ else {
1412
+ this.diagnostics.push(diagnostic("VEL2001", "Expected an inline string value after '=' in an enum member", this.current().span));
1413
+ this.synchronize();
1414
+ }
1415
+ }
1416
+ if (member.value)
1417
+ members.push({ name: member.value, value, ...(valueSpan ? { valueSpan } : {}), span: member.span });
1418
+ this.expectStatementEnd();
1419
+ this.consumeNewlines();
1420
+ }
1421
+ const close = this.expect("dedent", "Expected the end of enum members");
1422
+ if (members.length === 0) {
1423
+ this.diagnostics.push(diagnostic("VEL2017", `Enum '${name.value}' requires at least one member`, span(start, close.span.end)));
1424
+ }
1425
+ const last = members.at(-1);
1426
+ return { kind: "EnumDeclaration", exported, name: name.value, members, span: span(start, last?.valueSpan?.end ?? last?.span.end ?? close.span.end) };
1427
+ }
1428
+ parseClassDeclaration(start, exported, abstract) {
1429
+ const name = this.expect("identifier", "Expected a class name");
1430
+ if (this.check("less")) {
1431
+ this.parseTypeParameters();
1432
+ this.diagnostics.push(diagnostic("VEL2025", `Class '${name.value}' cannot declare type parameters; ${typeParameterDeclarationFormsPhrase()} take '<T>'`, name.span));
1433
+ }
1434
+ let parameters = [];
1435
+ if (this.match("leftParen")) {
1436
+ this.diagnostics.push(diagnostic("VEL2022", `Class '${name.value}' declares its constructor in the class body with 'constructor(...)'`, this.previous().span));
1437
+ if (!this.check("rightParen")) {
1438
+ do {
1439
+ const rest = this.match("ellipsis");
1440
+ let private_ = false;
1441
+ if (this.check("private") && (this.peekKind(1) === "const" || this.peekKind(1) === "let")) {
1442
+ this.advance();
1443
+ private_ = true;
1444
+ }
1445
+ const binding = this.match("const") ? "const" : this.match("let") ? "let" : null;
1446
+ const parameterName = this.expect("identifier", "Expected a class parameter name");
1447
+ const type = this.match("colon") ? this.parseTypeReference() : null;
1448
+ const defaultValue = this.match("assign") ? this.parseExpression() : null;
1449
+ if (rest) {
1450
+ this.diagnostics.push(diagnostic("VEL2016", "Class constructors do not support rest parameters", parameterName.span));
1451
+ }
1452
+ parameters.push({
1453
+ name: parameterName.value,
1454
+ binding,
1455
+ private: private_ && binding !== null,
1456
+ type,
1457
+ defaultValue,
1458
+ rest,
1459
+ span: span(parameterName.span.start, defaultValue?.span.end ?? type?.span.end ?? parameterName.span.end),
1460
+ });
1461
+ } while (this.match("comma") && !this.check("rightParen"));
1462
+ }
1463
+ this.expect("rightParen", "Expected ')' after class parameters");
1464
+ }
1465
+ let base = null;
1466
+ if (this.match("extends")) {
1467
+ const baseName = this.expect("identifier", "Expected a base class name after 'extends'");
1468
+ const arguments_ = [];
1469
+ let end = baseName.span.end;
1470
+ if (this.match("leftParen")) {
1471
+ this.diagnostics.push(diagnostic("VEL2022", "Pass base constructor arguments with an explicit 'super(...)' call inside the constructor", this.previous().span));
1472
+ if (!this.check("rightParen")) {
1473
+ do {
1474
+ arguments_.push(this.parseSpreadExpression());
1475
+ } while (this.match("comma") && !this.check("rightParen"));
1476
+ }
1477
+ end = this.expect("rightParen", "Expected ')' after base constructor arguments").span.end;
1478
+ }
1479
+ base = { name: baseName.value, arguments: arguments_, span: span(baseName.span.start, end) };
1480
+ }
1481
+ this.expect("colon", "Expected ':' before class body");
1482
+ this.expect("newline", "Expected a newline before class body");
1483
+ this.consumeNewlines();
1484
+ this.expect("indent", "Expected an indented class body");
1485
+ const fields = [];
1486
+ let initialization = null;
1487
+ let dispose = null;
1488
+ let iterate = null;
1489
+ const getters = [];
1490
+ const methods = [];
1491
+ this.consumeNewlines();
1492
+ while (!this.check("dedent") && !this.check("eof")) {
1493
+ const methodStart = this.current().span.start;
1494
+ // D43 item 67/69 + D68 rule 177: an `@name` member belongs to the
1495
+ // language. `@dispose:` and `@iterate:` are the two a class declares;
1496
+ // anything else gets the closed vocabulary named back. Both are parsed
1497
+ // and validated on one path because they are one idea — a question the
1498
+ // language asks the type, answered in a block that is not a method.
1499
+ if (this.check("at")) {
1500
+ const marker = this.advance();
1501
+ const memberName = this.expect("identifier", "Expected a compiler-known class member name after '@'");
1502
+ const keywordSpan = span(marker.span.start, memberName.span.end);
1503
+ const known = memberName.value === "dispose" || memberName.value === "iterate";
1504
+ if (!known) {
1505
+ this.diagnostics.push(diagnostic("VEL2022", `Unknown language member '@${memberName.value}'; a class declares '@dispose:' and '@iterate:', and no other '@' member`, keywordSpan));
1506
+ }
1507
+ const body = this.parseBlock();
1508
+ const blockSpan = span(methodStart, body.at(-1)?.span.end ?? this.previous().span.end);
1509
+ if (memberName.value === "iterate") {
1510
+ const block = { kind: "ClassIterateBlock", body, keywordSpan, span: blockSpan };
1511
+ if (iterate) {
1512
+ this.diagnostics.push(diagnostic("VEL2022", `Class '${name.value}' has more than one '@iterate' block`, block.span));
1513
+ }
1514
+ else {
1515
+ iterate = block;
1516
+ }
1517
+ }
1518
+ else if (memberName.value === "dispose") {
1519
+ const block = { kind: "ClassDisposeBlock", body, keywordSpan, span: blockSpan };
1520
+ if (dispose) {
1521
+ this.diagnostics.push(diagnostic("VEL2022", `Class '${name.value}' has more than one '@dispose' block`, block.span));
1522
+ }
1523
+ else {
1524
+ dispose = block;
1525
+ }
1526
+ }
1527
+ this.consumeNewlines();
1528
+ continue;
1529
+ }
1530
+ let methodAbstract = false;
1531
+ let methodOverride = false;
1532
+ let methodStatic = false;
1533
+ let methodPrivate = false;
1534
+ let asynchronous = false;
1535
+ let scanningModifiers = true;
1536
+ // CLS-I5: `readonly` is reported once the member kind is known, because
1537
+ // the advice differs — a field has `const`, while an executable member
1538
+ // has no read-only contract at all.
1539
+ let readonlyModifier = null;
1540
+ while (scanningModifiers) {
1541
+ if (this.match("abstract"))
1542
+ methodAbstract = true;
1543
+ else if (this.match("override"))
1544
+ methodOverride = true;
1545
+ else if (this.match("static"))
1546
+ methodStatic = true;
1547
+ else if (this.match("private"))
1548
+ methodPrivate = true;
1549
+ else if (this.checkWord(CORE_WORDS.readonly))
1550
+ readonlyModifier = this.advance();
1551
+ else if (this.match("async"))
1552
+ asynchronous = true;
1553
+ else
1554
+ scanningModifiers = false;
1555
+ }
1556
+ // D62 rule 157: `constructor` comes from Core's roster. `init` stays a
1557
+ // literal on purpose — it is the removed `init:` block, recognized only
1558
+ // to teach its replacement, so it is not a spelling the language has.
1559
+ if (this.checkWord(CORE_WORDS.constructor) || (this.check("identifier") && this.current().value === "init")) {
1560
+ const constructorName = this.current();
1561
+ this.advance();
1562
+ this.reportClassMemberReadonly(readonlyModifier, "executable", "VEL2021");
1563
+ if (constructorName.value === "init") {
1564
+ this.diagnostics.push(diagnostic("VEL2022", "Use 'constructor(...)' for class construction; the separate 'init:' block was removed", constructorName.span));
1565
+ }
1566
+ if (methodAbstract || methodOverride || methodStatic || methodPrivate || asynchronous) {
1567
+ this.diagnostics.push(diagnostic("VEL2022", "A constructor does not accept method modifiers", span(methodStart, this.previous().span.end)));
1568
+ }
1569
+ parameters = constructorName.value === CORE_WORDS.constructor ? [...this.parseClassConstructorParameters()] : [];
1570
+ const initBody = this.parseBlock();
1571
+ const block = {
1572
+ kind: "ClassInitBlock",
1573
+ body: initBody,
1574
+ span: span(methodStart, initBody.at(-1)?.span.end ?? this.previous().span.end),
1575
+ };
1576
+ if (initialization) {
1577
+ this.diagnostics.push(diagnostic("VEL2022", `Class '${name.value}' has more than one constructor`, block.span));
1578
+ }
1579
+ else {
1580
+ initialization = block;
1581
+ }
1582
+ this.consumeNewlines();
1583
+ continue;
1584
+ }
1585
+ if (this.check("const") || this.check("let")) {
1586
+ const binding = this.advance().kind;
1587
+ this.reportClassMemberReadonly(readonlyModifier, "field", "VEL2021");
1588
+ if (methodAbstract || methodOverride || asynchronous) {
1589
+ this.diagnostics.push(diagnostic("VEL2021", "Class fields support only the 'private' and 'static' modifiers; use 'const' for a read-only field", this.previous().span));
1590
+ }
1591
+ const fieldName = this.expectMemberName("Expected a class field name");
1592
+ // CLS-U7: `let name?: T` is the TypeScript optional-property shape.
1593
+ // The type here is explicit, so the missing-type message was simply
1594
+ // wrong; VelarScript has no optional-field syntax at all — the field
1595
+ // carries an optional type instead.
1596
+ const optionalMarker = this.check("question") ? this.advance() : null;
1597
+ let type;
1598
+ if (this.match("colon")) {
1599
+ type = this.parseTypeReference();
1600
+ }
1601
+ else if (optionalMarker) {
1602
+ type = { syntax: { kind: "NamedTypeSyntax", name: "unknown", span: fieldName.span }, span: fieldName.span };
1603
+ }
1604
+ else {
1605
+ this.diagnostics.push(diagnostic("VEL2021", "Class fields require an explicit type", fieldName.span));
1606
+ type = { syntax: { kind: "NamedTypeSyntax", name: "unknown", span: fieldName.span }, span: fieldName.span };
1607
+ }
1608
+ if (optionalMarker) {
1609
+ const written = formatTypeSyntax(type.syntax);
1610
+ const optional = written === "unknown" ? "T?" : written.endsWith("?") ? written : `${written}?`;
1611
+ this.diagnostics.push(diagnostic("VEL2021", `VelarScript has no optional-field syntax; a field carries an optional type instead — write '${binding} ${fieldName.value}: ${optional} = null'`, span(fieldName.span.start, optionalMarker.span.end)));
1612
+ }
1613
+ const initializer = this.match("assign") ? this.parseExpression() : null;
1614
+ fields.push({
1615
+ binding,
1616
+ static: methodStatic,
1617
+ private: methodPrivate,
1618
+ name: fieldName.value,
1619
+ type,
1620
+ initializer,
1621
+ span: span(methodStart, initializer?.span.end ?? type.span.end),
1622
+ });
1623
+ this.expectStatementEnd();
1624
+ this.consumeNewlines();
1625
+ continue;
1626
+ }
1627
+ if (this.checkWord(CORE_WORDS.get)) {
1628
+ this.advance();
1629
+ this.reportClassMemberReadonly(readonlyModifier, "executable", "VEL2021");
1630
+ getters.push(this.parseClassGetter(methodStart, methodAbstract, methodOverride, methodStatic, methodPrivate, asynchronous));
1631
+ this.consumeNewlines();
1632
+ continue;
1633
+ }
1634
+ // D45 rule 79 (CLS-U1): `set name(value):` is the JavaScript accessor
1635
+ // shape. VelarScript has no setters (section 19), and the shape used to
1636
+ // fall through to three generic cascades that never said so. `get` has
1637
+ // its own parse path, so `set` gets the matching recognition — used only
1638
+ // to teach the rejection. `def set(...)` and a field named `set` are
1639
+ // other shapes and stay legal.
1640
+ if (this.check("identifier") && this.current().value === "set"
1641
+ && this.peekKind(1) === "identifier" && this.peekKind(2) === "leftParen") {
1642
+ const keyword = this.advance();
1643
+ const setterName = this.advance();
1644
+ this.reportClassMemberReadonly(readonlyModifier, "executable", "VEL2021");
1645
+ const method = `set${setterName.value.slice(0, 1).toUpperCase()}${setterName.value.slice(1)}`;
1646
+ this.diagnostics.push(diagnostic("VEL2007", `VelarScript classes have no setters; assign the field directly, or declare a method such as 'def ${method}(value: T)'`, span(keyword.span.start, setterName.span.end)));
1647
+ this.skipMistypedDeclaration();
1648
+ this.consumeNewlines();
1649
+ continue;
1650
+ }
1651
+ if (!this.match("def")) {
1652
+ this.reportClassMemberReadonly(readonlyModifier, "field", "VEL2021");
1653
+ if (this.match("pass")) {
1654
+ this.expectStatementEnd();
1655
+ this.consumeNewlines();
1656
+ continue;
1657
+ }
1658
+ const keywordGuidance = this.check("identifier") && this.peekKind(1) === "identifier"
1659
+ ? declarationKeywordGuidance(this.current().value)
1660
+ : null;
1661
+ if (keywordGuidance) {
1662
+ const shape = this.peekKind(2);
1663
+ if (keywordGuidance.keyword === "def" && (shape === "leftParen" || shape === "less")) {
1664
+ this.diagnostics.push(recoveredDiagnostic("VEL2026", keywordGuidance.message, this.current().span, mechanicalFix(this.current().span, keywordGuidance.keyword, `Use '${keywordGuidance.keyword}'`)));
1665
+ this.advance();
1666
+ methods.push(this.parseClassMethod(methodStart, asynchronous, methodAbstract, methodOverride, methodStatic, methodPrivate));
1667
+ this.consumeNewlines();
1668
+ continue;
1669
+ }
1670
+ this.diagnostics.push(diagnostic("VEL2026", keywordGuidance.message, this.current().span));
1671
+ this.skipMistypedDeclaration();
1672
+ this.consumeNewlines();
1673
+ continue;
1674
+ }
1675
+ this.diagnostics.push(diagnostic("VEL2007", "Class bodies contain const/let fields, one constructor, get properties, methods, or 'pass'", this.current().span));
1676
+ this.synchronize();
1677
+ this.consumeNewlines();
1678
+ continue;
1679
+ }
1680
+ this.reportClassMemberReadonly(readonlyModifier, "executable", "VEL2021");
1681
+ const method = this.parseClassMethod(methodStart, asynchronous, methodAbstract, methodOverride, methodStatic, methodPrivate);
1682
+ methods.push(method);
1683
+ this.consumeNewlines();
1684
+ }
1685
+ const close = this.expect("dedent", "Expected the end of class body");
1686
+ return {
1687
+ kind: "ClassDeclaration",
1688
+ exported,
1689
+ abstract,
1690
+ name: name.value,
1691
+ parameters,
1692
+ base,
1693
+ fields,
1694
+ initialization,
1695
+ getters,
1696
+ methods,
1697
+ dispose,
1698
+ iterate,
1699
+ span: span(start, Math.max(methods.at(-1)?.span.end ?? 0, getters.at(-1)?.span.end ?? 0, fields.at(-1)?.span.end ?? 0, initialization?.span.end ?? 0, dispose?.span.end ?? 0, iterate?.span.end ?? 0, close.span.end)),
1700
+ };
1701
+ }
1702
+ parseClassGetter(start, abstract, override, static_, private_, asynchronous) {
1703
+ const name = this.expectMemberName("Expected a getter name");
1704
+ if (this.check("less")) {
1705
+ this.parseTypeParameters();
1706
+ this.diagnostics.push(diagnostic("VEL2023", "A getter cannot declare type parameters", name.span));
1707
+ }
1708
+ this.expect("leftParen", "Expected '(' after a getter name");
1709
+ if (!this.check("rightParen")) {
1710
+ this.diagnostics.push(diagnostic("VEL2023", "A getter cannot accept parameters", this.current().span));
1711
+ while (!this.check("rightParen") && !this.check("newline") && !this.check("eof"))
1712
+ this.advance();
1713
+ }
1714
+ this.expect("rightParen", "Expected ')' after a getter name");
1715
+ let returnType;
1716
+ if (this.match("arrow")) {
1717
+ returnType = this.parseTypeReference();
1718
+ }
1719
+ else {
1720
+ this.diagnostics.push(diagnostic("VEL2023", "A getter requires an explicit result type", name.span));
1721
+ returnType = { syntax: { kind: "NamedTypeSyntax", name: "unknown", span: name.span }, span: name.span };
1722
+ }
1723
+ if (asynchronous) {
1724
+ this.diagnostics.push(diagnostic("VEL2023", "A getter cannot be async; expose an ordinary async method instead", span(start, returnType.span.end)));
1725
+ }
1726
+ if (abstract) {
1727
+ this.expectStatementEnd();
1728
+ return {
1729
+ kind: "FunctionDeclaration",
1730
+ exported: false,
1731
+ asynchronous: false,
1732
+ accessor: true,
1733
+ abstract,
1734
+ override,
1735
+ static: static_,
1736
+ private: private_,
1737
+ name: name.value,
1738
+ parameters: [],
1739
+ returnType,
1740
+ signatureSpan: span(start, returnType.span.end),
1741
+ body: [],
1742
+ span: span(start, returnType.span.end),
1743
+ };
1744
+ }
1745
+ const body = this.parseBlock();
1746
+ return {
1747
+ kind: "FunctionDeclaration",
1748
+ exported: false,
1749
+ asynchronous: false,
1750
+ accessor: true,
1751
+ abstract,
1752
+ override,
1753
+ static: static_,
1754
+ private: private_,
1755
+ name: name.value,
1756
+ parameters: [],
1757
+ returnType,
1758
+ signatureSpan: span(start, returnType.span.end),
1759
+ body,
1760
+ span: span(start, body.at(-1)?.span.end ?? returnType.span.end),
1761
+ };
1762
+ }
1763
+ parseClassMethod(start, asynchronous, abstract, override, static_, private_) {
1764
+ const name = this.expectMemberName("Expected a method name");
1765
+ const typeParameters = this.parseTypeParameters();
1766
+ const parameters = this.parseParameters();
1767
+ const parameterListEnd = this.previous().span.end;
1768
+ const returnType = this.match("arrow") ? this.parseTypeReference() : null;
1769
+ if (abstract) {
1770
+ this.expectStatementEnd();
1771
+ return {
1772
+ kind: "FunctionDeclaration",
1773
+ exported: false,
1774
+ asynchronous,
1775
+ abstract,
1776
+ override,
1777
+ static: static_,
1778
+ private: private_,
1779
+ name: name.value,
1780
+ ...(typeParameters ? { typeParameters } : {}),
1781
+ parameters,
1782
+ returnType,
1783
+ signatureSpan: span(start, returnType?.span.end ?? parameterListEnd),
1784
+ body: [],
1785
+ span: span(start, returnType?.span.end ?? name.span.end),
1786
+ };
1787
+ }
1788
+ const body = this.parseBlock();
1789
+ return {
1790
+ kind: "FunctionDeclaration",
1791
+ exported: false,
1792
+ asynchronous,
1793
+ abstract,
1794
+ override,
1795
+ static: static_,
1796
+ private: private_,
1797
+ name: name.value,
1798
+ ...(typeParameters ? { typeParameters } : {}),
1799
+ parameters,
1800
+ returnType,
1801
+ ...(returnType ? { resultAnnotationSpan: span(parameterListEnd, returnType.span.end) } : {}),
1802
+ signatureSpan: span(start, returnType?.span.end ?? parameterListEnd),
1803
+ body,
1804
+ span: span(start, body.at(-1)?.span.end ?? returnType?.span.end ?? name.span.end),
1805
+ };
1806
+ }
1807
+ parseIf(start) {
1808
+ return this.withParseDepth(() => this.parseIfBody(start));
1809
+ }
1810
+ parseIfBody(start) {
1811
+ const condition = this.parseExpression();
1812
+ const thenBody = this.parseBlock();
1813
+ this.consumeNewlines();
1814
+ let elseBody = null;
1815
+ if (this.match("else")) {
1816
+ if (this.match("if")) {
1817
+ elseBody = [this.parseIf(this.previous().span.start)];
1818
+ }
1819
+ else {
1820
+ elseBody = this.parseBlock();
1821
+ }
1822
+ }
1823
+ const end = elseBody?.at(-1)?.span.end ?? thenBody.at(-1)?.span.end ?? condition.span.end;
1824
+ return { kind: "IfStatement", condition, thenBody, elseBody, span: span(start, end) };
1825
+ }
1826
+ parseMatch(start) {
1827
+ const value = this.parseExpression();
1828
+ this.expect("colon", "Expected ':' before match cases");
1829
+ this.expect("newline", "Expected a newline before match cases");
1830
+ this.consumeNewlines();
1831
+ this.expect("indent", "Expected indented match cases");
1832
+ const cases = [];
1833
+ this.consumeNewlines();
1834
+ while (!this.check("dedent") && !this.check("eof")) {
1835
+ const branchStart = this.current().span.start;
1836
+ if (this.matchWord("case")) {
1837
+ const pattern = this.parseMatchPattern(true);
1838
+ const guard = this.match("if") ? this.parseExpression() : null;
1839
+ const body = this.parseBlock();
1840
+ cases.push({ pattern, guard, body, span: span(branchStart, body.at(-1)?.span.end ?? this.previous().span.end) });
1841
+ }
1842
+ else if (this.match("else")) {
1843
+ // D28 item 4: 'case _:' is the only fallback spelling. The removed
1844
+ // 'else:' clause recovers as a wildcard case so exhaustiveness and
1845
+ // the rest of the block keep analyzing without cascades.
1846
+ const keyword = this.previous();
1847
+ this.diagnostics.push(recoveredDiagnostic("VEL2035", "Use 'case _:' for the fallback case; 'match' has no 'else' clause", keyword.span, mechanicalFix(keyword.span, "case _", "Use 'case _:' for the fallback case")));
1848
+ const body = this.parseBlock();
1849
+ cases.push({
1850
+ pattern: { kind: "MatchWildcardPattern", span: keyword.span },
1851
+ guard: null,
1852
+ body,
1853
+ span: span(branchStart, body.at(-1)?.span.end ?? this.previous().span.end),
1854
+ });
1855
+ }
1856
+ else {
1857
+ this.diagnostics.push(diagnostic("VEL2015", "A match block accepts only case branches", this.current().span));
1858
+ this.synchronize();
1859
+ }
1860
+ this.consumeNewlines();
1861
+ }
1862
+ const close = this.expect("dedent", "Expected the end of a match block");
1863
+ if (cases.length === 0) {
1864
+ this.diagnostics.push(diagnostic("VEL2015", "A match block requires at least one case", span(start, close.span.end)));
1865
+ }
1866
+ const end = cases.at(-1)?.span.end ?? value.span.end;
1867
+ return { kind: "MatchStatement", value, cases, span: span(start, end) };
1868
+ }
1869
+ startsMatchValue() {
1870
+ const kind = this.current().kind;
1871
+ return kind === "minus"
1872
+ || kind === "number"
1873
+ || kind === "string"
1874
+ || kind === "true"
1875
+ || kind === "false"
1876
+ || kind === "null"
1877
+ || (kind === "identifier" && this.peekKind(1) === "dot");
1878
+ }
1879
+ parseMatchPattern(root) {
1880
+ return this.withParseDepth(() => this.parseMatchPatternBody(root));
1881
+ }
1882
+ parseMatchPatternBody(root) {
1883
+ const start = this.current().span.start;
1884
+ let pattern;
1885
+ if (this.match("leftBrace")) {
1886
+ const entries = [];
1887
+ let rest = null;
1888
+ while (!this.check("rightBrace") && !this.check("eof")) {
1889
+ if (this.match("ellipsis")) {
1890
+ const binding = this.expect("identifier", "Expected an object rest binding after '...'");
1891
+ rest = { name: binding.value, span: binding.span };
1892
+ if (this.match("comma") && !this.check("rightBrace")) {
1893
+ this.diagnostics.push(diagnostic("VEL2015", "An object rest pattern must be last", this.current().span));
1894
+ }
1895
+ break;
1896
+ }
1897
+ const property = this.expectMemberName("Expected a field name in an object pattern");
1898
+ const renamed = this.match("colon");
1899
+ if (!renamed && property.kind !== "identifier") {
1900
+ this.diagnostics.push(diagnostic("VEL2015", `Keyword-named field '${property.value}' requires ': name' in an object pattern`, property.span));
1901
+ }
1902
+ const child = renamed
1903
+ ? this.parseMatchPattern(false)
1904
+ : {
1905
+ kind: "MatchCapturePattern",
1906
+ binding: { name: property.kind === "identifier" ? property.value : "_invalid", span: property.span },
1907
+ span: property.span,
1908
+ };
1909
+ entries.push({ property: property.value, pattern: child, span: span(property.span.start, child.span.end) });
1910
+ if (!this.match("comma"))
1911
+ break;
1912
+ }
1913
+ const close = this.expect("rightBrace", "Expected '}' after an object pattern");
1914
+ pattern = { kind: "MatchObjectPattern", entries, rest, span: span(start, close.span.end) };
1915
+ }
1916
+ else if (this.match("leftBracket")) {
1917
+ const elements = [];
1918
+ let rest = null;
1919
+ while (!this.check("rightBracket") && !this.check("eof")) {
1920
+ if (this.match("ellipsis")) {
1921
+ const binding = this.expect("identifier", "Expected a List rest binding after '...'");
1922
+ rest = { name: binding.value, span: binding.span };
1923
+ if (this.match("comma") && !this.check("rightBracket")) {
1924
+ this.diagnostics.push(diagnostic("VEL2015", "A List rest pattern must be last", this.current().span));
1925
+ }
1926
+ break;
1927
+ }
1928
+ elements.push(this.parseMatchPattern(false));
1929
+ if (!this.match("comma"))
1930
+ break;
1931
+ }
1932
+ const close = this.expect("rightBracket", "Expected ']' after a List pattern");
1933
+ pattern = { kind: "MatchListPattern", elements, rest, span: span(start, close.span.end) };
1934
+ }
1935
+ else if (this.check("identifier") && this.current().value === "_") {
1936
+ const wildcard = this.advance();
1937
+ pattern = { kind: "MatchWildcardPattern", span: wildcard.span };
1938
+ }
1939
+ else if (this.startsMatchValue()) {
1940
+ const values = [];
1941
+ do {
1942
+ const value = this.parseMatchValue();
1943
+ if (value)
1944
+ values.push(value);
1945
+ } while (root && this.match("comma") && !this.checkWord(CORE_WORDS.as) && !this.check("if") && !this.check("colon"));
1946
+ pattern = {
1947
+ kind: "MatchValuePattern",
1948
+ values,
1949
+ span: span(start, values.at(-1)?.span.end ?? this.current().span.start),
1950
+ };
1951
+ }
1952
+ else if (!root && this.check("identifier")) {
1953
+ const binding = this.advance();
1954
+ pattern = {
1955
+ kind: "MatchCapturePattern",
1956
+ binding: { name: binding.value, span: binding.span },
1957
+ span: binding.span,
1958
+ };
1959
+ }
1960
+ else {
1961
+ const type = this.parseTypeReference();
1962
+ pattern = { kind: "MatchTypePattern", type, span: span(start, type.span.end) };
1963
+ }
1964
+ // ENM-U3: `case a | b:` reads naturally to authors from either parent
1965
+ // language, but alternatives are spelled with a comma; '|' joins types
1966
+ // only inside type annotations. One diagnostic, and the alternatives are
1967
+ // consumed so the rest of the match parses cleanly.
1968
+ if (root && this.check("pipe")) {
1969
+ this.diagnostics.push(diagnostic("VEL2015", "Combine match alternatives with a comma — 'case a, b:'; '|' joins types only in type annotations", this.current().span));
1970
+ while (this.match("pipe") && !this.check("colon") && !this.check("newline") && !this.check("eof")) {
1971
+ if (this.startsMatchValue())
1972
+ this.parseMatchValue();
1973
+ else
1974
+ this.parseMatchPattern(false);
1975
+ }
1976
+ }
1977
+ if (this.matchWord(CORE_WORDS.as)) {
1978
+ const binding = this.expect("identifier", "Expected a binding name after 'as'");
1979
+ pattern = {
1980
+ kind: "MatchAsPattern",
1981
+ pattern,
1982
+ binding: { name: binding.value, span: binding.span },
1983
+ span: span(start, binding.span.end),
1984
+ };
1985
+ }
1986
+ return pattern;
1987
+ }
1988
+ parseMatchValue() {
1989
+ const negative = this.match("minus");
1990
+ const start = negative ? this.previous().span.start : this.current().span.start;
1991
+ const token = this.current();
1992
+ if (negative) {
1993
+ const number = this.expect("number", "A negative match case requires a numeric literal");
1994
+ if (!number.value)
1995
+ return null;
1996
+ return this.numberLiteral(number, true, span(start, number.span.end));
1997
+ }
1998
+ if (token.kind === "identifier" && this.peekKind(1) === "dot") {
1999
+ // ENM-U2: a dotted path is a value pattern at any depth — the same rule
2000
+ // the father language uses (dotted = value, bare name = binding).
2001
+ const object = this.advance();
2002
+ let expression = { kind: "IdentifierExpression", name: object.value, span: object.span };
2003
+ while (this.match("dot")) {
2004
+ // ENM-I7: keyword member names follow the member-access grammar, so
2005
+ // `case S.null:` parses. `pass` stays out: it is the placeholder line
2006
+ // and the one name an enum can never declare (ENM-I8).
2007
+ if (this.check("pass")) {
2008
+ this.diagnostics.push(diagnostic("VEL2015", "'pass' is the placeholder line, and the one name an enum never declares; no member spells it", this.current().span));
2009
+ this.advance();
2010
+ return null;
2011
+ }
2012
+ const property = this.expectMemberName("Expected an enum member after '.'");
2013
+ expression = {
2014
+ kind: "MemberExpression",
2015
+ object: expression,
2016
+ property: property.value,
2017
+ optional: false,
2018
+ span: span(object.span.start, property.span.end),
2019
+ };
2020
+ }
2021
+ return expression;
2022
+ }
2023
+ this.advance();
2024
+ switch (token.kind) {
2025
+ case "number": return this.numberLiteral(token);
2026
+ case "string": return { kind: "LiteralExpression", value: token.value, raw: token.value, span: token.span };
2027
+ case "true": return { kind: "LiteralExpression", value: true, raw: "true", span: token.span };
2028
+ case "false": return { kind: "LiteralExpression", value: false, raw: "false", span: token.span };
2029
+ case "null": return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
2030
+ default:
2031
+ this.diagnostics.push(diagnostic("VEL2015", "Match cases accept literals or qualified enum members", token.span));
2032
+ return null;
2033
+ }
2034
+ }
2035
+ parseTry(start) {
2036
+ const tryBody = this.parseBlock();
2037
+ this.consumeNewlines();
2038
+ let catchName = null;
2039
+ let catchBody = null;
2040
+ let finallyBody = null;
2041
+ if (this.match("catch")) {
2042
+ catchName = this.check("identifier") ? this.advance().value : "error";
2043
+ catchBody = this.parseBlock();
2044
+ this.consumeNewlines();
2045
+ }
2046
+ if (this.match("finally")) {
2047
+ finallyBody = this.parseBlock();
2048
+ }
2049
+ if (!catchBody && !finallyBody) {
2050
+ this.diagnostics.push(diagnostic("VEL2008", "A try block requires catch or finally", span(start, tryBody.at(-1)?.span.end ?? start)));
2051
+ }
2052
+ const end = finallyBody?.at(-1)?.span.end ?? catchBody?.at(-1)?.span.end ?? tryBody.at(-1)?.span.end ?? start;
2053
+ return { kind: "TryStatement", tryBody, catchName, catchBody, finallyBody, span: span(start, end) };
2054
+ }
2055
+ parseBlock() {
2056
+ this.expect("colon", "Expected ':' before an indented block");
2057
+ this.expect("newline", "Expected a newline before an indented block");
2058
+ this.consumeNewlines();
2059
+ this.expect("indent", "Expected an indented block");
2060
+ const statements = [];
2061
+ this.consumeNewlines();
2062
+ while (!this.check("dedent") && !this.check("eof")) {
2063
+ const statement = this.parseStatement();
2064
+ if (statement) {
2065
+ statements.push(statement);
2066
+ }
2067
+ else {
2068
+ this.synchronize();
2069
+ }
2070
+ this.finishStatementBoundary();
2071
+ this.consumeNewlines();
2072
+ }
2073
+ this.expect("dedent", "Expected the end of an indented block");
2074
+ return statements;
2075
+ }
2076
+ parseTypeReference(allowTrailingOptional = true) {
2077
+ return this.withParseDepth(() => this.parseTypeReferenceBody(allowTrailingOptional));
2078
+ }
2079
+ parseTypeReferenceBody(allowTrailingOptional) {
2080
+ const start = this.current().span.start;
2081
+ const members = [this.parseSingleTypeReference(allowTrailingOptional)];
2082
+ while (this.match("pipe")) {
2083
+ members.push(this.parseSingleTypeReference(allowTrailingOptional));
2084
+ }
2085
+ const referenceSpan = span(start, this.previous().span.end);
2086
+ return {
2087
+ syntax: members.length === 1 ? members[0] : { kind: "UnionTypeSyntax", members, span: referenceSpan },
2088
+ span: referenceSpan,
2089
+ };
2090
+ }
2091
+ parseSingleTypeReference(allowTrailingOptional = true) {
2092
+ if (this.checkWord(CORE_WORDS.readonly)) {
2093
+ const keyword = this.advance();
2094
+ const inner = this.parseSingleTypeReference(allowTrailingOptional);
2095
+ return { kind: "ReadonlyTypeSyntax", inner, span: span(keyword.span.start, inner.span.end) };
2096
+ }
2097
+ if (this.check("leftParen") && !this.isFunctionTypeParenthesis()) {
2098
+ const open = this.advance();
2099
+ const grouped = this.parseTypeReference();
2100
+ const close = this.expect("rightParen", "Expected ')' after grouped type");
2101
+ if (!allowTrailingOptional || !this.match("question"))
2102
+ return grouped.syntax;
2103
+ return this.makeOptionalTypeSyntax(grouped.syntax, span(open.span.start, this.previous().span.end));
2104
+ }
2105
+ if (this.match("leftParen")) {
2106
+ const open = this.previous();
2107
+ const parameters = [];
2108
+ let sawRest = false;
2109
+ let sawOptional = false;
2110
+ if (!this.check("rightParen")) {
2111
+ do {
2112
+ const parameterStart = this.current().span.start;
2113
+ const rest = this.match("ellipsis");
2114
+ if (sawRest)
2115
+ this.diagnostics.push(diagnostic("VEL2016", "A rest function type parameter must be final", this.current().span));
2116
+ const named = this.check("identifier")
2117
+ && (this.peekKind(1) === "colon" || (this.peekKind(1) === "question" && this.peekKind(2) === "colon"));
2118
+ const parameterName = named ? this.advance().value : null;
2119
+ const optional = parameterName !== null && this.match("question");
2120
+ if (parameterName)
2121
+ this.expect("colon", "Expected ':' after a function type parameter name");
2122
+ const type = this.parseTypeReference();
2123
+ if (rest && optional)
2124
+ this.diagnostics.push(diagnostic("VEL2016", "A rest function type parameter cannot be optional", span(parameterStart, type.span.end)));
2125
+ if (!optional && sawOptional && !rest) {
2126
+ this.diagnostics.push(diagnostic("VEL2016", "A required function type parameter cannot follow an optional parameter", span(parameterStart, type.span.end)));
2127
+ }
2128
+ parameters.push({ name: parameterName, type: type.syntax, rest, optional, span: span(parameterStart, type.span.end) });
2129
+ if (optional)
2130
+ sawOptional = true;
2131
+ if (rest)
2132
+ sawRest = true;
2133
+ } while (this.match("comma") && !this.check("rightParen"));
2134
+ }
2135
+ this.expect("rightParen", "Expected ')' after function type parameters");
2136
+ if (this.check("fatArrow"))
2137
+ this.reportTypePositionFatArrow(this.advance());
2138
+ else
2139
+ this.expect("arrow", "Expected '->' after function type parameters");
2140
+ const result = this.parseTypeReference();
2141
+ return { kind: "FunctionTypeSyntax", parameters, result: result.syntax, span: span(open.span.start, result.span.end) };
2142
+ }
2143
+ const name = this.check("null") ? this.advance() : this.expect("identifier", "Expected a type name");
2144
+ const nameGuidance = sourceTypeNameGuidance(name.value);
2145
+ if (nameGuidance) {
2146
+ // A guidance spelling with a replacement recovers as the guided type
2147
+ // name so semantic analysis still runs and reports its own guidance.
2148
+ this.diagnostics.push(nameGuidance.replacement && nameGuidance.title
2149
+ ? recoveredDiagnostic("VEL2012", nameGuidance.message, name.span, mechanicalFix(name.span, nameGuidance.replacement, nameGuidance.title))
2150
+ : diagnostic("VEL2012", nameGuidance.message, name.span));
2151
+ }
2152
+ const typeName = nameGuidance?.replacement ?? name.value;
2153
+ if (this.match("dot")) {
2154
+ const member = this.expect("identifier", "Expected an enum member after '.' in a singleton type");
2155
+ const syntax = {
2156
+ kind: "EnumMemberTypeSyntax",
2157
+ enumName: typeName,
2158
+ enumNameSpan: name.span,
2159
+ member: member.value,
2160
+ memberSpan: member.span,
2161
+ span: span(name.span.start, member.span.end),
2162
+ };
2163
+ return this.finishTypeReferenceSuffix(syntax, allowTrailingOptional);
2164
+ }
2165
+ let syntax = { kind: "NamedTypeSyntax", name: typeName, span: name.span };
2166
+ const angleArguments = this.match("less");
2167
+ const squareArguments = !angleArguments && this.match("leftBracket");
2168
+ if (angleArguments || squareArguments) {
2169
+ const open = this.previous();
2170
+ const closeKind = squareArguments ? "rightBracket" : "greater";
2171
+ const arguments_ = [];
2172
+ if (!(squareArguments ? this.check(closeKind) : this.checkTypeGreater())) {
2173
+ do {
2174
+ arguments_.push(this.parseTypeReference().syntax);
2175
+ } while (this.match("comma") && !(squareArguments ? this.check(closeKind) : this.checkTypeGreater()));
2176
+ }
2177
+ const close = squareArguments
2178
+ ? this.expect(closeKind, "Expected ']' after type arguments")
2179
+ : this.expectTypeGreater("Expected '>' after type arguments");
2180
+ if (squareArguments && arguments_.length === 0) {
2181
+ // A postfix 'Name[]' array annotation guides straight to the List
2182
+ // spelling and recovers as 'List<Name>'.
2183
+ this.diagnostics.push(recoveredDiagnostic("VEL2012", `Use 'List<${name.value}>' for ordered collections; VelarScript has no postfix '[]' array types`, span(name.span.start, close.span.end), mechanicalFix(span(name.span.start, close.span.end), `List<${name.value}>`, `Use 'List<${name.value}>'`)));
2184
+ return this.finishTypeReferenceSuffix({
2185
+ kind: "GenericTypeSyntax",
2186
+ name: "List",
2187
+ nameSpan: name.span,
2188
+ arguments: [syntax],
2189
+ span: span(name.span.start, close.span.end),
2190
+ }, allowTrailingOptional);
2191
+ }
2192
+ if (squareArguments) {
2193
+ this.diagnostics.push(recoveredDiagnostic("VEL2012", "Generic type arguments use '<...>', not '[...]'", span(open.span.start, close.span.end),
2194
+ // Brackets that follow a real type name are that name's arguments;
2195
+ // a bare '[...]' is some other mistake and names no rewrite.
2196
+ name.value === ""
2197
+ ? undefined
2198
+ : mechanicalEdits([{ span: open.span, text: "<" }, { span: close.span, text: ">" }], "Use angle brackets for generic type arguments")));
2199
+ }
2200
+ const expectedArguments = typeName === "Map" ? 2 : typeName === "List" || typeName === "Set" || typeName === "Record" || typeName === "Promise" || typeName === "Type" ? 1 : null;
2201
+ if (this.validateExtensionTypeArguments(typeName, arguments_, name.span)) {
2202
+ // The owning extension validates its own generic surface.
2203
+ }
2204
+ else if (typeName === "Function" && arguments_.length === 0) {
2205
+ this.diagnostics.push(diagnostic("VEL2012", "Write bare 'Function' for () -> null, or provide at least one type argument whose final type is the result", name.span));
2206
+ }
2207
+ else if (expectedArguments !== null && arguments_.length !== expectedArguments) {
2208
+ this.diagnostics.push(diagnostic("VEL2012", `Type '${typeName}' expects ${expectedArguments} type argument${expectedArguments === 1 ? "" : "s"}`, name.span));
2209
+ }
2210
+ syntax = { kind: "GenericTypeSyntax", name: typeName, nameSpan: name.span, arguments: arguments_, span: span(name.span.start, close.span.end) };
2211
+ }
2212
+ return this.finishTypeReferenceSuffix(syntax, allowTrailingOptional);
2213
+ }
2214
+ validateExtensionTypeArguments(_name, _arguments, _nameSpan) {
2215
+ return false;
2216
+ }
2217
+ finishTypeReferenceSuffix(syntax, allowTrailingOptional = true) {
2218
+ if (allowTrailingOptional && this.match("question")) {
2219
+ return this.makeOptionalTypeSyntax(syntax, span(syntax.span.start, this.previous().span.end));
2220
+ }
2221
+ return syntax;
2222
+ }
2223
+ makeOptionalTypeSyntax(inner, optionalSpan) {
2224
+ if (inner.kind === "NamedTypeSyntax" && inner.name === "null") {
2225
+ this.diagnostics.push(recoveredDiagnostic("VEL2012", "'null?' is redundant; use 'null'", optionalSpan, mechanicalFix(span(inner.span.end, optionalSpan.end), "", "Remove the redundant '?'")));
2226
+ return { ...inner, span: optionalSpan };
2227
+ }
2228
+ return { kind: "OptionalTypeSyntax", inner, span: optionalSpan };
2229
+ }
2230
+ /**
2231
+ * D63 rule 161: `=>` counts as evidence here even though it is the wrong
2232
+ * spelling. VelarScript writes the value-level arrow `=>` and the type-level
2233
+ * arrow `->`, and a TypeScript reader writes `(string, string) => T` in a
2234
+ * type position on the first try. Reading that as a *grouped* type produced
2235
+ * `Expected ')' after grouped type` plus five cascades, none of which said
2236
+ * `->` — a pile of cascades with no right answer in it is no diagnostic
2237
+ * (D42's standing criterion). Claiming the parenthesis as a function type
2238
+ * lets `reportTypePositionFatArrow` say the one thing worth saying, and the
2239
+ * rest of the annotation parses normally, so the cascade never starts.
2240
+ */
2241
+ isFunctionTypeParenthesis() {
2242
+ let depth = 0;
2243
+ for (let offset = 0; this.index + offset < this.tokens.length; offset += 1) {
2244
+ const kind = this.tokens[this.index + offset].kind;
2245
+ if (kind === "leftParen")
2246
+ depth += 1;
2247
+ else if (kind === "rightParen" && --depth === 0) {
2248
+ const next = this.tokens[this.index + offset + 1]?.kind;
2249
+ return next === "arrow" || next === "fatArrow";
2250
+ }
2251
+ else if (kind === "newline" || kind === "eof")
2252
+ return false;
2253
+ }
2254
+ return false;
2255
+ }
2256
+ /** The one diagnostic a `=>` in a type position gets, with its rewrite. */
2257
+ reportTypePositionFatArrow(token) {
2258
+ this.diagnostics.push(recoveredDiagnostic("VEL2012", "A function type writes its result after '->'; '=>' is the value-level arrow that introduces a lambda body", token.span, mechanicalFix(token.span, "->", "Use '->' in a function type")));
2259
+ }
2260
+ parseExpression(minimumPrecedence = 0) {
2261
+ return this.withParseDepth(() => this.parseExpressionBody(minimumPrecedence));
2262
+ }
2263
+ parseExpressionBody(minimumPrecedence = 0) {
2264
+ const asynchronousArrow = minimumPrecedence === 0
2265
+ && this.check("async")
2266
+ && ((this.peekKind(1) === "leftParen" && this.isParenthesizedArrow(1))
2267
+ || (this.peekKind(1) === "identifier" && this.peekKind(2) === "fatArrow"));
2268
+ if (asynchronousArrow) {
2269
+ const start = this.advance().span.start;
2270
+ return this.parseArrowExpression(start, true);
2271
+ }
2272
+ if (minimumPrecedence === 0 && this.check("leftParen") && this.isParenthesizedArrow()) {
2273
+ return this.parseArrowExpression(this.current().span.start, false);
2274
+ }
2275
+ if (minimumPrecedence === 0 && this.check("identifier") && this.peekKind(1) === "fatArrow") {
2276
+ return this.parseArrowExpression(this.current().span.start, false);
2277
+ }
2278
+ let left = this.parseUnary();
2279
+ while (true) {
2280
+ const javaScriptInstanceof = this.check("identifier") && this.current().value === "instanceof";
2281
+ const compoundNotIn = this.check("not") && this.peekKind(1) === "in";
2282
+ const precedence = javaScriptInstanceof
2283
+ ? binaryPrecedence.is
2284
+ : compoundNotIn
2285
+ ? binaryPrecedence.in
2286
+ : binaryPrecedence[this.current().kind];
2287
+ if (precedence === undefined || precedence < minimumPrecedence) {
2288
+ break;
2289
+ }
2290
+ const operator = this.advance();
2291
+ if (compoundNotIn)
2292
+ this.advance();
2293
+ const comparisonOperator = comparisonOperators[operator.kind];
2294
+ const membershipOrTypeTest = compoundNotIn || operator.kind === "in" || operator.kind === "is" || javaScriptInstanceof;
2295
+ if (this.isUnparenthesizedComparisonLayer(left)
2296
+ && (membershipOrTypeTest || this.isMembershipOrTypeTest(left))) {
2297
+ this.diagnostics.push(diagnostic("VEL2031", "Parenthesize an 'in' or 'is' test used inside another comparison, or split the tests with 'and'", operator.span));
2298
+ }
2299
+ if (comparisonOperator) {
2300
+ const operands = [left];
2301
+ const operators = [];
2302
+ let nextOperator = comparisonOperator;
2303
+ while (nextOperator) {
2304
+ operators.push(nextOperator);
2305
+ operands.push(this.parseExpression(precedence + 1));
2306
+ nextOperator = comparisonOperators[this.current().kind];
2307
+ if (nextOperator)
2308
+ this.advance();
2309
+ }
2310
+ if (operators.length > 1) {
2311
+ if (operators.some((item) => item === "==" || item === "!=")) {
2312
+ this.diagnostics.push(diagnostic("VEL2031", "Equality comparisons do not chain; split the comparisons with 'and'", span(operands[0].span.start, operands.at(-1).span.end)));
2313
+ }
2314
+ else {
2315
+ const ascending = operators.every((item) => item === "<" || item === "<=");
2316
+ const descending = operators.every((item) => item === ">" || item === ">=");
2317
+ if (!ascending && !descending) {
2318
+ this.diagnostics.push(diagnostic("VEL2031", "Comparison chains must point one way; split the comparisons with 'and'", span(operands[0].span.start, operands.at(-1).span.end)));
2319
+ }
2320
+ }
2321
+ }
2322
+ left = operators.length === 1
2323
+ ? {
2324
+ kind: "BinaryExpression",
2325
+ left: operands[0],
2326
+ operator: operators[0],
2327
+ right: operands[1],
2328
+ span: span(operands[0].span.start, operands[1].span.end),
2329
+ }
2330
+ : {
2331
+ kind: "ComparisonChainExpression",
2332
+ operands,
2333
+ operators,
2334
+ span: span(operands[0].span.start, operands.at(-1).span.end),
2335
+ };
2336
+ continue;
2337
+ }
2338
+ if (operator.kind === "is" || javaScriptInstanceof) {
2339
+ if (javaScriptInstanceof) {
2340
+ this.diagnostics.push(recoveredDiagnostic("VEL2031", "Use 'is' for a type test; VelarScript does not expose JavaScript 'instanceof'", operator.span, mechanicalFix(operator.span, "is", "Use 'is' for the type test")));
2341
+ }
2342
+ const negated = this.match("not");
2343
+ // 'is' tests runtime types and 'null' is a value, so 'is [not] null'
2344
+ // is the removed spelling of the equality test. Recovery builds the
2345
+ // '== null' / '!= null' comparison itself, so narrowing and every
2346
+ // later stage still run, and a '?' after the test keeps meaning '?:'
2347
+ // instead of being read as an optional-type marker.
2348
+ if (this.check("null") && this.peekKind(1) !== "pipe" && this.peekKind(1) !== "dot") {
2349
+ const nullToken = this.advance();
2350
+ // 'null?' spells the same removed test; consume the '?' only when it
2351
+ // ends the test (an optional-type marker), never when it opens a
2352
+ // conditional expression.
2353
+ const question = this.check("question") && !this.questionContinuesExpression() ? this.advance() : null;
2354
+ const end = question?.span.end ?? nullToken.span.end;
2355
+ this.diagnostics.push(recoveredDiagnostic("VEL2033", negated
2356
+ ? "Use '!= null' to test for a value; 'is' tests runtime types"
2357
+ : "Use '== null' to test for a value; 'is' tests runtime types", span(operator.span.start, end), mechanicalFix(span(operator.span.start, end), negated ? "!= null" : "== null", `Use '${negated ? "!=" : "=="} null' to test for a value`)));
2358
+ left = {
2359
+ kind: "BinaryExpression",
2360
+ left,
2361
+ operator: negated ? "!=" : "==",
2362
+ right: { kind: "LiteralExpression", value: null, raw: "null", span: nullToken.span },
2363
+ span: span(left.span.start, end),
2364
+ };
2365
+ continue;
2366
+ }
2367
+ const conditionalTypeTest = this.typeTestHasConditionalQuestion();
2368
+ const type = this.parseTypeReference(!conditionalTypeTest);
2369
+ if (conditionalTypeTest) {
2370
+ this.diagnostics.push(recoveredDiagnostic("VEL2031", "Parenthesize the type test before a conditional: '(value is Type) ? then : else'; '?' immediately after a type can also mean an optional type", span(left.span.start, this.current().span.end)));
2371
+ }
2372
+ left = {
2373
+ kind: "IsExpression",
2374
+ value: left,
2375
+ operator: negated ? "is not" : "is",
2376
+ type,
2377
+ span: span(left.span.start, type.span.end),
2378
+ };
2379
+ continue;
2380
+ }
2381
+ let binaryLeft = left;
2382
+ let prefixNotIn = false;
2383
+ if (operator.kind === "in" && !compoundNotIn
2384
+ && left.kind === "UnaryExpression" && left.operator === "not") {
2385
+ prefixNotIn = true;
2386
+ this.diagnostics.push(recoveredDiagnostic("VEL2031", "Use 'x not in y'; 'not x in y' puts 'not' on the wrong operand", span(left.span.start, operator.span.end)));
2387
+ binaryLeft = left.operand;
2388
+ }
2389
+ const right = this.parseExpression(precedence + 1);
2390
+ const spelledOperator = compoundNotIn || prefixNotIn ? "not in" : this.binaryOperator(operator);
2391
+ this.checkNullishBooleanMixing(spelledOperator, binaryLeft, right, operator.span);
2392
+ left = {
2393
+ kind: "BinaryExpression",
2394
+ left: binaryLeft,
2395
+ operator: spelledOperator,
2396
+ right,
2397
+ span: span(binaryLeft.span.start, right.span.end),
2398
+ };
2399
+ }
2400
+ if (minimumPrecedence === 0 && this.match("question")) {
2401
+ const thenValue = this.parseExpression();
2402
+ this.expect("colon", "Expected ':' in conditional expression");
2403
+ const elseValue = this.parseExpression();
2404
+ return { kind: "ConditionalExpression", condition: left, thenValue, elseValue, span: span(left.span.start, elseValue.span.end) };
2405
+ }
2406
+ if (minimumPrecedence === 0 && this.check("if") && this.hasPythonConditionalElse()) {
2407
+ // 'x if cond else y' guides to the '?:' spelling and recovers as the
2408
+ // equivalent conditional expression so deeper guidance still surfaces.
2409
+ this.diagnostics.push(recoveredDiagnostic("VEL2027", "Use 'cond ? x : y'; VelarScript writes conditional expressions with '?:', not 'x if cond else y'", this.current().span));
2410
+ this.advance();
2411
+ const condition = this.parseExpression();
2412
+ this.expect("else", "Expected 'else' in a conditional expression");
2413
+ const elseValue = this.parseExpression();
2414
+ return { kind: "ConditionalExpression", condition, thenValue: left, elseValue, span: span(left.span.start, elseValue.span.end) };
2415
+ }
2416
+ return left;
2417
+ }
2418
+ // True when an expression is followed by Python's 'x if cond else y'
2419
+ // conditional shape: an 'if' whose matching 'else' appears at the same
2420
+ // bracket depth before the expression context can end. Statement-level if
2421
+ // blocks never reach this check, and 'for x in xs if cond:' style filters
2422
+ // (no 'else') are left to ordinary parse errors.
2423
+ hasPythonConditionalElse() {
2424
+ let depth = 0;
2425
+ for (let offset = 1; this.index + offset < this.tokens.length; offset += 1) {
2426
+ const kind = this.peekKind(offset);
2427
+ if (kind === "leftParen" || kind === "leftBracket" || kind === "leftBrace")
2428
+ depth += 1;
2429
+ else if (kind === "rightParen" || kind === "rightBracket" || kind === "rightBrace") {
2430
+ if (depth === 0)
2431
+ return false;
2432
+ depth -= 1;
2433
+ }
2434
+ else if (depth === 0 && kind === "else")
2435
+ return true;
2436
+ else if (depth === 0 && (kind === "colon" || kind === "comma"))
2437
+ return false;
2438
+ else if (kind === "newline" || kind === "indent" || kind === "dedent" || kind === "eof")
2439
+ return false;
2440
+ }
2441
+ return false;
2442
+ }
2443
+ parseArrowExpression(start, asynchronous) {
2444
+ if (this.check("leftParen")) {
2445
+ this.contextualParameterDepth += 1;
2446
+ const parameters = this.parseParameters();
2447
+ this.contextualParameterDepth -= 1;
2448
+ this.expect("fatArrow", "Expected '=>' after arrow parameters");
2449
+ const body = this.parseArrowBody();
2450
+ return { kind: "ArrowFunctionExpression", asynchronous, parameters, body, span: span(start, body.span.end) };
2451
+ }
2452
+ const parameterToken = this.expect("identifier", "Expected an arrow parameter");
2453
+ this.expect("fatArrow", "Expected '=>' after arrow parameter");
2454
+ const body = this.parseArrowBody();
2455
+ const parameter = { name: parameterToken.value, type: null, defaultValue: null, rest: false, span: parameterToken.span };
2456
+ return { kind: "ArrowFunctionExpression", asynchronous, parameters: [parameter], body, span: span(start, body.span.end) };
2457
+ }
2458
+ // An arrow body is one expression by design (charter §7): '{' after '=>'
2459
+ // opens a record literal, never a JavaScript statement block. When the
2460
+ // braces clearly hold statements, one targeted diagnostic replaces the
2461
+ // record-literal error cascade and the braces are skipped whole. Record
2462
+ // shapes — '{...t, done: true}', '{id: value}', '{a, b}' — parse normally.
2463
+ parseArrowBody() {
2464
+ if (this.check("leftBrace") && this.arrowBraceHoldsStatements()) {
2465
+ const open = this.advance();
2466
+ let end = open.span.end;
2467
+ let depth = 1;
2468
+ while (depth > 0) {
2469
+ const kind = this.current().kind;
2470
+ if (kind === "eof" || kind === "newline" || kind === "indent" || kind === "dedent")
2471
+ break;
2472
+ if (kind === "leftBrace")
2473
+ depth += 1;
2474
+ else if (kind === "rightBrace")
2475
+ depth -= 1;
2476
+ end = this.advance().span.end;
2477
+ }
2478
+ this.diagnostics.push(diagnostic("VEL2030", "An arrow body is a single expression; write the expression directly or move multi-statement logic into a named 'def'", span(open.span.start, end)));
2479
+ return { kind: "LiteralExpression", value: null, raw: "null", span: span(open.span.start, end) };
2480
+ }
2481
+ return this.recoverExpressionAssignment(this.parseExpression());
2482
+ }
2483
+ // Scans the braces after '=>' without consuming tokens. Statement keywords
2484
+ // that cannot open a record field decide first; a top-level ':' or '...'
2485
+ // decides for a record; otherwise any token that cannot sit in a record
2486
+ // field list (call parentheses, operators, literals) marks statements.
2487
+ arrowBraceHoldsStatements() {
2488
+ let depth = 0;
2489
+ let sawNonRecordToken = false;
2490
+ for (let offset = 0; this.index + offset < this.tokens.length; offset += 1) {
2491
+ const token = this.tokens[this.index + offset];
2492
+ const kind = token.kind;
2493
+ if (kind === "eof" || kind === "newline" || kind === "indent" || kind === "dedent")
2494
+ break;
2495
+ if (kind === "leftBrace" || kind === "leftParen" || kind === "leftBracket") {
2496
+ if (depth === 1)
2497
+ sawNonRecordToken = true;
2498
+ depth += 1;
2499
+ continue;
2500
+ }
2501
+ if (kind === "rightBrace" || kind === "rightParen" || kind === "rightBracket") {
2502
+ depth -= 1;
2503
+ if (depth === 0)
2504
+ break;
2505
+ continue;
2506
+ }
2507
+ if (depth !== 1)
2508
+ continue;
2509
+ const next = this.tokens[this.index + offset + 1]?.kind;
2510
+ // D64 rule 165: a contextual keyword is only statement evidence in the
2511
+ // shape that claims it, and `{match}` is not that shape — it is the
2512
+ // record shorthand for a binding named `match`, which charter §3
2513
+ // promises. The word branch therefore stands down wherever the entry
2514
+ // ends: `}` closes the record and `,` starts the next field, exactly as
2515
+ // `:` already meant a keyword-named field below. Only the word branch
2516
+ // needs this — `statementStarterKinds` holds reserved token kinds, which
2517
+ // can never be a field name in the first place.
2518
+ const starter = statementStarterKinds.has(kind)
2519
+ || (kind === "identifier" && statementStarterWords.has(token.value) && next !== "rightBrace" && next !== "comma");
2520
+ if (starter && next !== "colon")
2521
+ return true;
2522
+ if (kind === "colon" || kind === "ellipsis")
2523
+ return false;
2524
+ if (!recordFieldLevelKinds.has(kind))
2525
+ sawNonRecordToken = true;
2526
+ }
2527
+ return sawNonRecordToken;
2528
+ }
2529
+ isParenthesizedArrow(initialOffset = 0) {
2530
+ let depth = 0;
2531
+ for (let offset = initialOffset; this.index + offset < this.tokens.length; offset += 1) {
2532
+ const kind = this.tokens[this.index + offset].kind;
2533
+ if (kind === "leftParen")
2534
+ depth += 1;
2535
+ else if (kind === "rightParen") {
2536
+ depth -= 1;
2537
+ if (depth === 0)
2538
+ return this.tokens[this.index + offset + 1]?.kind === "fatArrow";
2539
+ }
2540
+ if (kind === "newline" || kind === "eof")
2541
+ return false;
2542
+ }
2543
+ return false;
2544
+ }
2545
+ parseUnary() {
2546
+ if (this.check("identifier") && (this.current().value === "delete" || this.current().value === "typeof")
2547
+ && !["rightParen", "rightBracket", "rightBrace", "comma", "colon", "newline", "dedent", "eof"].includes(this.peekKind(1))) {
2548
+ const operator = this.advance();
2549
+ this.diagnostics.push(recoveredDiagnostic("VEL2031", operator.value === "delete"
2550
+ ? "VelarScript does not expose JavaScript 'delete'; use a collection remove operation or construct updated data explicitly"
2551
+ : "Use 'is' or a declared type; VelarScript does not expose JavaScript 'typeof'", operator.span));
2552
+ return this.withParseDepth(() => this.parseUnary());
2553
+ }
2554
+ if ((this.check("plus") || this.check("minus")) && this.peekKind(1) === this.current().kind
2555
+ && this.current().span.end === this.tokens[this.index + 1].span.start) {
2556
+ // GRM-D2: '++value' parses as '+(+value)' — a legal silent no-op — so
2557
+ // the stacked-operator spelling is taught directly.
2558
+ const first = this.current();
2559
+ const operator = first.kind === "plus" ? "+" : "-";
2560
+ const target = this.tokens[this.index + 2];
2561
+ const name = target?.kind === "identifier" ? target.value : "name";
2562
+ this.diagnostics.push(recoveredDiagnostic("VEL2031", `VelarScript has no '${operator}${operator}'; write '${name} ${operator}= 1'`, span(first.span.start, this.tokens[this.index + 1].span.end),
2563
+ // Only the prefix spelling '++value' reaches here, so the whole
2564
+ // '++name' text is replaced by the compound assignment statement.
2565
+ target?.kind === "identifier"
2566
+ ? mechanicalFix(span(first.span.start, target.span.end), `${name} ${operator}= 1`, `Write '${name} ${operator}= 1'`)
2567
+ : undefined));
2568
+ this.advance();
2569
+ this.advance();
2570
+ return this.withParseDepth(() => this.parseUnary());
2571
+ }
2572
+ if (this.match("not") || this.match("plus") || this.match("minus") || this.match("tilde")) {
2573
+ const operator = this.previous();
2574
+ return this.withParseDepth(() => {
2575
+ const operand = this.parseUnary();
2576
+ return {
2577
+ kind: "UnaryExpression",
2578
+ operator: operator.kind === "not" ? "not" : operator.kind === "plus" ? "+" : operator.kind === "minus" ? "-" : "~",
2579
+ operand,
2580
+ span: span(operator.span.start, operand.span.end),
2581
+ };
2582
+ });
2583
+ }
2584
+ return this.parsePower();
2585
+ }
2586
+ parsePower() {
2587
+ const left = this.parsePowerBase();
2588
+ if (!this.match("starStar"))
2589
+ return left;
2590
+ const operator = this.previous();
2591
+ return this.withParseDepth(() => {
2592
+ const right = this.parseUnary();
2593
+ return {
2594
+ kind: "BinaryExpression",
2595
+ operator: "**",
2596
+ left,
2597
+ right,
2598
+ span: span(left.span.start, right.span.end),
2599
+ };
2600
+ });
2601
+ }
2602
+ parsePowerBase() {
2603
+ // D39 item 51: `try` reaches exactly as far as `await` does — the whole
2604
+ // postfix chain — so `try User.parse(raw)` and `try await load()` both
2605
+ // read as one attempt.
2606
+ if (this.match("try")) {
2607
+ const keyword = this.previous();
2608
+ return this.withParseDepth(() => {
2609
+ const value = this.parsePowerBase();
2610
+ return { kind: "TryExpression", value, span: span(keyword.span.start, value.span.end) };
2611
+ });
2612
+ }
2613
+ if (!this.match("await"))
2614
+ return this.parsePostfix();
2615
+ const operator = this.previous();
2616
+ return this.withParseDepth(() => {
2617
+ const operand = this.check("not") || this.check("plus") || this.check("minus") || this.check("tilde")
2618
+ ? this.parseUnary()
2619
+ : this.parsePowerBase();
2620
+ return {
2621
+ kind: "UnaryExpression",
2622
+ operator: "await",
2623
+ operand,
2624
+ span: span(operator.span.start, operand.span.end),
2625
+ };
2626
+ });
2627
+ }
2628
+ parsePostfix() {
2629
+ let expression = this.parsePrimary();
2630
+ while (true) {
2631
+ const explicitTypeArgumentsEnd = this.explicitTypeArgumentsEnd(expression);
2632
+ if (explicitTypeArgumentsEnd !== null) {
2633
+ const start = this.current().span.start;
2634
+ while (this.index <= explicitTypeArgumentsEnd)
2635
+ this.advance();
2636
+ const name = expression.kind === "IdentifierExpression" ? expression.name
2637
+ : expression.kind === "MemberExpression" ? expression.property
2638
+ : "function";
2639
+ this.diagnostics.push(recoveredDiagnostic("VEL2031", `Type arguments are inferred at each call site; write '${name}(...)' without '<...>'`, span(start, this.previous().span.end), mechanicalFix(span(start, this.previous().span.end), "", "Remove the explicit type arguments")));
2640
+ continue;
2641
+ }
2642
+ let call = false;
2643
+ let optionalCall = false;
2644
+ if (this.match("leftParen")) {
2645
+ call = true;
2646
+ }
2647
+ else if (this.check("optionalDot") && this.peekKind(1) === "leftParen") {
2648
+ this.advance();
2649
+ this.advance();
2650
+ call = true;
2651
+ optionalCall = true;
2652
+ }
2653
+ if (call) {
2654
+ const arguments_ = [];
2655
+ const argumentNames = [];
2656
+ let sawNamed = false;
2657
+ let sawSpread = false;
2658
+ if (!this.check("rightParen")) {
2659
+ do {
2660
+ if (this.check("identifier") && this.peekKind(1) === "colon") {
2661
+ const name = this.advance();
2662
+ this.advance();
2663
+ this.diagnostics.push(diagnostic("VEL2024", `Write '=' between the name and value for named argument '${name.value}': ${name.value} = value`, name.span, mechanicalFix(this.previous().span, "=", "Use '=' for the named argument")));
2664
+ if (sawSpread)
2665
+ this.diagnostics.push(diagnostic("VEL2024", "Named arguments cannot be combined with a call spread", name.span));
2666
+ sawNamed = true;
2667
+ argumentNames.push(name.value);
2668
+ arguments_.push(this.parseExpression());
2669
+ this.recoverChainedNamedArgument();
2670
+ }
2671
+ else if (this.check("identifier") && this.peekKind(1) === "assign") {
2672
+ const name = this.advance();
2673
+ this.advance();
2674
+ if (sawSpread)
2675
+ this.diagnostics.push(diagnostic("VEL2024", "Named arguments cannot be combined with a call spread", name.span));
2676
+ sawNamed = true;
2677
+ argumentNames.push(name.value);
2678
+ arguments_.push(this.parseExpression());
2679
+ this.recoverChainedNamedArgument();
2680
+ }
2681
+ else {
2682
+ const argument = this.parseSpreadExpression();
2683
+ if (sawNamed)
2684
+ this.diagnostics.push(diagnostic("VEL2024", "Positional arguments must appear before named arguments", argument.span));
2685
+ if (argument.kind === "SpreadExpression")
2686
+ sawSpread = true;
2687
+ argumentNames.push(null);
2688
+ arguments_.push(argument);
2689
+ }
2690
+ } while (this.match("comma") && !this.check("rightParen"));
2691
+ }
2692
+ const close = this.expect("rightParen", "Expected ')' after arguments");
2693
+ expression = {
2694
+ kind: "CallExpression",
2695
+ callee: expression,
2696
+ arguments: arguments_,
2697
+ ...(sawNamed ? { argumentNames } : {}),
2698
+ optional: optionalCall,
2699
+ span: span(expression.span.start, close.span.end),
2700
+ };
2701
+ continue;
2702
+ }
2703
+ if (this.check("optionalDot") && this.peekKind(1) === "leftBracket") {
2704
+ this.advance();
2705
+ this.advance();
2706
+ const index = this.parseExpression();
2707
+ const close = this.expect("rightBracket", "Expected ']' after optional index");
2708
+ expression = { kind: "IndexExpression", object: expression, index, optional: true, span: span(expression.span.start, close.span.end) };
2709
+ continue;
2710
+ }
2711
+ if (this.match("dot") || this.match("optionalDot")) {
2712
+ const optional = this.previous().kind === "optionalDot";
2713
+ const property = this.expectMemberName();
2714
+ expression = { kind: "MemberExpression", object: expression, property: property.value, optional, span: span(expression.span.start, property.span.end) };
2715
+ continue;
2716
+ }
2717
+ if (this.match("leftBracket")) {
2718
+ const index = this.parseExpression();
2719
+ const close = this.expect("rightBracket", "Expected ']' after index");
2720
+ expression = { kind: "IndexExpression", object: expression, index, optional: false, span: span(expression.span.start, close.span.end) };
2721
+ continue;
2722
+ }
2723
+ break;
2724
+ }
2725
+ return expression;
2726
+ }
2727
+ explicitTypeArgumentsEnd(expression) {
2728
+ const callableName = expression.kind === "IdentifierExpression" ? expression.name
2729
+ : expression.kind === "MemberExpression" ? expression.property
2730
+ : null;
2731
+ if (callableName === null || !this.check("less") || this.current().span.start !== expression.span.end)
2732
+ return null;
2733
+ // Beyond the same-file generic-name list, the recovery extends to any
2734
+ // callee — imported generics and methods — when the angle content carries
2735
+ // type evidence: a builtin type name, a capitalized name, optional or
2736
+ // union syntax, or nested generics. Without that evidence the comparison
2737
+ // reading wins, so `a<b>(c)` over three numbers stays a chain (rule #41).
2738
+ const knownGeneric = this.genericCallableNames.has(callableName);
2739
+ const typeEvidence = (token) => {
2740
+ if (token.kind === "question" || token.kind === "pipe" || token.kind === "arrow")
2741
+ return true;
2742
+ if (token.kind !== "identifier")
2743
+ return false;
2744
+ if (["string", "number", "bool", "null", "unknown", "any", "List", "Set", "Map", "Record", "Promise", "Function", "Type", "readonly"].includes(token.value))
2745
+ return true;
2746
+ const first = token.value[0] ?? "";
2747
+ return first >= "A" && first <= "Z";
2748
+ };
2749
+ let sawTypeEvidence = false;
2750
+ let depth = 0;
2751
+ for (let index = this.index; index < this.tokens.length; index += 1) {
2752
+ const token = this.tokens[index];
2753
+ if (token.kind === "newline" || token.kind === "eof")
2754
+ return null;
2755
+ if (token.kind === "less") {
2756
+ depth += 1;
2757
+ if (depth >= 2)
2758
+ sawTypeEvidence = true;
2759
+ }
2760
+ else if (token.kind === "greater") {
2761
+ depth -= 1;
2762
+ if (depth === 0) {
2763
+ const call = this.tokens[index + 1];
2764
+ if (call?.kind !== "leftParen" || call.span.start !== token.span.end)
2765
+ return null;
2766
+ return knownGeneric || sawTypeEvidence ? index : null;
2767
+ }
2768
+ }
2769
+ else if (typeEvidence(token)) {
2770
+ sawTypeEvidence = true;
2771
+ }
2772
+ }
2773
+ return null;
2774
+ }
2775
+ // True when the '?' at the current position opens a conditional expression:
2776
+ // more of the expression follows on the logical line. A '?' that ends the
2777
+ // line is an optional-type marker. This is the same reading
2778
+ // typeTestHasConditionalQuestion applies to a '?' directly after a type.
2779
+ questionContinuesExpression() {
2780
+ const next = this.peekKind(1);
2781
+ return next !== "newline" && next !== "dedent" && next !== "eof";
2782
+ }
2783
+ typeTestHasConditionalQuestion() {
2784
+ let angles = 0;
2785
+ let parentheses = 0;
2786
+ let brackets = 0;
2787
+ for (let offset = 0; this.index + offset < this.tokens.length; offset += 1) {
2788
+ const token = this.tokens[this.index + offset];
2789
+ if (token.kind === "newline" || token.kind === "dedent" || token.kind === "eof")
2790
+ return false;
2791
+ if (token.kind === "less")
2792
+ angles += 1;
2793
+ else if (token.kind === "greater")
2794
+ angles = Math.max(0, angles - 1);
2795
+ else if (token.kind === "leftParen")
2796
+ parentheses += 1;
2797
+ else if (token.kind === "rightParen") {
2798
+ if (parentheses === 0)
2799
+ return false;
2800
+ parentheses -= 1;
2801
+ }
2802
+ else if (token.kind === "leftBracket")
2803
+ brackets += 1;
2804
+ else if (token.kind === "rightBracket") {
2805
+ if (brackets === 0)
2806
+ return false;
2807
+ brackets -= 1;
2808
+ }
2809
+ else if (token.kind === "question" && angles === 0 && parentheses === 0 && brackets === 0) {
2810
+ const next = this.tokens[this.index + offset + 1]?.kind;
2811
+ return next !== undefined && next !== "newline" && next !== "dedent" && next !== "eof";
2812
+ }
2813
+ }
2814
+ return false;
2815
+ }
2816
+ parsePrimary() {
2817
+ const token = this.advance();
2818
+ switch (token.kind) {
2819
+ case "number":
2820
+ return this.numberLiteral(token);
2821
+ case "unitNumber": {
2822
+ const match = /^(\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)([A-Za-z%]+)$/u.exec(token.value);
2823
+ if (!match) {
2824
+ this.diagnostics.push(diagnostic("VEL2002", "Invalid unit literal", token.span));
2825
+ return { kind: "LiteralExpression", value: 0, raw: "0", span: token.span };
2826
+ }
2827
+ const value = Number(match[1]);
2828
+ if (!Number.isFinite(value))
2829
+ this.diagnostics.push(diagnostic("VEL2017", "Numeric literals must be finite", token.span));
2830
+ const extensionExpression = this.parseExtensionNumericLiteral(token, Number.isFinite(value) ? value : 0, match[2]);
2831
+ if (extensionExpression)
2832
+ return extensionExpression;
2833
+ this.diagnostics.push(diagnostic("VEL2002", `No compiler extension accepts numeric suffix '${match[2]}'`, token.span));
2834
+ return { kind: "LiteralExpression", value: 0, raw: "0", span: token.span };
2835
+ }
2836
+ case "string":
2837
+ return { kind: "LiteralExpression", value: token.value, raw: token.value, span: token.span };
2838
+ case "import": {
2839
+ this.expect("leftParen", "Expected '(' after 'import'");
2840
+ const source = this.expect("string", "Dynamic imports require a literal relative .vel path");
2841
+ const close = this.expect("rightParen", "Expected ')' after dynamic import path");
2842
+ if ((!source.value.startsWith("./") && !source.value.startsWith("../")) || !source.value.endsWith(".vel")) {
2843
+ this.diagnostics.push(diagnostic("VEL2014", "Dynamic imports require a literal relative path ending in '.vel'", source.span));
2844
+ }
2845
+ return {
2846
+ kind: "DynamicImportExpression",
2847
+ source: source.value,
2848
+ sourceSpan: source.span,
2849
+ span: span(token.span.start, close.span.end),
2850
+ };
2851
+ }
2852
+ case "fstring":
2853
+ return this.parseFString(token);
2854
+ case "extensionToken": {
2855
+ const extensionExpression = this.parseExtensionExpression(token);
2856
+ if (extensionExpression)
2857
+ return extensionExpression;
2858
+ this.diagnostics.push(diagnostic("VEL2002", "No compiler extension accepts this embedded expression", token.span));
2859
+ return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
2860
+ }
2861
+ case "true":
2862
+ return { kind: "LiteralExpression", value: true, raw: token.value, span: token.span };
2863
+ case "false":
2864
+ return { kind: "LiteralExpression", value: false, raw: token.value, span: token.span };
2865
+ case "null":
2866
+ return { kind: "LiteralExpression", value: null, raw: token.value, span: token.span };
2867
+ case "identifier": {
2868
+ // An extension's contextual keyword is an ordinary name until its own
2869
+ // parser recognizes the shape it owns; only then does the extension
2870
+ // expression win over the identifier reading.
2871
+ if (this.contextualKeywords.has(token.value)) {
2872
+ const extensionExpression = this.parseExtensionExpression(token);
2873
+ if (extensionExpression)
2874
+ return extensionExpression;
2875
+ }
2876
+ // A block-valued Web word reaching Core means the extension is not
2877
+ // active: the module was moved, or velar.json is missing the entry. One
2878
+ // message names the cause instead of a statement-boundary cascade.
2879
+ if ((token.value === "keyframes" || token.value === "look") && this.check("colon") && this.peekKind(1) === "newline") {
2880
+ this.diagnostics.push(diagnostic("VEL2035", `'${token.value}:' belongs to @velarscript/web; add "@velarscript/web" to velar.json extensions, or move this module into a Web project`, token.span));
2881
+ this.skipMistypedDeclaration();
2882
+ return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
2883
+ }
2884
+ if (token.value === "function" && (this.check("leftParen") || (this.check("identifier") && this.peekKind(1) === "leftParen"))) {
2885
+ this.diagnostics.push(diagnostic("VEL2031", "VelarScript has no 'function' expressions; declare 'def name(...)' or write an arrow '(x) => value'", token.span));
2886
+ this.synchronize();
2887
+ return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
2888
+ }
2889
+ return { kind: "IdentifierExpression", name: token.value, span: token.span };
2890
+ }
2891
+ case "super":
2892
+ return { kind: "SuperExpression", span: token.span };
2893
+ case "leftParen": {
2894
+ const expression = this.parseExpression();
2895
+ this.expect("rightParen", "Expected ')' after expression");
2896
+ // Explicit parentheses are the author's grouping decision. Binary
2897
+ // nodes carry that fact so the '??' / 'and' / 'or' mixing rule can
2898
+ // tell a deliberate grouping from a bare chain.
2899
+ return expression.kind === "BinaryExpression" || expression.kind === "ComparisonChainExpression" || expression.kind === "IsExpression"
2900
+ ? { ...expression, parenthesized: true }
2901
+ : expression;
2902
+ }
2903
+ case "leftBracket": {
2904
+ const elements = [];
2905
+ if (!this.check("rightBracket")) {
2906
+ do {
2907
+ elements.push(this.parseSpreadExpression());
2908
+ } while (this.match("comma") && !this.check("rightBracket"));
2909
+ }
2910
+ const close = this.expect("rightBracket", "Expected ']' after list elements");
2911
+ return { kind: "ListExpression", elements, span: span(token.span.start, close.span.end) };
2912
+ }
2913
+ case "leftBrace": {
2914
+ const properties = [];
2915
+ if (!this.check("rightBrace")) {
2916
+ do {
2917
+ if (this.match("ellipsis")) {
2918
+ const spread = this.previous();
2919
+ const value = this.parseExpression();
2920
+ properties.push({ kind: "ObjectSpread", value, span: span(spread.span.start, value.span.end) });
2921
+ continue;
2922
+ }
2923
+ const name = memberNameKinds.has(this.current().kind) || this.check("string")
2924
+ ? this.advance()
2925
+ : this.expect("identifier", "Expected an object field name");
2926
+ const hasValue = this.match("colon");
2927
+ if (!hasValue && name.kind !== "identifier") {
2928
+ // D30 item 16 retired this teaching for the softened words; what
2929
+ // remains are the hard keywords and quoted names, and a hard
2930
+ // keyword is named so the reader knows why the shorthand cannot
2931
+ // reach a binding of that spelling.
2932
+ this.diagnostics.push(diagnostic("VEL2020", name.kind === "string"
2933
+ ? "A quoted object field requires ':' and a value"
2934
+ : `'${name.value}' is a VelarScript keyword, so no binding spells it; write '${name.value}: value'`, name.span));
2935
+ }
2936
+ const value = hasValue
2937
+ ? this.parseExpression()
2938
+ : name.kind === "identifier"
2939
+ ? { kind: "IdentifierExpression", name: name.value, span: name.span }
2940
+ : { kind: "LiteralExpression", value: null, raw: "null", span: name.span };
2941
+ properties.push({ kind: "ObjectProperty", name: name.value, value, ...(hasValue ? {} : { shorthand: true }), span: span(name.span.start, value.span.end) });
2942
+ } while (this.match("comma") && !this.check("rightBrace"));
2943
+ }
2944
+ const close = this.expect("rightBrace", "Expected '}' after object fields");
2945
+ return { kind: "ObjectExpression", properties, span: span(token.span.start, close.span.end) };
2946
+ }
2947
+ default:
2948
+ if (token.kind === "indent") {
2949
+ // GRM-A5's sibling shape: a line indented under a complete
2950
+ // statement continues nothing. One diagnostic owns the whole
2951
+ // orphan block so its lines do not cascade.
2952
+ this.diagnostics.push(diagnostic("VEL2002", "A statement ends at its newline; this indented line continues nothing — parenthesize an expression to span lines, or align the line with its block", token.span));
2953
+ let blocks = 1;
2954
+ while (blocks > 0 && !this.check("eof")) {
2955
+ if (this.check("indent"))
2956
+ blocks += 1;
2957
+ else if (this.check("dedent"))
2958
+ blocks -= 1;
2959
+ this.advance();
2960
+ }
2961
+ return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
2962
+ }
2963
+ if (token.kind === "newline" || token.kind === "dedent" || token.kind === "eof") {
2964
+ // GRM-A5: an operator dangling at the line end leaves the parser at
2965
+ // the statement boundary. GRM-D2's postfix shape lands here too
2966
+ // (`i++` parses as `i + +<nothing>`), so it gets its own teaching.
2967
+ const beforeBoundary = this.tokens[this.index - 2];
2968
+ const stacked = this.tokens[this.index - 3];
2969
+ const increment = beforeBoundary && stacked
2970
+ && beforeBoundary.kind === stacked.kind
2971
+ && (beforeBoundary.kind === "plus" || beforeBoundary.kind === "minus")
2972
+ && stacked.span.end === beforeBoundary.span.start;
2973
+ if (increment) {
2974
+ const operator = beforeBoundary.kind === "plus" ? "+" : "-";
2975
+ const target = this.tokens[this.index - 4];
2976
+ const name = target?.kind === "identifier" ? target.value : "name";
2977
+ this.diagnostics.push(diagnostic("VEL2031", `VelarScript has no '${operator}${operator}'; write '${name} ${operator}= 1'`, span(stacked.span.start, beforeBoundary.span.end)));
2978
+ }
2979
+ else {
2980
+ this.diagnostics.push(diagnostic("VEL2002", "A statement ends at its newline; parenthesize the expression to continue it across lines", token.span));
2981
+ }
2982
+ if (token.kind === "newline") {
2983
+ // A continuation line indented under the broken statement would
2984
+ // cascade line by line; it belongs to this one error, so its
2985
+ // whole block is consumed here.
2986
+ let ahead = 0;
2987
+ while (this.peekKind(ahead) === "newline")
2988
+ ahead += 1;
2989
+ if (this.peekKind(ahead) === "indent") {
2990
+ while (ahead > 0) {
2991
+ this.advance();
2992
+ ahead -= 1;
2993
+ }
2994
+ this.advance();
2995
+ let blocks = 1;
2996
+ while (blocks > 0 && !this.check("eof")) {
2997
+ if (this.check("indent"))
2998
+ blocks += 1;
2999
+ else if (this.check("dedent"))
3000
+ blocks -= 1;
3001
+ this.advance();
3002
+ }
3003
+ }
3004
+ else {
3005
+ this.index -= 1;
3006
+ }
3007
+ }
3008
+ else if (token.kind === "dedent") {
3009
+ this.index -= 1;
3010
+ }
3011
+ return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
3012
+ }
3013
+ // D43 item 67: '@name' is the language's own namespace for members that
3014
+ // sit where user names also sit. Outside a declaration body it is not an
3015
+ // expression, so the reader is told what the marker means instead of
3016
+ // receiving a bare 'Expected an expression'.
3017
+ if (token.kind === "at") {
3018
+ const name = this.check("identifier") ? this.advance().value : "";
3019
+ this.diagnostics.push(diagnostic("VEL2002", `'@${name}' names a language-owned member and appears only inside a declaration body, such as a component's '@mounted:' block`, span(token.span.start, this.previous().span.end)));
3020
+ this.skipMistypedDeclaration();
3021
+ return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
3022
+ }
3023
+ // A hard-reserved word standing where a value belongs is the same
3024
+ // mistake as one standing in a name position, so it gets the same
3025
+ // named message rather than a bare "Expected an expression".
3026
+ this.diagnostics.push(diagnostic("VEL2002", this.reservedWordMessageFor(token, "name") ?? "Expected an expression", token.span));
3027
+ return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
3028
+ }
3029
+ }
3030
+ // GRM-T1: `f(x=y=2)` chains a second '=' after the named value; the whole
3031
+ // chain is consumed under one diagnostic so ')' recovery does not cascade.
3032
+ recoverChainedNamedArgument() {
3033
+ while (this.check("assign")) {
3034
+ this.diagnostics.push(diagnostic("VEL2024", "A named argument takes one value; remove the extra '='", this.current().span));
3035
+ this.advance();
3036
+ this.parseExpression();
3037
+ }
3038
+ }
3039
+ parseSpreadExpression() {
3040
+ if (!this.match("ellipsis"))
3041
+ return this.parseExpression();
3042
+ const start = this.previous().span.start;
3043
+ const value = this.parseExpression();
3044
+ return { kind: "SpreadExpression", value, span: span(start, value.span.end) };
3045
+ }
3046
+ parseFString(token) {
3047
+ const payload = token.payload;
3048
+ const raw = payload?.raw ?? false;
3049
+ const contentOffset = (payload?.prefixLength ?? 1) + 1 + (payload?.layout ? 1 : 0);
3050
+ const sourceOffset = (index) => payload?.contentOffsets?.[index] ?? token.span.start + contentOffset + index;
3051
+ const parts = [];
3052
+ let textStart = 0;
3053
+ let index = 0;
3054
+ while (index < token.value.length) {
3055
+ if (!raw && token.value[index] === "\\") {
3056
+ index = scanStringEscape(token.value, index).end;
3057
+ continue;
3058
+ }
3059
+ if (token.value[index] === "$" && token.value[index + 1] === "{") {
3060
+ const close = token.value.indexOf("}", index + 2);
3061
+ index = close < 0 ? index + 2 : close + 1;
3062
+ continue;
3063
+ }
3064
+ if (token.value[index] === "{" && token.value[index + 1] === "{") {
3065
+ index += 2;
3066
+ continue;
3067
+ }
3068
+ if (token.value[index] === "}" && token.value[index + 1] === "}") {
3069
+ index += 2;
3070
+ continue;
3071
+ }
3072
+ if (token.value[index] === "}") {
3073
+ const offset = sourceOffset(index);
3074
+ this.diagnostics.push(diagnostic("VEL2009", "Unmatched '}' in interpolated string", span(offset, offset + 1)));
3075
+ index += 1;
3076
+ continue;
3077
+ }
3078
+ if (token.value[index] !== "{") {
3079
+ index += 1;
3080
+ continue;
3081
+ }
3082
+ if (index > textStart) {
3083
+ parts.push({ kind: "text", value: this.decodeFStringText(token.value.slice(textStart, index), payload) });
3084
+ }
3085
+ const close = findInterpolatedExpressionEnd(token.value, index + 1);
3086
+ if (close < 0) {
3087
+ const offset = sourceOffset(index);
3088
+ this.diagnostics.push(diagnostic("VEL2009", "Unclosed expression in interpolated string", span(offset, token.span.end)));
3089
+ parts.push({ kind: "text", value: this.decodeFStringText(token.value.slice(index), payload) });
3090
+ textStart = token.value.length;
3091
+ break;
3092
+ }
3093
+ const rawFragment = token.value.slice(index + 1, close);
3094
+ let fragment = rawFragment.trim();
3095
+ const leadingWhitespace = rawFragment.length - rawFragment.trimStart().length;
3096
+ const fragmentStart = index + 1 + leadingWhitespace;
3097
+ // TXT-I2: a top-level ':' in an interpolation is the Python
3098
+ // format-spec habit (f"{x:.2f}"). One directed diagnostic teaches the
3099
+ // real spelling, and only the value expression is parsed, so the spec
3100
+ // text never cascades into numeric-unit noise.
3101
+ const specColon = this.interpolationFormatSpecColon(fragment);
3102
+ if (specColon !== null) {
3103
+ const colonOffset = sourceOffset(fragmentStart + specColon);
3104
+ this.diagnostics.push(diagnostic("VEL2009", "An interpolation holds one expression; VelarScript has no ':' format specs. Format the value first — value.toFixed(2) for fixed decimals, str(value).padStart(size) for width", span(colonOffset, sourceOffset(close))));
3105
+ fragment = fragment.slice(0, specColon).trimEnd();
3106
+ }
3107
+ const offset = sourceOffset(fragmentStart);
3108
+ const fragmentOffsets = payload?.contentOffsets?.slice(fragmentStart, fragmentStart + fragment.length + 1);
3109
+ parts.push({ kind: "expression", value: this.parseNestedExpression(fragment, offset, false, fragmentOffsets) });
3110
+ index = close + 1;
3111
+ textStart = index;
3112
+ }
3113
+ if (textStart < token.value.length) {
3114
+ parts.push({ kind: "text", value: this.decodeFStringText(token.value.slice(textStart), payload) });
3115
+ }
3116
+ return { kind: "FStringExpression", parts, span: token.span };
3117
+ }
3118
+ numberLiteral(token, negative = false, literalSpan = token.span) {
3119
+ const value = Number(token.value) * (negative ? -1 : 1);
3120
+ if (!Number.isFinite(value))
3121
+ this.diagnostics.push(diagnostic("VEL2017", "Numeric literals must be finite", literalSpan));
3122
+ return {
3123
+ kind: "LiteralExpression",
3124
+ value: Number.isFinite(value) ? value : 0,
3125
+ raw: `${negative ? "-" : ""}${token.value}`,
3126
+ span: literalSpan,
3127
+ };
3128
+ }
3129
+ // TXT-I2: the offset of a top-level ':' in an interpolation fragment, or
3130
+ // null. Ternary colons are consumed by their pending '?', bracket and
3131
+ // string contents never count, and '?.'/'??' are not ternary heads — so
3132
+ // the only ':' that survives to depth zero is a format spec.
3133
+ interpolationFormatSpecColon(fragment) {
3134
+ let depth = 0;
3135
+ let pendingTernary = 0;
3136
+ for (let index = 0; index < fragment.length; index += 1) {
3137
+ const literal = scanStringLiteral(fragment, index);
3138
+ if (literal) {
3139
+ if (!literal.closed)
3140
+ return null;
3141
+ index = literal.end - 1;
3142
+ continue;
3143
+ }
3144
+ const character = fragment[index];
3145
+ if (character === "(" || character === "[" || character === "{")
3146
+ depth += 1;
3147
+ else if (character === ")" || character === "]" || character === "}")
3148
+ depth -= 1;
3149
+ else if (character === "?" && depth === 0) {
3150
+ const next = fragment[index + 1];
3151
+ if (next === "." || next === "?")
3152
+ index += 1;
3153
+ else
3154
+ pendingTernary += 1;
3155
+ }
3156
+ else if (character === ":" && depth === 0) {
3157
+ if (pendingTernary > 0)
3158
+ pendingTernary -= 1;
3159
+ else
3160
+ return index;
3161
+ }
3162
+ }
3163
+ return null;
3164
+ }
3165
+ decodeFStringText(value, payload) {
3166
+ const raw = payload?.raw ?? false;
3167
+ const quote = payload?.quote ?? '"';
3168
+ let decoded = "";
3169
+ for (let index = 0; index < value.length; index += 1) {
3170
+ const character = value[index];
3171
+ const next = value[index + 1];
3172
+ if (raw && !payload?.layout && character === quote && next === quote) {
3173
+ decoded += quote;
3174
+ index += 1;
3175
+ }
3176
+ else if (!raw && character === "\\" && next !== undefined) {
3177
+ const escaped = scanStringEscape(value, index);
3178
+ decoded += escaped.value ?? next;
3179
+ index = escaped.end - 1;
3180
+ }
3181
+ else if ((character === "{" && next === "{") || (character === "}" && next === "}")) {
3182
+ decoded += character;
3183
+ index += 1;
3184
+ }
3185
+ else {
3186
+ decoded += character;
3187
+ }
3188
+ }
3189
+ return decoded;
3190
+ }
3191
+ parseExtensionExpression(_token) {
3192
+ return undefined;
3193
+ }
3194
+ parseExtensionNumericLiteral(token, value, unit) {
3195
+ if (unit !== "ms" && unit !== "s")
3196
+ return undefined;
3197
+ return {
3198
+ kind: "ExtensionExpression:core:duration",
3199
+ value,
3200
+ unit,
3201
+ raw: token.value,
3202
+ span: token.span,
3203
+ };
3204
+ }
3205
+ // An extension-owned bracket fragment lexes with insignificant
3206
+ // newlines, matching ordinary bracket continuation. An indentation-owning
3207
+ // extension expression inside the fragment — an extension keyword followed
3208
+ // by ':' — still needs physical lines, so that
3209
+ // fragment falls back to the ordinary line-sensitive lex.
3210
+ parseNestedExpression(fragment, offset, bracketFragment = false, sourceOffsets) {
3211
+ let lexed = bracketFragment ? new Lexer(fragment, this.lexicalExtensions, { bracketFragment: true, scanSourceHygiene: false }).lex() : null;
3212
+ if (!lexed || containsExtensionBlockStart(lexed.tokens, this.contextualKeywords)) {
3213
+ lexed = new Lexer(fragment, this.lexicalExtensions, { scanSourceHygiene: false }).lex();
3214
+ }
3215
+ const mappedSpan = (local) => sourceOffsets
3216
+ ? span(sourceOffsets[local.start] ?? offset, sourceOffsets[local.end] ?? sourceOffsets.at(-1) ?? offset)
3217
+ : span(local.start + offset, local.end + offset);
3218
+ const shiftedTokens = lexed.tokens.map((item) => ({ ...item, span: mappedSpan(item.span) }));
3219
+ const shiftedDiagnostics = lexed.diagnostics.map((item) => ({ ...item, span: mappedSpan(item.span) }));
3220
+ const parsed = this.createNestedParser(shiftedTokens).parseExpressionFragment();
3221
+ this.diagnostics.push(...shiftedDiagnostics, ...parsed.diagnostics);
3222
+ return parsed.expression;
3223
+ }
3224
+ createNestedParser(tokens) {
3225
+ return new Parser(tokens, this.lexicalExtensions);
3226
+ }
3227
+ withParseDepth(parse) {
3228
+ this.parseDepth += 1;
3229
+ if (this.parseDepth > MAX_PARSE_DEPTH) {
3230
+ this.parseDepth -= 1;
3231
+ throw PARSER_COMPLEXITY_FAILURE;
3232
+ }
3233
+ try {
3234
+ return parse();
3235
+ }
3236
+ finally {
3237
+ this.parseDepth -= 1;
3238
+ }
3239
+ }
3240
+ binaryOperator(token) {
3241
+ const operators = {
3242
+ nullish: "??",
3243
+ or: "or",
3244
+ and: "and",
3245
+ in: "in",
3246
+ equal: "==",
3247
+ notEqual: "!=",
3248
+ less: "<",
3249
+ lessEqual: "<=",
3250
+ greater: ">",
3251
+ greaterEqual: ">=",
3252
+ plus: "+",
3253
+ minus: "-",
3254
+ star: "*",
3255
+ starStar: "**",
3256
+ slash: "/",
3257
+ percent: "%",
3258
+ pipe: "|",
3259
+ amp: "&",
3260
+ caret: "^",
3261
+ leftShift: "<<",
3262
+ rightShift: ">>",
3263
+ unsignedRightShift: ">>>",
3264
+ };
3265
+ return operators[token.kind] ?? "+";
3266
+ }
3267
+ isUnparenthesizedComparisonLayer(expression) {
3268
+ if (expression.kind === "ComparisonChainExpression" || expression.kind === "IsExpression")
3269
+ return !expression.parenthesized;
3270
+ return expression.kind === "BinaryExpression" && !expression.parenthesized
3271
+ && (expression.operator === "in" || expression.operator === "not in" || expression.operator === "=="
3272
+ || expression.operator === "!=" || expression.operator === "<" || expression.operator === "<="
3273
+ || expression.operator === ">" || expression.operator === ">=");
3274
+ }
3275
+ isMembershipOrTypeTest(expression) {
3276
+ return expression.kind === "IsExpression"
3277
+ || (expression.kind === "BinaryExpression" && (expression.operator === "in" || expression.operator === "not in"));
3278
+ }
3279
+ // '??' never shares a bare binary chain with 'and'/'or': the two groupings
3280
+ // read differently, so the mix requires explicit parentheses. '??' binds
3281
+ // loosest, so every unparenthesized mix surfaces as an 'and'/'or' node
3282
+ // becoming a direct operand of a '??' node; the symmetric test also covers
3283
+ // recovered shapes. One diagnostic marks each mixing operator, and the node
3284
+ // is still built with the current grouping so later stages keep running.
3285
+ checkNullishBooleanMixing(operator, left, right, operatorSpan) {
3286
+ const nullish = operator === "??";
3287
+ const boolean = operator === "and" || operator === "or";
3288
+ if (!nullish && !boolean)
3289
+ return;
3290
+ const conflicting = (operand) => operand.kind === "BinaryExpression" && !operand.parenthesized
3291
+ && (nullish ? operand.operator === "and" || operand.operator === "or" : operand.operator === "??")
3292
+ ? operand.operator
3293
+ : null;
3294
+ const adjacent = conflicting(left) ?? conflicting(right);
3295
+ if (!adjacent)
3296
+ return;
3297
+ const booleanOperator = nullish ? adjacent : operator;
3298
+ this.diagnostics.push(recoveredDiagnostic("VEL2034", `Parenthesize the mix of '??' and '${booleanOperator}'; the two groupings read differently`, operatorSpan));
3299
+ }
3300
+ atStatementEnd() {
3301
+ return this.check("newline") || this.check("dedent") || this.check("eof");
3302
+ }
3303
+ expectStatementEnd() {
3304
+ if (!this.atStatementEnd()) {
3305
+ this.diagnostics.push(diagnostic("VEL2003", "Expected the end of a statement", this.current().span));
3306
+ this.synchronize();
3307
+ }
3308
+ }
3309
+ // A statement owns exactly one logical line. Once it is complete the next
3310
+ // token must end that line, so a leftover token cannot silently open a
3311
+ // second statement whose value is discarded. Recovery consumes the rest of
3312
+ // the line, so one bad line reports once and every following line still
3313
+ // parses on its own.
3314
+ expectStatementBoundary() {
3315
+ if (this.atStatementEnd())
3316
+ return;
3317
+ const token = this.current();
3318
+ this.diagnostics.push(diagnostic("VEL2032", this.statementBoundaryMessage(token), token.span));
3319
+ this.synchronize();
3320
+ }
3321
+ finishStatementBoundary() {
3322
+ if (this.recoveredImportDelimiterBoundary) {
3323
+ this.recoveredImportDelimiterBoundary = false;
3324
+ return;
3325
+ }
3326
+ if (this.previous().kind !== "dedent")
3327
+ this.expectStatementBoundary();
3328
+ }
3329
+ // A numeric unit suffix binds tighter than any operator, so '10%3' lexes as
3330
+ // the percentage '10%' followed by a stray '3'. That reads as modulo, so the
3331
+ // spelling difference is named directly instead of reported as a generic
3332
+ // leftover token.
3333
+ statementBoundaryMessage(token) {
3334
+ const previous = this.previous();
3335
+ if (previous.kind === "unitNumber" && previous.value.endsWith("%") && previous.span.end === token.span.start) {
3336
+ const amount = previous.value.slice(0, -1);
3337
+ return `'${previous.value}' is a percentage literal, so '${token.value}' starts a second statement; write '${amount} % ${token.value}' with spaces for the remainder operator`;
3338
+ }
3339
+ // '|' spells a union only inside type annotations; at value level the
3340
+ // author almost always means 'or' (or a comma between match values).
3341
+ if (token.kind === "pipe") {
3342
+ return "'|' joins types only in type annotations; combine conditions with 'or'";
3343
+ }
3344
+ // GRM-A5: structural tokens have no source text, so they need prose —
3345
+ // 'move indent to its own line' taught nothing.
3346
+ if (token.kind === "indent") {
3347
+ return "A statement ends at its newline; this line is indented as a continuation, but only parenthesized expressions span lines — parenthesize, or align the line with its block";
3348
+ }
3349
+ if (token.kind === "dedent" || token.kind === "newline" || token.kind === "eof") {
3350
+ return "A statement ends at its newline; parenthesize an expression to continue it across lines";
3351
+ }
3352
+ return `A statement ends at its newline; move '${describeStatementToken(token)}' to its own line, or join it to the value before it with an operator`;
3353
+ }
3354
+ // The removed 'invert' statement's replacement names the value the author
3355
+ // wrote, so a plain name or member path is rendered from its own tokens
3356
+ // (identifier and dot tokens carry their exact source text). Any other
3357
+ // operand shape keeps a copyable generic spelling rather than a guess.
3358
+ describeInvertTarget(start) {
3359
+ if (this.tokens[start]?.kind !== "identifier")
3360
+ return "x";
3361
+ let text = this.tokens[start].value;
3362
+ let index = start + 1;
3363
+ while (this.tokens[index]?.kind === "dot" && this.tokens[index + 1]?.kind === "identifier") {
3364
+ text += `.${this.tokens[index + 1].value}`;
3365
+ index += 2;
3366
+ }
3367
+ const end = this.tokens[index]?.kind;
3368
+ if (end !== "newline" && end !== "dedent" && end !== "eof")
3369
+ return "x";
3370
+ return text;
3371
+ }
3372
+ synchronize() {
3373
+ while (!this.check("eof") && !this.check("newline") && !this.check("dedent")) {
3374
+ this.advance();
3375
+ }
3376
+ }
3377
+ /**
3378
+ * D38 rule 47 (BRG-D2): an extern signature is the entire contract — there
3379
+ * is no body to infer from — so a parameter without a type used to degrade
3380
+ * to `unknown` and accept every argument silently. The escape hatch may
3381
+ * never lose air quietly: the missing type is reported at the parameter
3382
+ * itself, and the member keeps its place in the module contract so the use
3383
+ * site is not blamed for a declaration defect.
3384
+ */
3385
+ reportUntypedExternParameters(parameters) {
3386
+ for (const parameter of parameters) {
3387
+ if (parameter.type)
3388
+ continue;
3389
+ this.diagnostics.push(diagnostic("VEL2010", `Extern parameter '${parameter.name}' requires an explicit type; there is no body to infer from`, parameter.span));
3390
+ }
3391
+ }
3392
+ /**
3393
+ * D38 rule 47, second half: the rejection of a body on an extern declaration
3394
+ * lands in the right place already, but "Expected the end of a statement"
3395
+ * never said why a body cannot be there.
3396
+ */
3397
+ reportExternDeclarationBody() {
3398
+ // Both spellings of "here comes a body" reach here: the VelarScript block
3399
+ // colon, and the brace a TypeScript declaration habit produces. Neither can
3400
+ // legally follow an extern signature.
3401
+ if (!this.check("colon") && !this.check("leftBrace"))
3402
+ return false;
3403
+ this.diagnostics.push(diagnostic("VEL2010", "Extern declarations have no body; the JavaScript package provides it", this.current().span));
3404
+ if (this.check("leftBrace")) {
3405
+ let depth = 0;
3406
+ do {
3407
+ if (this.check("leftBrace"))
3408
+ depth += 1;
3409
+ else if (this.check("rightBrace"))
3410
+ depth -= 1;
3411
+ this.advance();
3412
+ } while (depth > 0 && !this.check("eof"));
3413
+ return true;
3414
+ }
3415
+ this.skipMistypedDeclaration();
3416
+ return true;
3417
+ }
3418
+ /**
3419
+ * CLS-I5: `readonly` marks a data type, never a class member. A field's
3420
+ * read-only spelling is `const`; a method, getter, or constructor is
3421
+ * executable, so there is no read-only contract for the modifier to state
3422
+ * and pointing the author at `const` was advice they cannot take.
3423
+ */
3424
+ reportClassMemberReadonly(modifier, member, code) {
3425
+ if (!modifier)
3426
+ return;
3427
+ this.diagnostics.push(diagnostic(code, member === "field"
3428
+ ? "'readonly' is a data-type modifier, not a class member modifier; use 'const' for a read-only field"
3429
+ : "'readonly' is a data-type modifier, not a class member modifier; a method, getter, or constructor is executable and has no readonly contract — mark the data it works with, as in 'readonly List<number>'", modifier.span));
3430
+ }
3431
+ // After a mistyped declaration keyword is reported, consume the rest of the
3432
+ // statement line and any indented block that belongs to it so the wrong
3433
+ // spelling produces one directive diagnostic instead of a misleading cascade.
3434
+ skipMistypedDeclaration() {
3435
+ this.synchronize();
3436
+ let distance = 0;
3437
+ while (this.peekKind(distance) === "newline")
3438
+ distance += 1;
3439
+ if (this.peekKind(distance) !== "indent")
3440
+ return;
3441
+ this.consumeNewlines();
3442
+ this.advance();
3443
+ let depth = 1;
3444
+ while (depth > 0 && !this.check("eof")) {
3445
+ if (this.check("indent"))
3446
+ depth += 1;
3447
+ else if (this.check("dedent"))
3448
+ depth -= 1;
3449
+ this.advance();
3450
+ }
3451
+ }
3452
+ consumeNewlines() {
3453
+ while (this.match("newline")) {
3454
+ // Intentionally empty.
3455
+ }
3456
+ }
3457
+ expect(kind, message) {
3458
+ if (this.check(kind)) {
3459
+ return this.advance();
3460
+ }
3461
+ const token = this.current();
3462
+ this.diagnostics.push(diagnostic("VEL2001", message, token.span));
3463
+ return { kind, value: "", span: token.span };
3464
+ }
3465
+ checkTypeGreater() {
3466
+ return this.check("greater") || this.check("rightShift") || this.check("unsignedRightShift");
3467
+ }
3468
+ /** Consume one `>` from a generic close without changing shift lexing. */
3469
+ expectTypeGreater(message) {
3470
+ if (this.check("greater"))
3471
+ return this.advance();
3472
+ const token = this.current();
3473
+ if (token.kind === "rightShift" || token.kind === "unsignedRightShift") {
3474
+ const close = { kind: "greater", value: ">", span: span(token.span.start, token.span.start + 1) };
3475
+ const remainderKind = token.kind === "rightShift" ? "greater" : "rightShift";
3476
+ this.tokens[this.index] = {
3477
+ kind: remainderKind,
3478
+ value: token.value.slice(1),
3479
+ span: span(token.span.start + 1, token.span.end),
3480
+ };
3481
+ return close;
3482
+ }
3483
+ this.diagnostics.push(diagnostic("VEL2001", message, token.span));
3484
+ return { kind: "greater", value: "", span: token.span };
3485
+ }
3486
+ expectMemberName(message = "Expected a member name after '.'") {
3487
+ if (memberNameKinds.has(this.current().kind))
3488
+ return this.advance();
3489
+ const token = this.current();
3490
+ this.diagnostics.push(diagnostic("VEL2001", message, token.span));
3491
+ return { kind: "identifier", value: "", span: token.span };
3492
+ }
3493
+ match(kind) {
3494
+ if (!this.check(kind)) {
3495
+ return false;
3496
+ }
3497
+ this.advance();
3498
+ return true;
3499
+ }
3500
+ matchExtensionKeyword(value) {
3501
+ return this.matchWord(value);
3502
+ }
3503
+ /**
3504
+ * D30 item 16: a contextual keyword is an ordinary identifier token whose
3505
+ * spelling matches. `checkWord` asks the question without consuming; the
3506
+ * declaration shape decides at each statement head, and every other position
3507
+ * keeps the identifier reading.
3508
+ */
3509
+ /**
3510
+ * D30 item 16: the words that stay hard-reserved now say so. A bare
3511
+ * "Expected a binding name" never told the reader that the spelling itself
3512
+ * was the problem, which is exactly the question a reserved word raises.
3513
+ */
3514
+ reservedWordMessage(noun) {
3515
+ return this.reservedWordMessageFor(this.current(), noun);
3516
+ }
3517
+ reservedWordMessageFor(token, noun) {
3518
+ if (token.kind === "identifier" || token.kind === "string" || !/^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(token.value))
3519
+ return null;
3520
+ return `'${token.value}' is a VelarScript keyword and cannot be a ${noun}; choose another name`;
3521
+ }
3522
+ expectBindingName(message, noun) {
3523
+ if (this.check("identifier"))
3524
+ return this.advance();
3525
+ const token = this.current();
3526
+ const reserved = this.reservedWordMessage(noun);
3527
+ this.diagnostics.push(diagnostic("VEL2001", reserved ?? message, token.span));
3528
+ // A reserved word standing in a name position is a whole, well-formed
3529
+ // token; consuming it lets the rest of the list parse instead of
3530
+ // unravelling into a dozen follow-on expectations.
3531
+ if (reserved) {
3532
+ this.advance();
3533
+ return { kind: "identifier", value: token.value, span: token.span };
3534
+ }
3535
+ return { kind: "identifier", value: "", span: token.span };
3536
+ }
3537
+ checkWord(value) {
3538
+ const token = this.current();
3539
+ return token.kind === "identifier" && token.value === value;
3540
+ }
3541
+ matchWord(value) {
3542
+ if (!this.checkWord(value))
3543
+ return false;
3544
+ this.advance();
3545
+ return true;
3546
+ }
3547
+ /**
3548
+ * Consumes a contextual keyword that a production requires — `from` in an
3549
+ * import, `as` in a namespace import — and reports it in the same voice as a
3550
+ * missing hard keyword when it is absent.
3551
+ */
3552
+ expectWord(value, message) {
3553
+ if (this.checkWord(value))
3554
+ return this.advance();
3555
+ const token = this.current();
3556
+ this.diagnostics.push(diagnostic("VEL2001", message, token.span));
3557
+ return { kind: "identifier", value: "", span: token.span };
3558
+ }
3559
+ check(kind) {
3560
+ return this.current().kind === kind;
3561
+ }
3562
+ peekKind(distance) {
3563
+ return this.tokens[this.index + distance]?.kind ?? "eof";
3564
+ }
3565
+ peekValue(distance) {
3566
+ return this.tokens[this.index + distance]?.value ?? "";
3567
+ }
3568
+ advance() {
3569
+ const token = this.current();
3570
+ if (token.kind !== "eof") {
3571
+ this.index += 1;
3572
+ }
3573
+ return token;
3574
+ }
3575
+ current() {
3576
+ return this.tokens[this.index] ?? this.tokens[this.tokens.length - 1];
3577
+ }
3578
+ previous() {
3579
+ return this.tokens[Math.max(0, this.index - 1)] ?? this.current();
3580
+ }
3581
+ }
3582
+ //# sourceMappingURL=parser.js.map