@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.
- package/LICENSE +201 -0
- package/README.md +70 -0
- package/dist/analyzer.d.ts +1175 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +12130 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/application-package-host.d.ts +28 -0
- package/dist/application-package-host.d.ts.map +1 -0
- package/dist/application-package-host.js +2 -0
- package/dist/application-package-host.js.map +1 -0
- package/dist/ast.d.ts +853 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +245 -0
- package/dist/ast.js.map +1 -0
- package/dist/class-runtime.d.ts +6 -0
- package/dist/class-runtime.d.ts.map +1 -0
- package/dist/class-runtime.js +45 -0
- package/dist/class-runtime.js.map +1 -0
- package/dist/collection-lowering-runtime.d.ts +7 -0
- package/dist/collection-lowering-runtime.d.ts.map +1 -0
- package/dist/collection-lowering-runtime.js +952 -0
- package/dist/collection-lowering-runtime.js.map +1 -0
- package/dist/collection-runtime.d.ts +16 -0
- package/dist/collection-runtime.d.ts.map +1 -0
- package/dist/collection-runtime.js +231 -0
- package/dist/collection-runtime.js.map +1 -0
- package/dist/core-vocabulary.d.ts +141 -0
- package/dist/core-vocabulary.d.ts.map +1 -0
- package/dist/core-vocabulary.js +88 -0
- package/dist/core-vocabulary.js.map +1 -0
- package/dist/diagnostic.d.ts +42 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +66 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/embedded-javascript.d.ts +74 -0
- package/dist/embedded-javascript.d.ts.map +1 -0
- package/dist/embedded-javascript.js +341 -0
- package/dist/embedded-javascript.js.map +1 -0
- package/dist/embedded-module.d.ts +11 -0
- package/dist/embedded-module.d.ts.map +1 -0
- package/dist/embedded-module.js +67 -0
- package/dist/embedded-module.js.map +1 -0
- package/dist/embedded-source.d.ts +19 -0
- package/dist/embedded-source.d.ts.map +1 -0
- package/dist/embedded-source.js +61 -0
- package/dist/embedded-source.js.map +1 -0
- package/dist/emitter.d.ts +188 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +3254 -0
- package/dist/emitter.js.map +1 -0
- package/dist/error-runtime.d.ts +17 -0
- package/dist/error-runtime.d.ts.map +1 -0
- package/dist/error-runtime.js +109 -0
- package/dist/error-runtime.js.map +1 -0
- package/dist/extension.d.ts +373 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +25 -0
- package/dist/extension.js.map +1 -0
- package/dist/formatter.d.ts +13 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +1440 -0
- package/dist/formatter.js.map +1 -0
- package/dist/framework-host.d.ts +87 -0
- package/dist/framework-host.d.ts.map +1 -0
- package/dist/framework-host.js +2 -0
- package/dist/framework-host.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +932 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolated-string.d.ts +47 -0
- package/dist/interpolated-string.d.ts.map +1 -0
- package/dist/interpolated-string.js +344 -0
- package/dist/interpolated-string.js.map +1 -0
- package/dist/json-runtime.d.ts +2 -0
- package/dist/json-runtime.d.ts.map +1 -0
- package/dist/json-runtime.js +193 -0
- package/dist/json-runtime.js.map +1 -0
- package/dist/language-guidance.d.ts +31 -0
- package/dist/language-guidance.d.ts.map +1 -0
- package/dist/language-guidance.js +187 -0
- package/dist/language-guidance.js.map +1 -0
- package/dist/lexer.d.ts +85 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +1004 -0
- package/dist/lexer.js.map +1 -0
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -0
- package/dist/mechanical-fix.d.ts +26 -0
- package/dist/mechanical-fix.d.ts.map +1 -0
- package/dist/mechanical-fix.js +61 -0
- package/dist/mechanical-fix.js.map +1 -0
- package/dist/narrowing-runtime.d.ts +5 -0
- package/dist/narrowing-runtime.d.ts.map +1 -0
- package/dist/narrowing-runtime.js +27 -0
- package/dist/narrowing-runtime.js.map +1 -0
- package/dist/number-runtime.d.ts +3 -0
- package/dist/number-runtime.d.ts.map +1 -0
- package/dist/number-runtime.js +37 -0
- package/dist/number-runtime.js.map +1 -0
- package/dist/parser.d.ts +245 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +3582 -0
- package/dist/parser.js.map +1 -0
- package/dist/primitive-runtime.d.ts +4 -0
- package/dist/primitive-runtime.d.ts.map +1 -0
- package/dist/primitive-runtime.js +39 -0
- package/dist/primitive-runtime.js.map +1 -0
- package/dist/promise-runtime.d.ts +6 -0
- package/dist/promise-runtime.d.ts.map +1 -0
- package/dist/promise-runtime.js +86 -0
- package/dist/promise-runtime.js.map +1 -0
- package/dist/reactive-bridge-runtime.d.ts +7 -0
- package/dist/reactive-bridge-runtime.d.ts.map +1 -0
- package/dist/reactive-bridge-runtime.js +112 -0
- package/dist/reactive-bridge-runtime.js.map +1 -0
- package/dist/runtime-abi.d.ts +24 -0
- package/dist/runtime-abi.d.ts.map +1 -0
- package/dist/runtime-abi.js +24 -0
- package/dist/runtime-abi.js.map +1 -0
- package/dist/semantic.d.ts +142 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +845 -0
- package/dist/semantic.js.map +1 -0
- package/dist/source-names.d.ts +32 -0
- package/dist/source-names.d.ts.map +1 -0
- package/dist/source-names.js +101 -0
- package/dist/source-names.js.map +1 -0
- package/dist/source.d.ts +19 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +64 -0
- package/dist/source.js.map +1 -0
- package/dist/text-runtime.d.ts +2 -0
- package/dist/text-runtime.d.ts.map +1 -0
- package/dist/text-runtime.js +285 -0
- package/dist/text-runtime.js.map +1 -0
- package/dist/token.d.ts +19 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +51 -0
- package/dist/token.js.map +1 -0
- package/dist/type-registry-runtime.d.ts +7 -0
- package/dist/type-registry-runtime.d.ts.map +1 -0
- package/dist/type-registry-runtime.js +51 -0
- package/dist/type-registry-runtime.js.map +1 -0
- package/dist/type-validation-runtime.d.ts +17 -0
- package/dist/type-validation-runtime.d.ts.map +1 -0
- package/dist/type-validation-runtime.js +150 -0
- package/dist/type-validation-runtime.js.map +1 -0
- package/dist/types.d.ts +356 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1170 -0
- package/dist/types.js.map +1 -0
- package/dist/utf8-runtime.d.ts +7 -0
- package/dist/utf8-runtime.d.ts.map +1 -0
- package/dist/utf8-runtime.js +43 -0
- package/dist/utf8-runtime.js.map +1 -0
- 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
|