Je vais tenter de répondre avec un peu plus de détail à tes différentes interrogations.
- à l’aide d’une cli, j’informe mon compilateur quel fichier compiler ;
Ça dépend du choix que tu fais sur ce qui constitue une unité de compilation et comment ton compilateur s’en sort pour résoudre et vérifier les interdépendances. C’est pas ultra-important comme considération à ton niveau cela dit.
- le compilateur va alors lexer le fichier que je lui ai donné via son chemin et me ressortir une liste qui pourrait ressembler à celle-ci :
Tu parles simultanément d’une liste et d’une chaine de caractères. Comme souligné par @entwanne, en pratique un lexer va te sortir une liste d’objets (des lexèmes) qui correspondent chacun à un symbole. Par exemple, quelque chose qui pourrait ressembler à [Identifier("a"), EqualSymbol, Literal("2")]
. Note que j’ai écrit Literal("2")
plutôt que LiteralInteger(2)
, parce qu’on peut avoir tout intérêt à découpler la phase d’inférence de type de l’analyse lexicale/syntaxique et en faire une étape de l’analyse sémantique.
- ensuite, logiquement grâce à la fonction du lexer qui retourne la chaîne ci-dessus, le parser récupère ce dernier et va chercher à l’intérieur des "motifs" correspondant à des suites logiques de lexèmes, comme par exemple : Si il y a un IDENTIFIER suivit d’un EQUAL puis d’une autre valeur, alors je créé l’item suivant :
Affectation("IDENTIFIER", Num("VALUE"))
. J’ai cependant ici un trouble quant à "comment savoir le type de la valeur", j’ai ici mis Num
car le lexer avait retourné NUM
avant la valeur ;
Oui.
- l’analyseur sémantique va à son tour récupérer l’AST du parser à l’aide d’une fonction qui aura été définit par ce dernier et va grosso-modo faire quelques vérifications qui n’ont pour une raison qui m’échappe, pas été faites à la volée par le parser.
Les faire à la volée en même temps que le parser mélange deux considérations : le parser est chargé de vérifier que la suite de lexème est conforme à la grammaire qu’on s’est donné (par exemple, qu’on écrit pas un truc du genre if while a = 3;"bla bla" return
en Python). L’analyse sémantique fait des vérifications plus poussées, qu’il est intéressant de n’effectuer qu’une fois l’analyse syntaxique faite pour deux raisons :
- il est inutile de vérifier qu’un programme qui n’est même pas syntaxiquement correct vérifie d’autres invariants plus complexes ;
- il est en général impossible et/ou non défini de vérifier qu’un programme non syntaxiquement correct vérifie ces invariants en question.
Par exemple, vérifier que le typage de if while a = 3; "bla bla" return"
est correct n’est même pas une question qui a du sens.
- l’analyseur sémantique va passer cet AST enrichi au générateur de code intermédiaire qui va transformer ce que celui-ci veut faire en un langage que je ne connais pas. Pouvez vous me dire quel est ce langage dans le cas de l’utilisation de LLVM et sans LLVM et comment je suis censé créer ce convertisseur ? En tout cas, à la fin du processus de génération de code intermédiaire, on se retrouve avec un code dans un langage à part qui permettra par la suite, une transformation en langage machine plus aisé ;
Dans le cas de LLVM, ce langage est LLVM-IR ou le bitcode LLVM.
- des améliorations seront apportés à l’IR (LLVM s’en charge, où dois en fait tout faire tout seul, mais en me servant d’outil fournit par LLVM pour les 3 dernières phases ?) ;
Ça dépend de ton but. Si tu veux faire un langage sérieux, tu peux pas te contenter de te reposer sur LLVM pour optimiser ton code. Faut aussi s’assurer que le LLVM-IR que tu lui envoies est pas complètement pourri, ce qui nécessite de faire gaffe à deux choses :
- que le LLVM-IR que tu génères est une représentation aussi fine que possible de ce que tu as besoin ;
- que tu génères ton LLVM-IR à partir d’une IR qui est elle-même passé par une phase d’optimisation qui vient de ce que tu connais de la sémantique de ton langage (c’est pas pour rien que Rust optimise MIR avant de le traduire en LLVM-IR).
- l’IR amélioré sera transformé en code machine qui je crois est de l’assembleur, le tout, à l’aide d’outil de LLVM ;
Oui.
- finalement, l’utilisateur se retrouve avec un exécutable qui je crois, pourra être exécuté sur n’importe quel ordinateur.
Dans le cas général, non. Ton exécutable sera au mieux exécutable par une famille d’OS sur un ensemble d’architectures, et "au pire" sur une machine donnée (je mets "au pire" entre guillemets parce que ça peut être parfois souhaitable pour des raisons de performances). Les questions de compatibilité sont beaucoup trop vastes pour les aborder de façon intéressantes dans un message de forum. Si on regarde pas de trop prêt, le genre de garanties que tu peux avoir est par exemple que ton programme va tourner sous Linux pour une architecture processeur donnée (e.g. x86_64
). Tu vas en général avoir besoin de compiler explicitement pour pouvoir tourner sous d’autres OS (macOS, Windows) et/ou d’autres architectures (aarch64).
J’ai un peu de mal à suivre là. Une machine qui compile, c’est un compilateur pas une machine virtuelle. D’ailleurs LLVM n’est plus un acronyme depuis quelques années déjà.
C’est pas trop le sujet et ça peut nous emmener loin, mais la définition de machine virtuelle comme programme qui exécute effectivement du code, bien que courante, est naïve et peu intéressante (surtout lorsqu’on discute de language design). LLVM expose un langage dont la spécification repose sur un modèle de calcul qui ne correspond pas à une vraie machine (exactement comme C d’ailleurs). Dans le contexte de ce sujet, il est beaucoup plus intéressant de traiter une VM comme étant la machine représentée par ce modèle de calcul plutôt que de se restreindre aux vulgaires émulateurs (au passage, la seule raison pour laquelle LLVM n’est plus officiellement un acronyme pour Low Level Virtual Machine vient juste de la démocratisation des VM dans ce sens restreint, mais en fait cette description est toujours parfaitement valable).