Implémentation de l'effet de code

Comment la génération de variable se fait elle concrètement dans un langage de programmation, par exemple.

Le problème exposé dans ce sujet a été résolu.

Je ne suis pas sûr de ton deuxième point, tu veux exprimer tes lexèmes sous la forme d’une unique chaîne de caractères ? Il te faudrait plutôt une liste de tokens où chaque token aurait un nom et une valeur.

Au niveau de la représentation intermédiaire de LLVM il t’a été donné le lien du tuto officiel LLVM pour construire ton compilateur, tu devrais y trouver ton bonheur.
Sans LLVM c’est simplement une représentation de la suite d’instructions que suivra ton programme. Dans mon premier message je parlais par exemple de quads, tu auras souvent quelque chose de relativement bas-niveau qui pourra s’apparenter à de l’assembleur.

Le convertisseur va se charger de parcourir ton AST et de générer les structures de données voulues pour représenter les instructions.

Le convertisseur va se charger de parcourir ton AST et de générer les structures de données voulues pour représenter les instructions. - @entwanne

Ton boulot est justement de produire une LLVM-IR qui a le même sens que le programme écrit par l’utilisateur - @adri1

Est-ce que tu pourrais être vraiment plus explicite, parce que là tu ne fais que répéter d’une autre manière ce qui a déjà été dis.

Pour cela il faudrait déjà savoir ce qui te bloque actuellement.

Est-ce que tu as été en mesure de construire un arbre et de le parcourir ?

Pour être plus explicite sur la génération du code, encore une fois ça dépend du format de sortie voulu, et pour celui de LLVM je te renvoie à leur documentation.


Pour tenter d’être plus explicite je vais prendre un exemple. Imaginons que ta représentation finale soit un format type assembleur avec des opcodes pouvant prendre un argument. À l’aide d’une pile pour stocker les valeurs c’est suffisant pour créer une machine virtuelle.

Tu aurais en entrée une expression du type (1 + sqrt(5)) / 2.
Suite à la tokenization, ton entrée ressemblerait à quelque chose comme [<LPAREN>, <NUMBER:1>, <OP:+>, <NAME:sqrt>, <LPAREN>, <NUMBER:5>, <RPAREN>, <RPAREN>, <OP:/>, <NUMBER:2>].

Sur ces tokens tu viendrais lancer ton parseur, qui te retournerait alors un AST de la forme suivante :

BinOperator(
    op='/',
    left=BinOperator(
        op='+',
        left=Number(1),
        right=FunctionCall(name='sqrt', arg=Number(5)),
    ),
    right=Number(2),
)

Tu parcourrais alors cet arbre nœud par nœud pour transformer ton arborescence d’opérations en une liste d’instructions. Par exemple un BinOperator consisterait à charger les deux opérandes et appliquer l’opération voulue. En sortie tu pourrais avoir quelque chose comme ça :

LOAD      1
LOAD      5
LOAD_FUNC 'sqrt'
CALL
ADD
LOAD      2
DIV

Note que LLVM est une machine virtuelle.

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à.

+1 -1

Tu parcourrais alors cet arbre nœud par nœud pour transformer ton arborescence d’opérations en une liste d’instructions. Par exemple un BinOperator consisterait à charger les deux opérandes et appliquer l’opération voulue.

Tu parles de la phase de génération de code intermédiaire ? Qu’est ce que "BinOperator" ? En quel langage convertir l’AST (langage utilisé dans l’IR) ?

Un interpréteur, pour un langage de programmation, c’est un programme qui prend en entrée n’importe quel programme du langage, et exécute ce programme — effectue ses actions et renvoie son résultat. Ce n’est pas évident à écrire, ça s’apprend. Si tu veux en savoir plus, tu devrais chercher un cours qui écrit un petit interpréteur pour un langage très simple.

Un compilateur pour un langage de programmation, c’est un programme qui prend en entrée un programme du langage et le "traduit" dans un autre langage, typiquement un langage dont on a déjà une implémentation sur son ordinateur — ou alors carrément le code binaire que ton processeur attend en entrée pour faire des calculs.

