Pulp, un environnement d'exécution multivitaminé

... vers lequel compiler vos micro-langages

a marqué ce sujet comme résolu.

J'en ai marre des problèmes avec Flex et Bison, il est grand temps de cloner le repo et de commencer à bosser sur pulp, ça sera plus marrant !

mehdidou99

Le prototype en python devrait être assez rigolo à développer. J'ai inclus du code qui pose les bases d'un design facile à étendre, donc on devrait sans trop peiner arriver à :

  • Lire et écrire des fichiers de bytecode,
  • Exécuter un bytecode existant,
  • Coder des outils qui font du debugging (afficher la stack pas à pas, etc),
  • Implémenter un premier mini-langage pour le premier POC (une calculatrice avec des variables),

Par contre une fois le premier cap passé, les choses vont commencer à se compliquer. Il y aura plusieurs directions dans lesquelles partir plus ou moins en parallèle :

  • Designer les objets internes de la VM (types de données natifs et opérations associées), donc commencer à réfléchir non seulement à un design qui les rende simples et uniformes, mais également à un garbage collector…
  • Designer les fonctions (type de données à part entière), donc bien comprendre et concevoir la notion de closure et trouver une façon correcte de les inclure dans les fichiers de bytecode, ajouter une pile d'appels donc gérer des stack frames…
  • Passer à une vraie implémentation (pour mon cas une VM en rust que j'appellerai Lust) risque d'apporter son lot de soucis (typiquement je redoute le passage à un typage statique pour les objets internes… Le duck typing de python dégage complètement cette difficulté dans le proto)

Mais dans tous les cas bosser sur une machine qui représente le fond du fond des niveaux d'abstraction des langages qui vont compiler vers elle, ouais, d'expérience, c'est vraiment sympa.

+3 -0

Salut,

Je suis en train de faire un transpilateur Python vers BrainFuck. Ça sert à rien, mais on reste un peu dans le thème des VM (je transpile une à une les instructions du bytecode Python) donc je me disait que ça me ferait un petit entraînement.

En plus on parlait de BrainFuck tout à l'heure, j'imagine que j'ai pris les propos de SpaceFox un peu trop au pied de la lettre :p

Ensuite, une fois que je serai un peu plus à l'aise avec les VM, je contribuerai peut-être :)

Je viens de pousser un début de spec en français dans le dossier doc/ du repo… À compléter.

N'hésitez pas à relire et commenter le début qui pose les bases de fonctionnement de la VM et du vocabulaire utilisé. Si quelquechose n'est pas clair c'est maintenant qu'il faut le corriger.

+0 -0

Mes commentaires sur la spec dans son état actuel.

1
[PUSH 1 | PUSH 2 | ADD | STORE 'foo' |PUSH 4 | LOAD 'foo' | ADD]

Si on suit ta spec, il devrait y avoir LET à la place de STORE.

STORE <sym> (0x27) : Dépile le sommet de la stack

Tu utilisais « pile » jusqu’à présent.

Sinon, je l’annonce officiellement : j’ai écrit un interpréteur de bytecode PULP en Rust qui suit ce qui a été défini dans ce sujet. Il me reste un certain nombre de choses à faire avant de vous le présenter.

  • Intégrer le double champ size et count : j’attends de savoir si tu considères que ça fait partie de la v0.1.0 de la spec ou si tu en fais une v0.2.0 (je préférerais la première solution).
  • Changer le nom de quelques opcodes, pour correspondre à la spec.
  • Écrire des segments d’exemple pour tester de manière exhaustive le code (j’ai pas essayé tous les opcodes encore).
  • Intégrer le système que j’ai prévu pour que l’interpréteur comprenne plusieurs versions du bytecode, même s’il n’y en a qu’une pour l’instant.
  • Utiliser le md5 pour vérifier l’intégrité du code (je ne le ferai peut-être pas, ça me saoule de trouver une version en Rust de md5 qui corresponde à mon besoin).

Mais là, je fatigue, alors ce sera pour un autre jour. :)

+5 -0

Je suis curieux d'en lire le code. À la rigueur plutôt que refaire ma propre implementation en rust from scratch, ça serait sûrement plus facile pour moi de contribuer à la tienne.

+0 -0

Certainement. Peux-tu répondre à mon premier point, que je sache à quoi m’en tenir ? Merci. :)

Dominus Carnufex

Pardon. Oui c'est censé être un LET, tu as raison. Selon la même spec ce code devrait lever une erreur critique. Pour ma défense j'ai tapé ça rapido dans le train ce soir et poussé dans la foulée. :)

+0 -0

Ah oui je vais l'intégrer dans la 0.1.0.

