web-dev-qa-db-fra.com

Comment écrire un compilateur très basique

Les compilateurs avancés comme gcc compilent des codes dans des fichiers lisibles par machine selon le langage dans lequel le code a été écrit (par exemple C, C++, etc.). En fait, ils interprètent la signification de chaque code selon la bibliothèque et les fonctions des langues correspondantes. Corrige moi si je me trompe.

Je souhaite mieux comprendre les compilateurs en écrivant un compilateur très basique (probablement en C) pour compiler un fichier statique (par exemple Hello World dans un fichier texte). J'ai essayé des tutoriels et des livres, mais tous sont pour des cas pratiques. Ils traitent de la compilation de codes dynamiques avec des significations liées au langage correspondant.

Comment puis-je écrire un compilateur de base pour convertir un texte statique en un fichier lisible par machine?

La prochaine étape consistera à introduire des variables dans le compilateur; imaginez que nous voulons écrire un compilateur qui ne compile que certaines fonctions d'un langage.

L'introduction de didacticiels et de ressources pratiques est très appréciée :-)

229
Googlebot

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: éliminer les déclarations syntaxiquement correctes qui n'ont aucun sens, par ex. code inaccessible ou déclarations en double.
  • Transformations équivalentes et optimisation de haut niveau: le 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, éliminant les affectations locales excessives (voir aussi SSA ), etc.
  • Génération de code: le AST est transformé en code linéaire de bas niveau, avec des sauts, l'allocation des registres et autres. Certains appels de fonction peuvent être alignés à ce stade, certaines boucles déroulées, etc.
  • Optimisation des judas: le code de bas niveau est analysé pour détecter les inefficacités locales simples qui sont éliminées.

La plupart des compilateurs modernes (par exemple, gcc et clang) répètent à nouveau les deux dernières étapes. Ils utilisent un langage intermédiaire de bas niveau mais indépendant de la plateforme pour la génération initiale de code. Ensuite, ce langage est converti 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 comprend par exemple l'utilisation d'instructions vectorielles lorsque cela est possible, la réorganisation des instructions pour augmenter l'efficacité de la prédiction de branchement, 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 comme Java et la liaison C # peut être totalement dynamique, effectuée par le VM au moment du chargement).

Rappelez-vous les bases

  • Fais-le fonctionner
  • Rends-le beau
  • Rendez-le efficace

Cette séquence classique s'applique à tous les développements logiciels, mais 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 pourrait fonctionner.

Lisez les livres!

Lisez le Dragon Book par Aho et Ullman. Ceci est classique et est encore tout à fait applicable aujourd'hui.

Modern Compiler Design est également loué.

Si ce truc est trop difficile pour vous en ce moment, lisez d'abord quelques intros sur l'analyse; l'analyse des bibliothèques comprend 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 trucs 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 nouveau langage comme cas de test pour le futur compilateur.

Utilisez votre langue préférée

Il est tout à fait correct d'écrire un compilateur en Python ou Ruby ou tout autre langage qui vous est facile. Utilisez des algorithmes simples que vous comprenez bien. La première version n'a pas pour être rapide, efficace ou complet. Il suffit qu'il soit suffisamment correct et facile à modifier.

Il est également possible d'écrire différentes étapes d'un compilateur dans différentes langues, si nécessaire.

Préparez-vous à écrire de nombreux tests

Votre langue entière devrait être couverte par des cas de test; il sera effectivement défini par eux. Familiarisez-vous avec votre framework de test préféré. Écrivez des tests dès le premier jour. Concentrez-vous sur des tests "positifs" qui acceptent le code correct, par opposition à la détection d'un code incorrect.

Exécutez tous les tests régulièrement. Corrigez les tests cassés avant de continuer. Il serait dommage de se retrouver avec un langage mal défini qui ne peut pas accepter de code valide.

Créer un bon analyseur

Les générateurs d'analyseurs sont nombreux . Choisissez ce que vous voulez. Vous pouvez également écrire votre propre analyseur à partir de zéro, mais cela ne vaut la peine que si la syntaxe de votre langue est morte simple.

L'analyseur doit détecter et signaler les erreurs de syntaxe. Écrivez un grand nombre de cas de test, positifs et négatifs; réutilisez le code que vous avez écrit tout en définissant la langue.

La sortie de votre analyseur est un arbre de syntaxe abstrait.

Si votre langage possède 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 sauvegarder un arbre dans un fichier et de le recharger rapidement.