En pratique les implémentations réalistes / en production font un peu des mélanges, il y a des petits bouts de compilation (par exemple la plupart des interpréteurs compilent en fait dans un format intermédiaire plus rapide à interpréter) ou des petits bouts d’interprétation (par exemple pour l’optimisation ou l’analyse statique, etc.) aux endroits où on ne s’y attend pas forcément.

Tu parles de la phase de génération de code intermédiaire ? Qu’est ce que "BinOperator" ? En quel langage convertir l’AST (langage utilisé dans l’IR) ?

b4b4

Au vu de tes questions j’ai l’impression que tu n’as pas vraiment essayé de débuter l’implémentation pour le moment.

Je parle des différentes phases, chacune des étapes que je présente produit une représentation intermédiaire du même programme. Ici l’AST est converti en une suite d’instructions qui peuvent ensuite être interprétées par une machine virtuelle. Elles peuvent aussi être compilées vers un autre langage ou à l’aide de LLVM, c’est à toi de voir et de choisir les outils qui te conviennent pour cela.

Quant à BinOperator, c’est simplement ce qui me sert dans l’exemple à représenter une opération à deux opérantes dans l’AST (que ce soit une addition ou une division).

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 :
    IDENTIFIER a EQUAL NUM 2  # Le tout dans une chaîne de caractère, les éléments à la suite les uns les autres
    

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).

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).

adri1

Ouai … Pour moi, ça c’est une machine théorique. Je n’ai jamais vu ce genre de définition … L’ACM n’a pas l’air d’accord avec ta définition.

The term virtual machine initially described a 1960s operating system concept: a software abstraction with the looks of a computer system’s hardware (real machine).

J’ai plutôt l’impression que l’idée de base était d’avoir une sorte de bytes code (qui désormais est le format LLVM-IR) qui serait “interprété”, comme une compilation JAT, directement en langage machine à l’exécution. Et que de ça ils ont tout naturellement dévier vers un compilateur.

+0 -1

@Ache tu as oublié la seconde partie de la citation :

The term virtual machine initially described a 1960s operating system concept: a software abstraction with the looks of a computer system’s hardware (real machine). Forty years later, the term encompasses a large range of abstractions—for example, Java virtual machines that don’t match an existing real machine. Despite the variations, in all definitions the virtual machine is a target for a programmer or compilation system. In other words, software is written to run on the virtual machine.

Et donc pour moi l’acceptation moderne (on est plus dans les années 60, ni même dans les années 2000 – l’article que tu cites a bientôt 18 ans) est tout à fait compatible avec ce qu’explique @adri1.

@SpaceFox: Vraiment je vois pas ce qui dans la deuxième partie, où même dans le reste de l’article, se rapporte à ce que décrit @adri1. Aucune volonté de manipulation de ma part ici, ça me semblait juste pas pertinent.

Je me suis rapporter à la définition originel car la définition moderne de machine virtuelle ne me semble pas s’en rapprocher. Même sur Wikipédia en anglais.

Forty years later, the term encompasses a large range of abstractions—for example, Java virtual machines that don’t match an existing real machine.

Cette partie ? La JVM un modèle de calcul ?

Despite the variations, in all definitions the virtual machine is a target for a programmer or compilation system.

Toujours pas un modèle de calcule. LLVM me semble pas correspondre à ça encore.

In other words, software is written to run on the virtual machine.

Encore une fois. Une machine virtuelle, ça execute.

Non vraiment là le reste de la citation ne me semble pas apporter grand chose. Si on a un point de vu différent merci de me présenter le tiens car c’est pas évident là.

Edit: Je réponds à @entwanne ici. On est déjà assez HS comme ça.

LLVM correspond tout à fait puisque c’est bien une cible de compilation.

Ah oui. D’une compilation partielle, la conversion en LLVM-IR. Vu comme ça effectivement, ça rentre dans la définition. C’est légèrement tordre la définition de compilation 🤏 mais ok, on peut voir ça comme ça.

+0 -0

Despite the variations, in all definitions the virtual machine is a target for a programmer or compilation system.

Toujours pas un modèle de calcule. LLVM me semble pas correspondre à ça encore.

ache

LLVM correspond tout à fait puisque c’est bien une cible de compilation.

Encore une fois. Une machine virtuelle, ça execute.

ache

Parce que tu penses « machine virtuelle » en terme d’émulateur. Donc en effet LLVM n’est pas un émulateur pour faire tourner un certain type de code. Mais ça reste bien une « machine virtuelle ».


