Pulp, un environnement d'exécution multivitaminé

... vers lequel compiler vos micro-langages

a marqué ce sujet comme résolu.

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

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.

L’idée, c’est plutôt qu’un programme qui a réussi et termine sur une valeur de 0 renvoie TOUT_EST_BON 0 alors qu’un programme qui exécute abort(0) renverra C_EST_LA_MERDE 0. En Rust, ça correspondrait à un type Result (respectivement Ok(0) et Err(0)), mais je veux laisser la liberté à chaque langage de gérer ça comme il veut. :)

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

En fait, c’est plus que ça simplifierait les choses pour les langages typés statiquement. Par exemple, si on part là-dessus, ma bibliothèque aurait un point d’entrée unique, à savoir quelque chose comme…

1
pub fn run(bytecode : &[u8]) -> Result<Option<Const>, (u32, *const u8, usize)>

Si dans les versions ultérieures du bytecode le format de la sortie ultime du programme change, ça m’obligerait à modifier cette signature, et ça m’empêcherait d’avoir une bibliothèque capable de gérer plusieurs versions du bytecode en parallèle. Et ça me fait chier. ^^

+1 -0

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.

L’idée, c’est plutôt qu’un programme qui a réussi et termine sur une valeur de 0 renvoie TOUT_EST_BON 0 alors qu’un programme qui exécute abort(0) renverra C_EST_LA_MERDE 0. En Rust, ça correspondrait à un type Result (respectivement Ok(0) et Err(0)), mais je veux laisser la liberté à chaque langage de gérer ça comme il veut. :)

Du côté du langage (donc des choix spécifiques d'implémentation), il vaudrait mieux renvoyer Err(ErrUnknown) plutôt que d'utiliser des entiers magiques, mais au niveau du système qui exécute ton programme, tu n'as typiquement pas accès à ce genre de distinction mais seulement au code de retour du programme. Note qu'une solution pas chère serait de remplacer le code de l'erreur inconnue par 0x1 :-)

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

En fait, c’est plus que ça simplifierait les choses pour les langages typés statiquement. Par exemple, si on part là-dessus, ma bibliothèque aurait un point d’entrée unique, à savoir quelque chose comme…

1
pub fn run(bytecode : &[u8]) -> Result<Option<Const>, (u32, *const u8, usize)>

Si dans les versions ultérieures du bytecode le format de la sortie ultime du programme change, ça m’obligerait à modifier cette signature, et ça m’empêcherait d’avoir une bibliothèque capable de gérer plusieurs versions du bytecode en parallèle. Et ça me fait chier. ^^

Dominus Carnufex

Bien sûr, il vaut mieux faire les bons choix dès le début et éviter de changer des choses, parce qu'utilisateur ou pas, ça demande du travail. Ce que je voulais dire, c'est qu'il ne faut pas vous dire dès maintenant « si on fait ça, on ne change plus ».

Pour ce problème précis de vouloir gérer plusieurs versions en parallèle, there is no problem in computer science which cannot be solved by one more level of indirection : il suffit d'avoir une fonction principale qui appelle la fonction run qui va bien et roulez jeunesse. Ceci étant dit, d'un point de vue purement pratique, j'ai des doutes sur le fait que ce soit une bonne idée de prévoir dès à présent de gérer plusieurs versions, alors que c'est encore très instable. Mon avis très personnel est qu'il vaut mieux attendre la stabilité pour prévoir ça (stabilité qui viendra après un certain temps de trial and error).

+1 -0

Un point sur l'environnement, pour voir si j'ai bien compris.

  • Un scope lexical, c'est une association clé → valeur.
  • L'environnement est une pile de scope lexicaux, indépendante de la pile de valeurs de travail.
  • Le scope en bas de la pile-environnement est appelé « scope global » et est indestructible, en dehors de ça il n'a rien de spécial.
  • Si je cherche une valeur dans le scope de clé k, je regarde dans le scope en haut de l'environnement, puis le suivant, etc… jusqu'en bas. Et je crashe le programme si ne ne trouve pas.

J'ai bon ?

Une autre remarque : j'ai l'impression qu'on a un problème de boutisme quelque part. Sauf erreur de ma part, il n'est pas défini dans les specs.

Or, si je prends cet exemple de AlphaZeta, j'ai du little-endian :

1
2
3
0x02 0x00          2 constantes (6 et 7)
  0x01 0x06 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
  0x01 0x07 0x00 0x00 0x00 0x00 0x00 0x00 0x00  

Sauf que si je veux faire un interpréteur en Java, je suis en big-endian :

1
2
3
4
5
6
7
8
public class Write {
    public static void main(String... args) throws IOException {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("test.bin"))) {
            dos.writeLong(6L);
            dos.writeLong(7L);
        }
    }
}

