Construire un interpréteur en Java - Implémenter le moteur d'exécution

Précédent 1 2 3 Page 2 Suivant Page 2 de 3

Autres aspects: chaînes et tableaux

Deux autres parties du langage BASIC sont implémentées par l'interpréteur COCOA: les chaînes et les tableaux. Regardons d'abord l'implémentation des chaînes.

Pour implémenter les chaînes en tant que variables, la Expressionclasse a été modifiée pour inclure la notion d'expressions "chaîne". Cette modification a pris la forme de deux ajouts: isStringet stringValue. La source de ces deux nouvelles méthodes est indiquée ci-dessous.

String stringValue (Program pgm) jette BASICRuntimeError {throw new BASICRuntimeError ("Aucune représentation de chaîne pour cela."); } booléen isString () {return false; }

De toute évidence, il n'est pas trop utile pour un programme BASIC d'obtenir la valeur de chaîne d'une expression de base (qui est toujours une expression numérique ou booléenne). Vous pourriez conclure du manque d'utilité que ces méthodes n'appartenaient alors pas Expressionet appartenaient à une sous-classe de la Expressionplace. Cependant, en plaçant ces deux méthodes dans la classe de base, tous les Expressionobjets peuvent être testés pour voir si, en fait, ce sont des chaînes.

Une autre approche de conception consiste à renvoyer les valeurs numériques sous forme de chaînes à l'aide d'un StringBufferobjet pour générer une valeur. Ainsi, par exemple, le même code pourrait être réécrit comme:

String stringValue (Program pgm) jette BASICRuntimeError {StringBuffer sb = new StringBuffer (); sb.append (this.value (pgm)); return sb.toString (); }

Et si le code ci-dessus est utilisé, vous pouvez éliminer l'utilisation de isStringcar chaque expression peut renvoyer une valeur de chaîne. En outre, vous pouvez modifier la valueméthode pour essayer de renvoyer un nombre si l'expression correspond à une chaîne en l'exécutant via la valueOfméthode de java.lang.Double. Dans de nombreux langages tels que Perl, TCL et REXX, ce type de typage amorphe est utilisé à grand avantage. Les deux approches sont valables et vous devez faire votre choix en fonction de la conception de votre interprète. En BASIC, l'interpréteur doit renvoyer une erreur lorsqu'une chaîne est affectée à une variable numérique, j'ai donc choisi la première approche (renvoyer une erreur).

En ce qui concerne les tableaux, il existe différentes manières de concevoir votre langage pour les interpréter. C utilise les crochets autour des éléments du tableau pour distinguer les références d'index du tableau des références de fonction qui ont des parenthèses autour de leurs arguments. Cependant, les concepteurs de langage de BASIC ont choisi d'utiliser des parenthèses pour les fonctions et les tableaux. Ainsi, lorsque le texte NAME(V1, V2)est vu par l'analyseur, il peut s'agir d'un appel de fonction ou d'une référence de tableau.

