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.

Bonsoir,

ça fait un grand moment que j’étudie les langages de programmations (le fonctionnement, et donc le comment en développer un) et je me rend compte que la seule chose qui est mis en avant, sont les phases de traitement du code : analyse lexicale, analyse syntaxique, analyse sémantique, génération du code intermédiaire, amélioration de ce dernier et génération du code machine. Parmis tout ce que j’ai listé, on retrouve des tas d’explications sur Internet, mais malgré tout ce que j’ai pu lire je ne comprend ni où est implémenté le fonctionnement concret du langage, ni le comment on fait cela.

Ce que j’entend par fonctionnement concret du langage c’est ce qui permet réelement le fonctionnement des items mis en place (if, let, while, etc.).

Par exemple, en entrant le code Python suivant :

a = 2

Que se passe il réelement ? La chaîne est lexé, parsé, etc. Mais, en soit, comment le langage créé il la variable ? Faut il toucher aux allocations de registres ou autres choses de bas niveau ? Car, jusque-là, je pensais qu’il suffisait d’affecter "2", dans notre cas, à une variable écrite dans le langage dans lequel le langage a été conçue. Et de plus, à quelle fase suis je censé effectuer ces actions que sont de gérer le comportement fondamental de chaque action possible du langage (while, for, match, etc.).

Et par la même occasion, pourriez vous m’expliquer comment développer la création d’une varible, par exemple ?

Cordialement.

+0 -0

Salut,

Je suis étonné que tu n’aies pas trouvé de ressources sur le sujet parce qu’il me semble qu’il y en a pas mal.

Pour l’exécution de ton langage tu as grossièrement 3 options :

  • Tu compiles vers un autre langage existant (typiquement C, assembleur ou langage machine par exemple) et rends donc ton programme exécutable par ce biais.
  • Tu codes un interpréteur où tu évalues chaque élément du langage au fur et à mesure : tu entres dans une condition quand tu rencontres un bloc if, une boucle sur un while, etc.
  • Enfin la solution intermédiaire qui consiste à compiler ton code vers une représentation simple (quadruples par exemple) pour laquelle tu implémentes une machine virtuelle. La machine virtuelle étant ainsi un interpréteur gérant un jeu d’instruction restreint (opérations arithmétiques, assignations, appels de fonctions, sauts) proche de l’assembleur mais plus abstrait.

Pour ce qui est des variables, le plus simple pour un langage de haut-niveau est de maintenir une table d’association nom <=> valeur. Mais tu peux si tu le préfères fonctionner comme avec des registres (et donc remplacer les accès aux variables par des opérations sur des emplacements particuliers).

Double par entwanne !

Je ne sais pas répondre directement à ta question, mais je vais te fourrnir une piste :
De nombreux langages utilisent un langage intermédiaire parfois nommé bytecode. Java génère aussi du bytecode java.

Ce code intermédiaire est ensuite soumis à un autre programme (appelé "machine virtuelle") qui se débrouille pour faire le travail. Donc, les réponses à tes questions sont dans les entrailles de ces machines virtuelles.

Mais là, ça devient bien plus compliqué, car il y a un grand nombre de machines virtuelles, dont la documentation est accessible aux experts.

+0 -0

Salut,

Pour être un peu plus complet, il faut bien voir que vu avec un peu recul, les trois options (interprété, en VM, en langage machine) citées par @entwanne ne sont en fait fondamentalement pas si différentes :

  • un interpréteur n’est pas très différent d’une machine virtuelle : au lieu d’exécuter du bytecode, tu exécutes la représentation intermédiaire utilisée par ton interpéteur ;
  • une machine virtuelle qui exécute du bytecode n’est pas très différente d’un processeur qui exécute du langage machine : dans le premier cas, l’implémentation (du comportement du bytecode) est faite par un programme qui s’exécute lui-même, dans le second cas l’implémentation (du comportement du langage machine) est faite physiquement via des circuits électroniques.

