


Modifier un compilateur vers JavaScript et son système de build, en testant chaque couche
Pour une IA dans un monorepo de compilateur vers JavaScript. Elle compile via l'exécuteur de tâches, ne touche pas aux définitions gelées de l'arbre syntaxique, teste chaque couche modifiée, propage les nouvelles options jusqu'au bout et mesure la vitesse sans se fier au linter.
Pour utiliser ces règles d'IA
## Compilez via l'exécuteur de tâches, sans sauter de niveau
Utilisez l'exécuteur de tâches du dépôt plutôt que d'invoquer à la main les outils de build sous-jacents. La cible par défaut compile la chaîne d'outils de la plateforme ; des cibles distinctes ajoutent la bibliothèque standard, exécutent les suites de tests, formatent le code et vérifient le formatage sans le réécrire.
### Les cibles s'appuient les unes sur les autres
L'installation des dépendances s'exécute automatiquement pour toute cible qui a besoin de l'outillage JavaScript. La compilation par défaut produit les binaires de la chaîne d'outils — à la fois les exécutables du compilateur et le binaire distinct du système de build — et les copie dans le répertoire de paquet propre à la plateforme. La cible de la bibliothèque standard consomme ces sorties de la chaîne d'outils pour compiler les sources du runtime. Les cibles de test réutilisent tout ce qui se trouve en dessous d'elles.
### Ne sautez aucun niveau
Comme chaque cible dépend de celle qui se trouve en dessous d'elle, invoquer directement un outil de niveau inférieur peut laisser en place des binaires périmés et produire des résultats qui ne correspondent pas à une compilation propre. Quand un résultat de compilation paraît impossible, exécutez la cible de nettoyage et recompilez depuis le sommet avant d'enquêter plus loin.
## Ne modifiez pas l'arbre syntaxique gelé ; mettez à jour les modules de conversion
Certains fichiers sont délibérément gelés parce que des extensions d'analyseur syntaxique externes dépendent de leur forme exacte. Ne modifiez jamais la version gelée de la définition de l'arbre syntaxique. Quand l'arbre syntaxique vivant change, mettez plutôt à jour les deux modules de correspondance qui convertissent entre la représentation vivante et la représentation gelée, afin que les extensions continuent de fonctionner pendant que l'arbre principal reste libre d'évoluer.
### La compatibilité avec le fork d'origine n'est pas une contrainte
Le compilateur a commencé comme un fork d'un autre compilateur, mais le projet maintient son propre arbre syntaxique et peut introduire des changements incompatibles par rapport à l'original. Décidez selon ce qui sert le mieux cette cible de compilation, et non selon ce que fait le projet en amont.
### Évitez de réserver de nouveaux mots-clés
Préférez une implémentation qui n'exige pas de nouveau mot réservé, car en réserver un casse le code existant qui utilise déjà ce nom. Traitez les nouveaux mots-clés comme un dernier recours et cherchez d'abord une syntaxe qui réutilise les constructions existantes.
## Corrigez la cause au lieu de faire taire les avertissements ou d'utiliser des jokers provisoires
N'utilisez jamais un attribut de suppression d'avertissement pour faire taire le compilateur. Corrigez plutôt le problème sous-jacent — un avertissement supprimé masque exactement la catégorie d'erreur que cet avertissement existe pour détecter.
### Les jokers sont une décision, pas un raccourci
N'utilisez pas de motifs joker comme substituts pour de nouvelles fonctionnalités du langage qui finissent ensuite par être oubliées. N'en utilisez un que lorsque vous êtes certain que la valeur doit être ignorée dans ce cas précis, et vérifiez que chaque nouvelle fonctionnalité du langage est traitée complètement dans toutes les couches du compilateur.
### Écartez les résultats explicitement, et faites circuler l'état
Si une fonction n'est appelée que pour ses effets de bord, utilisez la fonction d'ignorance explicite plutôt que de lier le résultat à un joker. N'écartez jamais un résultat porteur d'état — faites-le plutôt circuler à travers la chaîne d'appels.
### De petites conventions qui gardent les motifs honnêtes
N'ajoutez pas de paramètre unité final à une fonction qui possède déjà un argument étiqueté obligatoire ; l'étiquette empêche déjà l'application partielle accidentelle. Omettez le joker final dans les motifs d'enregistrement lorsque l'avertissement qu'il vise est désactivé dans la base de code. Méfiez-vous des constructeurs portant des noms similaires dans différentes représentations intermédiaires, car un même nom de variante peut signifier des choses différentes selon les phases de compilation.
## Avant d'éditer, identifiez la couche du compilateur que vous changez
Le pipeline transforme le texte source en un arbre syntaxique de surface dans l'analyseur syntaxique, applique des transformations de frontend, produit un arbre typé dans le vérificateur de types, abaisse vers une représentation intermédiaire de niveau lambda, convertit vers une représentation intermédiaire de niveau JavaScript, et émet enfin le code. Identifiez la couche que vous éditez avant de toucher quoi que ce soit.
### Les couches correspondent à des répertoires
Le répertoire de syntaxe possède l'analyse syntaxique et la syntaxe de surface. Le répertoire frontend possède les transformations de l'arbre syntaxique et le traitement de l'interface de fonctions étrangères. Le répertoire du vérificateur de types possède la vérification de types et les transformations de l'arbre typé. Le répertoire core contient à la fois les modules de niveau lambda et ceux de niveau JavaScript, distingués par le préfixe de fichier. Des répertoires distincts contiennent les utilitaires étendus et les structures de données, la génération de définitions de types pour l'écosystème cible, ainsi que le serveur de langage et l'outillage.
### Les changements traversent généralement plusieurs couches
Une nouvelle fonctionnalité du langage demande typiquement du travail dans l'analyseur syntaxique, des mises à jour de la définition de l'arbre syntaxique, une prise en charge par la vérification de types, un traitement au niveau lambda, la génération JavaScript et des tests, dans cet ordre. Sauter une couche produit une fonctionnalité qui s'analyse proprement puis compile de travers.
## Déboguez en affichant la représentation intermédiaire de chaque étape
Le binaire du compilateur accepte des drapeaux qui affichent chaque étape : la source prétraitée, l'arbre d'analyse après l'analyse syntaxique, l'arbre typé après la vérification de types et la représentation lambda non optimisée. Comparez le vidage de chaque étape à ce que vous attendez avant de commencer à lire les sources du compilateur.
### Acheminez le symptôme vers la couche qui en est responsable
Les problèmes de mise en forme de la sortie relèvent du module d'affichage formaté. Les erreurs de types relèvent des modules du vérificateur de types. Les résultats erronés qui n'apparaissent qu'après optimisation relèvent des passes d'analyse de niveau lambda. Le code émis mal formé relève des modules de génération JavaScript.
### Reproduisez en petit
Compilez un seul fichier minimal plutôt que le projet entier pendant vos itérations. Ajoutez des affichages temporaires dans le module d'impression lambda quand une représentation a besoin d'être inspectée, et retirez cette instrumentation avant que le changement ne soit intégré.
## Ajoutez des tests à chaque couche dès que vous la modifiez
Ajoutez des tests dès que vous modifiez chaque couche du compilateur, et non une fois tous les changements terminés. Tester tard masque quelle couche a introduit une régression et transforme une petite bissection en une enquête sur l'ensemble du changement.
### Les cas qui exigent toujours un test
Les nouvelles fonctionnalités du langage et les corrections de bugs exigent toujours des tests. Modifier une passe d'analyse ou changer la génération JavaScript exige toujours des tests. Inspectez toujours la qualité du JavaScript émis, et pas seulement sa correction.
### Couvrez les bonnes suites
Les tests de l'analyseur syntaxique valident la syntaxe. Les tests d'intégration couvrent le comportement de bout en bout. Les tests unitaires couvrent les fonctions individuelles du compilateur. Les tests de build couvrent les cas d'erreur et les cas limites, y compris les erreurs de types dans un seul fichier et les erreurs inter-modules qui nécessitent des fichiers d'implémentation et d'interface séparés. Les suites d'aller-retour, de types générés, d'analyse et d'outillage ont chacune leur propre cible et doivent être exécutées quand la zone correspondante change.
## Mettez à jour le catalogue des erreurs et avertissements dans le même changement
Le dépôt tient un catalogue, par module, de chaque erreur et de chaque avertissement que le compilateur peut émettre, chaque entrée étant associée soit à une fixture, soit à une raison documentée expliquant qu'elle est inatteignable. Le catalogue est l'outil principal pour trouver les lacunes de couverture et les candidats à la suppression de code mort ; des entrées périmées rendent donc ces deux tâches plus difficiles.
### Mettez le catalogue à jour dans le même changement que le code
Quand vous ajoutez ou supprimez une variante d'erreur, ajoutez ou supprimez sa ligne dans la section du module correspondant, définissez son statut à couverte, inatteignable ou encore à faire, et soit reliez la fixture qui la couvre, soit consignez pourquoi la variante ne peut pas être atteinte. Quand vous ajoutez ou supprimez une fixture, mettez à jour le chemin de fixture et le statut de la ligne concernée pour que le catalogue et la suite de tests ne divergent jamais.
## Gardez une compilation rapide et un JavaScript généré lisible
Le compilateur est conçu pour des boucles de retour rapides et doit passer à l'échelle sur de grandes bases de code, et sa sortie générée est lue par des humains. Ces deux propriétés sont des critères de revue, pas des considérations secondaires.
### Gardez le code émis lisible
Évitez d'émettre des symboles générés dénués de sens et gardez la sortie lisible. Un changement qui produit une sortie correcte mais illisible n'est pas terminé.
### Mesurez avant et après
Tenez compte de l'impact de chaque changement sur la vitesse de compilation, utilisez les passes d'optimisation appropriées aux niveaux lambda et JavaScript, et profilez avant et après tout changement motivé par la performance au lieu de raisonner à son sujet à partir du code.
## Suivez les règles de nommage et notez dans le changelog les changements visibles
Respectez la convention de nommage du langage dans lequel vous écrivez : le langage d'implémentation du compilateur utilise le snake case, tandis que les sources écrites dans le langage compilé lui-même utilisent le camel case. Suivez les motifs existants dans la base de code, préférez les fonctions utilitaires existantes plutôt que de les réinventer, commentez les algorithmes complexes et la logique non évidente, et préservez la compatibilité ascendante là où il est raisonnable de le faire.
### Les commits portent des obligations
Signez vos commits dans la forme exigée par le projet, incluez les tests appropriés à chaque changement, et vérifiez que la compilation passe avant de committer.
### Consignez les changements visibles par les utilisateurs
Ajoutez une entrée au journal des modifications pour tout changement visible par les utilisateurs, qu'il s'agisse d'une correction de bug, d'une fonctionnalité ou d'un changement incompatible. Placez-la dans la section correspondante de la version non publiée en cours et terminez la ligne par un lien vers la contribution. On attend des soumissions qu'elles en incluent une.
## Ajoutez une nouvelle option du système de build à chaque endroit qu'elle traverse
Le système de build compagnon, écrit dans un langage système, fournit des compilations incrémentales rapides et un meilleur rapport d'erreurs. Ses modules se répartissent proprement : les structures de données de base telles que l'état de la build et les enregistrements de modules, la compilation et la génération des arguments du compilateur, la génération de l'arbre syntaxique et les arguments de l'analyseur syntaxique, la découverte des paquets et la résolution des dépendances, le graphe de dépendances des modules et la détection de cycles, le nettoyage des artefacts, la journalisation de la build et le rapport d'erreurs, l'interface en ligne de commande, l'analyse de la configuration du projet, le surveillant de fichiers et le point d'entrée.
### Ajoutez une option partout où elle voyage
Déclarez le drapeau à la fois dans le type d'arguments de build et dans celui de watch, et mettez à jour la conversion entre les deux. Étendez l'analyseur de configuration pour tout nouveau champ de configuration de projet. Modifiez les modules de build concernés. Puis faites circuler le paramètre dans toute la chaîne, du point d'entrée vers la fonction de build, vers l'initialisation de la build, puis vers l'état de la build, ainsi que le long du chemin watch. Ajoutez des tests unitaires pour le nouveau comportement.
### Réglez la précédence et la gestion des erreurs dès le départ
Les drapeaux en ligne de commande l'emportent sur le fichier de configuration du projet. Propagez les erreurs via le type résultat standard d'enveloppement d'erreurs du projet plutôt que de paniquer, et utilisez la journalisation de niveau debug pour les diagnostics de développement.
## Lancez le système de build directement, en le pointant vers le compilateur local
Exécutez les commandes de la propre chaîne d'outils du système de build depuis la racine du dépôt en pointant vers son fichier manifeste, comme le fait l'intégration continue, plutôt qu'en vous déplaçant dans le sous-projet. L'exécuteur de tâches expose aussi des raccourcis pour le compiler et pour exécuter ses tests d'intégration.
### Faites pointer le binaire de développement vers la chaîne d'outils locale
Exécutez-le directement et, sinon, le binaire ira chercher une version publiée installée. Exportez les variables d'environnement du chemin de l'exécutable du compilateur et du chemin du runtime, toutes deux résolues en chemins absolus à l'intérieur de la copie de travail actuelle, avant de le lancer. Notez que le binaire de développement peut porter le nom du paquet plutôt que celui de l'outil : vérifiez donc le nom du fichier produit au lieu de le supposer.
### Augmentez la verbosité quand le comportement n'est pas clair
Le système de build accepte des drapeaux de verbosité croissante pour la journalisation de niveau info et de niveau debug. C'est ainsi que vous voyez des détails comme les dossiers qu'une session de surveillance surveille réellement.
### Le traçage suit la spécification standard
Le traçage distribué se configure entièrement via les variables d'environnement de télémétrie standard — point de terminaison du collecteur, surcharge du point de terminaison spécifique aux traces, en-têtes d'exportateur, nom du service, attributs de ressource et niveau de journalisation — sans aucun réglage propre au projet. C'est le fait de définir un point de terminaison qui active la télémétrie ; si aucun n'est défini, le traçage ne fait rien. Émettez les événements via les macros de traçage plutôt que via les simples macros de journalisation quand vous voulez qu'ils apparaissent dans les traces, car les deux utilisent des systèmes distincts.
## Faites attendre une condition aux tests CI, pas une durée fixe
Les exécuteurs d'intégration continue sont plus lents que les machines de développement, ce qui rend les pauses de durée fixe fragiles. Interrogez en boucle la condition qui vous intéresse réellement, par exemple attendre l'apparition d'un fichier, au lieu de dormir pendant une durée devinée.
### Un arrêt asynchrone n'est pas un achèvement
Un utilitaire qui signale à un surveillant de se terminer — par exemple en supprimant un fichier de verrou — ne fait qu'envoyer le signal ; il n'attend pas la fin du processus. Évitez de déclencher des événements de changement de configuration juste avant de quitter, car le surveillant peut lancer une recompilation concurrente à laquelle vous ne vous attendiez pas.
### Ne bricolez pas vous-même des éditions de texte propres à une plateforme
L'édition de flux en place se comporte différemment selon les systèmes d'exploitation ; utilisez donc les utilitaires de remplacement et de normalisation de chemins du dépôt plutôt que des invocations brutes d'éditeur dans les tests shell. Un script d'intégration individuel peut être exécuté seul en exportant les chemins de l'exécutable et du runtime, en sourçant le fichier d'utilitaires partagé et en invoquant ce seul script, ce qui est utile pour itérer sans exécuter la suite complète.
## Jugez les conseils du linter selon les performances réelles
Les suggestions du linter sont une donnée d'entrée, pas des verdicts. Quand un remaniement suggéré risque de nuire aux performances ou à la clarté, évaluez explicitement le compromis au lieu d'appliquer la suggestion mécaniquement.
### N'acceptez la complexité que là où elle rapporte
Une structure de paramètres n'est pas automatiquement meilleure que plusieurs arguments ; autorisez localement la règle du linter pour les fonctions qui ont légitimement besoin de nombreux paramètres et où une structure n'ajouterait que du cérémonial. Une copie est acceptable quand elle est petite, ponctuelle, imposée par les règles de propriété et hors d'un chemin critique — ne sur-concevez pas pour la contourner.
### Optimisez sur des preuves
Profilez avant d'optimiser. La plupart des préoccupations de performance dans un système de build sont négligeables à côté du temps de compilation réel. Là où l'efficacité compte vraiment, gardez des types de paramètres cohérents tout au long d'une chaîne d'appels au lieu de convertir entre types de chaînes possédées et empruntées à chaque frontière, car ces conversions ajoutent des allocations sans aucun bénéfice. Ne recompilez que les modules modifiés, parallélisez le travail indépendant, surveillez l'empreinte mémoire de l'état de la build sur les grands projets, et minimisez les opérations sur le système de fichiers.
