Architecture¶
GMX est un compilateur multi-phase qui transforme des fichiers .gmx en code Go compilable. Voici l'architecture complète du pipeline de compilation.
Vue d'Ensemble¶
┌──────────┐ ┌───────┐ ┌────────┐ ┌───────────┐ ┌─────────┐
│ .gmx │─────▶│ Lexer │─────▶│ Parser │─────▶│ Generator │─────▶│ Go Code │
│ Source │ │ │ │ │ │ │ │ │
└──────────┘ └───────┘ └────────┘ └───────────┘ └─────────┘
│ │ │
▼ ▼ ▼
Tokens AST Transpiler
│ │
│ ▼
│ Script → Go
│
▼
script.Parse()
Pipeline de Compilation¶
Phase 1: Lexing¶
Fichier : internal/compiler/lexer/lexer.go
Responsabilité : Transformer le texte brut en tokens
input := `model Task { id: uuid @pk }`
lexer := lexer.New(input)
tokens := []
// TOKEN_MODEL "model"
// TOKEN_IDENT "Task"
// TOKEN_LBRACE "{"
// TOKEN_IDENT "id"
// TOKEN_COLON ":"
// TOKEN_IDENT "uuid"
// TOKEN_AT "@"
// TOKEN_IDENT "pk"
// TOKEN_RBRACE "}"
Tokens Spéciaux :
- RAW_GO — contenu de <script>...</script>
- RAW_TEMPLATE — contenu de <template>...</template>
- RAW_STYLE — contenu de <style>...</style>
Ces tokens contiennent le contenu brut sans parsing, pour déléguer aux phases suivantes.
Phase 2: Parsing¶
Fichier : internal/compiler/parser/parser.go
Responsabilité : Construire l'AST depuis les tokens
parser := parser.New(lexer)
file := parser.ParseGMXFile()
// file.Models = []*ast.ModelDecl
// file.Services = []*ast.ServiceDecl
// file.Script = *ast.ScriptBlock (avec Source brut)
// file.Template = *ast.TemplateBlock
// file.Style = *ast.StyleBlock
Structures Parsées :
- Models et fields
- Services et config
- Annotations (@pk, @min(3), @env("VAR"))
- Bloc Script (mais pas encore le contenu GMX Script)
Phase 2.5: Script Parsing¶
Fichier : internal/compiler/script/parser.go
Responsabilité : Parser le contenu GMX Script en AST de fonctions
Le parser principal appelle le script parser pour le contenu de <script> :
// Dans parser/parser.go
case token.RAW_GO:
source := p.curToken.Literal
funcs, parseErrors := script.Parse(source, lineOffset)
scriptBlock := &ast.ScriptBlock{
Source: source,
Funcs: funcs,
StartLine: lineOffset,
}
Résultat :
- ScriptBlock.Source — le code brut (fallback)
- ScriptBlock.Funcs — les fonctions parsées
Phase 3: Transpilation¶
Fichier : internal/compiler/script/transpiler.go
Responsabilité : Convertir les fonctions GMX Script en fonctions Go
// GMX Script
func toggleTask(id: uuid) error {
let task = try Task.find(id)
task.done = !task.done
try task.save()
return render(task)
}
// ↓↓↓ Transpilation ↓↓↓
// Go
func toggleTask(ctx *GMXContext, id string) error {
task, err := TaskFind(ctx.DB, id)
if err != nil {
return err
}
task.Done = !task.Done
if err := TaskSave(ctx.DB, task); err != nil {
return err
}
return renderFragment(ctx.Writer, "task", task)
}
Phase 4: Generation¶
Fichier : internal/compiler/generator/generator.go + modules
Responsabilité : Orchestrer la génération du fichier Go final
Le generator appelle différents sous-générateurs :
- Imports (
gen_imports.go) — détecte les dépendances - Helpers (
gen_helpers.go) — UUID, email validation, etc. - Models (
gen_models.go) — structs GORM + validation - Services (
gen_services.go) — config + interfaces - Script (via transpiler) — fonctions métier
- Handlers (
gen_handlers.go) — HTTP wrappers - Template (
gen_template.go) — template init + constantes - Main (
gen_main.go) — fonction main() complète
Code généré :
package main
import (...)
// ===== Helpers =====
func generateUUID() string { ... }
func isValidEmail(email string) bool { ... }
// ===== Models =====
type Task struct { ... }
func (t *Task) Validate() error { ... }
func (t *Task) BeforeCreate(tx *gorm.DB) error { ... }
// ===== Services =====
type DatabaseConfig struct { ... }
func initDatabase() *DatabaseConfig { ... }
// ===== Script (Transpiled) =====
type GMXContext struct { ... }
func TaskFind(db *gorm.DB, id string) (*Task, error) { ... }
func toggleTask(ctx *GMXContext, id string) error { ... }
// ===== Handlers =====
func handleToggleTask(w http.ResponseWriter, r *http.Request) { ... }
// ===== Template =====
var tmpl = template.Must(...)
const templateSource = `...`
// ===== Main =====
func main() {
db, err := gorm.Open(...)
db.AutoMigrate(&Task{})
http.HandleFunc("/", handleRoot)
http.HandleFunc("/toggleTask", handleToggleTask)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Structure des Packages¶
gmx/
├── cmd/
│ └── gmx/
│ └── main.go # CLI entry point
├── internal/
│ └── compiler/
│ ├── token/
│ │ ├── token.go # Token types et constantes
│ │ └── token_test.go
│ ├── lexer/
│ │ ├── lexer.go # Lexer principal
│ │ └── lexer_test.go
│ ├── ast/
│ │ ├── ast.go # Tous les types AST
│ │ └── ast_test.go
│ ├── parser/
│ │ ├── parser.go # Parser GMX principal
│ │ └── parser_test.go
│ ├── script/
│ │ ├── parser.go # Parser GMX Script
│ │ ├── transpiler.go # Transpileur Script → Go
│ │ ├── parser_test.go
│ │ └── transpiler_test.go
│ ├── generator/
│ │ ├── generator.go # Orchestrateur
│ │ ├── analysis.go # Analyse de l'AST
│ │ ├── gen_imports.go # Génération imports
│ │ ├── gen_helpers.go # Helpers (UUID, email, etc.)
│ │ ├── gen_models.go # Génération models GORM
│ │ ├── gen_services.go # Génération services
│ │ ├── gen_handlers.go # Génération HTTP handlers
│ │ ├── gen_template.go # Génération template setup
│ │ ├── gen_main.go # Génération main()
│ │ └── generator_test.go
│ ├── utils/
│ │ ├── utils.go # PascalCase, ReceiverName, etc.
│ │ └── utils_test.go
│ └── errors/
│ ├── errors.go # Error handling
│ └── errors_test.go
└── examples/
└── demo.gmx # Full feature showcase
Flow Détaillé¶
Entrée : cmd/gmx/main.go¶
func main() {
inputFile := os.Args[1]
data, _ := os.ReadFile(inputFile)
// 1. Lexing
l := lexer.New(string(data))
// 2. Parsing
p := parser.New(l)
file := p.ParseGMXFile()
// 3. Generation
gen := generator.New()
code, err := gen.Generate(file)
// 4. Write output
os.WriteFile("main.go", []byte(code), 0644)
}
Génération : generator/generator.go¶
func (g *Generator) Generate(file *ast.GMXFile) (string, error) {
var b strings.Builder
// 1. Package + imports
b.WriteString("package main\n\n")
b.WriteString(g.genImports(file))
// 2. Helpers
b.WriteString(g.genHelpers(file))
// 3. Models
b.WriteString(g.genModels(file.Models))
// 4. Services
b.WriteString(g.genServices(file.Services))
// 5. Script (transpilation)
if file.Script != nil && file.Script.Funcs != nil {
result := script.Transpile(file.Script, modelNames)
b.WriteString(result.GoCode)
b.WriteString(g.genScriptHandlers(file.Script))
}
// 6. Template
if file.Template != nil {
b.WriteString(g.genTemplateInit(routes))
b.WriteString(g.genTemplateConst(file))
}
// 7. Handlers
b.WriteString(g.genHandlers(file, routes))
// 8. Main
b.WriteString(g.genMain(file))
// 9. Format with gofmt
formatted, err := format.Source([]byte(b.String()))
return string(formatted), err
}
Détails des Générateurs¶
gen_imports.go¶
Détecte automatiquement les imports nécessaires :
needsGorm := len(file.Models) > 0
needsHTTP := file.Template != nil
needsTime := g.hasDateTimeField(file.Models)
needsUUID := g.needsUUIDHelper(file)
needsEmail := g.needsEmailHelper(file)
needsSMTP := g.hasSMTPService(file.Services)
gen_models.go¶
Génère :
- Struct GORM avec tags
- Méthode Validate() (si annotations présentes)
- Hook BeforeCreate() (si @default(uuid_v4))
gen_services.go¶
Génère pour chaque service :
- Struct de config
- Fonction init<Service>()
- Interface (si méthodes déclarées)
- Implémentation (SMTP, HTTP) ou stub
gen_handlers.go¶
Génère pour chaque fonction script :
func handle<FunctionName>(w http.ResponseWriter, r *http.Request) {
// 1. Method guard
if r.Method != "POST" {
http.Error(w, "Method not allowed", 405)
return
}
// 2. CSRF validation
// ...
// 3. Extract parameters
id := r.PathValue("id") || r.FormValue("id")
// 4. Call script function
ctx := &GMXContext{DB: db, Writer: w, Request: r}
if err := functionName(ctx, id); err != nil {
http.Error(w, err.Error(), 500)
return
}
}
gen_template.go¶
Génère :
- Route registry (détecté par regex)
- Template FuncMap avec helper route
- Parsing du template
- Injection du script CSRF HTMX
gen_main.go¶
Génère : - Init database - AutoMigrate - Route registration - Server start
Error Handling¶
Parser Error Recovery¶
Le parser utilise synchronize() pour récupérer 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()
}
}
Résultat : Le parser peut continuer même après une erreur, pour afficher toutes les erreurs d'un coup.
Script Parser Fallback¶
Si le parsing du script échoue, le ScriptBlock garde le source brut :
scriptBlock := &ast.ScriptBlock{
Source: source, // ✅ Toujours présent
Funcs: funcs, // nil si erreur
StartLine: lineOffset,
}
Le generator peut alors utiliser le fallback.
Optimisations Possibles¶
Voir AUDIT_REPORT.md pour les duplications identifiées :
- genRouteRegistry appelé 3 fois → 1 seul appel
- needsXxxHelper répété 4 fois → généraliser
- toPascalCase dupliqué → package utils
- Regex compilation à chaque appel → compiler une fois
Métriques¶
| Package | LOC | Complexité | Couverture |
|---|---|---|---|
| lexer | 466 | Faible | 87.7% |
| parser | 289 | Faible | 86.6% |
| script/parser | 790 | Moyenne | 72.6% |
| script/transpiler | 625 | Moyenne | 72.6% |
| generator | 915 | Élevée | 78.5% |
Total : ~3085 LOC de logique de compilation (hors tests)
Prochaines Étapes¶
- AST — Structures de données AST
- Lexer & Parser — Détails du parsing
- Generator — Détails de la génération
- Script Transpiler — Transpilation GMX → Go
- Testing — Stratégie de test