Intro
Un compilateur typique effectue les étapes suivantes:
- Analyse: le texte source est converti en un arbre de syntaxe abstraite (AST).
- Résolution des références à d'autres modules (C reporte cette étape jusqu'à la liaison).
- Validation sémantique: élimination d'instructions syntaxiquement correctes qui n'ont aucun sens, par exemple code inaccessible ou déclarations en double.
- Transformations équivalentes et optimisation de haut niveau: l'AST est transformé pour représenter un calcul plus efficace avec la même sémantique. Cela inclut, par exemple, le calcul précoce de sous-expressions communes et d'expressions constantes, l'élimination des affectations locales excessives (voir aussi SSA ), etc.
- Génération de code: l'AST est transformé en code de bas niveau linéaire, avec sauts, allocation de registre, etc. Certains appels de fonction peuvent être intégrés à ce stade, certaines boucles déroulées, etc.
- Optimisation des peepholes: le code de bas niveau est analysé pour détecter les inefficacités locales simples éliminées.
La plupart des compilateurs modernes (par exemple, gcc et clang) répètent encore une fois les deux dernières étapes. Ils utilisent un langage intermédiaire de bas niveau mais indépendant de la plate-forme pour la génération initiale du code. Ensuite, cette langue est convertie en code spécifique à la plate-forme (x86, ARM, etc.) faisant à peu près la même chose d'une manière optimisée pour la plate-forme. Cela inclut, par exemple, l'utilisation d'instructions vectorielles lorsque cela est possible, la réorganisation d'instructions pour augmenter l'efficacité de la prédiction de branche, etc.
Après cela, le code objet est prêt pour la liaison. La plupart des compilateurs de code natif savent comment appeler un éditeur de liens pour produire un exécutable, mais ce n'est pas une étape de compilation en soi. Dans des langages tels que Java et C #, la liaison peut être totalement dynamique, effectuée par la VM au moment du chargement.
Rappelez-vous les bases
- Fais-le fonctionner
- Le rendre beau
- Le rendre efficace
Cette séquence classique s’applique à tous les développements logiciels, mais elle mérite d’être répétée.
Concentrez-vous sur la première étape de la séquence. Créez la chose la plus simple qui puisse fonctionner.
Lisez les livres!
Lisez le livre du dragon par Aho et Ullman. Ceci est classique et est encore tout à fait applicable aujourd'hui.
La conception du compilateur moderne est également appréciée.
Si cela vous pose trop de problèmes en ce moment, lisez d'abord quelques intros sur l'analyse; Les bibliothèques d'analyse comprennent généralement des intros et des exemples.
Assurez-vous que vous êtes à l'aise avec les graphiques, en particulier les arbres. Ces choses sont les choses dont les programmes sont faits au niveau logique.
Définissez bien votre langue
Utilisez la notation que vous voulez, mais assurez-vous d'avoir une description complète et cohérente de votre langue. Cela inclut à la fois la syntaxe et la sémantique.
Il est grand temps d'écrire des extraits de code dans votre nouvelle langue en tant que cas de test pour le futur compilateur.
Utilisez votre langue préférée
Écrire un compilateur en Python, en Ruby ou dans n’importe quel langage qui vous convient est tout à fait acceptable. Utilisez des algorithmes simples que vous comprenez bien. La première version ne doit pas nécessairement être rapide, efficace ou complète. Il doit seulement être suffisamment correct et facile à modifier.
Il est également correct d'écrire différentes étapes d'un compilateur dans différentes langues, si nécessaire.
Préparez-vous à écrire beaucoup de tests
Toute votre langue devrait être couverte par des cas de test; effectivement, il sera défini par eux. Familiarisez-vous avec votre framework de test préféré. Écrire des tests dès le premier jour. Concentrez-vous sur les tests «positifs» acceptant le code correct, par opposition à la détection de code incorrect.
Exécutez tous les tests régulièrement. Corrigez les tests brisés avant de continuer. Il serait dommage de se retrouver avec un langage mal défini qui ne puisse accepter un code valide.
Créer un bon analyseur
Les générateurs de parseurs sont nombreux . Choisissez ce que vous voulez. Vous pouvez également écrire votre propre analyseur à partir de rien, mais cela ne vaut que si la syntaxe de votre langue est extrêmement simple.
L'analyseur doit détecter et signaler les erreurs de syntaxe. Écrivez beaucoup de cas tests, à la fois positifs et négatifs; réutilisez le code que vous avez écrit en définissant la langue.
La sortie de votre analyseur est un arbre de syntaxe abstraite.
Si votre langage comporte des modules, la sortie de l'analyseur peut être la représentation la plus simple du "code objet" que vous générez. Il existe de nombreuses façons simples de déposer un arbre dans un fichier et de le recharger rapidement.
Créer un validateur sémantique
Très probablement, votre langage permet des constructions syntaxiquement correctes qui peuvent ne pas avoir de sens dans certains contextes. Un exemple est une déclaration en double de la même variable ou la transmission d'un paramètre d'un type incorrect. Le validateur détectera de telles erreurs en regardant l’arbre.
Le validateur résoudra également les références à d'autres modules écrits dans votre langue, chargera ces autres modules et les utilisera dans le processus de validation. Par exemple, cette étape s'assurera que le nombre de paramètres transmis à une fonction par un autre module est correct.
Encore une fois, écrivez et exécutez beaucoup de cas de test. Les cas triviaux sont aussi indispensables au dépannage que intelligents et complexes.
Générer du code
Utilisez les techniques les plus simples que vous connaissez. Il est souvent correct de traduire directement une construction de langage (comme une if
instruction) en un modèle de code légèrement paramétré, semblable à un modèle HTML.
Encore une fois, ignorez l'efficacité et concentrez-vous sur la correction.
Ciblez une machine virtuelle de bas niveau indépendante de la plate-forme
Je suppose que vous ignorez les éléments de bas niveau, à moins que vous ne vous intéressiez vraiment aux détails spécifiques au matériel. Ces détails sont sanglants et complexes.
Vos options:
- LLVM: permet une génération de code machine efficace, généralement pour x86 et ARM.
- CLR: cible .NET, principalement sous x86 / Windows; a un bon JIT.
- JVM: cible le monde Java, assez multiplateforme, a un bon JIT.
Ignorer l'optimisation
L'optimisation est difficile. Presque toujours, l'optimisation est prématurée. Générer un code inefficace mais correct. Implémentez l'ensemble du langage avant d'essayer d'optimiser le code résultant.
Bien sûr, des optimisations triviales sont acceptables. Mais évitez toute substance rusée et poilue avant que votre compilateur ne soit stable.
Et alors?
Si tout cela ne vous intimide pas trop, continuez! Pour un langage simple, chacune des étapes peut être plus simple que vous ne le pensez.
Voir un «bonjour» à partir d’un programme créé par votre compilateur pourrait en valoir la peine.