Si on reprend ta liste, la définition du comportement du langage se situe à plusieurs niveau :

  • analyse lexicale et syntaxique : celles-ci sont la plus éloignée du comportement du langage, c’est plutôt une vérification que le programme suit la grammaire qu’on s’est donné et est donc, effectivement, un programme ;
  • analyse sémantique : on commence déjà à toucher du doigt ce qui permet de faire le pont entre le programme en tant que suite de symboles dans un fichier texte et l’exécution sur la machine : on vérifie que le programme a du sens. Pour Python, ça se réduit à pas grand chose parce qu’il accepte à peu près n’importe quoi qui vérifie la grammaire. Pour un langage un peu plus strict comme C, on vérifie quelque petites choses élémentaires, du genre que l’utilisateur ne fait pas trivialement n’importe quoi avec les types. Pour des langages beaucoup plus stricts comme Rust, il y a un énorme boulot qui est fait ici pour s’assurer que le programme ne viole pas tout un tas d’invariants (les types sont corrects, les règles d’ownership sont respectées, les match sont exhaustifs, etc) qui offrent par la suite des garanties que le comportement du programme est correct.
  • génération du code intermédiaire, amélioration de ce dernier et génération du code machine : c’est là que le gros du travail est fait pour effectivement implémenter le comportement du programme, en générant du code machine (ou du code pour une machine virtuelle) qui correspond au comportement décrit par le programme. Et enfin, comme mentionné plus haut, le comportement de ce code machine est effectivement implémenté par les circuits électroniques (ou la VM choisie).

Ce que j’entend par fonctionnement concret du langage c’est ce qui permet réelement le fonctionnement des items mis en place (if, let, while, etc.).

Par exemple, en entrant le code Python suivant :

a = 2

Que se passe il réelement ? La chaîne est lexé, parsé, etc. Mais, en soit, comment le langage créé il la variable ? Faut il toucher aux allocations de registres ou autres choses de bas niveau ?

b4b4

Que se passe-t-il ? Beaucoup de choses. Comme tu le dis, Python va déjà lexer et parser ton programme. Puis il va le transformer en bytecode, qui est l’équivalent de l’assembleur de la machine virtuelle Python.

$ cat test.py
a = 2
$ python -m py_compile test.py
$ xxd __pycache__/test.cpython-310.pyc
00000000: 6f0d 0d0a 0000 0000 7173 d862 0600 0000  o.......qs.b....
00000010: e300 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0001 0000 0040 0000 0073 0800 0000 6400  .....@...s....d.
00000030: 5a00 6401 5300 2902 e902 0000 004e 2901  Z.d.S.)......N).
00000040: da01 61a9 0072 0300 0000 7203 0000 00fa  ..a..r....r.....
00000050: 0774 6573 742e 7079 da08 3c6d 6f64 756c  .test.py..<modul
00000060: 653e 0100 0000 7302 0000 0008 00         e>....s......

La sortie de xxd est une vue en hexa du contenu du fichier. C’est du binaire, que la machine virtuelle comprend. Pour avoir une forme un peu plus lisible (en vrai, le binaire montré ci-dessus contient des métadonnées en plus) :

$ python -m dis test.py
  1           0 LOAD_CONST               0 (2)
              2 STORE_NAME               0 (a)
              4 LOAD_CONST               1 (None)
              6 RETURN_VALUE

Les colonnes sont la ligne dans le code Python, l’adresse de l’instruction, l’instruction elle-même, l’adresse de la valeur/nom (si applicable), et la valeur/nom en clair (si applicable).

La VM Python est une VM à pile. Les deux premières instructions disent à la VM de mettre la valeur 2 sur la pile, et lui attribuer le nom a. Les deux dernières instructions sont là parce que Python traite ton programme un peu comme une fonction, qui renvoie None par défaut.

Quand tu donnes ce bytecode à la VM, elle exécute ces instructions les unes à la suite des autres. En l’occurrence, il y a toute une machinerie qui se met en route pour créer le runtime Python et implémenter le comportement voulu (en pratique en créant plein de PyObject en mémoire et en appelant leurs méthodes). Si on rend la chose un peu plus intéressante en ajoutant un print(a) à la fin du programme, voici ce que ça donne :

  1           0 LOAD_CONST               0 (2)
              2 STORE_NAME               0 (a)

  2           4 LOAD_NAME                1 (print)
              6 LOAD_NAME                0 (a)
              8 CALL_FUNCTION            1
             10 POP_TOP
             12 LOAD_CONST               1 (None)
             14 RETURN_VALUE

On empile la valeur 2, on lui attribue le nom a (première ligne du code). Puis on empile la fonction print (qui est un built-in, donc pas besoin de la définir), et on l’appelle. On appelle POP_TOP pour virer le retour de print du haut de la pile. Puis l’exécution se termine comme précédemment.

Si on fait le même exercice en Rust, avec ce programme :

fn main() {
    let a = 2;
    println!("{a}");
}

On peut obtenir (dans le répertoire target/debug/deps, ou target/release/deps) sous forme texte la sortie en représentation LLVM (qui serait l’équivalent du bytecode pour VM Python) ainsi que le code machine (sortie de LLVM, qui tourne directement sur le métal) avec la commande

$ cargo rustc -- --emit llvm-ir,asm

Je vais pas le copier/coller parce que c’est gros et carrément illisible sans un peu d’habitude.

+4 -0

Ma réponse part du principe que tu essaies ou comptes essayer d’implémenter un langage. Les solutions exposées restent donc dans le cadre d’un « langage jouet » et ne sont pas représentatives de la façon dont les implémentations sophistiquées des langages usuels fonctionnent.

Que se passe il réelement ? La chaîne est lexé, parsé, etc. Mais, en soit, comment le langage créé il la variable ? Faut il toucher aux allocations de registres ou autres choses de bas niveau ?

Peu de chance d’avoir besoin de toucher à ça à moins d’écrire un compilateur qui émet effectivement du code assembleur ou un JIT.

Et de plus, à quelle fase suis je censé effectuer ces actions que sont de gérer le comportement fondamental de chaque action possible du langage (while, for, match, etc.).

Je suppose qu’on pourrait appeler ça la phase d’évaluation. Une fois parsée, tu manipules en principe un arbre que tu peux parcourir et directement évaluer dans le cadre d’un langage jouet (sans passer par du bytecode comme le fait CPython par exemple). Par exemple, un if e1 then e2 else e3 pourrait finir en un objet de forme If(e1, e2, e3) qu’il sera possible d’évaluer comme suit :

  • Évaluer e1, (note qu’on parle d’évaluer dans la fonction d’évaluation, c’est récursif),
  • Si l’évaluation de e1 donne true, alors mon objet If(...) est évalué lui-même en l’évaluation de e2 (encore la récursion),
  • Sinon en l’évaluation de e3.

Le pattern matching qu’on a brièvement évoqué sur Discord joue son rôle ici-même, si ton langage le supporte. Mais sinon, ça reste bien-sûr possible de l’implémenter sans.

Ça peut te paraître très abstrait à ce stade. Quand tu l’implémenteras, les idées seront plus claires je pense.

Car, jusque-là, je pensais qu’il suffisait d’affecter "2", dans notre cas, à une variable écrite dans le langage dans lequel le langage a été conçue. […] Et par la même occasion, pourriez vous m’expliquer comment développer la création d’une varible, par exemple ?

Pour un langage jouet, ça peut être tout bêtement un dict d’environnement dans le langage qui implémente et interprète. C’est la première version « évidente », mais il y a bien-sûr beaucoup plus sophistiqué et performant, notamment quand il faut gérer correctement les portées. Mais je pense que tu auras le temps de voir arriver tout ça.

+0 -0

Ici, je vais prendre la création d’un variable dans un langage compilé. Python ayant déjà été répondu.

Il faut comprendre que le processeur comprend un certain nombre d’instructions. C’est le jeu d’instructions de ton processeur.

Mais alors, personne n’envoie d’instruction processeur directement. C’est le système d’exploitation qui envoie les instructions de ton programme. C’est son rôle de faire l’interface avec les composants.

Mais alors comment tu passes d’un code source à un programme ? Eh bien pour ça il faut le convertir en langage machine (ou du moins une représentation très proche du langage machine). Et ensuite traduire ce code machine dans un format compréhensible par le système d’exploitation, par exemple ELF sous Linux, ou PE sous Windows. À ce niveau là, les variables dites globales (ou plutôt statiques) ont généralement une section appropriée au niveau du code machine. La création d’une variable locale à une fonction s’effectue sur la pile. C’est à dire que c’est avec les instructions de gestions de pile que la variable va être “créer” (empilée en fait simplement). Comment on fait ça est spécifié dans ce qu’on appelle un ABI. Pour une allocation dynamique, il y a réservation d’un espace mémoire dans la RAM … et donc c’est composant externe, c’est au système d’exploitation de gérer ça. Généralement tu as des appels système pour ça.

Je pense avoir tout dis mais c’est assez condensé. N’hésite pas à poser des questions là où c’est pas claire.

+0 -0

Mais alors, personne n’envoie d’instruction processeur directement. C’est le système d’exploitation qui envoie les instructions de ton programme. C’est son rôle de faire l’interface avec les composants. Mais alors comment tu passes d’un code source à un programme ? Eh bien pour ça il faut le convertir en langage machine (ou du moins une représentation très proche du langage machine). Et ensuite traduire ce code machine dans un format compréhensible par le système d’exploitation, par exemple ELF sous Linux, ou PE sous Windows.

Bien sûr qu’on peut parfaitement envoyer des instructions au processeur directement. Déjà parce qu’il faut bien pouvoir programmer les OS (et autres composants systèmes) eux-mêmes, ensuite parce qu’on ne tourne pas toujours sur une plate-forme qui a un OS. Ce qu’un OS fait, c’est gérer l’état global du système (typiquement pour ce qui est de la distribution de la mémoire ainsi que du temps CPU, c’est un gros scheduler quoi) et fournir des interfaces pour les appels systèmes. Mais la plupart des instructions seront des instructions machines passées directement au CPU (encore heureux d’ailleurs). Un fichier typique ELF est bourré d’instructions processeurs (il suffit de faire un objdump -d de n’importe quel exécutable pour s’en rendre compte), l’intérêt du format est que plusieurs OS le comprennent pour pouvoir exécuter du code sur plusieurs architectures. Mais le contenu lui-même est principalement du code machine directement, pour l’architecture cible. Pas un truc d’un niveau intermédiaire. C’est un wrappeur bien pratique autour du code machine, mais pas grand chose de plus (c’est bien tout l’intérêt d’ailleurs!).

+0 -0

Bien sûr qu’on peut parfaitement envoyer des instructions au processeur directement.

Non. Si tu as un OS, c’est que tu exploites une faille de sécurité pour faire ça, tu casses tout d’ailleurs. Alors oui, si tu n’as pas d’OS alors tu peux le faire; En fait, si tu n’as pas d’OS, t’as juste pas le choix.
Alors oui quand j’ai dis personne c’était pas précis merci de préciser.

Mais la plupart des instructions seront des instructions machines passées directement au CPU (encore heureux d’ailleurs)

Heureusement que tu précises la plupart … L’OS vérifie ce que tu fais. L’idée ici c’était de préciser que c’était l’OS qui passait les instructions machines au proco. Afin justement de pas avoir besoin de parler d’ordonnancement.

Un fichier typique ELF est bourré d’instructions processeurs

Je sais bien … Tu avais déjà parler des méta-data associées au fichier objets pour leur donné un sens. C’était sur du bytecode python mais l’idée est exactement la même pour un fichier ELF ou PE.

J’ai juste dit qu’il fallait utiliser un format pour que l’OS l’interprète. Alors le terme traduire était peut-être pas forcément des plus claire. Mais l’idée était qu’à partir d’un format texte ou quelconque du langage machine, là il fallait bien des instructions processeurs et que ça suive le format d’exécutable de l’OS (pour que l’OS comprenne que tes données là, c’est du code machine à exécuter).

Ce qu’un OS fait, […] c’est un gros scheduler quoi

Non, ce n’est qu’une partie de ce que fait l’OS. Une définition simple de la tâche d’un OS c’est offrir une surcouche applicative en faisant l’interface avec le matériel. C’est pas juste un “scheduler”.

+0 -2

Bonjour,

Merci pour vos réponses, j´ai alors de nouvelles questions :

  1. vos explications étaient pour la grande majorité basé sur l´idée que j´utiliserai une VM pour exécuter le programme, or j´avais plutôt en tête d´utiliser LLVM ou d´utiliser la manière classique qu´est de générer de l´assembleur puis de l´exécuter. Admettons, que je veuille utiliser LLVM, @adri1 a dit que la phase dans laquelle j´implémenterai le fonctionnement de chaques mots clé serai dans les 4 derniéres (analyse sémantique, etc.). Il a aussi dit que on ne touchait qu´un peut du doight au niveau de l´analyse sémantique. Mais alors, étant donné que LLVM s´occupe tout seul des 3 dernières fases que sont le back-end, je dois tout gèrer à l´analyse sémantique ?
  2. qu´est ce qui est transformé en bytecode / ASM / cible LLVM ? Est-ce le programme de l´utilisateur converti par de multiples couches, où est-ce ce qui doit être effectué suite au programme de l´utilisateur ? S´il s´avère que c´est la proposition numéro 2, alors ça veut dire que le code que j´ai justement développé comme étant le back-end de tous les mot clés est l´essence même de mon langage de programmation ? Et donc, est-ce que ce sont ces lignes de codes dédié au fonctionnement du langage qui sont empilés les unes sur les autres par rapport au code source, auquel cas ce n´est qu´un vulgaire tas d´informations répété. Et si c´est le cas, c´est que je dois avoir fais une représentation intermédiaire du fonctionnement du programme avant de passer la main â LLVM ? Sauf qu´il me semblait que la génération de code IR était effectué par LLVM. Comment faire pour correctement emboîter ces pavés ?

Cordialement.

+0 -0

je dois tout gèrer à l´analyse sémantique ?

Oui, mais tu as aussi la génération du code LLVM correspondant (LLVM-IR) qui n’est pas à négliger.

Je n’ai pas compris ton deuxième point. Par-contre :

Sauf qu´il me semblait que la génération de code IR était effectué par LLVM.

Non, c’est à toi de le faire. Mais LLVM produit également un IR optimisé (à partir de celui que tu fourni) et est capable de convertir cet IR en langage machine.

+0 -0
  1. vos explications étaient pour la grande majorité basé sur l´idée que j´utiliserai une VM pour exécuter le programme, or j´avais plutôt en tête d´utiliser LLVM ou d´utiliser la manière classique qu´est de générer de l´assembleur puis de l´exécuter.

Note que LLVM est une machine virtuelle. C’est juste qu’au lieu d’exécuter le bytecode ou l’IR que tu lui donnes, elle le compile en code machine. C’est pour ça que je dis que le llvm-ir généré par rustc (en fait, il passe du bytecode à LLVM, voir la sortie llvm-bc) est conceptuellement à peu près la même chose que le bytecode sorti par py_compile.

Admettons, que je veuille utiliser LLVM, @adri1 a dit que la phase dans laquelle j´implémenterai le fonctionnement de chaques mots clé serai dans les 4 derniéres (analyse sémantique, etc.). Il a aussi dit que on ne touchait qu´un peut du doight au niveau de l´analyse sémantique.

Ce que j’ai dit pouvait prêter à confusion. L’analyse sémantique est vraiment un travail préparatoire à l’implémentation elle-même, c’est une vérification que ton programme a certaines propriétés demandées par ton langage (un gros morceau pour les langages à typage statique étant souvent que le typage est correct et cohérent). Note que cette phase est purement de la vérification, ça n’affecte pas la génération de code pour la VM choisie (tiens d’ailleurs, un exemple de ça est que le projet ajoutant un frontend Rust à GCC viendra sans borrow checker dans un premier temps, et va donc accepter du code incorrect). Il y a des chances que pour ton langage, cette phase soit plutôt réduite.

qu´est ce qui est transformé en bytecode / ASM / cible LLVM ? Est-ce le programme de l´utilisateur converti par de multiples couches, où est-ce ce qui doit être effectué suite au programme de l´utilisateur ? S´il s´avère que c´est la proposition numéro 2, alors ça veut dire que le code que j´ai justement développé comme étant le back-end de tous les mot clés est l´essence même de mon langage de programmation ? Et donc, est-ce que ce sont ces lignes de codes dédié au fonctionnement du langage qui sont empilés les unes sur les autres par rapport au code source, auquel cas ce n´est qu´un vulgaire tas d´informations répété. Et si c´est le cas, c´est que je dois avoir fais une représentation intermédiaire du fonctionnement du programme avant de passer la main â LLVM ? Sauf qu´il me semblait que la génération de code IR était effectué par LLVM. Comment faire pour correctement emboîter ces pavés ?

Je t’invite à relire ton précédent sujet où on t’explique déjà que c’est à toi de générer la LLVM-IR. Le terme LLVM-IR peut être trompeur, dans le sens où ce n’est pas l’IR de LLVM elle-même mais est en fait l’entrée de LLVM. C’est une IR du point de vue de ton compilateur (en général, la dernière que tu vas manipuler, puisque c’est ensuite LLVM qui prend la main). Tu peux voir ça comme un programme écrit dans le langage de LLVM, que LLVM va compiler en langage machine. La représentation intermédiaire entre LLVM-IR et le code machine s’appelle MIR (Machine Intermediate Representation), à ne pas confondre avec MIR en Rust (mid-level IR, dont tu as peut être déjà entendu parler puisque miri n’est rien d’autre qu’un interpréteur de cette IR) qui est une représentation interne utilisée par rustc juste avant d’émettre la LLVM-IR.

Je pense que tu y verrais beaucoup plus clair en suivant le tuto de LLVM.

+1 -0

Bien sûr qu’on peut parfaitement envoyer des instructions au processeur directement.

Non. Si tu as un OS, c’est que tu exploites une faille de sécurité pour faire ça, tu casses tout d’ailleurs. Alors oui, si tu n’as pas d’OS alors tu peux le faire; En fait, si tu n’as pas d’OS, t’as juste pas le choix.

[…]

Mais la plupart des instructions seront des instructions machines passées directement au CPU (encore heureux d’ailleurs)

Heureusement que tu précises la plupart … L’OS vérifie ce que tu fais. L’idée ici c’était de préciser que c’était l’OS qui passait les instructions machines au proco. Afin justement de pas avoir besoin de parler d’ordonnancement.

ache

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 ?

qu´est ce qui est transformé en bytecode / ASM / cible LLVM ?

Dans un cas général, c’est l’AST dont on a parlé plus haut (généralement obtenu comme produit après avoir parsé le langage alors en format textuel) qui permet de commencer à travailler.

C’est un peu abstrait alors voici un exemple peut-être un peu plus parlant. Imagine un bout de texte écrit en JSON ou XML. Tu as peut-être déjà eu l’occasion de travailler avec ces formats, auquel cas, nous sommes d’accord que tu n’as pas manipulé la donnée textuelle brute, mais plutôt un objet directement manipulable dans ton langage de travail (des dict et des list en Python par exemple, ou des objets plus complexes comme ElementTree). À ce stade, tu es donc capable de travailler correctement avec une représentation abstraite pour faire plein de choses : parcourir l’arbre au gré de tes besoins, analyser son contenu, etc.

Là c’est un peu pareil : on ne sait que faire du texte brut d’entrée, on veut donc une forme qui te permette de travailler. a = 42 ne veut rien dire, mais sa forme abstraite Affectation("a", LiteralInt(42)) (c’est un exemple) possède la teneur en information nécessaire pour travailler et servir à plusieurs choses (une ou plusieurs, au choix et selon tes besoins) :

  • faire une analyse de cohérence,
  • produire du bytecode,
  • produire des instructions LLVM,
  • produire du code machine,
  • évaluer directement,
  • remanier la structure, etc.
+0 -0

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.

+0 -0

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 ?

Ben je peux me tromper mais pour moi il vérifie déjà que ce soit des instructions valides et pas des données aléatoires n’ayant aucun sens (ne correspondant à aucune instruction par exemple). Ensuite je pensais au appels systèmes qu’il nous interdit de faire tant qu’on a pas les droits nécessaires. C’est peut-être des mécanismes gérés autrement et j’ai mal compris cette partie là.

Pour moi, le pointeur d’exécution était géré par l’OS, ce n’est pas le cas ?

+0 -0

Avec tout ce que vous avez rédiger, ma question va peut être paraître stupide mais, ce qui est transformé en IR puis en code machine, c´est bien la représentation de l´AST (après la phase d´analyse sémantique) ?

b4b4

Oui. Mais c’est à toi de faire le passage de l’AST à la représentation intermédiaire. LLVM ne peut pas comprendre juste ton AST.

+0 -0

Ben je peux me tromper mais pour moi il vérifie déjà que ce soit des instructions valides et pas des données aléatoires n’ayant aucun sens (ne correspondant à aucune instruction par exemple).

Non, ça c’est le boulot du compilateur. Si t’envoies n’importe quoi à ton CPU, soit il va faire n’importe quoi, soit il va se plaindre à l’OS. Mais l’OS vérifie pas en temps réel que tu envoies pas n’importe quoi à ton CPU.

Ensuite je pensais au appels systèmes qu’il nous interdit de faire tant qu’on a pas les droits nécessaires. C’est peut-être des mécanismes gérés autrement et j’ai mal compris cette partie là.

ache

Les appels systèmes sont gérés par l’OS. Cela dit, certaines questions de permissions au sens large peuvent aussi être déléguées au matériel lui-même (genre les trucs à la Intel PTT).

Avec tout ce que vous avez rédiger, ma question va peut être paraître stupide mais, ce qui est transformé en IR puis en code machine, c´est bien la représentation de l´AST (après la phase d´analyse sémantique) ?

Note que tu manipules un AST quasiment dès le début, la phase d’analyse syntaxique s’en sert. Puis tu peux avoir plusieurs représentations internes successives. Par exemple, rustc fait le type checking (partie de l’analyse sémantique) sur une représentation dite de haut niveau, qui est un AST un peu travaillé, puis passe à la représentation intermédiaire MIR sur laquelle il passe le borrow checker (reste de l’analyse sémantique), puis optimise cette IR, puis la traduit en LLVM-IR et passe la main à LLVM.

L’AST est déjà une IR. Tu peux la traduire directement en LLVM-IR, ou bien avoir d’autres IR entre les deux.

+0 -0

Ca m’arrange que le travail à faire se base intégralement sur la représentation alambiqué du code source originel, sauf que c’est conceptuellement illogique, car, si c’est bien l’AST qui terminera en code machine, celui_ci n’a aucun sens ! Ca voudrait dire que le code décrivant le réel fonctionnement des mot clés et du code en général ne sert à rien.

Je suis confus, bien trop confus.

Euh… Quoi ? Ton boulot est justement de produire une LLVM-IR qui a le même sens que le programme écrit par l’utilisateur. Puis le boulot de LLVM est de produire du code machine qui a le même sens que l’IR que tu lui donnes. Vois ça comme des traductions successives. J’ai un peu de mal à comprendre ce que tu ne comprends pas…

+2 -0

Ca a déjà nettement plus de sens. Donc, je récapitule l’ensemble des étapes, en prennant en compte l’objet de ce sujet :

  • à l’aide d’une cli, j’informe mon compilateur quel fichier compiler ;
  • 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
    
  • 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 ;
  • 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.
  • 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é ;
  • 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 ?) ;
  • l’IR amélioré sera transformé en code machine qui je crois est de l’assembleur, le tout, à l’aide d’outil de LLVM ;
  • finalement, l’utilisateur se retrouve avec un exécutable qui je crois, pourra être exécuté sur n’importe quel ordinateur.

J’ai certainement fais des erreurs, alors j’attend vos remarques.

Cordialement.

+0 -0

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.

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