Crea un interprete in Java: implementa il motore di esecuzione

Indietro 1 2 3 Pagina 2 Avanti Pagina 2 di 3

Altri aspetti: stringhe e array

Altre due parti del linguaggio BASIC sono implementate dall'interprete COCOA: stringhe e array. Diamo prima un'occhiata all'implementazione delle stringhe.

Per implementare le stringhe come variabili, la Expressionclasse è stata modificata per includere la nozione di espressioni "stringa". Questa modifica ha preso la forma di due aggiunte: isStringe stringValue. La fonte per questi due nuovi metodi è mostrata di seguito.

String stringValue (Program pgm) genera BASICRuntimeError {throw new BASICRuntimeError ("Nessuna rappresentazione di stringa per questo."); } boolean isString () {return false; }

Chiaramente, non è troppo utile per un programma BASIC ottenere il valore stringa di un'espressione di base (che è sempre un'espressione numerica o booleana). Si potrebbe concludere dalla mancanza di utilità che questi metodi non appartenevano Expressione non appartenevano invece a una sottoclasse Expression. Tuttavia, inserendo questi due metodi nella classe base, tutti gli Expressionoggetti possono essere testati per vedere se, in effetti, sono stringhe.

Un altro approccio di progettazione consiste nel restituire i valori numerici come stringhe utilizzando un StringBufferoggetto per generare un valore. Quindi, ad esempio, lo stesso codice potrebbe essere riscritto come:

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

E se viene utilizzato il codice precedente, è possibile eliminare l'uso di isStringperché ogni espressione può restituire un valore stringa. Inoltre, è possibile modificare il valuemetodo per provare a restituire un numero se l'espressione restituisce una stringa eseguendola tramite il valueOfmetodo di java.lang.Double. In molti linguaggi come Perl, TCL e REXX, questo tipo di tipizzazione amorfa è usata con grande vantaggio. Entrambi gli approcci sono validi e dovresti fare la tua scelta in base al design del tuo interprete. In BASIC, l'interprete deve restituire un errore quando una stringa viene assegnata a una variabile numerica, quindi ho scelto il primo approccio (restituendo un errore).

Per quanto riguarda gli array, ci sono diversi modi in cui puoi progettare il tuo linguaggio per interpretarli. C utilizza le parentesi quadre attorno agli elementi dell'array per distinguere i riferimenti all'indice dell'array dai riferimenti a funzioni che hanno parentesi attorno ai loro argomenti. Tuttavia, i progettisti del linguaggio per BASIC hanno scelto di usare le parentesi sia per le funzioni che per gli array così quando il testo NAME(V1, V2)viene visto dal parser, potrebbe essere una chiamata di funzione o un riferimento ad un array.

