Je suppose que vous voulez une machine virtuelle plutôt qu'un simple interprète. Je pense que ce sont deux points sur un continuum. Un interpréteur travaille sur quelque chose de proche de la représentation originale du programme. Une machine virtuelle travaille sur des instructions plus primitives (et autonomes). Cela signifie que vous avez besoin d'une étape de compilation pour traduire l'un en l'autre. Je ne sais pas si vous voulez travailler sur ce point en premier ou si vous avez déjà une syntaxe d'entrée en tête.
Pour un langage dynamique, vous voulez un endroit qui stocke les données (sous forme de paires clé/valeur) et certaines opérations qui agissent sur elles. La VM maintient le magasin. Le programme qui s'exécute sur elle est une séquence d'instructions (y compris le flux de contrôle). Vous devez définir le jeu d'instructions. Je suggérerais un ensemble simple pour commencer, par exemple :
- les opérations arithmétiques de base, notamment les comparaisons arithmétiques, l'accès au magasin
- flux de contrôle de base
- impression intégrée
Il se peut que vous souhaitiez utiliser une approche de calcul par pile pour l'arithmétique, comme le font de nombreuses VM. Il n'y a pas encore beaucoup de dynamique dans ce qui précède. Pour y parvenir, nous avons besoin de deux choses : la possibilité de calculer les noms des variables à l'exécution (ce qui signifie simplement des opérations sur les chaînes de caractères), et un certain traitement du code comme des données. Cela pourrait être aussi simple que d'autoriser les références aux fonctions.
L'entrée dans la VM devrait idéalement se faire en bytecode. Si vous n'avez pas encore de compilateur, il pourrait être généré à partir d'un assembleur de base (qui pourrait faire partie de la VM).
La VM elle-même est constituée de la boucle :
1. Look at the bytecode instruction pointed to by the instruction pointer.
2. Execute the instruction:
* If it's an arithmetic instruction, update the store accordingly.
* If it's control flow, perform the test (if there is one) and set the instruction pointer.
* If it's print, print a value from the store.
3. Advance the instruction pointer to the next instruction.
4. Repeat from 1.
La gestion des noms de variables calculées peut être délicate : une instruction doit spécifier dans quelles variables se trouvent les noms calculés. Cela pourrait être fait en permettant aux instructions de se référer à un ensemble de constantes de chaînes de caractères fournies en entrée.
Un exemple de programme (en assembleur et en bytecode) :
offset bytecode (hex) source
0 01 05 0E // LOAD 5, .x
3 01 03 10 // .l1: LOAD 3, .y
6 02 0E 10 0E // ADD .x, .y, .x
10 03 0E // PRINT .x
12 04 03 // GOTO .l1
14 78 00 // .x: "x"
16 79 00 // .y: "y"
Les codes d'instruction impliqués sont :
"LOAD x, k" (01 x k) Load single byte x as an integer into variable named by string constant at offset k.
"ADD k1, k2, k3" (02 v1 v2 v3) Add two variables named by string constants k1 and k2 and put the sum in variable named by string constant k3.
"PRINT k" (03 k) Print variable named by string constant k.
"GOTO a" (04 a) Go to offset given by byte a.
Vous avez besoin de variantes pour quand les variables sont nommées par d'autres variables, etc. (et les niveaux d'indirection deviennent délicats à raisonner). L'assembleur regarde les arguments comme "ADD .x, .y, .x" et génère le bytecode correct pour l'addition à partir de constantes de chaîne (et non de variables calculées).
3 votes
Si vous êtes toujours intéressé, j'ai écrit une VM vraiment très simple en C. Jetez-y un coup d'oeil : github.com/tekknolagi/carp