Skip to content

Commit 16f4e8e

Browse files
SevInfclaude
andcommitted
feat(psl-parser): add lossless red/green syntax tree and typed AST wrappers
Implements a Roslyn/rust-analyzer style red/green tree infrastructure for PSL: - Green layer: immutable GreenToken/GreenNode with GreenNodeBuilder - Red layer: SyntaxNode with parent pointers, offsets, and navigation - Typed AST wrappers for all PSL constructs (documents, models, enums, fields, attributes, expressions) - Exported via new `@prisma-next/psl-parser/syntax` subpath Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 16977ab commit 16f4e8e

17 files changed

Lines changed: 1843 additions & 0 deletions

File tree

packages/1-framework/2-authoring/psl-parser/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
".": "./dist/index.mjs",
3333
"./parser": "./dist/parser.mjs",
3434
"./tokenizer": "./dist/tokenizer.mjs",
35+
"./syntax": "./dist/syntax.mjs",
3536
"./types": "./dist/types.mjs",
3637
"./package.json": "./package.json"
3738
},
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export {
2+
AttributeArgListAst,
3+
FieldAttributeAst,
4+
ModelAttributeAst,
5+
} from '../syntax/ast/attributes';
6+
export {
7+
BlockDeclarationAst,
8+
DocumentAst,
9+
EnumDeclarationAst,
10+
EnumValueDeclarationAst,
11+
FieldDeclarationAst,
12+
KeyValuePairAst,
13+
ModelDeclarationAst,
14+
NamedTypeDeclarationAst,
15+
TypesBlockAst,
16+
} from '../syntax/ast/declarations';
17+
export type { ExpressionAst } from '../syntax/ast/expressions';
18+
export {
19+
ArrayLiteralAst,
20+
AttributeArgAst,
21+
BooleanLiteralExprAst,
22+
castExpression,
23+
FunctionCallAst,
24+
NumberLiteralExprAst,
25+
StringLiteralExprAst,
26+
} from '../syntax/ast/expressions';
27+
// AST wrappers
28+
export { IdentifierAst } from '../syntax/ast/identifier';
29+
export { TypeAnnotationAst } from '../syntax/ast/type-annotation';
30+
export type { AstNode } from '../syntax/ast-helpers';
31+
export { filterChildren, findChildToken, findFirstChild } from '../syntax/ast-helpers';
32+
export type { GreenElement, GreenNode, GreenToken } from '../syntax/green';
33+
export { greenNode, greenToken } from '../syntax/green';
34+
export { GreenNodeBuilder } from '../syntax/green-builder';
35+
// Red layer
36+
export type { SyntaxElement } from '../syntax/red';
37+
export { createSyntaxTree, SyntaxNode } from '../syntax/red';
38+
export type { SyntaxKind } from '../syntax/syntax-kind';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Token, TokenKind } from '../tokenizer';
2+
import { SyntaxNode } from './red';
3+
4+
export interface AstNode {
5+
readonly syntax: SyntaxNode;
6+
}
7+
8+
export function findChildToken(node: SyntaxNode, kind: TokenKind): Token | undefined {
9+
for (const child of node.children()) {
10+
if (!(child instanceof SyntaxNode) && child.kind === kind) {
11+
return child;
12+
}
13+
}
14+
return undefined;
15+
}
16+
17+
export function findFirstChild<T>(
18+
node: SyntaxNode,
19+
cast: (node: SyntaxNode) => T | undefined,
20+
): T | undefined {
21+
for (const child of node.childNodes()) {
22+
const result = cast(child);
23+
if (result !== undefined) return result;
24+
}
25+
return undefined;
26+
}
27+
28+
export function* filterChildren<T>(
29+
node: SyntaxNode,
30+
cast: (node: SyntaxNode) => T | undefined,
31+
): Iterable<T> {
32+
for (const child of node.childNodes()) {
33+
const result = cast(child);
34+
if (result !== undefined) yield result;
35+
}
36+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { Token } from '../../tokenizer';
2+
import type { AstNode } from '../ast-helpers';
3+
import { filterChildren, findChildToken, findFirstChild } from '../ast-helpers';
4+
import type { SyntaxNode } from '../red';
5+
import { AttributeArgAst } from './expressions';
6+
import { IdentifierAst } from './identifier';
7+
8+
export class AttributeArgListAst implements AstNode {
9+
readonly syntax: SyntaxNode;
10+
11+
constructor(syntax: SyntaxNode) {
12+
this.syntax = syntax;
13+
}
14+
15+
lparen(): Token | undefined {
16+
return findChildToken(this.syntax, 'LParen');
17+
}
18+
19+
rparen(): Token | undefined {
20+
return findChildToken(this.syntax, 'RParen');
21+
}
22+
23+
*args(): Iterable<AttributeArgAst> {
24+
yield* filterChildren(this.syntax, AttributeArgAst.cast);
25+
}
26+
27+
static cast(node: SyntaxNode): AttributeArgListAst | undefined {
28+
return node.kind === 'AttributeArgList' ? new AttributeArgListAst(node) : undefined;
29+
}
30+
}
31+
32+
export class FieldAttributeAst implements AstNode {
33+
readonly syntax: SyntaxNode;
34+
35+
constructor(syntax: SyntaxNode) {
36+
this.syntax = syntax;
37+
}
38+
39+
at(): Token | undefined {
40+
return findChildToken(this.syntax, 'At');
41+
}
42+
43+
name(): IdentifierAst | undefined {
44+
if (this.dot()) {
45+
let count = 0;
46+
for (const child of this.syntax.childNodes()) {
47+
if (child.kind === 'Identifier') {
48+
count++;
49+
if (count === 2) return new IdentifierAst(child);
50+
}
51+
}
52+
return undefined;
53+
}
54+
return findFirstChild(this.syntax, IdentifierAst.cast);
55+
}
56+
57+
dot(): Token | undefined {
58+
return findChildToken(this.syntax, 'Dot');
59+
}
60+
61+
namespaceName(): IdentifierAst | undefined {
62+
if (!this.dot()) return undefined;
63+
return findFirstChild(this.syntax, IdentifierAst.cast);
64+
}
65+
66+
argList(): AttributeArgListAst | undefined {
67+
return findFirstChild(this.syntax, AttributeArgListAst.cast);
68+
}
69+
70+
static cast(node: SyntaxNode): FieldAttributeAst | undefined {
71+
return node.kind === 'FieldAttribute' ? new FieldAttributeAst(node) : undefined;
72+
}
73+
}
74+
75+
export class ModelAttributeAst implements AstNode {
76+
readonly syntax: SyntaxNode;
77+
78+
constructor(syntax: SyntaxNode) {
79+
this.syntax = syntax;
80+
}
81+
82+
doubleAt(): Token | undefined {
83+
return findChildToken(this.syntax, 'DoubleAt');
84+
}
85+
86+
name(): IdentifierAst | undefined {
87+
return findFirstChild(this.syntax, IdentifierAst.cast);
88+
}
89+
90+
argList(): AttributeArgListAst | undefined {
91+
return findFirstChild(this.syntax, AttributeArgListAst.cast);
92+
}
93+
94+
static cast(node: SyntaxNode): ModelAttributeAst | undefined {
95+
return node.kind === 'ModelAttribute' ? new ModelAttributeAst(node) : undefined;
96+
}
97+
}

0 commit comments

Comments
 (0)