L'analizzatore lessicale discrimina tra i token che sono seguiti da parentesi assumendo prima che siano funzioni e verificando ciò. Quindi passa a vedere se sono parole chiave o variabili. È questa decisione che impedisce al programma di definire una variabile denominata "SIN". Qualsiasi variabile il cui nome corrispondesse al nome di una funzione sarebbe stata restituita dall'analizzatore lessicale come un token di funzione. Il secondo trucco utilizzato dall'analizzatore lessicale è controllare se il nome della variabile è immediatamente seguito da `('. Se lo è, l'analizzatore presume che sia un riferimento ad array. Analizzando questo nell'analizzatore lessicale, eliminiamo la stringa` MYARRAY ( 2 )'dall'essere interpretato come un array valido (notare lo spazio tra il nome della variabile e la parentesi aperta).

Il trucco finale per implementare gli array è nella Variableclasse. Questa classe viene utilizzata per un'istanza di una variabile e, come ho discusso nella colonna del mese scorso, è una sottoclasse di Token. Tuttavia, ha anche alcuni macchinari per supportare gli array ed è quello che mostrerò di seguito:

class Variable extends Token {// Sottotipi di variabili legali final static int NUMBER = 0; final static int STRING = 1; int finale statico NUMBER_ARRAY = 2; int finale statico STRING_ARRAY = 4; Nome stringa; int subType; / * * Se la variabile è nella tabella dei simboli, questi valori sono * inizializzati. * / int ndx []; // indici di matrice. int mult []; // moltiplicatori di array double nArrayValues ​​[]; String sArrayValues ​​[];

Il codice sopra mostra le variabili di istanza associate a una variabile, come nella ConstantExpressionclasse. Si deve fare una scelta sul numero di classi da utilizzare rispetto alla complessità di una classe. Una scelta progettuale potrebbe essere quella di creare una Variableclasse che contenga solo variabili scalari e quindi aggiungere una ArrayVariablesottoclasse per gestire le complessità degli array. Ho scelto di combinarli, trasformando le variabili scalari essenzialmente in array di lunghezza 1.

Se leggi il codice sopra, vedrai indici e moltiplicatori di array. Questi sono qui perché gli array multidimensionali in BASIC sono implementati utilizzando un singolo array Java lineare. L'indice lineare nell'array Java viene calcolato manualmente utilizzando gli elementi dell'array moltiplicatore. La validità degli indici usati nel programma BASIC viene verificata confrontandoli con il massimo indice legale nell'array ndx degli indici .

Ad esempio, un array BASIC con tre dimensioni di 10, 10 e 8, avrà i valori 10, 10 e 8 memorizzati in ndx. Ciò consente al valutatore di espressioni di verificare una condizione "indice fuori limite" confrontando il numero utilizzato nel programma BASIC con il numero massimo consentito che è ora memorizzato in ndx. La matrice del moltiplicatore nel nostro esempio conterrebbe i valori 1, 10 e 100. Queste costanti rappresentano i numeri utilizzati per mappare da una specifica di indice di matrice multidimensionale a una specifica di indice di matrice lineare. L'equazione effettiva è:

Indice Java = Index1 + Index2 * Max Size of Index1 + Index3 * (MaxSize of Index1 * MaxSizeIndex 2)

Il successivo array Java nella Variableclasse è mostrato di seguito.

 Espressione expns []; 

L' array expns viene utilizzato per gestire gli array scritti come " A(10*B, i)." In tal caso, gli indici sono in realtà espressioni anziché costanti, quindi il riferimento deve contenere puntatori a quelle espressioni che vengono valutate in fase di esecuzione. Infine c'è questo pezzo di codice dall'aspetto piuttosto brutto che calcola l'indice a seconda di ciò che è stato passato nel programma. Questo metodo privato è mostrato di seguito.

private int computeIndex (int ii []) genera BASICRuntimeError {int offset = 0; if ((ndx == null) || (ii.length! = ndx.length)) lancia un nuovo BASICRuntimeError ("Numero di indici errato."); for (int i = 0; i <ndx.length; i ++) {if ((ii [i] ndx [i])) lancia un nuovo BASICRuntimeError ("Indice fuori intervallo."); offset = offset + (ii [i] -1) * mult [i]; } offset di ritorno; }

Guardando il codice sopra, noterai che il codice controlla prima che sia stato utilizzato il numero corretto di indici quando si fa riferimento all'array, e quindi che ogni indice rientrava nell'intervallo legale per quell'indice. Se viene rilevato un errore, viene generata un'eccezione all'interprete. I metodi numValuee stringValuerestituiscono un valore dalla variabile come un numero o una stringa rispettivamente. Questi due metodi sono mostrati di seguito.

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

Esistono metodi aggiuntivi per impostare il valore di una variabile che non vengono mostrati qui.

Nascondendo gran parte della complessità di come ogni pezzo è implementato, quando finalmente arriva il momento di eseguire il programma BASIC, il codice Java è abbastanza semplice.

Esecuzione del codice

Il codice per interpretare le istruzioni BASIC ed eseguirle è contenuto nel file

run

metodo del

Program

classe. Il codice per questo metodo è mostrato di seguito e lo esaminerò per evidenziare le parti interessanti.

1 public void run (InputStream in, OutputStream out) genera BASICRuntimeError {2 PrintStream pout; 3 Enumerazione e = stmts.elements (); 4 stmtStack = nuovo Stack (); // non assumere istruzioni in pila ... 5 dataStore = new Vector (); // ... e nessun dato da leggere. 6 dataPtr = 0; 7 Dichiarazione s; 8 9 vars = nuovo RedBlackTree (); 10 11 // se il programma non è ancora valido. 12 if (! E.hasMoreElements ()) 13 return; 14 15 if (out instanceof PrintStream) {16 pout = (PrintStream) out; 17} else {18 pout = new PrintStream (out); 19}

Il codice precedente mostra che il runmetodo accetta un InputStreame un OutputStreamda utilizzare come "console" per il programma in esecuzione. Nella riga 3, l'oggetto di enumerazione e è impostato sull'insieme di istruzioni della raccolta denominata stmts . Per questa raccolta ho utilizzato una variazione su un albero di ricerca binario chiamato albero "rosso-nero". (Per ulteriori informazioni sugli alberi di ricerca binari, vedere la mia colonna precedente sulla creazione di raccolte generiche.) Successivamente, vengono create due raccolte aggiuntive: una utilizzando a Stacke una utilizzando aVector. Lo stack viene utilizzato come lo stack in qualsiasi computer, ma il vettore viene utilizzato espressamente per le istruzioni DATA nel programma BASIC. La raccolta finale è un altro albero rosso-nero che contiene i riferimenti per le variabili definite dal programma BASIC. Questo albero è la tabella dei simboli utilizzata dal programma durante l'esecuzione.

Dopo l'inizializzazione, vengono impostati i flussi di input e output, quindi se e non è nullo, iniziamo raccogliendo tutti i dati che sono stati dichiarati. Ciò viene eseguito come illustrato nel codice seguente.

/ * Per prima cosa carichiamo tutte le istruzioni dei dati * / while (e.hasMoreElements ()) {s = (Statement) e.nextElement (); if (s.keyword == Statement.DATA) {s.execute (this, in, pout); }}

Il ciclo precedente esamina semplicemente tutte le istruzioni e tutte le istruzioni DATA che trova vengono quindi eseguite. L'esecuzione di ciascuna istruzione DATA inserisce i valori dichiarati da tale istruzione nel vettore dataStore . Successivamente eseguiamo il programma corretto, che viene eseguito utilizzando questo pezzo di codice successivo:

e = stmts.elements (); s = (Statement) e.nextElement (); do {int yyy; / * Durante l'esecuzione saltiamo le istruzioni Data. * / try {yyy = in.available (); } catch (IOException ez) {yyy = 0; } if (yyy! = 0) {pout.println ("Stopped at:" + s); spingere (s); rompere; } if (s.keyword! = Statement.DATA) {if (traceState) {s.trace (this, (traceFile! = null)? traceFile: pout); } s = s.execute (this, in, pout); } else s = nextStatement (s); } while (s! = null); }

Come puoi vedere nel codice sopra, il primo passo è reinizializzare e . Il passo successivo è recuperare la prima istruzione nella variabile s e poi entrare nel ciclo di esecuzione. C'è del codice per controllare l'input in sospeso sul flusso di input per consentire l'interruzione del progresso del programma digitando nel programma, quindi il ciclo controlla per vedere se l'istruzione da eseguire sarebbe un'istruzione DATA. In tal caso, il ciclo salta l'istruzione poiché era già stata eseguita. La tecnica piuttosto complicata di eseguire prima tutte le istruzioni dei dati è necessaria perché BASIC consente alle istruzioni DATA che soddisfano un'istruzione READ di apparire ovunque nel codice sorgente. Infine, se la traccia è abilitata, viene stampato un record di traccia e l'istruzione molto poco convincentes = s.execute(this, in, pout);viene invocato. Il bello è che tutto lo sforzo di incapsulare i concetti di base in classi di facile comprensione rende il codice finale banale. Se non è banale, forse hai la minima idea che potrebbe esserci un altro modo per dividere il tuo design.

Conclusione e ulteriori pensieri

L'interprete è stato progettato in modo da poter essere eseguito come thread, quindi possono esserci più thread dell'interprete COCOA in esecuzione simultaneamente nello spazio del programma nello stesso momento. Inoltre, con l'uso dell'espansione delle funzioni possiamo fornire un mezzo con cui questi thread possono interagire tra loro. C'era un programma per Apple II e successivamente per PC e Unix chiamato C-robots che era un sistema di entità "robotiche" interagenti che erano programmate usando un semplice linguaggio derivato BASIC. Il gioco ha fornito a me e ad altri molte ore di intrattenimento, ma è stato anche un ottimo modo per introdurre i principi di base del calcolo agli studenti più giovani (che erroneamente credevano di giocare e non di imparare).I sottosistemi di interprete basati su Java sono molto più potenti delle loro controparti pre-Java perché sono immediatamente disponibili su qualsiasi piattaforma Java. COCOA è stato eseguito su sistemi Unix e Macintosh lo stesso giorno in cui ho iniziato a lavorare su un PC basato su Windows 95. Sebbene Java venga picchiato da incompatibilità nelle implementazioni di thread o toolkit di finestre, ciò che viene spesso trascurato è questo: molto codice "funziona e basta".