(Bon en toute rigueur ça devrait être la 0.1.1, mais ça va je suis pas au boulot donc je vais pas commencer à vous péter les noyaux avec des considérations sur les releases d'un toy project, et puis de toute façon tu es le premier client sérieux de cette spec, alors autant t'arranger :D)

+3 -0

On peut le voir comme une métaclasse, oui. En fait c'est surtout une fonction utilitaire qui crée une classe en fonction des paramètres que tu lui passes. La différence avec une vraie métaclasse, c'est que les métaclasses de Python sont traditionnellement des classes qui héritent de type.

+2 -0

On peut le voir comme une métaclasse, oui. En fait c'est surtout une fonction utilitaire qui crée une classe en fonction des paramètres que tu lui passes. La différence avec une vraie métaclasse, c'est que les métaclasses de Python sont traditionnellement des classes qui héritent de type.

nohar

Et une métaclasse est censée pouvoir s'utiliser avec un metaclass= quand on construit une classe. Ce qui n'est pas le cas de namedtuple qui ne respecte pas les paramètres standards d'une métaclasse.

J’ai avancé sur l’interpréteur de PULP en Rust. Voici les nouveautés de la journée.

  • La vérification de l’intégrité du fichier grâce à son condensat MD5 est opérationnelle.
  • Les tests de la phase de parsing sont exhaustifs et commentés.
  • La fonction principale a également sa série de tests élémentaires.
  • Les tests sont regroupés dans un même module.
  • Un certain nombre d’erreurs possibles supplémentaires sont détectées.

Le plus important qu’il reste à faire, c’est compléter les tests. Et si vous voulez participer, vous êtes les bienvenus. Même si vous ne connaissez pas beaucoup de Rust, vous pouvez vous entraîner, car ce qui reste, ce sont les tests les plus simples à écrire. En effet, il manque les tests portant sur cette fonction. Ou plus exactement, il faudrait compléter les tests existants pour atteindre l’exhaustivité : vous pouvez suivre le modèle pour vous aider.

Par ailleurs, il manque du matériel pour des tests « grandeur nature », c’est-à-dire des fichiers de bytecode complets réalisant un véritable travail. N’hésitez pas à en générer, c’est le meilleur moyen de tester intégralement l’interpréteur.

Il va cependant y avoir un problème avec ça, et là, je m’adresse surtout à @nohar. À l’heure actuelle, comme rien ne définit de quelle manière les différents segments doivent être exécutés, j’ai pris le parti de tous les exécuter dans l’ordre où ils apparaissent dans le fichier, et d’afficher à l’écran le sommet de la pile tel qu’il est à la fin de l’exécution de chaque segment.

Cela n’est cependant pas idéal, car cela ne me permet pas de tester automatiquement qu’un programme complet donne bien le résultat attendu. Il faudrait :

  • d’une part, qu’une règle soit définie quant à la manière dont les segments sont exécutés. Je vois deux possibilités pour cette première version de la spec (ça peut changer dans les versions suivantes de la spec, ce n’est pas gênant) :
    • seul le premier segment est exécuté et les suivants sont ignorés,
    • un nouveau type de segment est introduit qui décide dans quel ordre les segments sont exécutés, et ce qui doit advenir du résultat des segments qui ne sont pas le dernier (sans doute overkill pour une v0.1.0) ;
  • d’autre part, que la fonction principale de l’interpréteur renvoie soit le résultat du tout dernier segment exécuté (c’est-à-dire le sommet de la pile), soit un code d’erreur si le programme abort avant terme (ce qui n’est pas possible dans la version actuelle du bytecode, mais sera très certainement indispensable dans une version postérieure). Et surtout, que cette API ne change plus jamais dans les versions ultérieures du bytecode, pour ne pas se retrouver avec des signatures de type différentes pour la fonction principale.

Ces deux points permettraient également de considérablement simplifier l’écriture d’une fonction wrapper pour que la bibliothèque soit appelable depuis C, C++, Python, Haskell, etc., permettant à d’autres participants de l’utiliser pour leurs exécutables.

+1 -0