Edit:

Ah oui. D’une compilation partielle, la conversion en LLVM-IR. Vu comme ça effectivement, ça rentre dans la définition. C’est légèrement tordre la définition de compilation 🤏 mais ok, on peut voir ça comme ça.

ache

Ah non ce n’est pas non plus tordre la définition de la compilation puisque dans la théorie ça s’applique à toute conversion d’un langage vers un autre.

Le problème c’est que le terme « machine virtuelle » recouvre deux concepts assez différents :

  1. La définition d’une machine virtuelle, au sens le plus terre-à-terre des mots : l’ensemble des règles qui définissent une machine qui permet d’exécuter un programme. On peut aussi appeler cet ensemble de règles « modèle de calcul », ça ne change pas grand-chose.
  2. Le logiciel qui va implémenter la définition précédente, et que l’on appelle aussi « machine virtuelle » (mais qui n’est qu’un émulateur, comme le signale entwanne).

Par exemple, la JVM est la définition d’une machine virtuelle au sens du point 1.
Elle possède différentes implémentations au sens du point 2, les plus connues récemment étant HotSpot (et dérivées), OpenJ9 d’IBM et GraalVM.
Elle a même possédé une implémentation matérielle (qui ne correspond pas au point 2) avec Jazelle.

Un autre exemple, c’est BrainFuck. On appelle ça un langage de programmation, mais en fait c’est un bytecode pour une machine virtuelle ultra simple. Et les implémentations de cette machine virtuelle Brainfuck sont très variées : certaines sont effectivement des machines virtuelles au sens du point 2 (des émulateurs, donc), qui vont lire le code ; d’autres sont des compilateurs indirects (compilation du code en un langage intermédiaire – C par exemple – puis on rentre dans la chaine de traitement normale de ce nouveau langage) ; on trouve aussi des implémentations avec LLVM ou même des implémentations matérielles.

Et pour moi, non seulement l’article parle bien des machines virtuelles au sens général (le sens 1), mais en plus il le dit plutôt clairement quand il dit que la notion de machine virtuelle recouvre des réalités très différentes.

LLVM n’est pas une machine virtuelle, c’est une représentation intermédiaire (IR). Le nom porte à confusion car il traduit l’intention de départ mais pas la réalité du projet une fois la thèse de master terminée.

Une façon de voir que LLVM n’est pas une machine virtuelle est que les machines virtuelles ont une implémentation, un interpréteur. À ma connaissance il n’existe pas d’interprète LLVM couramment utilisé. (Il y a des trucs avec GraalVM, peut-être dans le projet Sulong de mémoire ? Mais c’est des projets de niche, pas des trucs maintenus par les mainteneurs de LLVM et utilisés par une partie substantielle des utilisateurs LLVM.) Et si on voulait implémenter un interpréteur pour LLVM, on commencerait par traduire le programme dans un format se prêtant mieux à l’interprétation: c’est une représentation intermédiaire qui n’a pas été pensée pour être facilement interprétable.

Ah non ce n’est pas non plus tordre la définition de la compilation puisque dans la théorie ça s’applique à toute conversion d’un langage vers un autre.

entwanne

C’est de la génération de bytescode, pas une compilation totale. Et c’est pas vraiment une transpilation non plus.

@SpaceFox: Franchement tes explications sont super. Mais du coup ton point c’est que LLVM c’est un VM au sens 1 comme la JVM ?

Alors que pour moi, y a pas de rapport entre la JVM et LLVM. Je ne te fais pas l’insulte de définir la JVM, tu là connais bien mieux que moi. Et du coup, LLVM là-dedans ? Il n’y a pas de contexte d’exécution de défini. Ça ne me semble pas là même chose que LLVM, il n’y a pas de définition de comment doit être exécuter du bitcode LLVM.

Soit dit en passant “modèle de calcul” c’est un truc précis en info qui n’a rien à voir avec une VM.

@gasche: Il me semble que lli est un interpréteur capable d’exécuter du LLVM bitcode en JIT-compilant. Mais ce n’est qu’un des projets de LLVM et en rien ça ne permet de dire que LLVM est une VM.

Pour finir, je cite Chris Lattner :

