|
Il JavaCup (e il JavaCC) utilizzano una grammatica per descrivere il linguaggio
di programmazione che devono analizzare. Le grammatiche possono essere definite,
in maniera un po' pomposa, come "un formalismo per descrivere tutti i programmi
esprimibili in un dato linguaggio". Più semplicemente con una grammatica
serve a verificare se una certa stringa è un costrutto sintatticamente valido
per il linguaggio da essa descritto. Poiché generalmente i costrutti esprimibili
con una grammatica sono infiniti, la grammatica li può descrivere soltanto
dando delle regole per generarli: sarebbe impensabile che li elencasse tutti.
Per esempio:
Grammatica 1
(a) lang ::= LPAREN RPAREN
(b) lang ::= LPAREN lang RPAREN
In questo caso abbiamo una grammatica che descrive tutte le stringhe che
cominciano con n parentesi tonde aperte e finiscono con altrettante parentesi
tonde chiuse. In questa notazione abbiamo due tipi di simboli: i terminali,
come LPAREN e RPAREN , e i non-terminali (come lang); le regole, come (a)
e (b), si dicono produzioni. I terminali sono simboli “definitivi”, cioè se
compaiono non possono essere più rimpiazzati; i non-terminale possono invece
essere rimpiazzati dal corpo di una regola che ha quel non-terminale in testa.
In pratica un parser analizza il testo; quando incontra una "(" produce un
simbolo LPAREN, quando incontra un ")" produce RPAREN.
Vediamo di capire come si usano le regole di una grammatica. Per verificare
se un costrutto appartiene al linguaggio si verifica se si riesce a generarlo
applicando le regole, a partire da un non-terminale che viene normalmente
chiamato simbolo iniziale. Nel nostro caso il simbolo iniziale sarà necessariamente
lang (non abbiamo molta scelta) e possiamo cominciare una generazione. La
generazione (che potrebbe anche non terminare mai) termina quando nel programma
generato non ci sono più simboli non terminali. Per esempio:
lang
(b)=> LPAREN lang RPAREN
(a)=> LPAREN LPAREN RPAREN RPAREN
Ovvero "(())"; quindi questa stringa fa parte del linguaggio generato dalla
nostra grammatica. In pratica le grammatiche non vengono utilizzate in questo
modo ma a ritroso: cioè dato un programma si cerca di stabilire se la grammatica
è in grado di generare quel programma (il che vuol dire che il programma
è grammaticalmente corretto) e, altrettanto importante, la sequenza di regole
che hanno consentito di generare quel programma. I generatori di parser intercettano
il riconoscimento delle regole, eseguendo delle operazioni al verificarsi
del "matching" di una regola.
Un parser è un programma capace di verificare, a partire da una grammatica,
se una stringa appartiene al linguaggio della grammatica, e soprattutto è
in grado di riconoscere la sequenza di regole da applicare. Il problema teorico
della costruzione del parser di una grammatica è stato studiato a fondo e
ha prodotto strumenti automatici per la loro costruzione.
Questo problema comunque non è risolvibile nella sua formulazione più
generale: non è possibile in generale riconoscere se un programma appartiene
ad una grammatica per qualsiasi grammatica. È invece risolvibile solo per
particolari grammatiche, con determinate limitazioni nel formato delle regole.
La casistica è ampia e variegata, e non entreremo nei dettagli. Diciamo solo
che sia Yacc che JavaCup sono generatori di parser per grammatiche di tipo
LALR(1) non ambigua. Il primo genera codice C mentre il secondo genera codice
Java.
|