Lexer & Parser¶
Le lexer et le parser transforment le texte .gmx brut en AST utilisable par le generator.
Lexer¶
Fichier : internal/compiler/lexer/lexer.go
Responsabilité¶
Transformer le texte source en stream de tokens.
Token Structure¶
type Token struct {
Type TokenType
Literal string
Pos Position
}
type Position struct {
Line int
Column int
}
Tokens Principaux¶
| Type | Literal | Usage |
|---|---|---|
MODEL |
"model" |
Déclaration model |
SERVICE |
"service" |
Déclaration service |
FUNC |
"func" |
Déclaration fonction |
IDENT |
variable | Identifiants |
STRING |
"..." |
Chaînes |
INT |
42 |
Nombres entiers |
FLOAT |
3.14 |
Nombres décimaux |
AT |
@ |
Annotations |
COLON |
: |
Séparateur type |
LBRACE |
{ |
Ouverture bloc |
RBRACE |
} |
Fermeture bloc |
RAW_GO |
script content | Contenu <script> |
RAW_TEMPLATE |
template content | Contenu <template> |
RAW_STYLE |
style content | Contenu <style> |
Traitement des Sections¶
Le lexer détecte <script>, <template>, <style> et retourne un token unique avec tout le contenu :
if strings.HasPrefix(l.input[l.position:], "<script>") {
content := l.readUntil("</script>")
return Token{Type: RAW_GO, Literal: content}
}
Avantage : Le parser principal n'a pas besoin de gérer la syntaxe HTML/CSS.
Parser¶
Fichier : internal/compiler/parser/parser.go
Responsabilité¶
Construire l'AST depuis le stream de tokens.
Structure¶
Entrée : ParseGMXFile¶
func (p *Parser) ParseGMXFile() *ast.GMXFile {
file := &ast.GMXFile{
Models: []*ast.ModelDecl{},
Services: []*ast.ServiceDecl{},
}
for !p.curTokenIs(token.EOF) {
switch p.curToken.Type {
case token.MODEL:
model := p.parseModelDecl()
file.Models = append(file.Models, model)
case token.SERVICE:
svc := p.parseServiceDecl()
file.Services = append(file.Services, svc)
case token.RAW_GO:
file.Script = p.parseScriptBlock()
case token.RAW_TEMPLATE:
file.Template = &ast.TemplateBlock{Source: p.curToken.Literal}
case token.RAW_STYLE:
file.Style = p.parseStyleBlock()
}
p.nextToken()
}
return file
}
Parsing Models¶
func (p *Parser) parseModelDecl() *ast.ModelDecl {
// model Task { ... }
p.expectPeek(token.IDENT) // "Task"
modelName := p.curToken.Literal
p.expectPeek(token.LBRACE)
p.nextToken()
fields := []*ast.FieldDecl{}
for !p.curTokenIs(token.RBRACE) {
field := p.parseFieldDecl()
fields = append(fields, field)
}
return &ast.ModelDecl{Name: modelName, Fields: fields}
}
Parsing Fields¶
func (p *Parser) parseFieldDecl() *ast.FieldDecl {
// title: string @min(3) @max(255)
fieldName := p.curToken.Literal
p.expectPeek(token.COLON)
p.nextToken()
fieldType := p.curToken.Literal
annotations := []*ast.Annotation{}
for p.curTokenIs(token.AT) {
ann := p.parseAnnotation()
annotations = append(annotations, ann)
}
return &ast.FieldDecl{
Name: fieldName,
Type: fieldType,
Annotations: annotations,
}
}
Parsing Annotations¶
func (p *Parser) parseAnnotation() *ast.Annotation {
// @min(3) ou @relation(references: [id])
p.expectPeek(token.IDENT)
annName := p.curToken.Literal
args := make(map[string]string)
if p.peekTokenIs(token.LPAREN) {
p.nextToken()
p.parseAnnotationArgs(args)
}
return &ast.Annotation{Name: annName, Args: args}
}
Error Recovery¶
Le parser utilise synchronize() pour continuer après une erreur :
func (p *Parser) synchronize() {
for !p.curTokenIs(token.EOF) {
switch p.curToken.Type {
case token.MODEL, token.SERVICE, token.RAW_GO, token.RAW_TEMPLATE:
return
}
if p.curTokenIs(token.RBRACE) {
p.nextToken()
return
}
p.nextToken()
}
}
Avantage : Affiche toutes les erreurs en une seule passe.
Script Parser¶
Fichier : internal/compiler/script/parser.go
Responsabilité¶
Parser le contenu GMX Script (TypeScript-inspired) en AST de fonctions.
Différence avec le Parser Principal¶
Le script parser utilise Pratt parsing pour gérer les expressions avec précédence :
const (
LOWEST
OR // ||
AND // &&
EQUALS // == !=
LESSGREATER // < > <= >=
SUM // + -
PRODUCT // * / %
UNARY // ! -
CALL // . ()
)
Parsing d'Expressions¶
func (p *Parser) parseExpression(precedence int) ast.Expression {
// Prefix parsing
prefix := p.prefixParseFns[p.curToken.Type]
if prefix == nil {
return nil
}
leftExp := prefix()
// Infix parsing (while precedence is lower)
for precedence < p.peekPrecedence() {
infix := p.infixParseFns[p.peekToken.Type]
if infix == nil {
return leftExp
}
p.nextToken()
leftExp = infix(leftExp)
}
return leftExp
}
Prefix Parsers¶
p.registerPrefix(token.IDENT, p.parseIdentifier)
p.registerPrefix(token.INT, p.parseIntLiteral)
p.registerPrefix(token.STRING, p.parseStringLiteral)
p.registerPrefix(token.TRY, p.parseTryExpression)
p.registerPrefix(token.RENDER, p.parseRenderExpression)
p.registerPrefix(token.ERROR, p.parseErrorExpression)
Infix Parsers¶
p.registerInfix(token.PLUS, p.parseBinaryExpression)
p.registerInfix(token.MINUS, p.parseBinaryExpression)
p.registerInfix(token.EQ, p.parseBinaryExpression)
p.registerInfix(token.AND, p.parseBinaryExpression)
p.registerInfix(token.DOT, p.parseMemberExpression)
p.registerInfix(token.LPAREN, p.parseCallExpression)
Tests¶
Lexer Tests¶
func TestLexer(t *testing.T) {
input := `model Task { id: uuid @pk }`
tests := []struct {
expectedType token.TokenType
expectedLiteral string
}{
{token.MODEL, "model"},
{token.IDENT, "Task"},
{token.LBRACE, "{"},
{token.IDENT, "id"},
{token.COLON, ":"},
{token.IDENT, "uuid"},
{token.AT, "@"},
{token.IDENT, "pk"},
{token.RBRACE, "}"},
}
l := lexer.New(input)
for _, tt := range tests {
tok := l.NextToken()
assert.Equal(t, tt.expectedType, tok.Type)
assert.Equal(t, tt.expectedLiteral, tok.Literal)
}
}
Parser Tests¶
func TestParseModel(t *testing.T) {
input := `
model Task {
id: uuid @pk @default(uuid_v4)
title: string @min(3) @max(255)
}
`
l := lexer.New(input)
p := parser.New(l)
file := p.ParseGMXFile()
assert.Len(t, file.Models, 1)
assert.Equal(t, "Task", file.Models[0].Name)
assert.Len(t, file.Models[0].Fields, 2)
}
Prochaines Étapes¶
- Generator — Utilisation de l'AST pour générer Go
- Script Transpiler — Transpilation du script AST
- Testing — Stratégie de test complète