Titolo | Un interprete per un semplice linguaggio |
Autore | Zavaroni Andrea matr. 2006 43339 |
Sommario | Un interprete di un semplice linguaggio particolarmente dedicato ad un primissimo contatto con la programmazione. |
Indice del contenuto |
Questo semplice interprete si propone di realizzare una interfaccia
user-friendly con il programmatore che puo' costruire un semplice
programma utilizzando comandi con un linguaggio a lui famigliare (scrivi
il file ..., apri l'archivio ..., ecc.).
L'interprete da me realizzato e' scritto in Java (vers. 1.1.5) ed e'
volutamente semplice per mettere in evidenza tutta una serie di problemi
legati a questo tipo di programmazione, come ad esempio il parsing di
un file di testo, il controllo degli errori, ecc... .
Tale interprete e' comunque espandibile, attraverso l'aggiunta di
ulteriori comandi.
Inoltre, e' interessante notare che e' naturalmente possibile usare
altri linguaggi di programmazione per codificare l'interprete. Ad esempio
io ho impostato in Prolog (SICStus 3) alcune operazioni.
Tale interprete si presenterebbe utilissimo ad esempio per avvicinare
i bambini alla programmazione: con un linguaggio simile al loro (ed in
italiano) possono costruire semplici programmi, imparando i rudimenti
essenziali della programmazione (come la lettura e scrittura di file,
lettura e scrittura di variabili, operazioni con variabili e costanti,
ecc...).
La grammatica G = (VN,VT,S,P) dove :
VN = {PROG, ISTRUZ, COMANDO, CICLO, ISTRUZCICLO, COMANDO1, COMANDO2, COMANDO3,
COMANDO4, REST1, REST2, REST3, REST4, SPECIFICA, ORIGINE, PREP, OPERANDO,
OP, RIS, STR, NUM, COST, COSTN},
VT = {a, apri, archivio, da, dalla, diviso, fai, file, fine, il, in, l',
leggi, meno, per, piu, ripeti, scrivi, su, video, volte, "},
S = PROG,
P = {
PROG ::= {ISTRUZ} fine
ISTRUZ ::= COMANDO | [CICLO]
CICLO ::= ripeti ISTRUZCICLO volte NUM
ISTRUZCICLO ::= {COMANDO}
COMANDO ::= COMANDO1 | COMANDO2 | COMANDO3 | COMANDO4
COMANDO1 ::= apri REST1
COMANDO2 ::= leggi REST2
COMANDO3 ::= scrivi REST3
COMANDO4 ::= fai REST4
REST1 ::= identfile | SPECIFICA identfile
SPECIFICA ::= l'archivio | il file
REST2 ::= REST1 | identvar
REST3 ::= ORIGINE PREP identfile | ORIGINE a video
ORIGINE ::= identvar | identfile | COST
PREP ::= in | su
REST4 ::= OPERANDO OP OPERANDO in RIS
OPERANDO ::= identvarn | COSTN
OP ::= piu | meno | per | diviso
RIS ::= identvar
STR ::= stringalfanumerica
NUM ::= numint
COST ::= " NUM " | " STR "
COSTN ::= " NUM "
NOTA :
identvar e' il nome di una variabile
identfile e' il nome di un file
identvarn e' il nome di una variabile istanziata a un numero intero
numint e' un numero intero
stringalfanumerica e' una stringa (di caratteri)
La grammatica G descritta appartiene alla classe LL(1) : infatti e' sempre
possibile decidere la transizione di stato utilizzando un solo simbolo
(terminale) di ingresso.
Come si vede dalle regole di produzione, che definiscono la sintassi, e'
possibile estendere la grammatica con ulteriori comandi : l'estendibilita'
e' semplice, basta aggiungere, ad esempio, COMANDO5 nella riga :
COMANDO ::= COMANDO1 | ... | COMANDO4
che diventa cosi' :
COMANDO ::= COMANDO1 | ... | COMANDO4 | COMANDO5
e poi definire COMANDO5, analogamente agli altri 4 comandi.
Ho pensato di mettere a disposizione del programmatore tutta una serie di comandi fondamentali (che ripeto puo' essere ampliata).
apri il file nomeFile
apri l'archivio
nomeFile
apri nomeFile
leggi
var : legge da tastiera e pone in var quanto letto
leggi il file nomeFile :
legge la riga corrente di nomeFile
leggi l'archivio nomeFile : legge la riga
corrente di nomeFile
leggi nomeFile : legge la riga corrente di nomeFile
scrivi var in|su nomeFile : scrive il contenuto di var nel file nomeFile
scrivi var in|su|a video : scrive il contenuto di var a video
scrivi nomeFile in|su|a video : scrive la riga corrente di nomeFile a video
scrivi nomeFile1 in|su nomeFile2 : scrive la riga corrente di nomeFile1 sul
file nomeFile2
fai var1 piu var2 in risvar : esegue la somma tra var1 e var2
mettendo il risultato in risvar
fai var1 meno var2 in risvar : esegue la sottrazione tra var1 e var2
mettendo il risultato in risvar
fai var1 per var2 in risvar : esegue il prodotto tra var1 e var2
mettendo il risultato in risvar
fai var1 diviso var2 in risvar : esegue la divisione tra var1 e var2
mettendo il risultato in risvar
Rispetto all'interprete piu' complesso per SCHEME (in Java), anche qui e'
rispettata una certa estensibilita', permettendo cosi' al programmatore di
sviluppare nuove istruzioni (nuovi comandi) per rendere piu' "potente"
l'interprete.
Ma l'aspetto ancora piu' fondamentale e' quello di generalizzare
il procedimento di costruzione che vede il passaggio del problema iniziale
a una sua formalizzazione sintattica, in quanto la codifica con l'uno o l'altro
linguaggio e' si importante, ma secondario rispetto alla generalizzazione
suddetta.
Questa "generalizzazione" si ottiene appunto formulando una
sintassi che sia chiara e che permetta di realizzare una grammatica non
ambigua di classe LL(1).
Fatto questo esistono meccanismi (come i
riconoscitori a tabelle di linguaggi LL(1) ) che permettono di codificare
(automaticamente) la sintassi in esame attraverso un'analisi deterministica
discendente con uno stile di programmazione guidato dai dati. (Uso di tabelle
dette "parsing table" che indicano il comportamento dell'automa riconoscitore).
Per scrivere la sintassi adeguata ho utilizzato del formalismo BNF (Bakus
Naus Form). Tale formalismo permette di isolare le categorie sintattiche al
fine di definire (e successivamente di costruire) l'interprete vero e proprio.
Nella grammatica in esame le categorie sono legate alle istruzioni di ciclo
(ripeti ... volte ...) e ai quattro comandi (apri, leggi, scrivi e fai),
mentre, ad esempio, le categorie del meta-interprete LISP visto a lezione
sono 8.
La realizzazione dell'interprete mediante il linguaggio Java ha
messo in luce gli aspetti positivi legati all'adozione di tale linguaggio,
come ad esempio la possibilita' di avere a disposizione classi e metodi gia'
pronti (librerie di classi e metodi di Java) per il parsing di file di testo
(StreamTokenizer()). Inoltre, per la realizzazione di un environment, ho
potuto usare i metodi legati alla classe HashTable().
In Prolog, ad
esempio, non si hanno meccanismi particolari per il parsing e per realizzare
un environment.
Esistono pero' dei predicati (predicati definiti built-in)
che permettono di realizzare in modo opportuno queste e altre operazioni. In
particolare, per aprire un file in lettura (aprire il canale di input) esiste
il predicato see(X).
Usando tali predicati e sfruttando le potenzialita' proprie del linguaggio
Prolog e' possibile definire un interprete del mio linguaggio in Prolog invece
che in Java.
Ovviamente la scelta del linguaggio di programmazione dipende
da molti fattori : una ditta di sw puo' voler utilizzare un dato linguaggio (
o un insieme di linguaggi), oppure il programmatore conosce e preferisce
programmare con un linguaggio particolare invece che un altro.
Supponendo pero' di avere una scelta libera e illimitata riguardo al linguaggio
da utilizzare, la scelta cadra' naturalmente su quello che presenta piu'
vantaggi.
Io ho scelto Java perche' tra le altre cose e' fornito di una vastissima libreria
in grado di aiutare il programmatore a risolvere molti problemi, oltre ad essere
un linguaggio particolarmente adatto alla programmazione su rete (vedi l'uso
di applet su Internet). Infatti uno sviluppo interessante di un interprete e'
quello di essere "portabile" su Internet e reso disponibile a chiunque ne abbia
bisogno.
Da parte sua il Prolog permette un approccio piu' conversazionale, quindi piu'
vicino alle esigenze umane, permettendo, grazie alle sua caratteristica
dichiarativa, di concentrarsi sulle specifiche del problema ed a risolverlo,
senza preoccuparsi molto dei dettagli della realizzazione in se. ( vedi
E in Prolog?).
Cio' che e' importante osservare e' che qualsiasi sia il linguaggio scelto,
la grammatica e in particolare le produzioni (sintassi) non cambiano. Quindi
individuata una sintassi, la codifica puo' avvenire in un secondo momento e con
il linguaggio scelto.
Occorre quindi definire in modo opportuno la grammatica, come piu' volte detto.
Volendo passare dall'interprete da me realizzato ad un interprete Prolog, ad
esempio, basta modificare il componente software che effettua la valutazione
di una frase F. Occorre poi introdurre una diversa interpretazione dei simboli
di F, attraverso l'unificazionee la risoluzione, che rappresentano le caratteristiche
peculiari (ed essenziali) del linguaggio Prolog.
L'interprete e' realizzato attraverso 3 classi appartenenti al package
interp.
Ogni classe e' definita all'interno di un file. Il relativo
file deve (e in effetti ha) lo stesso nome della classe che contiene.
Per poter utilizzare l'interprete occorre avere Java ed un editor di testo.
Dopo aver scritto il programma da interpretare (nomefile.z) basta digitare
al prompt dei comandi di MS-DOS java interp.Interp nomeFile.zav ricordando di
mettersi nel direttorio ...\interp.
Se alla fine dell'interpretazione a
video compare il messaggio "Elaborazione terminata" allora tutto e' andato a
buon fine, altrimenti viene visualizzato un particolare messaggio legato al
tipo di errore riscontrato e viene interrotta l'interpretazione.
Oltre al messaggio "Elaborazione terminata" vengono visualizzati a video anche
il numero di linee elaborate del file sorgente e l'indicazione dei file utilizzati
e chiusi.
FileF.java (contattatemi per avere il codice)
Frasi.java (contattatemi per avere il codice)
Interp.java (contattatemi per avere il codice)
Come esempio ho implementato le 4 operazioni, rese possibili dal comando
fai, attraverso il linguaggio Prolog (SICStus 3).
prolint.pro.
Come si evince dal codice l'approccio e' totalmente diverso rispetto al
linguaggio Java. In particolare il Prolog si basa su chiamate ricorsive
e definizioni di regole, fatti e goal.
La formalizzazione detta all'inizio del progetto e' quindi estrinsecata dalla
stesura di una grammatica che racchiuda in se tutte le proprieta' dell'interprete
che si vuole realizzare.
L'interprete da me creato mi ha permesso di sviluppare concretamente alcune
potenzialita' del linguaggio Java.
Inoltre la stesura e soprattutto la
prova dell'interprete su numerosi file sorgente hanno messo in luce l'importanza
di dotare il software creato di ogni sorta di controlli degli errori : il controllo
degli errori prende una parte preponderante di tutto il programma e se questo e'
vero in teoria ho potuto ancora una volta constatare che cio' e' vero anche
nella pratica.
Un software e' "vincente" se e' sicuro, affidabile ed estendibile.
Il linguaggio Java permette tali caratteristiche.
Aver costruito questo
interprete mi ha reso inoltre possibile sviluppare il concetto di oggetto calato
in una realta' che si scontra con i vecchi problemi della programmazione :
controllo di errori, scelta delle strutture dati, scelta dello stesso linguaggio
di programmazione, ecc... .