Implementazione dell'inteprete Scheme
Introduzione - Analisi lessicale - Analisi sintattica - Valutazione - Estensione - Demo

Analisi lessicale

    Riguardo all'AL un discorso particolare meritano i numeri. Infatti i numeri, come i realtà anche tutti gli identificatori, sono costruiti a partire da una grammatica che definisce una sottoparte del linguaggio. Per esempio nella pagina precedente si è mostrato come si costruisce un Ident a partire dalle singole lettere. Allo stesso modo si procede con i numeri. Per esempio per costruire i numeri naturali si può usare la seguente grammatica (N è lo scopo, cioè è un numero naturale):

N ::= C | C N
C ::= 0,1,2 .... (tutte le cifre)

    Se si vogliono considerare gli interi e poi anche i reali le cose si complicano un po'. Il tutto appesantisce il Lexer che deve analizzare carattere per carattere decidendo se sta cotruendo un numero, un identificatore, o altro. Java fornisce a tal proposito la classe StreamTokenizer che riceve una stringa, si fa carico di tutti questi problemi e restituisce automaticamente i token già costruiti. Il Lexer allora può essere una sottoclasse di questa che si occupa semplicemente di filtrare i tokens costruiti dallo StreamTokeinzer e costruire i corrispondenti oggetti della gerarchia Sexp. Tuttavia ciò porta alcuni problemi. Per primo la funzione che nello StreamTokenizer costruisce i numeri nasconde l'iniziale rappresentazione del numero stesso, in quanto non restituisce la stringa numerica trovata, ma solo il valore in formato double. In questo modo se l'interprete vuole accettare i numeri reali l'utente può scrivere 3 o 3.0, ma entrambi diventano 3.0, poiché così li costruisce lo StreamTokenizer. Questo è un problema se si vogliono implementare funzioni il cui risultato dipenda dalla forma del numero, come ad esempio la funzione integer(N) che restituisca true se N è un numero intero. In Lisp il problema si presenta nell'implementazione della funzione eq. La funzione Lisp eq si applica a due argomenti e restituisce true se i due argomenti sono lo stesso oggetto. Allora se l'utente scrive (eq x x) si aspetta che venga restituito true poiché x ed x sono lo stesso oggetto. Tuttavia per far sì che l'interprete si accorga che si tratta dello stesso oggetto è necessario che il lexer ogni volta che incontra lo stesso simbolo restituisca lo stesso oggetto. Ciò significa che quando il lexer trova per la prima volta il simbolo x deve generare un IdentSexp per quel simbolo e, prima di restituirlo, deve salvarlo in un "contenitore" locale. Questo potrà essere una semplice lista o una hash table se si vuole maggiore efficienza. L'importante è che quando il lexer trova per la seconda volta il simbolo x prima di generare un nuovo oggetto controlli in quel contenitore se è già presente un oggetto uguale a quello che dovrebbe creare, poiché in caso favorevole dovrà restituire quello stesso oggetto creato in precedenza e non crearne un'altro uguale. In questo modo per implementare la primitiva eq è sufficiente dotare la Sexp del metodo:

public boolean isEq(Sexp el) {  return (this==el); }

    È da notare che non è possibile un'altra soluzione, poiché solo a livello di analisi sintattica si può verificare l'uguaglianza (che per l'appunto è sintattica) fra due stringhe scritte dall'utente. Una soluzione di questo tipo è stata realizzata a scopo di esercizio. Una possibile soluzione alternativa è quella di controllare l'uguaglianza della rappresentazione esterna dei due argomenti (la loro stampa in forma di stringa): se la rappresentazione esterna è la stessa i due argomenti sono considerati uguali. questa è la soluzione adottata nella versione finale dell'interprete, così non è necessario mantenere in una lista locale al Lexer tutti i token creati dall'inizio della computazione. Tuttavia entrambe le soluzioni non possono trattare il caso dei numeri. Infatti si consideri l'esempio seguente: le due stringhe "3" e "3.0" indicano due numeri fra loro diversi: il primo intero di valore 3 ed il secondo reale di valore 3.0. Evidentemente scrivendolì così ci si aspetta che vengano creati 2 oggetti differenti e diversi, ovvero che producano false le valutazioni di entrambe le seguenti frasi:

(eq 3 3.0)
(equal 3 3.0)  ; la primitiva Lisp equal restituisce true se i due argomenti denotano oggetti isomorfi

    Tuttavia con l'interprete realizzato ciò non avviene ed anzi entrambe le valutazioni danno come esito il valore true. Ciò accade per i motivi detti in precedenza ed imputabili all'utilizzo della classe Java StreamTokenizer. Ne segue che nell'esempio citato per poter ottenere la costruzione di oggetti Sexp corretti (e fra loro differenti) è necessario non utilizzare la funzione di parsing dei numeri dello StreamTokenizer, ma implementarla nel Lexer. Tuttavia ciò produce il fastidioso svantaggio di doversi far carico nel lexer anche dei caratteri di separazione. Tali caratteri (fra i quali il lo "space", l'"end of line"...) nell'attuale realizzazione sono filtrati dallo StreamTokenizer e non arrivano al Lexer. Se però il Lexer si deve far carico del parsing dei numeri allora dovrà trattare anche i separatori poiché se non lo facesse allora ottenendo la successione di stringhe "3","." e "0" non potrebbe sapere se in origine si trattava di "3.0" oppure "3 . 0", ovvero il numero reale 3.0 oppure la coppia di interi 3 e 0 separati da un punto. In conclusione, per comodità, si è deciso di tralasciare il problema e quindi considerare tutti i numeri come reali e rendere quindi indistinguibili i numeri 3.0 e 3. Una facile soluzione per il problema la si potrebbe ottenere se il Tokenizer oltre che restituire il valore numerico in forma di double restituisse anche la stringa trovata. Per questo e per altri motivi la soluzione finale non fa uso della classe StreamTokenizer di Java, ma di una classe detta StreamTokenizerWithAtom e definita in modo da fornire tutte le funzionalità necessarie alla realizzazione dell'interprete. Tale argomento verrà ripreso nella discussione relativa all'interprete Prolog.