Ce qui donne :

1
2
3
4
5
6
$ xxd -c8 test.bin
00000000: 0000 0000 0000 0006  ........
00000008: 0000 0000 0000 0007  ........
$ hd test.bin 
00000000  00 00 00 00 00 00 00 06  00 00 00 00 00 00 00 07  |................|
00000010

Il faudrait donc choisir, et j'imagine que quel que soit le choix, ça va emmerder quelqu'un pour la réalisation de l'implémentation.

Ce n’est pas encore intégré à la spec disponible sur le repo, mais il y a ça dans le message d’origine (le gras est de moi).

À haut niveau, un fichier de bytecode est un binaire qui va se représenter comme suit (tout est représenté en little-endian, si chaîne de caractères il doit y avoir, elles seront encodées en utf-8 ou bien en ASCII…, sachant que si vous pouvez décoder de l'utf-8, vous gèrerez l'ASCII automatiquement) :

nohar

+0 -0

Il serait peut-être bien de rajouter au moins une ou deux instructions de saut conditionnels, non ?

A l'heure actuel on peut guerre faire mieux qu'une calculatrice avec variable et la moitié des instructions de manipulation de la pile ont peut de chances d’être utilisé.

Au moins avec un saut conditionnel on pourrait faire un petit langage d'entrée qui faciliterai la génération de codes de tests, non ?

Les sauts sont prévus, oui.

Je n'ai pas eu le temps de m'y recoller depuis la semaine dernière. Je rattrape tout ça demain.

PS : La bonne raison pour laquelle ils ne sont pas dans la première itération, c'est qu'ils demandent d'avoir des objets booléens, donc un type de données différent des entiers, qui sont LE seul type auquel on se restreint au départ, le temps de valider tout le boilerplate.

+0 -0

Quoi ? Tu ne veux pas considérer que 0 = False et tout le reste c'est True ?

Saroupille

Je veux éviter d'avoir à le refaire plus tard. Ces opcodes doivent accepter n'importe quel type d'argument convertible en booléen, donc ça demande de prévoir un mécanisme de conversion automatique plutôt que de l'implémenter de façon ad-hoc sur des entiers, C-style, dès le départ et devoir recoder tous ces opcodes par la suite.

+0 -0

Ces opcodes doivent accepter n'importe quel type d'argument convertible en booléen

Dans un certain nombre de langages fortement typés, comme Haskell ou Rust, les blocs conditionnels acceptent des booléens, et c’est tout. Et il n’existe pas de conversion entre entiers ou autre chose et booléens. Perso, je trouve ça plus propre. :)

+5 -0

Tu pourrais en effet très bien laisser à la charge du niveau au dessus de s'assurer la conversion si il veut le permettre. Après je m'en fout un peu, c'est juste qu'en l'absence de saut ça limite fortement l'utilité de la machine. c'est pluz fondamentale selon moi que les environnements par exemple.

Ces opcodes doivent accepter n'importe quel type d'argument convertible en booléen

Dans un certain nombre de langages fortement typés, comme Haskell ou Rust, les blocs conditionnels acceptent des booléens, et c’est tout. Et il n’existe pas de conversion entre entiers ou autre chose et booléens. Perso, je trouve ça plus propre. :)

Dominus Carnufex

Et si tu veux implémenter un langage dynamique qui tourne sur cette machine, il faut que ce soit possible.