Créer un validateur sémantique

Votre langage permet très probablement 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 ces 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 à partir d'un autre module est correct.

Encore une fois, écrivez et exécutez de nombreux 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. Souvent, il est OK de traduire directement une construction de langage (comme une instruction if) en un modèle de code légèrement paramétré, un peu comme un modèle HTML.

Encore une fois, ignorez l'efficacité et concentrez-vous sur l'exactitude.

Cibler une machine virtuelle de bas niveau indépendante de la plate-forme

Je suppose que vous ignorez les choses de bas niveau, sauf si vous êtes vivement intéressé par les détails spécifiques au matériel. Ces détails sont sanglants et complexes.

Vos options:

  • LLVM: permet une génération efficace de code machine, généralement pour x86 et ARM.
  • CLR: cible .NET, principalement basé sur x86/Windows; a un bon JIT.
  • JVM: cibles Java, assez multiplateforme, a un bon JIT.

Ignorer l'optimisation

L'optimisation est difficile. L'optimisation est presque toujours prématurée. Générez du 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 peuvent être introduites. Mais évitez tout truc rusé et poilu avant que votre compilateur ne soit stable.

Et alors?

Si tout cela n'est pas trop intimidant pour vous, continuez! Pour une langue simple, chacune des étapes peut être plus simple que vous ne le pensez.

Voir un "Hello world" à partir d'un programme créé par votre compilateur pourrait valoir la peine.

335
9000

Créons un compilateur de Jack Crenshaw, bien qu'il soit inachevé, est une introduction et un tutoriel éminemment lisibles.

Compiler Construction de Nicklaus Wirth est un très bon manuel sur les bases de la construction d'un compilateur simple. Il se concentre sur la descente récursive de haut en bas, qui, avouons-le, est beaucoup plus facile que Lex/yacc ou flex/bison. Le compilateur Pascal original que son groupe a écrit a été fait de cette façon.

D'autres personnes ont mentionné les différents livres Dragon.

29
John R. Strohm

Je commencerais en fait par écrire un compilateur pour Brainfuck . C'est un langage assez obtus pour programmer mais il n'a que 8 instructions à mettre en œuvre. C'est aussi simple que possible et il existe des instructions C équivalentes pour les commandes impliquées si vous trouvez la syntaxe rebutante.

15
World Engineer

Si vous voulez vraiment écrire du code lisible par machine uniquement et non destiné à une machine virtuelle, vous devrez lire les manuels Intel et comprendre

  • une. Liaison et chargement de code exécutable

  • b. Formats COFF et PE (pour Windows), comprendre également le format ELF (pour Linux)

  • c. Comprendre les formats de fichiers .COM (plus faciles que PE)
  • ré. Comprendre les assembleurs
  • e. Comprendre les compilateurs et le moteur de génération de code dans les compilateurs.

Beaucoup plus difficile que prévu. Je vous suggère de lire les compilateurs et interprètes en C++ comme point de départ (par Ronald Mak). Alternativement, "permet de construire un compilateur" par Crenshaw est OK.

Si vous ne voulez pas faire cela, vous pouvez aussi bien écrire votre propre VM et écrire un générateur de code ciblé pour cette VM.

Conseils: apprenez d'abord Flex et Bison. Continuez ensuite à construire votre propre compilateur/VM.

Bonne chance!

12
Aniket Inge

L'approche de bricolage pour un compilateur simple pourrait ressembler à ceci (du moins c'est à quoi ressemblait mon projet uni):

  1. Définissez la grammaire de la langue. Sans contexte.
  2. Si votre grammaire n'est pas encore LL (1), faites-le maintenant. Notez que certaines règles qui semblaient correctes dans la grammaire ordinaire des FC peuvent s'avérer laides. Peut-être que votre langue est trop complexe ...
  3. Écrivez Lexer qui coupe le flux de texte en jetons (mots, chiffres, littéraux).
  4. Écrivez un analyseur descendant récursif descendant pour votre grammaire, qui accepte ou rejette les entrées.
  5. Ajoutez une génération d'arborescence de syntaxe dans votre analyseur.
  6. Écrivez le générateur de code machine à partir de l'arbre de syntaxe.
  7. Profit & Beer, vous pouvez également commencer à réfléchir à la façon de faire un analyseur plus intelligent ou de générer un meilleur code.

Il devrait y avoir beaucoup de littérature décrivant chaque étape en détail.

10
MaR