"LLVM" is officially no longer an acronym. The acronym it once expanded too was confusing, and inappropriate almost from day 1. 🙂 As LLVM has grown to encompass other subprojects, it became even less useful and meaningless.

The name of LLVM sur le forum officiel de LLVM. L’emphase est de moi.
+0 -2

C’est de la génération de bytescode, pas une compilation totale. Et c’est pas vraiment une transpilation non plus.

ache

Il n’y a pas vraiment de différence, puisque rien à priori ne permet de savoir si ton bytecode généré va être exécuté par un émulateur, un complateur JIT ou même une machine physique (cf encore l’exemple de Jazelle). La seule différence entre une compilation classique et une compilation de bytecode, c’est que à priori dans une compilation classique les octets générés vont être exécutés directement par un processeur physique, et que dans une compilation vers un bytecode, les octets générés vont être exécutés par un émulateur.

Mais conceptuellement, c’est exactement la même opération.

Quant à la transpilation, c’est juste un néologisme utilisé pour ne pas dire « compilation » dans les milieux où ça ferait peur (le dev JS) ou ne pas vexer les ayatollahs de la définition la plus stricte de « compilation », qui implique du code binaire en sortie. Mais là encore, fonctionnellement c’est exactement la même chose : un processus de transformation d’une représentation A d’un programme en une représentation B fonctionnellement identique, mais en utilisant un jeu d’instructions (au sens large, incluant des langages de programmation ou des jeux de bytecode) différent.

@SpaceFox: Franchement tes explications sont super. Mais du coup ton point c’est que LLVM c’est un VM au sens 1 comme la JVM ?

Alors que pour moi, y a pas de rapport entre la JVM et LLVM. Je ne te fais pas l’insulte de définir la JVM, tu là connais bien mieux que moi. Et du coup, LLVM là-dedans ? Il n’y a pas de contexte d’exécution de défini. Ça ne me semble pas là même chose que LLVM, il n’y a pas de définition de comment doit être exécuter du bitcode LLVM.

ache

Je ne connais pas assez LLVM pour répondre à ces questions.

Soit dit en passant “modèle de calcul” c’est un truc précis en info qui n’a rien à voir avec une VM.

ache

En fait, ça n’a pas « rien à voir » : la définition générique d’une VM au sens 1. de mon message précédent est plutôt bien couverte par celle du modèle de calcul – dans le sens où une VM peut être vue comme un modèle de calcul, complexe certes mais bien un modèle de calcul. Si je reprends la définition de ton article, une machine virtuelle :

  • Décrit comment les fonctions « mathématiques » (ici des programmes) produisent leurs sorties par rapport aux entrées ;
  • Décrit des unités de calcul, de mémoire et leurs communications associées ;
  • Permet de calculer la complexité algorithmique associée.

Note que je n’ai pas besoin d’avoir une implémentation réelle de la VM définie ici pour que ça fonctionne.

D’ailleurs, dans l’article que tu cites renvoie à la machine de Turing, ou aux machines à piles – or la JVM ou la VM Python sont, en première approche, des machines à pile.

Là j’avaoue que je ne comprends pas. L’OS vérifierait donc les instructions machine de mon programme utilisateur avant qu’elles soit effectivement exécutées par le processeur ? Je n’ai jamais entendu parler d’une telle chose, pour moi l’OS met (schématiquement) le pointeur d’instructions au bon endroit (là où il y a les instructions de mon programme utilisateur) et à partir de là c’est « bare metal » pour mon programme utilisateur. Ce qui n’exclut aucunement les mécanismes de contrôle que les CPU modernes implémentent pour travailler main dans la main avec l’OS en lui faisant état des faults si l’exécution donne n’importe quoi. J’ai raté quelque chose ?

C’est bien ce qui se passe (avec en plus des communications entre l’OS et ton programme lorsque tu fais des appels systèmes, et l’aspect scheduler dont on a déjà parlé). Je sais pas ce que ache essaye de dire pour être franc… :-° Encore heureux que l’OS s’amuse pas à inspecter tout ce qui passe, ce serait très lent.

adri1

Ok, après vérification, les vérifications que je pensais être faite par le système d’exploitation sont faites par le MMU. Qui est donc reconfiguré à chaque changement de programme.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte