Siamo arrivati all'ultimo articolo di una serie di tre, che hanno avuto come obiettivo lo sviluppo di un interprete di espressioni. Nei due precedenti abbiamo trattato le fasi di analisi lessicale [\[1\]](#BIBLIO1) e analisi sintattica [\[2\]](#BIBLIO1), nonché una descrizione generale del progetto. Lo scopo del presente articolo è l'esame della fase di analisi semantica, generazione, esecuzione e debugging del codice applicate all'interprete di espressioni numeriche descritto negli articoli precedenti ([\[1\]\[2\]](#BIBLIO1)); verranno inoltre descritte alcune classi del package "java.util", che saranno impiegate nel corso dell'implementazione: HashTable, Vector e Stack.
In genere l'analisi semantica compie principalmente la verifica di compatibilità tra operandi (typechecking) e la conversione automatica di tipi (typecasting). Nel nostro caso questa fase si limiterà soltanto alla verifica dell'esistenza delle variabili utilizzate all'interno delle espressioni, visto e considerato che che il tipo numerico è uguale per tutti gli operandi.
La verifica delle variabili utilizza una tabella di simboli, tabella che in altri casi non contiene soltanto gli identificatori di variabili ma altresì gli identificatori di funzione, metodo, oggetto, ecc. Le variabili vengono registrate nella tabella di simboli quando esse sono dichiarate. Oltre all'identificatore di variabile possono venire registrate altre informazioni come ad esempio il tipo, il valore, il dominio, le proprietà (public, private), ecc. Nel nostro caso verrà registrato soltanto l'identificatore e il valore.
La struttura più utilizzata per la tabella di simboli è stata e continua ad essere la Hashtable. Questa struttura permette di registrare degli insiemi (chiave, valore); la ricerca di un valore si esegue attraverso la sua chiave. L'algoritmo di ricerca utilizza come indice il valore numerico (hashcode), ottenuto con l'applicazione alla chiave di una funzione di Hash. Le funzioni possono essere svariate e dipendono dal tipo di chiave; per le stringhe è molto utilizzata la somma dei valori interi dei caratteri. Tra le classi Java del package "java.util" troviamo la classe Hashtable, questa classe contiene due metodi essenziali che descriviamo di seguito.
- public synchronized Object put(Object key, Object value)
> Inserisce nella tabella l'elemento value, utilizzando la chiave key. Il hashcode per qualsiasi oggetto Java viene calcolato con il metodo native java.lang.Object.hashCode(). Nessuno dei due parametri può essere null. Questo metodo restituisce il precedente valore associato alla chiave key, o riporta null nel caso questa non sia stata mai inserita.
- public synchronized Object get(Object key)
> Restituisce l'oggetto associato alla chiave contenuta in key. Se la chiave non si trova riporta null.
Ora vediamo l'implementazione della tabella di variabili come estensione della classe Hashtable.
File ExpressionAnalizer.java
72 class VariablesTable extends Hashtable {
73 public VariablesTable() {
74 super();
75 }
77 public VariablesTable( Applet app ) {
78 super();
79
80 StringTokenizer st;
81 String varname,s = app.getParameter("variables").toLowerCase();
82
83 if (s==null)
84 app.getAppletContext().showStatus("Avvertenza: No sono state definite variabili");
85 else {
86 st = new StringTokenizer(s);
87 while (st.hasMoreTokens()) {
88 varname = st.nextToken().toLowerCase();
89 s = app.getParameter("_"+varname);
90 if (s==null)
91 app.getAppletContext().showStatus("Variabile \""+varname+"\" non dichiarata");
92 else
93 put(varname,new Double(s));
94 }
95 }
96 }
98 public void toList( List li ) {
99 Enumeration en = keys();
100 String sKey;
101
102 while (en.hasMoreElements()) {
103 sKey = (String)en.nextElement();
104 li.addItem(sKey+"="+get(sKey));
105 }
106 }
107 }
Il secondo costruttore (riga 77) permette di inserire le variabili, con i loro valori, a partire dai parametri dell'Applet. Nel parametro "variables" (r.81) si specificano tutti gli identificatori delle variabili separati da spazio (es. x y alfa). L'oggetto StringTokenizer st (r.86) è utilizzato per estrarre gli identificatori delle variabili (r.88) dalla stringa del parametro "variables". I valori delle variabili vengono presi dai parametri che hanno il nome della variabile preceduto dal carattere '\' (r.89 - es. \x). L'identificatore della variabile e il suo valore sono aggiunti alla tabella con il metodo Hashtable.put (r.93). La verifica delle variabili e il recupero dei propri valori verranno esposti più avanti quando analizzeremo il codice di analisi sintattico, semantico e di generazione di codice.
Infine abbiamo il metodo toList (r.98) che ci fornisce una rappresentazione della tabella attraverso una istanza della classe java.awt.List contenente tutte le variabili e i suoi valori.
Le espressioni analizzate, in genere, vengono rappresentate con la notazione Postfix. Questo tipo di notazione rispetta la corretta precedenza degli operatori ed elimina le parentesi.
La notazione Postfix per una espressione E può essere induttivamente definita come segue:
1. Se E è una variabile o una costante, la notazione per E è E stessa. 2. Se E è una espressione della forma E<sub>1</sub> op E<sub>2</sub>, dove op è un operatore binario, la notazione per E è E<sub>1</sub>' E<sub>2</sub>' op, dove E<sub>1</sub>' e E<sub>2</sub>' sono le notazioni postfix per E<sub>1</sub> e E<sub>2</sub> rispettivamente. 3. Se E è una espressione della forma op E<sub>1</sub>, dove op è un operatore unitario, la notazione per E è E<sub>1</sub>' op, dove E<sub>1</sub>' è la notazione postfix per E<sub>1</sub>. 4. Se E è una espressione della forma ( E<sub>1</sub> ), la notazione per E è E<sub>1</sub>.
Se prendiamo come esempio la espressione -2+x\(y-7)la notazione postfix è 2-xy7-\+ (il primo operatore '-' è l'operatore di negazione unitaria).
L'esecuzione di una espressione registrata in questo modo è abbastanza semplice e ha bisogno soltanto di uno stack per registrare i valori temporanei delle operazioni. Uno stack è una struttura di tipo LIFO (Last In First Out), vale a dire una struttura dove l'ultimo elemento inserito diventa il primo elemento da prelevare. L'algoritmo di esecuzione consiste nel prelevare uno ad uno gli elementi della notazione ed eseguire le seguenti diverse operazioni secondo il tipo di elemento.
1. Se l'elemento è un numero, inserire tale numero nello stack. 2. Se l'elemento è una variabile, prelevare il suo valore e inserirlo nello stack. 3. Se l'elemento è un operatore unitario, estrarre l'ultimo valore dello stack, applicare l'operatore e rimettere il risultato nello stack. 4. Se l'elemento è un operatore binario, estrarre l'ultimo e il penultimo valore dallo stack, applicare l'operatore tra i due e rimettere il risultato nello stack.
Al termine della notazione lo stack contiene come unico elemento il risultato dellespressione. Per mostrare il funzionamento di quest'algoritmo prendiamo come esempio lespressione in notazione postfix ottenuta precedentemente : 2-xy7-\*+ con i valori x=2 e y=5.
| Passo | Operazione | Notazione rimanente | Stack | |---:|---|---|---| | 1 | | 2 - x y 7 - + | | | 2 | 2 | - x y 7 - + | 2 | | 3 | - 2 | x y 7 - + | -2 | | 4 | x | y 7 - + | 2, -2 | | 5 | y | 7 - + | 5, 2, -2 | | 6 | 7 | - + | 7, 5, 2, -2 | | 7 | 5 - 7 | + | -2, 2, -2 | | 8 | 2 -2 | + | -4, -2 | | 9 | -4 + -2 | | -6 |
La classe Java che più si adatta a rappresentare la notazione Postfix è la classe java.util.Vector. Questa classe ha il vantaggio della ridimensionabilità dinamica automatica, caratteristica questa che non è disponibili con gli array. Utilizzeremo come elementi del Vector i medesimi oggetti tokens prodotti dall'analisi lessicale [\[1\]](#BIBLIO1). Vediamo di seguito la dichiarazione della classe ExprCode che rappresenterà il codice come notazione Postfix.
File ExpressionAnalizer.java
40 class ExprCode extends Vector {
42 public ExprCode() {
43 super();
44 }
46 public void add( GenericToken o ) {
47 addElement(o);
48 }
50 public GenericToken peek() throws ExprCodeError {
51 GenericToken o;
52
53 try {
54 o = (GenericToken)firstElement();
55 removeElementAt(0);
56 }
57 catch (NoSuchElementException e) throw new ExprCodeError();
58 catch (ArrayIndexOutOfBoundsException e) throw new ExprCodeError();
59
60 return o;
61 }
63 public void toList( List li ) {
64 int i;
65
66 for (i=0;i<size();i++)
67 li.addItem(elementAt(i).toString());
68 }
70 }
Sulla base dell'algoritmo di esecuzione spiegato in precedenza, e la generazione lineare del codice, notiamo che le uniche operazioni necessarie per gestire il codice sono le due seguenti:
- add (r.46): Aggiunge un token alla fine della notazione utilizzando il metodo di Vector:addElement (r.47): Aggiunge un oggetto alla fine della struttura.
> Sarà utilizzato in fase di generazione del codice.
- peek (r.50): Preleva e rimuove il primo token della notazione utilizzando la combinazione dei due metodi di
Vector:firstElement (r.54): Restituisce il primo elemento della struttura.
RemoveElementAt (r.55): Rimuove l'elemento che si trova nella posizione specificata come parametro. Nel nostro caso si tratta del primo (0).
Sarà utilizzato in fase di esecuzione e debugging del codice.
Il metodo toList (r.63) ci fornisce una rappresentazione della notazione tramite una istanza della classe java.awt.List contenente tutte le descrizioni dei tokens.
Il tentativo di prelevare un token in un ExprCode vuoto solleva leccezione ExprCodeError (r.57). Questa eccezione è stata definita come una semplice estensione di Exception, e ci servirà a distinguere la provenienza degli errori.
L'analisi semantica e la generazione del codice possono avvenire a partire dall'albero sintattico [\[5\]](#BIBLIO1) o direttamente nei metodi corrispondenti ai non terminali [\[2\]](#BIBLIO1). La scelta, nel nostro caso, è ricaduta sulla seconda possibilità poiché non abbiamo creato l'albero sintattico. Basandoci sulla classe SyntaxAnalizer, implementata nell'articolo precedente [\[2\]](#BIBLIO1), passiamo ora alla definizione della classe ExpressionAnalizer. Questa classe incorporerà all'interno dell'analisi sintattica le istruzioni per svolgere i nuovi compiti.
File ExpressionAnalizer.java
109 class ExpressionAnalizer {
110 static final String factorErr = "Aspetto un numero, una variabile, \'-\' o \'(\'";
111 LexicalAnalizer la;
112 VariablesTable vt;
113 ExprCode ec;
114 OperatorToken lastOp;
115 List sList;
116 int indent=0;
117
118 public ExpressionAnalizer( InputStream is, List _sList, VariablesTable _vt ) {
119 la = new LexicalAnalizer(is);
120 sList = _sList;
121 vt = _vt;
122 lastOp = null;
123 }
125 void show(String s) {
126 int i;
127 StringBuffer sb = new StringBuffer(indent);
128
129 for (i=0;i<indent;i++) sb.append(' ');
130
131 sList.addItem(sb+s);
132 }
134 public ExprCode Parse() throws SemanticError, SyntaxError, LexicalError, IOException {
135 GenericToken gt;
136
137 ec = new ExprCode();
138
139 Expression();
140
141 gt = la.nextToken();
142 if (gt.getType()!=GenericToken.EOE) {
143 throw new SyntaxError("Aspetto un operatore.");
144 } else
145 return ec;
146 }
148 boolean checkVariable( String varname ) {
149 return vt.containsKey(varname);
150 }
152 void Expression() throws SemanticError, SyntaxError, LexicalError, IOException {
153 GenericToken gt;
154
155 show("<Espressione>");
156 indent++;
157 Term();
158 while (true) {
159 gt = la.nextToken();
160 if (gt.getType()==GenericToken.OPERATOR
161 && (((OperatorToken)gt).getOp()==OperatorToken.SUB ||
162 ((OperatorToken)gt).getOp()==OperatorToken.ADD)) {
163 show(gt.toString());
164 Term();
165 ec.add((OperatorToken)gt);
166 } else {
167 la.pushBack();
168 break;
169 }
170 }
171 indent--;
172 }
174 void Term() throws SemanticError, SyntaxError, LexicalError, IOException {
175 GenericToken gt;
176
177 show("<Termine>");
178 indent++;
179 Factor();
180 while (true) {
181 gt = la.nextToken();
182 if (gt.getType()==GenericToken.OPERATOR
183 && (((OperatorToken)gt).getOp()==OperatorToken.MUL ||
184 ((OperatorToken)gt).getOp()==OperatorToken.DIV)) {
185 show(gt.toString());
186 Factor();
187 ec.add((OperatorToken)gt);
188 } else {
189 la.pushBack();
190 break;
191 }
192 }
193 indent--;
194 }
196 void Factor() throws SemanticError, SyntaxError, LexicalError, IOException {
197 GenericToken gt;
198 OperatorToken ot;
199
200 show("<Fattore>");
201 indent++;
202 gt = la.nextToken();
203 switch (gt.getType()) {
204 case GenericToken.NUMBER:
205 show(gt.toString());
206 ec.add((NumberToken)gt);
207 break;
208 case GenericToken.VARIABLE:
209 show(gt.toString());
210 if (checkVariable(((VariableToken)gt).getName()))
211 ec.add((VariableToken)gt);
212 else
213 throw new SemanticError("Variable \""+((VariableToken)gt).getName()+"\" non trovata");
214 break;
215 case GenericToken.OPERATOR:
216 if (((OperatorToken)gt).getOp()==OperatorToken.SUB) {
217 ot = new OperatorToken(OperatorToken.MINUS);
218 show(ot.toString());
219 Factor();
220 ec.add(ot);
221 break;
222 } else
223 throw new SyntaxError(factorErr);
224 case GenericToken.SYMBOL:
225 if (((SymbolToken)gt).getSymbol()=='(') {
226 show(gt.toString());
227 Expression();
228 gt = la.nextToken();
229 if (gt.getType()==GenericToken.SYMBOL
230 && ((SymbolToken)gt).getSymbol()==')') {
231 show(gt.toString());
232 break;
233 } else
234 throw new SyntaxError("Aspetto \')\'");
235 }
236 default:
237 throw new SyntaxError(factorErr);
238 }
239 indent--;
240 }
242 }
La prima modifica si trova nel costruttore della classe (r.118), al quale viene passata un'istanza di VariablesTable contenente tutte le variabili dichiarate. Nel metodo Parse (r.134) viene creata un'istanza di ExprCode (r.137) dove sarà immesso il codice generato; se il parsing finisce senza errore, il metodo restituisce l'istanza creata (r.145). Ogni volta che si processa un token, esso viene aggiunto al codice seguendo le regole della notazione Postfix (r.165, 187, 206, 211, 220). Quando un token di tipo VariableToken viene processato (r.208) si verifica l'esistenza della variabile col metodo checkVariable (r.148). All'interno di quest'ultimo si controlla, col metodo Hashtable.containsKey (r.149) l'esistenza di una chiave con lo stesso nome della variabile. Il metodo containsKey restituisce true se il parametro "key" si trova della struttura.
Il codice generato dal metodo Parse, se non si verificano degli errori, è pronto per l'esecuzione con la classe ExpressionRunner, che descriveremo di seguito.
File ExpressionRunner.java
15 class ValuesStack extends Stack {
17 public ValuesStack() {
18 super();
19 }
21 public void toList( List li ) {
22 GenericToken gt;
23 Stack aux = new Stack();
24
25 while (!empty()) {
26 gt = (GenericToken)pop();
27 li.addItem(gt.toString());
28 aux.push(gt);
29 }
30
31 while (!aux.empty()) {
32 gt = (GenericToken)aux.pop();
33 push(gt);
34 }
35 }
36 }
37
39 class ExpressionRunner {
40 public ExprCode ec;
41 public ValuesStack ts;
42 VariablesTable vt;
43
44 public ExpressionRunner( ExprCode _ec, VariablesTable _vt ) {
45 ec = (ExprCode)_ec.clone();
46 ts = new ValuesStack();
47 vt = _vt;
48 }
50 double getVarValue( VariableToken vtok ) throws ExprCodeError {
51 Double v = (Double)vt.get(vtok.getName());
52
53 if (v==null)
54 throw new ExprCodeError("Variabile \""+vtok.getName()+"\" non trovata");
55 else
56 return v.doubleValue();
57 }
59 public boolean Step() throws ExprCodeError {
60 GenericToken gt;
61 OperatorToken ot;
62 NumberToken n1,n2;
63
64 gt = ec.peek();
65
66 switch (gt.getType()) {
67 case GenericToken.NUMBER: ts.push(gt); break;
68 case GenericToken.VARIABLE: ts.push(new NumberToken(getVarValue((VariableToken)gt))); break;
69 case GenericToken.OPERATOR:
70 ot = (OperatorToken)gt;
71 try {
72 n1 = (NumberToken)ts.pop();
73 if (ot.getArity()==2)
74 n2 = (NumberToken)ts.pop();
75 else
76 n2 = n1;
77 try
78 ts.push(new NumberToken(ot.doOp(n2,n1)));
79 catch (ArithmeticException e) throw new ExprCodeError("Divisione per zero");
80 }
81 catch (EmptyStackException e) throw new ExprCodeError();
82 break;
83 }
85 return !ec.isEmpty();
87 }
89 public double Execute() throws ExprCodeError {
90 while (Step());
91 return ((NumberToken)ts.pop()).getValue();
92 }
94 }
La classe ValuesStack (r.15) ci servirà come stack temporaneo dei valori nell'esecuzione del codice. L'unico metodo aggiunto, toList (r.21), fornisce una rappresentazione visuale del contenuto dello stack tramite una istanza della classe java.awt.List. Questo metodo utilizza uno stack ausiliare temporaneo (aux - r.23) per risistemare i valori che sono stati estratti ai fini della visualizzazione. Ogni valore estratto (r. 25-29) viene aggiunto alla List li e allo stack ausiliare aux; dopodiché vengono rimessi nello stack originale (r. 31-34). Per inserire i valori vengono impiegati i metodi push e pop, ereditati da java.util.Stack. Il metodo push (r. 28, 33, 67, 68, 78) inserisce un qualsiasi oggetto nello stack; nel nostro interprete questi oggetti saranno di tipo NumberToken. Il metodo pop (r. 26, 32, 72, 74, 91) invece restituisce l'ultimo oggetto inserito.
Nel costruttore della classe ExpressionRunner (r.44), il codice, passato come parametro nella struttura \_ec di tipo ExprCode, viene clonato (r.45) col metodo Vector.clone (interface Clonable). Questa clonazione ha come obiettivo di preservare il contenuto del codice originale, dato che l'algoritmo di esecuzione estrae tutti gli elementi dalla struttura ExprCode.
Il metodo Execute (r.89) esegue ordinatamente tutte le operazioni della notazione ExprCode ec; per la realizzazione di ogni singola operazione viene utilizzato il metodo Step. Arrivati alla fine del codice, il risultato dell'esecuzione viene prelevato dallo stack temporaneo ts.
Il metodo Step (r.59) estrae un token dalla notazione, e in base al tipo esegue una delle operazioni descritte in precedenza. Se prendiamo come riferimento i numeri assegnati nell'elenco delle operazioni, le righe corrispondenti a queste ultime sono:
1. (riga 67) - Inserisce il NumberToken nello stack. 2. (riga 68) - Inserisce un NumberToken creato con il valore della variabile. Il valore viene preso col metodo getVarValue, il quale a sua volta lo ottiene col metodo get della hashtable. Una eccezione di tipo ExprCodeError viene generata nel caso in cui la variabile non venga reperita nella tabella. 3. e 4. (righe 70-81) Sulla base del numero di operandi dell'operatore sono estratti uno o due NumberToken dallo stack temporaneo ts. Il numero di operandi ci viene fornito attraverso il metodo OperatorToken.getArity (r.73). L'esecuzione dell'operazione è svolta dal metodo OperatorToken.doOp (r.78), il cui risultato viene inserito nello stack.
Dopo un'approfondita descrizione delle classi ExpressionAnalizer ed ExpressionRunner, è possibile vederne il suo utilizzo nell'applet che segue. La espressione -2+x\*(y-7), analizzata in precedenza, può servire come un primo esempio di compilazione ed esecuzione.
Di seguito analizzeremo le parti più importanti del codice sorgente di questo applet.
File ex3.java
13 public class ex3 extends java.applet.Applet {
14 static final String DO_BUTTON = "Compila ed esegui";
15 static final String EXEC_BUTTON = "Esegui";
16 static final String STEP_BUTTON = "Passo a passo";
17 List li,codeList,stackList,varsList;
18 TextField edExpr,edRes;
19 boolean success = false;
20 VariablesTable vt;
21 ExprCode ec;
22 ExpressionRunner er;
23
24 public void init() {
25 /* Creazione e disposizione dei componenti nell'Applet.
26 I sorgento possono essere scaricati come indicato
27 alla fine dell'articolo */
183
184 vt = new VariablesTable(this);
185 vt.toList(varsList);
186 }
207 void addError( Exception e ) {
208 li.addItem(e.toString());
209 li.select(li.countItems()-1);
210 }
212 void doCompile() {
213 ExpressionAnalizer ea;
214
215 success = false;
216 try {
217 ea = new ExpressionAnalizer(new StringBufferInputStream(edExpr.getText()),li,vt);
218 li.clear();
219 codeList.clear();
220 stackList.clear();
221 edRes.setText("");
222 ec = ea.Parse();
223 ec.toList(codeList);
224 success = true;
225 }
226 catch (IOException e) getAppletContext().showStatus(e.toString());
227 catch (LexicalError e) addError(e);
228 catch (SyntaxError e) addError(e);
229 catch (SemanticError e) addError(e);
230 if (success) {
231 getAppletContext().showStatus("Operazione completata con successo");
232 doExecute();
233 } else
234 getAppletContext().showStatus("Operazione interrotta per errore");
235 }
237 void doExecute() {
238
239 if (success) {
240 try {
241 er = new ExpressionRunner(ec,vt);
242 stackList.clear();
243 codeList.clear();
244 ec.toList(codeList);
245 edRes.setText(String.valueOf(er.Execute()));
246 }
247 catch (ExprCodeError e) {
248 getAppletContext().showStatus(e.toString());
249 }
250 er = null;
251 }
252 }
254 void doStep() {
255
256 if (success) {
257 try {
258 if (er==null) {
259 er = new ExpressionRunner(ec,vt);
260 edRes.setText("");
261 }
262 stackList.clear();
263 codeList.clear();
264 if (er.Step()) {
265 er.ts.toList(stackList);
266 er.ec.toList(codeList);
267 } else {
268 ec.toList(codeList);
269 edRes.setText(String.valueOf(((NumberToken)er.ts.pop()).getValue()));
270 er = null;
271 }
272 }
273 catch (ExprCodeError e) {
274 getAppletContext().showStatus(e.toString());
275 er = null;
276 codeList.clear();
277 ec.toList(codeList);
278 }
279 }
280 }
282 public boolean handleEvent(Event evt) {
284 switch (evt.id) {
285 case Event.ACTION_EVENT: {
286 if (DO_BUTTON.equals(evt.arg)) doCompile();
287 else if (EXEC_BUTTON.equals(evt.arg)) doExecute();
288 else if (STEP_BUTTON.equals(evt.arg)) doStep();
289 break;
290 }
291 case Event.KEY_PRESS: {
292 if (evt.target instanceof TextField &&
293 evt.key == '\n' ) doCompile();
294 break;
295 }
296 }
297 return false;
298 }
299 }
Il costruttore dell'applet (r.24) crea e dispone i diversi componenti (non inseriti in questo listato per ragioni di spazio) e crea una istanza della classe VariablesTable (r.184) a partire dai parametri dello stesso applet. Nella riga 185 vengono inseriti all'interno della List varList (casella di elenco "Variabili") gli identificatori e i valori della VariablesTable vt.
Nel metodo handleEvent (r.282) vengono catturati gli eventi di pressione del tasto Invio e del pulsante "Compila ed esegui" per eseguire tutta lanalisi, la generazione e l'esecuzione del codice nel metodo doCompile (r.61). Il debugging del codice può essere fatto utilizzando il pulsante "Passo a passo", il quale esegue una singola istruzione del codice e aggiorna le caselle "Valori temporanei" e "Codice". Il pulsante "Esegui" può essere utile nel caso si voglia arrivare alla fine nella modalità passo a passo. La pressione dei tasti "Esegui" e "Passo a passo" eseguono rispettivamente i metodi doExecute e doStep.
Il metodo doCompile (r.217) crea una istanza della classe ExpressionAnalizer usando come InputStream il contenuto della casella "Espressione"; gli altri parametri sono la List li (casella di elenco "Tracce semantiche, sintattiche e lessicali") per visualizzare i risultati dell'analisi sintattica e semantica, e la tabella delle variabili vt. Nelle righe seguenti (r. 218-221) viene pulito il contenuto delle caselle "Tracce semantiche, sintattiche e lessicali", "Valori temporanei", "Codice" e "Risultato". La chiamata al metodo Parse (r.222) esegue la compilazione e restituisce il codice generato. La rappresentazione visuale del codice viene fatta attraverso il metodo toList della istanza ExprCode ea. La visualizzazione degli errori (r.227-229) avviene tramite il metodo addError (r.207), il quale aggiunge (r.208) il testo dell'errore nella casella "Tracce semantiche, sintattiche e lessicali" selezionando (r.209) la riga inserita per garantire la sua visibilità. Le istruzioni finali del metodo doCompile (r.230-234) notificano, nella barra di stato, il successo delloperazione. In caso di successo positivo, il codice prodotto viene eseguito mediante il metodo doExecute.
Il metodo doExecute (r.237) crea una istanza della classe ExpressionRunner con il codice generato (ec) e la tabella di variabili (vt), e lo esegue con il metodo Execute (r.245). Il valore risultante dall'esecuzione è inserito della casella "Risultato". Se si verifica un errore, il testo viene visualizzato nella barra di status (r.248).
Il metodo doStep (r. 254) crea, se non esiste (r.258), una istanza della classe ExpressionRunner con il codice generato (ec) e la tabella di variabili (vt), ed esegue una singola istruzione del codice ec con il metodo Step (r.264). Quest'ultimo restituisce true se non viene raggiunta la fine del codice, quindi vengono aggiornate le caselle "Codice" e "Valori temporanei". L'ultimo valore dello stack temporaneo, quando si è arrivati all'ultima istruzione, viene messo nella casella "Risultato" e il codice originale reinserito nella casella "Codice". Se si verifica un errore, il testo verrà visualizzato nella barra di status (r.274) e il codice originale reinserito nella casella "Codice" (r.277).
Benché le tematiche di teoria di linguaggi e tecniche di compilazione siano così vaste, abbiamo cercato di fornire le basi per lo sviluppo di applicazioni più complesse in questo campo. Una di queste applicazioni potrebbe essere lo sviluppo di un motore di ricerca; il quale interpreti, in query SQL, le espressioni d'interrogazione inserite dall'utente. Questi tre articoli sono serviti inoltre a dimostrare nella pratica l'utilizzo delle classi Java StreamTokenizer, HashTable, Vector e Stack. In futuro potrebbero essere affrontate altre fasi come il controllo di tipo (type checking) e l'ottimizzazione del codice, tutto ciò "religiosamente" implementato in Java.
Download
I sorgenti esposti possono essere scaricati ai seguenti indirizzi:
- ExpressionAnalizer.java (classi VariablesTable, ExprCode e ExpressionAnalizer)
- ExpressionRunner.java (classi ValuesStack e ExpressionRunner)
- ex3.java (applet di esempio)
Bibliografia
- 1. *"Interprete di espressioni: analisi lessicale con lausilio della classe java.io.StreamTokenizer"*, Rafael Andrés Marín de la Cruz, MokaByte Numero 7 Aprile 1997. URL:
- [http://www.programmers.net/Riviste/moka/0497/interpre.htm](../../../1997/04/interpre.htm)
- 2. *"Interprete di espressioni: analisi sintattica utilizzando un algoritmo di discesa ricorsiva"*, Rafael Andrés Marín de la Cruz, MokaByte Numero 8 Maggio 1997. URL:
- [http://www.programmers.net/Riviste/moka/0597/interpre.htm](../../../1997/05/interprete.htm)
3. Documentazione API JDK 1.0.2. URL: <http://java.sun.com/products/jdk/1.0.2/api> 4. "The theory of parsing, translation, and compiling", A.V. Aho and J.D. Ullman, 1973; ISBN 0-13-914564-8 5. "Compilers Principles, Techniques and Tools", A.V. Aho, R. Sethi and J.D. Ullman, 1986; ISBN 0-201-10088-6 6. "Peter Nortons Guide to Java Programming", Peter Norton and William Stanek, 1996; ISBN 1-57521-088-6
Rafael Andrés Marín de la Cruz
- Laureato in Matematica-Cibernetica allUniversità dellAvana (Cuba) nel 1990, da quattro anni vive e lavora a Milano. Con la sua società ha sviluppato software per non vedenti sotto DOS e un tool di installazione per Windows. Svolge inoltre consulenza, in qualità di progettista, nel campo delle telecomunicazioni. Ha programmato un gioco-applet *GeniusMind* valutato
- ["JARS Top 25%"](http://www.jars.com); la versione italiana può essere vista all'indirizzo <http://www.galactica.it/dialectica/genius/geniusmind.html>.
Il suo sito si trova al seguente indirizzo: <http://www.galactica.it/dialectica>.
Può essere contattato tramite la redazione o direttamente all'indirizzo: <dialectica.sas@galactica.it>