Attention aux responsabilités ici : si tu veux faire un langage fortement typé, c'est à toi de faire un typeur fort dans ton compilateur, mais pas du tout à la machine virtuelle de renforcer ce type de règles. Au contraire, la machine peut elle-même fournir des constructions natives (types d'objets, etc.), et ces constructions doivent former un ensemble cohérent. Typiquement si la machine gère un type string natif, il faut qu'il soit utilisable si on le passe en argument d'un POP_JMP_IF_TRUE. Si on propose un type flottant, il faut qu'on sache gérer le cast en entier, etc.

Tu pourrais en effet très bien laisser à la charge du niveau au dessus de s'assurer la conversion si il veut le permettre. Après je m'en fout un peu, c'est juste qu'en l'absence de saut ça limite fortement l'utilité de la machine. c'est pluz fondamentale selon moi que les environnements par exemple.

Kje

C'est plus fondamental en termes d'utilisation. En termes d'implémentation c'est juste remplacer un incrément du code_pointer par un saut vers une nouvelle adresse. Maintenant si vous voulez proposer une spec pour ces opcodes, vous en êtes tout à fait libres, je viens juste de donner la raison pour laquelle ce n'était pas dans ma première itération, à savoir qu'il y avait des trouzillions de choses plus urgentes et plus risquées sur lesquelles travailler avant de rajouter les pré-requis de ces opcodes.

PS : Sans environnement pas de variable. Sans variable, existe-t-il un programme utile qui réalise un saut ?

+0 -0

Attention aux responsabilités ici : si tu veux faire un langage fortement typé, c'est à toi de faire un typeur fort dans ton compilateur, mais pas du tout à la machine virtuelle de renforcer ce type de règles.

Dans ce cas, la machine n’a pas à définir de types. Elle doit se contenter de définir des valeurs sur $n$ octets, à charge pour le niveau au-dessus de lui donner du sens. Par exemple, le type Int que tu as défini dans la première version devrait être Var8, une variable sur 8 octets, qui sera interprétée comme un entier signé si elle est utilisée avec un opcode Add et comme un f64 si elle est utilisée avec FAdd, etc.

Si tu considères qu’il y a une différence de nature entre une constante Int(97) et une constante Char('a') et que celle-ci doit être intégrée comme telle dans le bytecode, alors le bytecode doit faire respecter cette différence de nature, et utiliser FAdd sur un Int doit causer une erreur.

Rien n’empêche d’avoir des opcodes permettant de faire de la conversion entre différents types (particulièrement entre différents types numériques, et entre entiers et caractères (ord et chr, en gros)), qu’un langage dynamique pourra utiliser pour sa tambouille interne, mais rien n’oblige à ce qu’il y ait une conversion possible entre un entier et un booléen, parce que cela n’a pas nécessairement de sens. :)

+0 -0

En fait on peut voir le problème dans les deux sens. Soit la machine virtuelle est haut niveau et elle se permet de faire des conversions, à charge du langage fortement typé qui l'utilise de provoquer une erreur si il ne veut pas l'accepter, soit la machine virtuelle n'accepte que des bools pour les saut et c'est au langage haut niveau qui utilise cette machine virtuelle d'enclencher une conversion implicite avant le saut.

Instinctivement le deuxième me semble plus logique si on veut permettre aux deux types de langages de fonctionner correctement mais en soit c'est un parti prit : faire une vm haut niveau (typiquement celle de Python) ou une plus bas niveau (avec des bytecode typé)

Voilà, c'est précisément parce qu'il faut faire ce style de choix que j'ai gardé le sujet pour plus tard (ou maintenant si vous préférez). Perso je ne vois pas du tout l'avantage de faire une VM si c'est pour manipuler des blobs non-typés et faire des castings dangereux comme ceux du C. Ça veut dire multiplier les opcodes et saturer le vocabulaire du langage avec des instructions typées qui sont responsables de donner du sens aux données et ça me rappelle beaucoup trop Perl pour me plaire.

Mais il y a peut-être des avantages à cette approche que je ne mesure pas.

+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