@prisma-next/psl-parser 0.13.0 → 0.14.0
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/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{parser-Cw_zV0M5.mjs → parser-CaplKvRs.mjs} +110 -141
- package/dist/parser-CaplKvRs.mjs.map +1 -0
- package/dist/parser.mjs +1 -1
- package/dist/syntax.d.mts +115 -40
- package/dist/syntax.d.mts.map +1 -1
- package/dist/syntax.mjs +815 -105
- package/dist/syntax.mjs.map +1 -1
- package/dist/tokenizer-1hAHZzmp.mjs +228 -0
- package/dist/tokenizer-1hAHZzmp.mjs.map +1 -0
- package/dist/tokenizer.mjs +1 -190
- package/package.json +5 -5
- package/src/exports/index.ts +13 -2
- package/src/exports/syntax.ts +9 -4
- package/src/parse.ts +742 -0
- package/src/parser.ts +125 -196
- package/src/source-file.ts +89 -0
- package/src/syntax/ast/attributes.ts +5 -24
- package/src/syntax/ast/declarations.ts +14 -67
- package/src/syntax/ast/expressions.ts +187 -19
- package/src/syntax/ast/identifier.ts +4 -0
- package/src/syntax/ast/qualified-name.ts +87 -0
- package/src/syntax/ast/type-annotation.ts +11 -41
- package/src/syntax/ast-helpers.ts +12 -0
- package/src/syntax/syntax-kind.ts +8 -4
- package/src/tokenizer.ts +47 -7
- package/dist/parser-Cw_zV0M5.mjs.map +0 -1
- package/dist/tokenizer.mjs.map +0 -1
package/src/parse.ts
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
import type { PslDiagnosticCode } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { UNSPECIFIED_PSL_NAMESPACE_ID } from '@prisma-next/framework-components/psl-ast';
|
|
3
|
+
import { type Range, SourceFile } from './source-file';
|
|
4
|
+
import { DocumentAst } from './syntax/ast/declarations';
|
|
5
|
+
import type { GreenNode } from './syntax/green';
|
|
6
|
+
import { GreenNodeBuilder } from './syntax/green-builder';
|
|
7
|
+
import { createSyntaxTree } from './syntax/red';
|
|
8
|
+
import type { SyntaxKind } from './syntax/syntax-kind';
|
|
9
|
+
import { isTerminatedStringLiteral, type Token, Tokenizer, type TokenKind } from './tokenizer';
|
|
10
|
+
|
|
11
|
+
export interface ParseDiagnostic {
|
|
12
|
+
readonly code: PslDiagnosticCode;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly range: Range;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ParseResult {
|
|
18
|
+
readonly document: DocumentAst;
|
|
19
|
+
readonly diagnostics: readonly ParseDiagnostic[];
|
|
20
|
+
readonly sourceFile: SourceFile;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TRIVIA_KINDS: ReadonlySet<TokenKind> = new Set<TokenKind>([
|
|
24
|
+
'Whitespace',
|
|
25
|
+
'Newline',
|
|
26
|
+
'Comment',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The source span of a token, captured eagerly so it stays valid after the
|
|
31
|
+
* cursor advances past the token it points at.
|
|
32
|
+
*/
|
|
33
|
+
export interface DiagnosticMark {
|
|
34
|
+
readonly offset: number;
|
|
35
|
+
readonly length: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The fault-tolerant parser substrate the grammars drive. Trivia is flushed
|
|
40
|
+
* into the enclosing open node, so every child node spans exactly its first
|
|
41
|
+
* through last significant token.
|
|
42
|
+
*/
|
|
43
|
+
export class Cursor {
|
|
44
|
+
readonly #tokenizer: Tokenizer;
|
|
45
|
+
readonly #sourceFile: SourceFile;
|
|
46
|
+
readonly #builder = new GreenNodeBuilder();
|
|
47
|
+
readonly #diagnostics: ParseDiagnostic[] = [];
|
|
48
|
+
#offset = 0;
|
|
49
|
+
#depth = 0;
|
|
50
|
+
|
|
51
|
+
constructor(source: string) {
|
|
52
|
+
this.#tokenizer = new Tokenizer(source);
|
|
53
|
+
this.#sourceFile = new SourceFile(source);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get diagnostics(): readonly ParseDiagnostic[] {
|
|
57
|
+
return this.#diagnostics;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get sourceFile(): SourceFile {
|
|
61
|
+
return this.#sourceFile;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
peekKind(ahead = 0): TokenKind {
|
|
65
|
+
return this.peekToken(ahead).kind;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
peekToken(ahead = 0): Token {
|
|
69
|
+
let rawIndex = 0;
|
|
70
|
+
let remaining = ahead;
|
|
71
|
+
for (;;) {
|
|
72
|
+
const token = this.#tokenizer.peek(rawIndex);
|
|
73
|
+
if (token.kind === 'Eof') return token;
|
|
74
|
+
if (TRIVIA_KINDS.has(token.kind)) {
|
|
75
|
+
rawIndex++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (remaining === 0) return token;
|
|
79
|
+
remaining--;
|
|
80
|
+
rawIndex++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Span of the significant token `lookahead` positions ahead (`mark(0)` = the next). */
|
|
85
|
+
mark(lookahead = 0): DiagnosticMark {
|
|
86
|
+
let rawIndex = 0;
|
|
87
|
+
let offset = this.#offset;
|
|
88
|
+
let remaining = lookahead;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const token = this.#tokenizer.peek(rawIndex);
|
|
91
|
+
if (token.kind === 'Eof') {
|
|
92
|
+
return { offset, length: token.text.length };
|
|
93
|
+
}
|
|
94
|
+
if (!TRIVIA_KINDS.has(token.kind) && remaining === 0) {
|
|
95
|
+
return { offset, length: token.text.length };
|
|
96
|
+
}
|
|
97
|
+
if (!TRIVIA_KINDS.has(token.kind)) {
|
|
98
|
+
remaining--;
|
|
99
|
+
}
|
|
100
|
+
offset += token.text.length;
|
|
101
|
+
rawIndex++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Zero-width mark just past the last consumed significant token — anchors an
|
|
107
|
+
* "expected here" diagnostic, e.g. the `{` missing after a declaration's name.
|
|
108
|
+
*/
|
|
109
|
+
markAfterLastToken(): DiagnosticMark {
|
|
110
|
+
return { offset: this.#offset, length: 0 };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
startNode(kind: SyntaxKind): void {
|
|
114
|
+
if (this.#depth > 0) {
|
|
115
|
+
this.flushTrivia();
|
|
116
|
+
}
|
|
117
|
+
this.#builder.startNode(kind);
|
|
118
|
+
this.#depth++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
finishNode(): GreenNode {
|
|
122
|
+
this.#depth--;
|
|
123
|
+
return this.#builder.finishNode();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
bump(): Token {
|
|
127
|
+
this.flushTrivia();
|
|
128
|
+
const token = this.#tokenizer.peek();
|
|
129
|
+
if (token.kind === 'Eof') return token;
|
|
130
|
+
this.#builder.token(token.kind, token.text);
|
|
131
|
+
this.#advance();
|
|
132
|
+
return token;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
recoverToSyncPoint(): void {
|
|
136
|
+
for (;;) {
|
|
137
|
+
const token = this.#tokenizer.peek();
|
|
138
|
+
if (token.kind === 'Eof' || token.kind === 'Newline' || token.kind === 'RBrace') {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.#builder.token(token.kind, token.text);
|
|
142
|
+
this.#advance();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
flushTrivia(): void {
|
|
147
|
+
for (;;) {
|
|
148
|
+
const token = this.#tokenizer.peek();
|
|
149
|
+
if (!TRIVIA_KINDS.has(token.kind)) return;
|
|
150
|
+
this.#builder.token(token.kind, token.text);
|
|
151
|
+
this.#advance();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
diagnostic(code: PslDiagnosticCode, message: string, mark: DiagnosticMark): void {
|
|
156
|
+
const start = mark.offset;
|
|
157
|
+
const end = start + mark.length;
|
|
158
|
+
this.#diagnostics.push({
|
|
159
|
+
code,
|
|
160
|
+
message,
|
|
161
|
+
range: {
|
|
162
|
+
start: this.#sourceFile.positionAt(start),
|
|
163
|
+
end: this.#sourceFile.positionAt(end),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#advance(): void {
|
|
169
|
+
this.#offset += this.#tokenizer.next().text.length;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseIdentifier(cursor: Cursor): void {
|
|
174
|
+
cursor.startNode('Identifier');
|
|
175
|
+
cursor.bump();
|
|
176
|
+
cursor.finishNode();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Returns `undefined` when the next significant token does not start a
|
|
181
|
+
* recognised expression, leaving recovery to the caller.
|
|
182
|
+
*/
|
|
183
|
+
export function parseExpression(cursor: Cursor): GreenNode | undefined {
|
|
184
|
+
return (
|
|
185
|
+
parseStringLiteralExpr(cursor) ??
|
|
186
|
+
parseNumberLiteralExpr(cursor) ??
|
|
187
|
+
parseArrayLiteral(cursor) ??
|
|
188
|
+
parseObjectLiteralExpr(cursor) ??
|
|
189
|
+
parseFunctionCall(cursor) ??
|
|
190
|
+
parseBooleanLiteralExpr(cursor) ??
|
|
191
|
+
parseIdentifierExpr(cursor)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function parseStringLiteralExpr(cursor: Cursor): GreenNode | undefined {
|
|
196
|
+
if (cursor.peekKind() !== 'StringLiteral') return undefined;
|
|
197
|
+
const stringMark = cursor.mark();
|
|
198
|
+
const text = cursor.peekToken().text;
|
|
199
|
+
cursor.startNode('StringLiteralExpr');
|
|
200
|
+
cursor.bump();
|
|
201
|
+
if (!isTerminatedStringLiteral(text)) {
|
|
202
|
+
cursor.diagnostic('PSL_UNTERMINATED_STRING', 'Unterminated string literal', stringMark);
|
|
203
|
+
}
|
|
204
|
+
return cursor.finishNode();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function parseNumberLiteralExpr(cursor: Cursor): GreenNode | undefined {
|
|
208
|
+
if (cursor.peekKind() !== 'NumberLiteral') return undefined;
|
|
209
|
+
cursor.startNode('NumberLiteralExpr');
|
|
210
|
+
cursor.bump();
|
|
211
|
+
return cursor.finishNode();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Parses a namespace-qualified name `[space ':']? Ident ('.' Ident)*`. The
|
|
216
|
+
* caller guarantees a leading `Ident`.
|
|
217
|
+
*
|
|
218
|
+
* Parsing the whole chain up front lets a position decide
|
|
219
|
+
* constructor-vs-reference by peeking exactly one token for `(`, with no scan of
|
|
220
|
+
* the dotted chain's length.
|
|
221
|
+
*/
|
|
222
|
+
export function parseQualifiedName(cursor: Cursor): void {
|
|
223
|
+
cursor.startNode('QualifiedName');
|
|
224
|
+
parseIdentifier(cursor); // first segment: the space, namespace, or bare name
|
|
225
|
+
parseQualifiedSegments(cursor, 'Colon');
|
|
226
|
+
parseQualifiedSegments(cursor, 'Dot');
|
|
227
|
+
cursor.finishNode();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A well-formed name carries at most one colon space and one dot namespace, so
|
|
232
|
+
* each separator past the first of its kind reports `PSL_INVALID_QUALIFIED_NAME`.
|
|
233
|
+
* The separator is consumed regardless, keeping the lossless round-trip intact.
|
|
234
|
+
*/
|
|
235
|
+
function parseQualifiedSegments(cursor: Cursor, separator: 'Colon' | 'Dot'): void {
|
|
236
|
+
let seen = 0;
|
|
237
|
+
while (cursor.peekKind() === separator) {
|
|
238
|
+
seen++;
|
|
239
|
+
const separatorMark = cursor.mark();
|
|
240
|
+
cursor.bump();
|
|
241
|
+
if (seen > 1) {
|
|
242
|
+
cursor.diagnostic(
|
|
243
|
+
'PSL_INVALID_QUALIFIED_NAME',
|
|
244
|
+
'Qualified name has too many segments',
|
|
245
|
+
separatorMark,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (cursor.peekKind() === 'Ident') {
|
|
249
|
+
parseIdentifier(cursor);
|
|
250
|
+
} else {
|
|
251
|
+
cursor.diagnostic(
|
|
252
|
+
'PSL_INVALID_QUALIFIED_NAME',
|
|
253
|
+
'Qualified name is missing a name after the separator',
|
|
254
|
+
cursor.mark(),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Ordering among the `Ident`-leading alternatives is load-bearing: the
|
|
261
|
+
// `LParen`/`Dot` lookahead of `parseCall` must win before the boolean check, so
|
|
262
|
+
// `true(` stays a function call named `true` rather than a boolean literal.
|
|
263
|
+
export function parseBooleanLiteralExpr(cursor: Cursor): GreenNode | undefined {
|
|
264
|
+
if (cursor.peekKind() !== 'Ident') return undefined;
|
|
265
|
+
const text = cursor.peekToken().text;
|
|
266
|
+
if (text !== 'true' && text !== 'false') return undefined;
|
|
267
|
+
cursor.startNode('BooleanLiteralExpr');
|
|
268
|
+
cursor.bump();
|
|
269
|
+
return cursor.finishNode();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function parseIdentifierExpr(cursor: Cursor): GreenNode | undefined {
|
|
273
|
+
if (cursor.peekKind() !== 'Ident') return undefined;
|
|
274
|
+
cursor.startNode('Identifier');
|
|
275
|
+
cursor.bump();
|
|
276
|
+
return cursor.finishNode();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function parseArrayLiteral(cursor: Cursor): GreenNode | undefined {
|
|
280
|
+
if (cursor.peekKind() !== 'LBracket') return undefined;
|
|
281
|
+
cursor.startNode('ArrayLiteral');
|
|
282
|
+
cursor.bump();
|
|
283
|
+
while (cursor.peekKind() !== 'RBracket' && cursor.peekKind() !== 'Eof') {
|
|
284
|
+
const element = parseExpression(cursor);
|
|
285
|
+
if (!element) break;
|
|
286
|
+
if (cursor.peekKind() === 'Comma') {
|
|
287
|
+
cursor.bump();
|
|
288
|
+
} else {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (cursor.peekKind() === 'RBracket') {
|
|
293
|
+
cursor.bump();
|
|
294
|
+
}
|
|
295
|
+
return cursor.finishNode();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function parseObjectLiteralExpr(cursor: Cursor): GreenNode | undefined {
|
|
299
|
+
if (cursor.peekKind() !== 'LBrace') return undefined;
|
|
300
|
+
const braceMark = cursor.mark();
|
|
301
|
+
cursor.startNode('ObjectLiteralExpr');
|
|
302
|
+
cursor.bump();
|
|
303
|
+
while (cursor.peekKind() !== 'RBrace' && cursor.peekKind() !== 'Eof') {
|
|
304
|
+
parseObjectField(cursor);
|
|
305
|
+
if (cursor.peekKind() === 'Comma') {
|
|
306
|
+
cursor.bump();
|
|
307
|
+
} else if (cursor.peekKind() === 'Ident') {
|
|
308
|
+
// A following identifier key with no comma re-enters the loop; the next
|
|
309
|
+
// parseObjectField consumes ≥1 token, so progress is guaranteed.
|
|
310
|
+
cursor.diagnostic(
|
|
311
|
+
'PSL_INVALID_OBJECT_LITERAL',
|
|
312
|
+
'Expected "," between object-literal fields',
|
|
313
|
+
cursor.markAfterLastToken(),
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (cursor.peekKind() === 'RBrace') {
|
|
320
|
+
cursor.bump();
|
|
321
|
+
} else {
|
|
322
|
+
cursor.diagnostic('PSL_INVALID_OBJECT_LITERAL', 'Unterminated object literal', braceMark);
|
|
323
|
+
}
|
|
324
|
+
return cursor.finishNode();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function parseObjectField(cursor: Cursor): GreenNode {
|
|
328
|
+
cursor.startNode('ObjectField');
|
|
329
|
+
const keyMark = cursor.mark();
|
|
330
|
+
const keyText = cursor.peekToken().text;
|
|
331
|
+
if (cursor.peekKind() === 'Ident') {
|
|
332
|
+
parseIdentifier(cursor);
|
|
333
|
+
} else if (cursor.peekKind() === 'StringLiteral') {
|
|
334
|
+
// A string-literal key (e.g. `{ "length": 35 }`) is accepted; its logical
|
|
335
|
+
// name is the unquoted string.
|
|
336
|
+
parseStringLiteralExpr(cursor);
|
|
337
|
+
}
|
|
338
|
+
if (cursor.peekKind() === 'Colon') {
|
|
339
|
+
cursor.bump(); // Colon
|
|
340
|
+
const value = parseExpression(cursor);
|
|
341
|
+
if (!value) {
|
|
342
|
+
cursor.diagnostic('PSL_INVALID_OBJECT_LITERAL', 'Expected a value after ":"', cursor.mark());
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
cursor.diagnostic('PSL_INVALID_OBJECT_LITERAL', `Expected ":" after "${keyText}"`, keyMark);
|
|
346
|
+
const followsWithKey = cursor.peekKind() === 'Ident' && cursor.peekKind(1) === 'Colon';
|
|
347
|
+
if (!followsWithKey) {
|
|
348
|
+
parseExpression(cursor); // best-effort: consume a value if one follows
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return cursor.finishNode();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Whether the next tokens open a call: a bare `Ident(` or a namespace-qualified
|
|
356
|
+
* `Ident.Ident(`. The lookahead is deliberately bounded so a bare dotted
|
|
357
|
+
* reference like `a.b` is not mistaken for a call, rather than scanning an
|
|
358
|
+
* unbounded dotted chain ahead to find the paren.
|
|
359
|
+
*/
|
|
360
|
+
function isCallAhead(cursor: Cursor): boolean {
|
|
361
|
+
if (cursor.peekKind() !== 'Ident') return false;
|
|
362
|
+
if (cursor.peekKind(1) === 'LParen') return true;
|
|
363
|
+
return (
|
|
364
|
+
cursor.peekKind(1) === 'Dot' &&
|
|
365
|
+
cursor.peekKind(2) === 'Ident' &&
|
|
366
|
+
cursor.peekKind(3) === 'LParen'
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Parses a function/constructor call — bare `autoincrement()` or qualified
|
|
372
|
+
* `temporal.updatedAt()`. Returns `undefined` unless {@link isCallAhead}
|
|
373
|
+
* confirms a trailing `(`, so the `parseExpression` chain falls through to the
|
|
374
|
+
* boolean and bare-identifier forms.
|
|
375
|
+
*/
|
|
376
|
+
export function parseFunctionCall(cursor: Cursor): GreenNode | undefined {
|
|
377
|
+
if (!isCallAhead(cursor)) return undefined;
|
|
378
|
+
cursor.startNode('FunctionCall');
|
|
379
|
+
parseQualifiedName(cursor);
|
|
380
|
+
if (cursor.peekKind() === 'LParen') {
|
|
381
|
+
parseParenArgs(cursor);
|
|
382
|
+
}
|
|
383
|
+
return cursor.finishNode();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Parses a parenthesised, comma-separated `AttributeArg` list into the currently open node. */
|
|
387
|
+
function parseParenArgs(cursor: Cursor): void {
|
|
388
|
+
cursor.bump();
|
|
389
|
+
while (cursor.peekKind() !== 'RParen' && cursor.peekKind() !== 'Eof') {
|
|
390
|
+
parseAttributeArg(cursor);
|
|
391
|
+
if (cursor.peekKind() === 'Comma') {
|
|
392
|
+
cursor.bump();
|
|
393
|
+
} else {
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (cursor.peekKind() === 'RParen') {
|
|
398
|
+
cursor.bump();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function parseAttributeArg(cursor: Cursor): GreenNode {
|
|
403
|
+
cursor.startNode('AttributeArg');
|
|
404
|
+
if (cursor.peekKind() === 'Ident' && cursor.peekKind(1) === 'Colon') {
|
|
405
|
+
parseIdentifier(cursor);
|
|
406
|
+
cursor.bump();
|
|
407
|
+
}
|
|
408
|
+
parseArgValue(cursor);
|
|
409
|
+
return cursor.finishNode();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function parseArgValue(cursor: Cursor): void {
|
|
413
|
+
parseExpression(cursor);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function parseAttributeArgList(cursor: Cursor): GreenNode {
|
|
417
|
+
cursor.startNode('AttributeArgList');
|
|
418
|
+
parseParenArgs(cursor);
|
|
419
|
+
return cursor.finishNode();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function parseAttribute(cursor: Cursor): GreenNode {
|
|
423
|
+
const isBlockAttribute = cursor.peekKind() === 'DoubleAt';
|
|
424
|
+
const attributeMark = cursor.mark();
|
|
425
|
+
cursor.startNode(isBlockAttribute ? 'ModelAttribute' : 'FieldAttribute');
|
|
426
|
+
cursor.bump();
|
|
427
|
+
if (cursor.peekKind() === 'Ident') {
|
|
428
|
+
parseQualifiedName(cursor);
|
|
429
|
+
} else {
|
|
430
|
+
cursor.diagnostic('PSL_INVALID_ATTRIBUTE_SYNTAX', 'Attribute name expected', attributeMark);
|
|
431
|
+
}
|
|
432
|
+
if (cursor.peekKind() === 'LParen') {
|
|
433
|
+
parseAttributeArgList(cursor);
|
|
434
|
+
}
|
|
435
|
+
return cursor.finishNode();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** A type annotation: `QualifiedName (argList)? ([])? (?)?`, e.g. `pgvector.Vector(1536)[]?`. */
|
|
439
|
+
export function parseTypeAnnotation(cursor: Cursor): GreenNode {
|
|
440
|
+
cursor.startNode('TypeAnnotation');
|
|
441
|
+
if (cursor.peekKind() === 'Ident') {
|
|
442
|
+
parseQualifiedName(cursor);
|
|
443
|
+
if (cursor.peekKind() === 'LParen') {
|
|
444
|
+
parseAttributeArgList(cursor);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (cursor.peekKind() === 'LBracket') {
|
|
448
|
+
cursor.bump();
|
|
449
|
+
if (cursor.peekKind() === 'RBracket') {
|
|
450
|
+
cursor.bump();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (cursor.peekKind() === 'Question') {
|
|
454
|
+
cursor.bump();
|
|
455
|
+
}
|
|
456
|
+
return cursor.finishNode();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
type MemberParser = (cursor: Cursor) => void;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Parses a full PSL document. Never throws — malformed input yields diagnostics
|
|
463
|
+
* and a recovered tree, not an exception.
|
|
464
|
+
*/
|
|
465
|
+
export function parse(source: string): ParseResult {
|
|
466
|
+
const cursor = new Cursor(source);
|
|
467
|
+
const green = parseDocument(cursor);
|
|
468
|
+
const root = createSyntaxTree(green);
|
|
469
|
+
const document = DocumentAst.cast(root) ?? new DocumentAst(root);
|
|
470
|
+
return { document, diagnostics: cursor.diagnostics, sourceFile: cursor.sourceFile };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function parseDocument(cursor: Cursor): GreenNode {
|
|
474
|
+
cursor.startNode('Document');
|
|
475
|
+
while (cursor.peekKind() !== 'Eof') {
|
|
476
|
+
parseDeclaration(cursor, false);
|
|
477
|
+
}
|
|
478
|
+
cursor.flushTrivia(); // attach trailing trivia so the round-trip stays lossless
|
|
479
|
+
return cursor.finishNode();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const RESERVED_BLOCK_KEYWORDS: ReadonlySet<string> = new Set([
|
|
483
|
+
'model',
|
|
484
|
+
'namespace',
|
|
485
|
+
'type',
|
|
486
|
+
'types',
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
function keywordIs(cursor: Cursor, keyword: string): boolean {
|
|
490
|
+
return cursor.peekKind() === 'Ident' && cursor.peekToken().text === keyword;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Each alternative is a no-op on non-match, consuming nothing, so the
|
|
495
|
+
* forward-only cursor is never left half-consumed by a rejected alternative.
|
|
496
|
+
* Recovery runs via the `if (!node)` tail rather than as a `??` arm, because it
|
|
497
|
+
* appends raw tokens to the open parent instead of returning a child node.
|
|
498
|
+
*/
|
|
499
|
+
function parseDeclaration(cursor: Cursor, insideNamespace: boolean): void {
|
|
500
|
+
const name = cursor.peekKind(1) === 'Ident' ? cursor.peekToken(1).text : '';
|
|
501
|
+
if (insideNamespace && keywordIs(cursor, 'namespace')) {
|
|
502
|
+
cursor.diagnostic(
|
|
503
|
+
'PSL_INVALID_NAMESPACE_BLOCK',
|
|
504
|
+
`Recursive "namespace ${name}" block is not allowed; namespace blocks may not nest`,
|
|
505
|
+
cursor.mark(),
|
|
506
|
+
);
|
|
507
|
+
} else if (insideNamespace && keywordIs(cursor, 'types')) {
|
|
508
|
+
cursor.diagnostic(
|
|
509
|
+
'PSL_INVALID_NAMESPACE_BLOCK',
|
|
510
|
+
'`types` blocks must be declared at the document top level, not inside a namespace block',
|
|
511
|
+
cursor.mark(),
|
|
512
|
+
);
|
|
513
|
+
} else if (keywordIs(cursor, 'namespace') && name === UNSPECIFIED_PSL_NAMESPACE_ID) {
|
|
514
|
+
cursor.diagnostic(
|
|
515
|
+
'PSL_INVALID_NAMESPACE_BLOCK',
|
|
516
|
+
`Namespace name "${UNSPECIFIED_PSL_NAMESPACE_ID}" is reserved for the parser-synthesised bucket for top-level declarations`,
|
|
517
|
+
cursor.mark(1),
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const node =
|
|
522
|
+
parseModel(cursor) ??
|
|
523
|
+
parseNamespace(cursor) ??
|
|
524
|
+
parseCompositeType(cursor) ??
|
|
525
|
+
parseTypesBlock(cursor) ??
|
|
526
|
+
parseGenericBlock(cursor);
|
|
527
|
+
if (!node) {
|
|
528
|
+
parseUnsupportedTopLevel(cursor);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Reports only the first missing piece — a missing name suppresses the
|
|
534
|
+
* missing-brace diagnostic. `nameRequired` is false only for the `types` block.
|
|
535
|
+
*/
|
|
536
|
+
function parseBlock(
|
|
537
|
+
cursor: Cursor,
|
|
538
|
+
kind: SyntaxKind,
|
|
539
|
+
nameRequired: boolean,
|
|
540
|
+
parseMember: MemberParser,
|
|
541
|
+
): GreenNode {
|
|
542
|
+
const keyword = cursor.peekToken().text;
|
|
543
|
+
const keywordMark = cursor.mark();
|
|
544
|
+
cursor.startNode(kind);
|
|
545
|
+
cursor.bump();
|
|
546
|
+
const hasName = nameRequired && cursor.peekKind() === 'Ident';
|
|
547
|
+
if (hasName) {
|
|
548
|
+
parseIdentifier(cursor);
|
|
549
|
+
}
|
|
550
|
+
if (nameRequired && !hasName) {
|
|
551
|
+
cursor.diagnostic('PSL_INVALID_DECLARATION', `Expected a name after "${keyword}"`, keywordMark);
|
|
552
|
+
} else if (cursor.peekKind() !== 'LBrace') {
|
|
553
|
+
cursor.diagnostic(
|
|
554
|
+
'PSL_INVALID_DECLARATION',
|
|
555
|
+
`Expected "{" to open the "${keyword}" block`,
|
|
556
|
+
cursor.markAfterLastToken(),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
if (cursor.peekKind() === 'LBrace') {
|
|
560
|
+
parseBlockBody(cursor, parseMember);
|
|
561
|
+
} else {
|
|
562
|
+
cursor.recoverToSyncPoint();
|
|
563
|
+
}
|
|
564
|
+
return cursor.finishNode();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function parseModel(cursor: Cursor): GreenNode | undefined {
|
|
568
|
+
if (!keywordIs(cursor, 'model')) return undefined;
|
|
569
|
+
return parseBlock(cursor, 'ModelDeclaration', true, parseModelMember);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Excluding the reserved keywords keeps a malformed reserved block (e.g. `model
|
|
574
|
+
* {` with no name) routed to its dedicated parser. The generic keyword set is
|
|
575
|
+
* open, so a bare identifier with no brace (e.g. `oops`) is read as an unfinished
|
|
576
|
+
* custom declaration rather than unsupported content.
|
|
577
|
+
*/
|
|
578
|
+
export function parseGenericBlock(cursor: Cursor): GreenNode | undefined {
|
|
579
|
+
if (cursor.peekKind() !== 'Ident') return undefined;
|
|
580
|
+
const keyword = cursor.peekToken().text;
|
|
581
|
+
if (RESERVED_BLOCK_KEYWORDS.has(keyword)) return undefined;
|
|
582
|
+
const hasName = cursor.peekKind(1) === 'Ident' && cursor.peekKind(2) === 'LBrace';
|
|
583
|
+
cursor.startNode('GenericBlockDeclaration');
|
|
584
|
+
cursor.bump();
|
|
585
|
+
if (hasName) {
|
|
586
|
+
parseIdentifier(cursor);
|
|
587
|
+
}
|
|
588
|
+
if (cursor.peekKind() === 'LBrace') {
|
|
589
|
+
parseBlockBody(cursor, parseKeyValueMember);
|
|
590
|
+
} else {
|
|
591
|
+
cursor.diagnostic(
|
|
592
|
+
'PSL_INVALID_DECLARATION',
|
|
593
|
+
`Expected "{" to open the "${keyword}" block`,
|
|
594
|
+
cursor.markAfterLastToken(),
|
|
595
|
+
);
|
|
596
|
+
cursor.recoverToSyncPoint();
|
|
597
|
+
}
|
|
598
|
+
return cursor.finishNode();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function parseNamespace(cursor: Cursor): GreenNode | undefined {
|
|
602
|
+
if (!keywordIs(cursor, 'namespace')) return undefined;
|
|
603
|
+
return parseBlock(cursor, 'Namespace', true, (inner) => parseDeclaration(inner, true));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function parseCompositeType(cursor: Cursor): GreenNode | undefined {
|
|
607
|
+
if (!keywordIs(cursor, 'type')) return undefined;
|
|
608
|
+
return parseBlock(cursor, 'CompositeTypeDeclaration', true, parseModelMember);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** `types` (plural) is the no-name types block; the singular `type` is the composite type above. */
|
|
612
|
+
export function parseTypesBlock(cursor: Cursor): GreenNode | undefined {
|
|
613
|
+
if (!keywordIs(cursor, 'types')) return undefined;
|
|
614
|
+
return parseBlock(cursor, 'TypesBlock', false, parseNamedTypeMember);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Every `parseMember` consumes at least one significant token, so the loop always terminates. */
|
|
618
|
+
function parseBlockBody(cursor: Cursor, parseMember: MemberParser): void {
|
|
619
|
+
const braceMark = cursor.mark();
|
|
620
|
+
cursor.bump();
|
|
621
|
+
for (;;) {
|
|
622
|
+
const kind = cursor.peekKind();
|
|
623
|
+
if (kind === 'RBrace' || kind === 'Eof') break;
|
|
624
|
+
parseMember(cursor);
|
|
625
|
+
}
|
|
626
|
+
if (cursor.peekKind() === 'RBrace') {
|
|
627
|
+
cursor.bump();
|
|
628
|
+
} else {
|
|
629
|
+
cursor.diagnostic('PSL_UNTERMINATED_BLOCK', 'Unterminated block declaration', braceMark);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function parseUnsupportedTopLevel(cursor: Cursor): void {
|
|
634
|
+
const offending = cursor.peekToken().text;
|
|
635
|
+
const message =
|
|
636
|
+
cursor.peekKind(1) === 'LBrace'
|
|
637
|
+
? `Unsupported top-level block "${offending}"`
|
|
638
|
+
: `Unsupported top-level declaration "${offending}"`;
|
|
639
|
+
cursor.diagnostic('PSL_UNSUPPORTED_TOP_LEVEL_BLOCK', message, cursor.mark());
|
|
640
|
+
cursor.bump();
|
|
641
|
+
cursor.recoverToSyncPoint();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Matches a leading `@@` block attribute, a no-op otherwise. Single-`@`
|
|
646
|
+
* attributes belong to fields and are parsed inside `parseField`.
|
|
647
|
+
*/
|
|
648
|
+
export function parseBlockAttribute(cursor: Cursor): GreenNode | undefined {
|
|
649
|
+
if (cursor.peekKind() !== 'DoubleAt') return undefined;
|
|
650
|
+
return parseAttribute(cursor);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function parseModelMember(cursor: Cursor): void {
|
|
654
|
+
const node = parseBlockAttribute(cursor) ?? parseField(cursor);
|
|
655
|
+
if (!node) {
|
|
656
|
+
invalidMember(
|
|
657
|
+
cursor,
|
|
658
|
+
'PSL_INVALID_MODEL_MEMBER',
|
|
659
|
+
`Invalid model member declaration "${cursor.peekToken().text}"`,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function parseNamedTypeMember(cursor: Cursor): void {
|
|
665
|
+
const node = parseNamedType(cursor);
|
|
666
|
+
if (!node) {
|
|
667
|
+
invalidMember(
|
|
668
|
+
cursor,
|
|
669
|
+
'PSL_INVALID_TYPES_MEMBER',
|
|
670
|
+
`Invalid types declaration "${cursor.peekToken().text}"`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* A generic-block member is either a `@@`-block attribute or a `key = value`
|
|
677
|
+
* entry. The block-attribute alternative is purely syntactic — it does not judge
|
|
678
|
+
* whether the attribute is valid for the block's kind.
|
|
679
|
+
*/
|
|
680
|
+
function parseKeyValueMember(cursor: Cursor): void {
|
|
681
|
+
const node = parseBlockAttribute(cursor) ?? parseKeyValue(cursor);
|
|
682
|
+
if (!node) {
|
|
683
|
+
invalidMember(cursor, 'PSL_INVALID_EXTENSION_BLOCK_MEMBER', 'Invalid block entry');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function invalidMember(cursor: Cursor, code: PslDiagnosticCode, message: string): void {
|
|
688
|
+
cursor.diagnostic(code, message, cursor.mark());
|
|
689
|
+
cursor.bump(); // consume the offending token so the member loop makes progress
|
|
690
|
+
cursor.recoverToSyncPoint();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function parseField(cursor: Cursor): GreenNode | undefined {
|
|
694
|
+
if (cursor.peekKind() !== 'Ident') return undefined;
|
|
695
|
+
cursor.startNode('FieldDeclaration');
|
|
696
|
+
parseIdentifier(cursor);
|
|
697
|
+
parseTypeAnnotation(cursor);
|
|
698
|
+
while (cursor.peekKind() === 'At') {
|
|
699
|
+
parseAttribute(cursor);
|
|
700
|
+
}
|
|
701
|
+
return cursor.finishNode();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function parseNamedType(cursor: Cursor): GreenNode | undefined {
|
|
705
|
+
if (cursor.peekKind() !== 'Ident') return undefined;
|
|
706
|
+
cursor.startNode('NamedTypeDeclaration');
|
|
707
|
+
const nameMark = cursor.mark();
|
|
708
|
+
const nameText = cursor.peekToken().text;
|
|
709
|
+
parseIdentifier(cursor);
|
|
710
|
+
if (cursor.peekKind() === 'Equals') {
|
|
711
|
+
cursor.bump();
|
|
712
|
+
} else {
|
|
713
|
+
cursor.diagnostic('PSL_INVALID_TYPES_MEMBER', `Expected "=" after "${nameText}"`, nameMark);
|
|
714
|
+
}
|
|
715
|
+
parseTypeAnnotation(cursor);
|
|
716
|
+
while (cursor.peekKind() === 'At') {
|
|
717
|
+
parseAttribute(cursor);
|
|
718
|
+
}
|
|
719
|
+
return cursor.finishNode();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* A generic-block entry is either `key = value` or a bare `key` (committing a
|
|
724
|
+
* `KeyValuePair` carrying only the key). A `key =` with no following expression
|
|
725
|
+
* is flagged.
|
|
726
|
+
*/
|
|
727
|
+
export function parseKeyValue(cursor: Cursor): GreenNode | undefined {
|
|
728
|
+
if (cursor.peekKind() !== 'Ident') return undefined;
|
|
729
|
+
cursor.startNode('KeyValuePair');
|
|
730
|
+
parseIdentifier(cursor);
|
|
731
|
+
if (cursor.peekKind() === 'Equals') {
|
|
732
|
+
cursor.bump();
|
|
733
|
+
if (!parseExpression(cursor)) {
|
|
734
|
+
cursor.diagnostic(
|
|
735
|
+
'PSL_INVALID_EXTENSION_BLOCK_MEMBER',
|
|
736
|
+
'Expected a value after "="',
|
|
737
|
+
cursor.mark(),
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return cursor.finishNode();
|
|
742
|
+
}
|