Lugo : Parser Lua 5.4, LSP et MCP Zéro-Allocation
/ 14 min de lecture
Table des matières
Dans l’écosystème du développement de jeux vidéo et des frameworks de modding multijoueur (en particulier FiveM / GTA V), Lua règne en maître. Réputé pour sa légèreté et sa facilité d’intégration au sein des runtimes C/C++, Lua 5.4 propulse des architectures logicielles colossales composées de centaines de micro-ressources interconnectées.
Pourtant, dès que la taille d’un projet dépasse plusieurs milliers de scripts, l’outillage de développement traditionnel montre ses limites. Les serveurs de langage (LSP) historiques s’effondrent sous le poids de la mémoire : structures d’AST fragmentées sur le tas (heap fragmentation), garbage collection bloquante provoquant des saccades à la frappe, minutes interminables requises pour réindexer un espace de travail, et aveuglement total face aux subtilités de l’environnement de jeu (fichiers manifests, isolation des contextes client/serveur, événements réseau asynchrones et contrats inter-ressources).
Pour résoudre ce goulet d’étranglement, j’ai développé Lugo (FRFlo/lugo), un moteur d’analyse syntaxique, serveur de langage (LSP) et serveur Model Context Protocol (MCP) pour Lua 5.4 écrit de zéro en Go. Conçu avec une obsession absolue pour la performance et le cache CPU, Lugo repose sur une architecture zéro-allocation basée sur des arènes de nœuds plats, une vitesse d’indexation fulgurante et une compréhension native de l’écosystème FiveM.
Voici l’analyse détaillée de ses choix d’architecture, de son moteur de compilation, de ses garde-fous de sécurité et de son intégration avec les agents d’intelligence artificielle.
Vue d’ensemble de l’Architecture
Lugo n’est pas une simple surcouche autour d’un parseur générique : chaque étape de sa chaîne de traitement est conçue pour éliminer les allocations dynamiques et maintenir les données dans les registres CPU et le cache L1/L2.
flowchart TB
subgraph Input["Fichiers Sources Lua & Manifests"]
Src["Code Source ([]byte)"]
Manifest["fxmanifest.lua / __resource.lua"]
end
subgraph CoreEngine["Moteur d'Analyse Zéro-Allocation"]
Lexer["Lexer SIMD / Table charProps [256]uint8<br>TokenSet [2]uint64 (0 allocs)"]
Parser["Pratt Parser (Precedence Climbing)<br>Lookahead 3 tokens & Panic Recovery"]
Arena["Arène Plate Tree.Nodes ([]Node)<br>NodeID (uint32) & Offsets d'octets"]
Resolver["Semantic Resolver & Type Inference<br>Contrôle de flux & Narrowing"]
end
subgraph FiveMModule["Intelligence FiveM & Sécurité"]
Graph["Graphe de Ressources & Scopes (Client/Server/Shared)"]
EventBus["Indexation des Événements Réseau & Arithmétique d'Arité"]
TrustBoundary["Trust-Boundary Diagnostics (Sinks SQL/HTTP/Commandes)"]
NUIContract["Validation des Contrats NUI (Lua <-> JS)"]
end
subgraph Protocols["Protocoles de Surface"]
LSPHandler["LSP Server (JSON-RPC Framed)<br>Hover, Completion, Refactor, Safe Fixes"]
MCPHandler["lugo-mcp Server (stdio)<br>Ressources lugo:// & Outils IA Lecture Seule"]
Formatter["Formateur AST Natif & Moteur de Diagnostics"]
end
subgraph Consumers["Consommateurs"]
VSCode["VS Code Extension (Marketplace)"]
Editor["Éditeurs Externes (Neovim, Helix)"]
AIAgents["Agents de Code IA (Pi, Claude, Hermes)"]
end
Input --> CoreEngine
CoreEngine --> FiveMModule
FiveMModule --> Protocols
Protocols --> Consumers
1. L’Architecture Zéro-Allocation : L’AST en Arène Plate
La principale cause de dégradation des performances dans les serveurs de langage conventionnels est la gestion de l’arbre syntaxique abstrait (AST). Dans une implémentation classique en Go ou TypeScript, chaque nœud est un pointeur vers une structure allouée sur le tas (*BinaryExpr, *FunctionDecl, *Ident). Pour un projet de 3 000 fichiers Lua comprenant 30 millions d’octets de code, cela engendre des dizaines de millions de petits objets mémoires, saturant le garbage collector et brisant la localité spatiale du cache CPU.
Le Nœud Compact Uniforme (ast.Node)
Lugo élimine complètement ce paradigme. Tous les nœuds de l’AST sont uniformisés au sein d’une unique structure compacte stockée dans une tranche contiguë ([]Node) :
type NodeID uint32
type Node struct { Start, End uint32 Parent NodeID Left NodeID Right NodeID Extra uint32 // Index dans Tree.ExtraList Count uint32 // Nombre d'éléments dans ExtraList Kind NodeKind // uint8 Flags uint8}Cette structure présente des caractéristiques fondamentales :
- Taille fixe et compacte : Chaque nœud occupe exactement 28 à 32 octets avec l’alignement mémoire, sans aucun pointeur direct vers le tas.
- Indexation par
NodeID(uint32) : Les relations parent-enfant (Left,Right,Parent) ne sont pas des pointeurs, mais de simples indices dans la trancheTree.Nodes. L’identifiant spécialInvalidNode = 0matérialise l’absence de nœud sans nécessiter de valeurnil. - Listes arbitraires sans pointeurs : Pour les structures à nombre variable d’enfants (paramètres de fonction, arguments d’appel, champs de table), Lugo stocke leurs
NodeIDconsécutivement dans un tableau plat auxiliaire (Tree.ExtraList []NodeID). Le nœud parent retient simplement un décalage de départ (Extra) et un compteur (Count).
Stockage par Offsets d’Octets
Au lieu de stocker des chaînes de caractères (string) pour chaque identifiant, mot-clé ou littéral, Lugo ne retient que deux entiers uint32 : Start et End, correspondant aux offsets d’octets dans le buffer source original (Tree.Source []byte).
La conversion en string Go est strictement différée au moment où l’utilisateur en a réellement besoin (par exemple lors de l’affichage d’un infobulle Hover ou de l’envoi d’une liste de complétion). Lors du parsing et de la résolution sémantique, les comparaisons d’identifiants s’effectuent directement sur des slices d’octets sans la moindre allocation sur le tas :
func (t *Tree) IdentText(id NodeID) string { node := &t.Nodes[id] return string(t.Source[node.Start:node.End])}2. Tokenizer SIMD et Pratt Parser
La chaîne de compilation amont est optimisée pour exploiter au maximum les registres de la machine hôte.
Table de Propriétés de Caractères et Masques Bitmask
Le lexer de Lugo (lexer/lexer.go) évite les embranchements conditionnels imprévisibles (branch mispredictions) lors de l’identification des caractères grâce à une table de consultation statique précalculée de 256 octets :
const ( propIdent = 1 << iota propDigit propHex propLetter)
var charProps [256]uint8L’appartenance d’un token à un ensemble syntaxique (par exemple : les opérateurs unaires ou les délimiteurs de fin de bloc) repose sur token.TokenSet, matérialisé par un bitmask [2]uint64 :
type TokenSet [2]uint64
func (s TokenSet) Has(k Kind) bool { idx := uint(k) return (s[idx/64] & (1 << (idx % 64))) != 0}Ce test s’exécute en une seule instruction CPU, sans aucun test séquentiel ni allocation dynamique.
Pratt Parsing avec Precedence Climbing et Récupération d’Erreurs
Le parseur (parser/parser.go) implémente l’algorithme de Pratt (Precedence Climbing) avec un fenêtrage de 3 tokens d’avance (three-token lookahead). Cette technique permet de parser la grammaire d’expressions de Lua 5.4 (opérateurs binaires avec précédence arithmétique, bitwise, concaténation associative à droite, et indexations chaînées) sans aucun retour arrière (backtracking).
Lorsqu’une erreur de syntaxe survient (par exemple pendant la frappe d’un développeur avec un bloc if non clôturé), le parseur bascule en mode panique (panic-mode recovery) : il synchronise son curseur sur la limite d’instruction suivante (end, else, until, local, return) et reprend le parsing du reste du fichier. Le document continue ainsi de bénéficier de l’autocomplétion et de la navigation de code même en présence de fragments invalides.
Le Gardien des Benchmarks CI : 0 Allocations Imposées
Pour garantir qu’aucune régression de performance ne s’infiltre dans le code, la suite de continuous integration (CI) exécute un binaire de validation dédié (scripts/benchmark-gate) :
go run ./scripts/benchmark-gateCe script exécute BenchmarkLexer et BenchmarkParser avec -benchmem. Si le rapport affiche ne serait-ce qu’une seule allocation par opération (> 0 allocs/op), le pipeline CI échoue immédiatement.
Résultat en production :
- Indexation à froid d’un espace de travail complet de 3 042 fichiers FiveM (29,6 Mo de code Lua brut) : 746 ms.
- Publication des diagnostics pour l’ensemble des 3 018 fichiers : 720 ms.
- Réindexation à chaud (modifications incrémentales via hachage SHA-256 de fichiers et réutilisation des pools avec
clear()) : quelques dizaines de millisecondes.
3. L’Intelligence FiveM : Au-Delà du Lua Standard
La particularité de Lugo réside dans son intégration profonde avec le runtime FiveM / CitizenFX. Les serveurs FiveM ne sont pas de simples scripts Lua isolés : ils fonctionnent comme un ensemble de ressources interconnectées régies par un fichier manifeste (fxmanifest.lua ou __resource.lua).
flowchart LR
Manifest["fxmanifest.lua"] --> Discovery["Détection des Fichiers & Contextes"]
Discovery --> ClientScope["client_scripts<br>Natives Client + Shared"]
Discovery --> ServerScope["server_scripts<br>Natives Server + Shared"]
Discovery --> SharedScope["shared_scripts<br>Intersection Commune"]
ClientScope --> NetFlow["Validation Réseau<br>TriggerServerEvent"]
ServerScope --> NetFlow["Validation Réseau<br>TriggerClientEvent"]
NetFlow --> SecurityScan["Détection de Failles<br>(Trust-Boundary & SQL Injection)"]
Contextes d’Exécution Isolés (client, server, shared)
Lugo lit dynamiquement le manifest de chaque ressource pour déterminer la portée d’exécution des fichiers :
- Fichiers Client : Ont accès aux fonctions natives du moteur graphique/physique (GTA V natives, NUI,
Citizen.CreateThread), mais ne voient pas les fonctions de base de données ni les méthodes d’accès serveur. - Fichiers Serveur : Ont accès aux drivers de bases de données (
oxmysql,mysql-async), aux identifiants des joueurs et aux natives serveur (DropPlayer,PerformHttpRequest). - Fichiers Partagés : Seule l’intersection des deux contextes est autorisée.
Si un développeur tente d’appeler GetPlayerPed(-1) (native purement client) dans un script catégorisé comme serveur, ou MySQL.query dans un script client, Lugo émet instantanément un diagnostic contextuel sans nécessiter d’exécution runtime.
Comptabilité Stricte des Ressources
Lugo inspecte l’arborescence physique du projet. Si un fichier .lua réside dans le dossier d’une ressource mais est omis des directives client_scripts, server_scripts ou shared_scripts du fxmanifest.lua, Lugo émet un avertissement d’incohérence (resource accounting warning). Ce fichier reste isolé afin de ne pas fausser l’index global des symboles.
Événements Réseau et Analyse aux Frontières de Confiance (Trust-Boundary)
Les vulnérabilités les plus critiques sur FiveM proviennent de l’exposition d’événements réseau non assainis (RegisterNetEvent). Lugo traque statiquement les flux de données réseau :
- Arité et Signature : Vérification des types et du nombre de paramètres attendus entre
TriggerServerEvent("maRessource:action", arg1, arg2)etRegisterNetEvent("maRessource:action", function(arg1, arg2) ...). - Direction de Flux : Détection des événements déclarés côté serveur mais déclenchés par erreur avec
TriggerServerEventau lieu deTriggerClientEvent. - Trust Boundary : Lorsqu’un événement réseau est reçu côté serveur, ses paramètres sont marqués comme non fiables (untrusted). Si l’un de ces paramètres transite vers un récepteur sensible (sink) tel qu’une requête SQL (
oxmysql), un appel HTTP, une exécution de commande système ou unsourceutilisé après unWait(), Lugo signale une alerte de sécurité proactive.
Validation des Contrats NUI (Lua <-> JavaScript)
Les interfaces de jeu (NUI) reposent sur un pont de communication entre le runtime Lua et le moteur Web Chromium du jeu. Lugo analyse conjointement les scripts Lua et les assets JavaScript du projet pour garantir la symétrie des appels :
- Les appels
RegisterNUICallback('monAction', ...)en Lua sont vérifiés face auxfetch('https://resource/monAction')en JS. - Les envois
SendNUIMessage({ action = 'openUI' })sont confrontés aux listeners d’événements JavaScriptwindow.addEventListener('message', ...). Les handlers manquants ou orphelins sont immédiatement signalés.
4. Capacités LSP & Refactorings AST
Lugo implémente l’intégralité du protocole LSP avec une suite complète de fonctionnalités de confort et de transformation de code :
- Inférence de Types Avancée & Narrowing : Évaluation paresseuse des types avec prise en compte des filtres de contrôle de flux (
if type(x) == "string" then ...), déballage des itérateurs (pairs/ipairs), et résolution de l’héritage par métatables (setmetatable(t, { __index = Base })). - Support des Annotations LuaCATS : Interprétation complète des commentaires typés (
---@class,---@type,---@param,---@return,---@generic,---@field,---@alias,---@overload,---@deprecated). - Évaluation Constante au Survol (Hover Evaluation) : Lugo calcule statiquement le résultat des expressions mathématiques et logiques constantes et affiche la valeur résolue directement dans l’infobulle.
- Refactorings AST Natifs :
- Inversion de conditions logiques.
- Transformation de chaînes
if-elseen retours anticipés (early returns). - Optimisation automatique de
table.insert(t, val)ent[#t+1] = val. - Bascule transparente entre syntaxe de méthode à point (
obj.method(self)) et à deux-points (obj:method()). - Safe Fixes en masse : Nettoyage sécurisé des variables et paramètres inutilisés sur l’intégralité du projet sans rompre les effets de bord d’expressions.
5. La Passerelle IA : Le Binaire lugo-mcp
Avec l’avènement des assistants et des agents de développement autonomes (tels que Pi Coding Agent, Claude Code ou Hermes), la manière dont une IA explore une base de code est primordiale. Les agents classiques passent leur temps à exécuter des commandes grep, find ou cat, gaspillant des milliers de tokens de contexte dans des lectures partielles et des erreurs d’interprétation.
Pour répondre à ce besoin, Lugo embarque une commande dédiée : lugo-mcp.
lugo-mcp /chemin/vers/mon-projet-fivemCe binaire lance un serveur Model Context Protocol (sur stdio), branché directement sur l’arbre syntaxique et l’index sémantique de Lugo :
flowchart LR
Agent["Agent de Code IA<br>(Pi / Claude / Hermes)"] <-->|"MCP JSON-RPC (stdio)"| LugoMCP["lugo-mcp Process<br>(1 instance / workspace)"]
LugoMCP <-->|"Direct Memory Access<br>(sync.RWMutex)"| LSPServer["LSP Workspace Model<br>(AST Arena & Graphe FiveM)"]
subgraph ExposedCapabilities["Capacités Exposées à l'IA"]
Tools["Outils : lugo_hover, lugo_definition,<br>lugo_diagnostics, lugo_references,<br>lugo_fivem_events, lugo_fivem_contracts"]
Resources["Ressources : lugo://workspace/summary,<br>lugo://workspace/resource/{name}"]
Safety["Garde-fou : Lecture Seule &<br>Aperçu (lugo_preview_workspace_edit)"]
end
LugoMCP --- ExposedCapabilities
Des Outils de Précision Chirurgicale pour l’IA
Au lieu de faire des hypothèses sur le code, l’agent IA dispose d’outils précis et structurés :
lugo_diagnostics: Récupère la liste exacte des erreurs et alertes de sécurité sur un fichier avant ou après modification.lugo_symbol_context: Fournit en un seul appel l’emplacement de définition, la signature, le typage LuaDoc et la portée d’un symbole sous le curseur.lugo_fivem_events&lugo_fivem_contracts: Donne à l’IA une cartographie exhaustive de tous les événements réseau et exports de l’écosystème du serveur.lugo://workspace/summary: Expose une synthèse instantanée de la fraîcheur du projet (hash de révision, nombre de ressources indexées, compteurs de diagnostics).
Principe de Sécurité : Zéro Écriture Directe
Pour garantir l’intégrité de la base de code, lugo-mcp applique un principe de sécurité strict : le serveur MCP est en lecture seule. Il refuse catégoriquement d’écrire ou de modifier des fichiers sur disque par lui-même. Les outils de refactorisation (lugo_validate_workspace_edit et lugo_preview_workspace_edit) se contentent de valider la syntaxe et de renvoyer un aperçu des changements (WorkspaceEdit). C’est l’agent ou le développeur qui applique ensuite explicitement les modifications sur le disque.
6. Fiabilisation et Audit de Gestion Mémoire
Garantir la stabilité d’un serveur de langage sur des sessions de développement s’étirant sur plusieurs jours nécessite une rigueur extrême vis-à-vis des fuites de mémoire et des dénis de service locaux.
En septembre 2026, Lugo a fait l’objet d’un audit mémoire et sécurité exhaustif (docs/memory-audit.md), couvrant 15 constats majeurs résolus avant la publication :
| Constat | Sévérité | Problème Identifié | Solution Appliquée |
|---|---|---|---|
| M-01 | Critique | Dépassement signé sur le parsing de Content-Length pouvant provoquer une panique process sur flux malformé. |
Utilisation de strconv.ParseUint, rejet strict de tout format invalide et contrôle de limite avant conversion. |
| M-02 | Critique | Un tabSize négatif ou disproportionné dans les options de formatage entraînait un plantage ou un épuisement de mémoire. |
Validation stricte et bornage du tabSize (plage 1 à 16) avec retour d’erreur JSON-RPC propre. |
| M-03 | Élevée | Absence de plafond sur la longueur des lignes d’en-tête JSON-RPC avant saut de ligne (\n). |
Borne stricte sur la taille cumulée des en-têtes rejetant tout flux malveillant. |
| M-04 | Élevée | Les fichiers soumis via didOpen/didChange n’étaient pas soumis au plafond MaxFileSize, provoquant des surallocations. |
Application systématique de la taille maximale de fichier (MaxFileSize) avant toute désérialisation. |
| M-05 | Élevée | Risque de débordement de pile (stack overflow) sur du code source comportant des blocs récursifs excessivement imbriqués. | Compteur de profondeur universel bornant l’imbrication et retournant une erreur de parsing récupérable. |
| M-06 | Élevée | Conservation de ressources et d’arêtes de graphe orphelines dans l’index après suppression de fichiers. | Nettoyage complet des structures de graphe et des ressources lorsqu’aucun fichier ne les référence. |
Au terme de cet audit, le moteur a été éprouvé par une campagne de fuzzing intensif de 3 777 986 cas de tests générés aléatoirement sans aucune panique, et la suite complète s’exécute avec le détecteur de courses de Go (-race) sans signaler le moindre conflit mémoire.
7. Pipeline de Build et Distribution Multi-Plateformes
Lugo est distribué sous forme de binaire natif autonome ne nécessitant aucun interpréteur externe.
Cross-Compilation Statique via Zig
Le pipeline de publication GitHub Actions exploite Zig 0.15.2 comme compilateur CGO croisé pour produire des binaires statiques musl optimisés pour 6 architectures et systèmes :
linux/amd64&linux/arm64windows/amd64&windows/arm64darwin/amd64&darwin/arm64(macOS Apple Silicon et Intel)
Extension VS Code
Pour les utilisateurs de Visual Studio Code, Lugo est disponible directement sur le VS Code Marketplace. L’extension détecte automatiquement le système d’exploitation et l’architecture CPU de la machine hôte pour lancer en tâche de fond le binaire Go adapté, sans nécessiter la moindre configuration manuelle.
Conclusion
Lugo démontre qu’en abandonnant les structures d’objets fragmentées au profit d’arènes plates contiguës en mémoire et d’un traitement axé sur les offsets d’octets, il est possible d’offrir une vitesse d’exécution d’un ordre de grandeur supérieur aux serveurs de langage classiques.
En combinant un moteur d’analyse zéro-allocation, une connaissance intime de l’écosystème FiveM et une interface MCP ouverte aux agents d’IA modernes, Lugo transforme l’expérience de développement sur les architectures de serveurs de jeu complexes.
Le projet est entièrement open source et accessible sur GitHub : github.com/FRFlo/lugo.