Riassunto
Grammatica / Linguaggi liberi (context-free)
-
Una grammatica è libera se ogni produzione è del tipo:
dove
è un singolo non-terminale e è una stringa composta da terminali e/o non-terminali. -
Un linguaggio
è libero se e solo se esiste una grammatica libera tale che .
Pumping lemma per linguaggi liberi
Il negato di questo lemma ci permette di dimostrare che non esiste una determinata grammatica libera che genera un dato linguaggio.
Enunciato
Sia
Proprietà
I linguaggi liberi sono chiusi per UNIONE, CONCATENAZIONE ma NON per intersezione.
Espressioni regolari
Valgono:
| Operazione | Sintassi | Linguaggio |
|---|---|---|
| ALTERNANZA | ||
| CONCATENAZIONE | ||
| MOLTEPLICITÀ | ||
| PRECEDENZA |
Precedenze
Esempio:
Conversioni
Da un'espressione regolare è possibile:
-
Ottenere un NFA utilizzando la costruzione di Thompson
simulare l'NFA. -
Ottenere un DFA dato l'NFA utilizzando la subset construction
simulare il DFA. - Ottenere un DFA minimo dato un DFA utilizzando il partition refinement.
-
Simulazione di un NFA
-
Si parte da
, che contiene lo stato iniziale più quelli a cui possiamo arrivare facendo una -transizione. -
A questo punto prendiamo il primo carattere della parola analizzata e facciamo una transizione di quel carattere.
-
Facciamo pure una
-chiusura e aggiungiamo gli stati raggiunti. -
Prendiamo ora il secondo carattere e ripartiamo con la procedura.
-
Se
(l'ultimo insieme di stati raggiunto) contiene uno stato finale, allora la parola (appartiene al linguaggio).
Costo:
Trasformazione di un NFA in DFA
La procedura segue questi passaggi:
-
Inizio: Si parte da
, che contiene lo stato iniziale più tutti quelli raggiungibili tramite -transizioni. -
Costruzione Tabella: Si crea una tabella dove:
-
Sulle righe si pongono i vari insiemi di stati
. -
Sulle colonne si pongono tutti i terminali dell'alfabeto.
-
-
Iterazione:
-
Partendo da
, per ogni terminale nelle colonne si calcola la transizione, creando così un nuovo stato del DFA. -
Nota bene: Bisogna ricordarsi di calcolare la
-chiusura (o -transizione) per ognuno degli stati così ottenuti!
-
Minimizzare un DFA
Bisogna intanto essere sicuri che il DFA sia totale, ovvero che ogni stato abbia una transizione per tutti i terminali. Se non è così, si aggiunge un SINK a cui si fanno arrivare tutte le transizioni mancanti.
A questo punto si inseriscono in un set tutti gli stati non finali ed in un altro set tutti gli stati finali. A questo punto si parte con le n-equivalenze.
Due stati appartengono allo stesso set se:
-
La transizione di entrambi finisce nello stesso set.
-
La transizione di entrambi finisce nello stesso stato.
Se queste condizioni non valgono, i due stati non appartengono allo stesso set.
Linguaggi regolari
Un linguaggio
-
esiste un'espressione regolare t.c. -
esiste un NFA t.c. -
esiste un DFA t.c. -
esiste una grammatica regolare t.c.
Ricordiamo che ogni grammatica regolare è anche una grammatica libera (context-free).
Proprietà di Chiusura
I linguaggi regolari sono chiusi rispetto alle seguenti operazioni:
-
UNIONE
-
CONCATENAZIONE
-
COMPLEMENTAZIONE
-
INTERSEZIONE
Pumping lemma per linguaggi regolari
Sia
FIRST
In linea generica, il FIRST di una stringa (di terminali / non-terminali) è il primo carattere che compare in ogni produzione di quella stringa.
Dunque:
-
Per una produzione del tipo
: -
Per ogni produzione con inizio di un terminale (
): -
Per ogni produzione del tipo
con terminali o non-terminali: -
Se
-
Se
-
Per calcolare
Se
FOLLOW
In linea generica il FOLLOW di una stringa (di non-terminali) è l'insieme dei terminali che seguono la stringa in qualche derivazione.
Procedura di calcolo
-
Inizio: Come primo passo si aggiunge il simbolo $ (fine input) ai follow dello start symbol.
-
Iterazione: Per ogni produzione del tipo
(dove è un non-terminale e è la stringa che segue): -
Se
: -
Se
:
-
Nello schema sopra, A rappresenta un non-terminale e
Parsing Top-Down: LL(1) parser
Sta per "Left-to-Right Leftmost" parser. Prende in input una parola e la parsing table e restituisce in output la derivazione leftmost per ottenere la parola (se
Procedimento
-
Calcolare First e Follow di ogni non-terminale.
-
Costruire la tabella di parsing.
-
Riconoscimento della parola usando uno stack e la parsing table.
Il punto 1 l'abbiamo già esplorato in precedenza.
(2) Costruzione della tabella di parsing
Innanzitutto si crea una tabella con:
-
I NON terminali sulle righe.
-
I terminali sulle colonne.
Si prende poi ogni produzione della grammatica e si applica la seguente logica:
Algoritmo di riempimento:
foreach
foreach (terminale
inserire
if
foreach (terminale
inserire
(3) Grammatica LL(1)
Se la tabella di parsing non ha più di una produzione per una certa cella (non ha "multiple defined entries"), allora la grammatica è LL(1).
Riconoscimento della parola
In questa fase ci servono:
-
La parola
(a cui attacchiamo alla fine il simbolo ). -
Uno stack (in cui pushiamo subito
).
Procedimento
L'algoritmo legge la parola e utilizza lo stack per verificare la correttezza della derivazione:
stack s;
s.push($);
s.push(S); // S è lo start symbol
x = s.top();
while (x != $) {
b = read(); // legge un carattere dalla parola
if (x == b) {
s.pop();
}
else if (x is terminal || M[x, b] is error) {
error();
}
else if (M[x, b] == A -> Y1 ... Yn) {
s.pop();
s.push(Y1 ... Yn); // Push dei simboli della produzione
}
x = s.top();
}
// Se il ciclo termina correttamente: w ∈ L
La funzione read() legge un carattere alla volta dalla parola in input.
Tips per riconoscere se una grammatica è LL(1)
Data una grammatica
-
Se
è -
Se
, allora è -
Se
, allora è
Casi di non-LL(1):
-
Le grammatiche ricorsive sinistre NON sono
-
Le grammatiche fattorizzabili a sinistra NON sono
-
Le grammatiche ambigue NON sono
Rimuovere la ricorsione a sinistra
Generalmente, dato lo schema:
con
Si trasforma in:
Le produzioni non affette da ricorsione le tieni, ma ci attacchi
Ricorsione sinistra non immediata
In caso di ricorsione sinistra non immediata, si sostituiscono le produzioni del 1° NON-terminale nel 2° NON-terminale.
Esempio:
Dati i seguenti passaggi:
In questo caso, abbiamo una ricorsione sinistra su
1. Procediamo per sostituzione
Sostituiamo le produzioni di
La produzione
Otteniamo quindi la grammatica aggiornata:
2. Togliamo ora la ricorsione (immediata)
Applichiamo l'algoritmo standard per eliminare la ricorsione sinistra su
Fattorizzazione a sinistra
Data una grammatica del tipo:
Questa si trasforma in:
E si continua iterativamente finché la grammatica non diventa LL(1).
Rimuovere le ambiguità
Non esiste un vero e proprio algoritmo universale, ma generalmente si segue questa regola logica:
Si cerca di tenere nelle prime derivazioni (quelle più vicine allo start symbol) i caratteri/operatori con minore priorità, e nelle ultime derivazioni quelli con maggiore priorità. In questo modo, nell'albero di derivazione, gli operatori con maggiore priorità si troveranno più in basso (e verranno quindi valutati per primi).
Esempio pratico
Data la grammatica ambigua:
Con priorità degli operatori:
La grammatica trasformata diventa:
-
Livello priorità bassa (
): -
Livello priorità media (
): -
Livello priorità alta (
): -
SLR(1)
Sta per Simple Left-to-right Rightmost parser. Siamo nel Bottom-Up parsing.
Procedimento
-
Costruzione dell'automa
-
Costruzione della tabella di parsing
-
Riconoscimento della parola
1. Costruzione dell'automa
-
Si parte dallo stato 0 che contiene
e si aggiungono tutte le produzioni del tipo . -
A questo punto, per tutti i caratteri che si trovano dopo il punto, creiamo uno stato.
-
Dobbiamo poi visitare ognuno di quelli, spostando in avanti il
(per quella relativa produzione). -
Quando il
arriva in fondo alla produzione, lo stato che contiene quella produzione è "SPECIALE". -
Lo stato con la produzione
è detto ACCETTATO.
2. Costruzione della tabella di parsing
-
Creiamo una tabella che ha nelle righe tutti gli stati (da
a ) e nelle colonne tutti i terminali + $ + i non terminali. -
Ci calcoliamo i First e i Follow dei non terminali.
Ci sono essenzialmente 4 casi per il riempimento:
-
Caso 1: Se abbiamo una transizione del tipo
dove è un terminale: - Inseriamo in
(ovvero shift allo stato ).
- Inseriamo in
-
Caso 2: Se abbiamo una transizione del tipo
dove è un non terminale: (ovvero go-to allo stato ).
-
Caso 3: Nella cella
inseriamo Acc. -
Caso 4: Mancano ora gli stati speciali. Per ognuno di questi eseguiamo i semplici passi:
Per ogni produzione che presenta ilalla fine (stato speciale): -
Si individua il non-terminale driver (quello a sinistra della produzione, es.
in ). -
Si recupera l'insieme
. -
Si riempie la tabella come segue:
-
3. Riconoscimento della parola
L'algoritmo vero e proprio serve per ottenere il parsing di una parola error().
Configurazione iniziale:
-
Stringa di input:
seguita dal simbolo . -
stSt: Stack per gli stati.
-
symSt: Stack per i simboli.
Algoritmo
b = first_symbol_in_the_input_buffer;
stSt.push(0); // Inizializza lo stack degli stati con lo stato 0
while (true) {
S = stSt.top();
if (M[S, b] == shift T) {
symSt.push(b); // Push del simbolo letto
stSt.push(T); // Push del nuovo stato
b = next_symbol(); // Avanza nell'input
}
else if (M[S, b] == reduce A -> beta) {
pop |beta| symbol off symSt; // Rimuove simboli pari alla lunghezza di beta
symSt.push(A); // Push del non-terminale driver
pop |beta| state off stSt; // Rimuove gli stati corrispondenti
stSt.push(M[stSt.top(), symSt.top()]); // Esegue il GOTO
print(A -> beta);
}
else if (M[S, b] == Accept) {
return; // Parsing completato con successo
}
else {
error(); // Errore di sintassi
}
}
Generazione del codice intermedio
Questa fase si occupa di tradurre le istruzioni della grammatica in un linguaggio intermedio (solitamente simile all'assembly o a tre indirizzi) utilizzando degli attributi sintetizzati o ereditati.
Produzione
-
Logica: Si crea una nuova etichetta per segnare la fine dello statement.
-
Regole:
-
Produzione
-
Logica: Se la condizione
è vera, si salta al codice di . -
Regole:
-
Produzione
-
Logica: Vengono create etichette separate per i rami true e false. Al termine del ramo true è necessario un salto incondizionato alla fine dello statement.
-
Regole:
-
Produzione
-
Logica: Viene definita un'etichetta iniziale per consentire il salto all'indietro alla fine del corpo del ciclo. La condizione
determina se eseguire o saltare alla fine dello statement. -
Regole:
-