Je n'ai pas du tout eu le temps d'avancer depuis 2 jours (bouclage d'un papier pour une conf dont la date de soumission est demain), mais dans mon idée les segments de code sont censés être indépendants et constituer un module. La présence d'un segment special (un point d'entrée, main) rend le module exécutable (en fait un programme).

Il reste pas mal de choses à définir pour en arriver là mais toutes les suggestions/contributions à la spec sont les bienvenues.

+0 -0

Du coup, je propose d’ajouter les points suivants à la spec.

(1) Dans l’en-tête de chaque segment, après le champ type et le padding, se trouve un champ identifiant, composé d’une taille sur un octet et d’une suite des caractères UTF-8, sur le modèle des symboles dans les segments de code.

(2) Si un segment porte l’identifiant main, il constitue le point d’entrée du programme. En l’absence d’instructions de saut dans cette première version, il sera le seul exécuté. Si aucun segment ne porte cet identifiant, une erreur est levée quand on tente d’exécuter le programme.

(3) Une instruction abort(arg) d’opcode 0x601 est ajoutée. Elle permet d’interrompre le programme avant terme en renvoyant une information succincte sur le motif de l’erreur. Dans cette première version, un seul code d’erreur est reconnu, 0x0000 → Motif d’erreur inconnu.

(4) Un programme qui s’interrompt renvoie :

  • la valeur au sommet de la pile s’il est arrivé à terme ;
  • le code d’erreur et un moyen d’accéder à l’état interne du programme (par exemple, un pointeur) si l’instruction abort a été utilisée.

Évidemment, s’ils sont acceptés, tu pourras les rédiger autrement, pour que ça colle mieux avec ton style.

Il est à noter que si le 4e point est adopté, il devra l’être de manière définitive dans toutes les versions ultérieures du bytecode, afin que l’API principale ne change pas. À noter également que le fait de permettre l’accès à l’état interne du programme est une idée un peu folle que l’on peut laisser tomber : je sais que c’est faisable en C, C++ ou Rust sans changer l’API principale si le fonctionnement de l’état interne vient à changer, mais si c’est trop compliqué à mettre en place dans d’autres langages, on peut laisser tomber, et attendre une prochaine version du bytecode pour fournir une instruction comme dump qui permettrait de faire ça sans toucher à l’API principale.

À présent, c’est à vous. Si vous approuvez les 4 points, mettez un +1, si vous désapprouvez les 4, mettez un -1, et si vous êtes partagés, mettez un message pour dire pourquoi. :)


  1. Je suggère de réserver la série des 0x50 aux futures instructions de saut, et d’utiliser les 0x60 pour la communication avec l’extérieur du programme. On y trouvera dans des versions ultérieures les appels système, la FFI, etc. 

+4 -0

(3) Une instruction abort(arg) d’opcode 0x601 est ajoutée. Elle permet d’interrompre le programme avant terme en renvoyant une information succincte sur le motif de l’erreur. Dans cette première version, un seul code d’erreur est reconnu, 0x0000 → Motif d’erreur inconnu.

(4) Un programme qui s’interrompt renvoie :

  • la valeur au sommet de la pile s’il est arrivé à terme ;
  • le code d’erreur et un moyen d’accéder à l’état interne du programme (par exemple, un pointeur) si l’instruction abort a été utilisée.

Je ne suis pas sûr de comprendre : un programme qui exécute abort(0) renvoie 0 ? C'est un détail, mais c'est étrange.

Il est à noter que si le 4e point est adopté, il devra l’être de manière définitive dans toutes les versions ultérieures du bytecode, afin que l’API principale ne change pas.

C'est encore un peu tôt pour parler de ce genre de choses, mais soyez souples : vous n'avez pas d'utilisateur (non, zlang ça compte pas :p), ne vous laissez pas enfermer dans un choix juste pour préserver une compatibilité qui n'est pas vraiment indispensable :-)

À noter également que le fait de permettre l’accès à l’état interne du programme est une idée un peu folle que l’on peut laisser tomber : je sais que c’est faisable en C, C++ ou Rust sans changer l’API principale si le fonctionnement de l’état interne vient à changer, mais si c’est trop compliqué à mettre en place dans d’autres langages, on peut laisser tomber, et attendre une prochaine version du bytecode pour fournir une instruction comme dump qui permettrait de faire ça sans toucher à l’API principale.

Je ne suis pas sûr que ce soit très compliqué, et c'est probablement utile pour débuguer. L'avantage d'une VM, c'est que l'état est simple : il suffit de dumper la pile, les environnements et le code pointer dans un format quelconque et voilà.

À présent, c’est à vous. Si vous approuvez les 4 points, mettez un +1, si vous désapprouvez les 4, mettez un -1, et si vous êtes partagés, mettez un message pour dire pourquoi. :)

Dominus Carnufex

Globalement, je ne suis pas un expert du sujet, mais j'aime bien ce qui se dit ici :-)


  1. Je suggère de réserver la série des 0x50 aux futures instructions de saut, et d’utiliser les 0x60 pour la communication avec l’extérieur du programme. On y trouvera dans des versions ultérieures les appels système, la FFI, etc. 

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