L'analyseur lexical distingue les jetons suivis de parenthèses en supposant d'abord qu'ils sont des fonctions et en testant cela. Ensuite, il continue pour voir s'il s'agit de mots-clés ou de variables. C'est cette décision qui empêche votre programme de définir une variable nommée «SIN». Toute variable dont le nom correspondait à un nom de fonction serait renvoyée par l'analyseur lexical comme un jeton de fonction à la place. La deuxième astuce utilisée par l'analyseur lexical est de vérifier si le nom de la variable est immédiatement suivi de `('. Si c'est le cas, l'analyseur suppose qu'il s'agit d'une référence de tableau. En analysant cela dans l'analyseur lexical, nous éliminons la chaîne` MYARRAY ( 2 )'d'être interprété comme un tableau valide (notez l'espace entre le nom de la variable et la parenthèse ouverte).

La dernière astuce pour implémenter des tableaux est dans la Variableclasse. Cette classe est utilisée pour une instance d'une variable, et comme je l'ai expliqué dans la colonne du mois dernier, il s'agit d'une sous-classe de Token. Cependant, il dispose également de certaines machines pour prendre en charge les tableaux et c'est ce que je vais montrer ci-dessous:

class La variable étend le jeton {// Sous-types de variables légales final static int NUMBER = 0; int statique final STRING = 1; int statique final NUMBER_ARRAY = 2; int statique final STRING_ARRAY = 4; Nom de la chaîne; int subType; / * * Si la variable est dans la table des symboles, ces valeurs sont * initialisées. * / int ndx []; // index du tableau. int mult []; // multiplicateurs de tableau double nArrayValues ​​[]; String sArrayValues ​​[];

Le code ci-dessus montre les variables d'instance associées à une variable, comme dans la ConstantExpressionclasse. Il faut faire un choix sur le nombre de classes à utiliser par rapport à la complexité d'une classe. Un choix de conception pourrait être de créer une Variableclasse qui ne contient que des variables scalaires, puis d'ajouter une ArrayVariablesous-classe pour traiter les subtilités des tableaux. J'ai choisi de les combiner, transformant les variables scalaires essentiellement en tableaux de longueur 1.

Si vous lisez le code ci-dessus, vous verrez des indices de tableau et des multiplicateurs. Celles-ci sont ici car les tableaux multidimensionnels en BASIC sont implémentés à l'aide d'un seul tableau Java linéaire. L'index linéaire dans le tableau Java est calculé manuellement à l'aide des éléments du tableau multiplicateur. La validité des indices utilisés dans le programme BASIC est vérifiée en les comparant à l'indice légal maximum dans le tableau ndx des indices .

Par exemple, un tableau BASIC avec trois dimensions de 10, 10 et 8 aurait les valeurs 10, 10 et 8 stockées dans ndx. Cela permet à l'évaluateur d'expression de tester une condition «index hors limites» en comparant le nombre utilisé dans le programme BASIC au nombre légal maximum qui est maintenant stocké dans ndx. Le tableau multiplicateur dans notre exemple contiendrait les valeurs 1, 10 et 100. Ces constantes représentent les nombres que l'on utilise pour mapper à partir d'une spécification d'index de tableau multidimensionnel dans une spécification d'index de tableau linéaire. L'équation réelle est:

Index Java = Index1 + Index2 * Taille maximale de l'Index1 + Index3 * (Taille maximale de l'Index1 * MaxSizeIndex 2)

Le tableau Java suivant de la Variableclasse est illustré ci-dessous.

 Expression expns []; 

Le tableau expns est utilisé pour traiter les tableaux écrits comme " A(10*B, i)." Dans ce cas, les indices sont en fait des expressions plutôt que des constantes, la référence doit donc contenir des pointeurs vers les expressions qui sont évaluées lors de l'exécution. Enfin, il y a ce morceau de code assez laid qui calcule l'index en fonction de ce qui a été passé dans le programme. Cette méthode privée est illustrée ci-dessous.

private int computeIndex (int ii []) renvoie BASICRuntimeError {int offset = 0; if ((ndx == null) || (ii.length! = ndx.length)) throw new BASICRuntimeError ("Mauvais nombre d'indices."); for (int i = 0; i <ndx.length; i ++) {if ((ii [i] ndx [i])) throw new BASICRuntimeError ("Index out of range."); offset = offset + (ii [i] -1) * mult [i]; } retour offset; }

En regardant le code ci-dessus, vous remarquerez que le code vérifie d'abord que le nombre correct d'indices a été utilisé lors du référencement du tableau, puis que chaque index était dans la plage légale pour cet index. Si une erreur est détectée, une exception est renvoyée à l'interpréteur. Les méthodes numValueet stringValuerenvoient une valeur de la variable sous forme de nombre ou de chaîne respectivement. Ces deux méthodes sont présentées ci-dessous.

double numValue (int ii []) jette BASICRuntimeError {return nArrayValues ​​[computeIndex (ii)]; } String stringValue (int ii []) lance BASICRuntimeError {if (subType == NUMBER_ARRAY) return "" + nArrayValues ​​[computeIndex (ii)]; return sArrayValues ​​[computeIndex (ii)]; }

Il existe des méthodes supplémentaires pour définir la valeur d'une variable qui ne sont pas affichées ici.

En cachant une grande partie de la complexité de la façon dont chaque élément est implémenté, quand vient enfin le temps d'exécuter le programme BASIC, le code Java est assez simple.

Exécuter le code

Le code pour interpréter les instructions BASIC et les exécuter est contenu dans le

run

méthode de la

Program

classe. Le code de cette méthode est montré ci-dessous, et je vais le parcourir pour en souligner les parties intéressantes.

1 exécution publique de void (InputStream in, OutputStream out) jette BASICRuntimeError {2 PrintStream pout; 3 Énumération e = stmts.elements (); 4 stmtStack = nouvelle pile (); // suppose qu'aucune instruction empilée ... 5 dataStore = new Vector (); // ... et aucune donnée à lire. 6 dataPtr = 0; 7 Énoncé s; 8 9 vars = nouveau RedBlackTree (); 10 11 // si le programme n'est pas encore valide. 12 if (! E.hasMoreElements ()) 13 return; 14 15 if (out instanceof PrintStream) {16 pout = (PrintStream) out; 17} else {18 pout = new PrintStream (sortie); 19}

Le code ci-dessus montre que la runméthode prend un InputStreamet un OutputStreampour être utilisé comme "console" pour le programme en cours d'exécution. À la ligne 3, l'objet d'énumération e est défini sur l'ensemble d'instructions de la collection nommée stmts . Pour cette collection, j'ai utilisé une variation sur un arbre de recherche binaire appelé un arbre "rouge-noir". (Pour plus d'informations sur les arbres de recherche binaires, consultez ma colonne précédente sur la création de collections génériques.) Ensuite, deux collections supplémentaires sont créées - une en utilisant a Stacket une en utilisant unVector. La pile est utilisée comme la pile dans n'importe quel ordinateur, mais le vecteur est utilisé expressément pour les instructions DATA dans le programme BASIC. La collection finale est un autre arbre rouge-noir qui contient les références des variables définies par le programme BASIC. Cet arbre est la table de symboles utilisée par le programme lors de son exécution.

Suite à l'initialisation, les flux d'entrée et de sortie sont mis en place, puis si e n'est pas nul, nous commençons par collecter les données qui ont été déclarées. Cela se fait comme indiqué dans le code suivant.

/ * Commençons par charger toutes les instructions de données * / while (e.hasMoreElements ()) {s = (Statement) e.nextElement (); if (s.keyword == Statement.DATA) {s.execute (this, in, pout); }}

La boucle ci-dessus regarde simplement toutes les instructions, et toutes les instructions DATA qu'elle trouve sont ensuite exécutées. L'exécution de chaque instruction DATA insère les valeurs déclarées par cette instruction dans le vecteur dataStore . Ensuite, nous exécutons le programme proprement dit, ce qui est fait en utilisant ce morceau de code suivant:

e = stmts.elements (); s = (Instruction) e.nextElement (); do {int yyy; / * Lors de l'exécution, nous ignorons les instructions Data. * / essayez {yyy = in.available (); } catch (IOException ez) {yyy = 0; } if (yyy! = 0) {pout.println ("Arrêté à:" + s); pousser (s); Pause; } if (s.keyword! = Statement.DATA) {if (traceState) {s.trace (this, (traceFile! = null)? traceFile: pout); } s = s.execute (this, in, moue); } else s = nextStatement (s); } while (s! = null); }

Comme vous pouvez le voir dans le code ci-dessus, la première étape consiste à réinitialiser e . L'étape suivante consiste à récupérer la première instruction dans la variable s , puis à entrer dans la boucle d'exécution. Il y a du code pour vérifier l'entrée en attente sur le flux d'entrée pour permettre à la progression du programme d'être interrompue en tapant sur le programme, puis la boucle vérifie si l'instruction à exécuter serait une instruction DATA. Si c'est le cas, la boucle ignore l'instruction car elle a déjà été exécutée. La technique plutôt compliquée d'exécuter d'abord toutes les instructions de données est requise car BASIC permet aux instructions DATA qui satisfont une instruction READ d'apparaître n'importe où dans le code source. Enfin, si le traçage est activé, un enregistrement de trace est imprimé et l'instruction très peu convaincantes = s.execute(this, in, pout);est invoquée. La beauté est que tous les efforts pour encapsuler les concepts de base dans des classes faciles à comprendre rendent le code final trivial. Si ce n'est pas anodin, vous avez peut-être une idée qu'il pourrait y avoir une autre façon de diviser votre conception.

Conclusion et autres réflexions

The interpreter was designed so that it could run as a thread, thus there can be several COCOA interpreter threads running simultaneously in your program space at the same time. Further, with the use of function expansion we can provide a means whereby those threads can interact with each other. There was a program for the Apple II and later for the PC and Unix called C-robots that was a system of interacting "robotic" entities that were programmed using a simple BASIC derivative language. The game provided me and others with many hours of entertainment but was also an excellent way to introduce the basic principles of computation to younger students (who mistakenly believed they were just playing and not learning). Java based interpreter subsystems are much more powerful than their pre-Java counterparts because they are instantly available on any Java platform. COCOA ran on Unix systems and Macintoshes the same day I got working on a Windows 95 based PC. While Java gets beaten up by incompatibilities in the thread or window toolkit implementations, what is often overlooked is this: A lot of code "just works."