Qual è la differenza tra i parser LR, SLR e LALR?


103

Qual è la differenza effettiva tra i parser LR, SLR e LALR? So che SLR e LALR sono tipi di parser LR, ma qual è la differenza effettiva per quanto riguarda le loro tabelle di analisi?

E come mostrare se una grammatica è LR, SLR o LALR? Per una grammatica LL dobbiamo solo mostrare che qualsiasi cella della tabella di analisi non deve contenere più regole di produzione. Esistono regole simili per LALR, SLR e LR?

Ad esempio, come possiamo dimostrare che la grammatica

S --> Aa | bAc | dc | bda
A --> d

è LALR (1) ma non SLR (1)?


EDIT (ybungalobill) : non ho ottenuto una risposta soddisfacente per qual è la differenza tra LALR e LR. Quindi le tabelle di LALR sono di dimensioni inferiori ma possono riconoscere solo un sottoinsieme di grammatiche LR. Qualcuno può approfondire di più sulla differenza tra LALR e LR, per favore? LALR (1) e LR (1) saranno sufficienti per una risposta. Entrambi usano 1 token look-ahead ed entrambi sono guidati da una tabella! In che modo sono diversi?


beh, anche io sto cercando una risposta adeguata su questo, LALR (1) è solo una leggera modifica di LR (1), dove la dimensione della tabella è ridotta in modo da poter ridurre al minimo l'utilizzo della memoria ...
vikkyhacks

Risposte:


64

I parser SLR, LALR e LR possono essere implementati utilizzando esattamente lo stesso macchinario azionato da tabella.

Fondamentalmente, l'algoritmo di analisi raccoglie il successivo token di input T e consulta lo stato corrente S (e le tabelle di lookahead, GOTO e riduzione associate) per decidere cosa fare:

  • SHIFT: se la tabella corrente dice di SHIFT sul token T, la coppia (S, T) viene inserita nello stack di analisi, lo stato viene modificato in base a ciò che la tabella GOTO dice per il token corrente (ad esempio, GOTO (T) ), viene recuperato un altro token di input T 'e il processo si ripete
  • RIDUZIONE: ogni stato ha 0, 1 o molte possibili riduzioni che potrebbero verificarsi nello stato. Se il parser è LR o LALR, il token viene verificato rispetto ai set di lookahead per tutte le riduzioni valide per lo stato. Se il token corrisponde a un set di lookahead per una riduzione per la regola grammaticale G = R1 R2 .. Rn, si verifica una riduzione dello stack e uno spostamento: viene chiamata l'azione semantica per G, lo stack viene estratto n (da Rn) volte, la coppia ( S, G) viene inserito nello stack, il nuovo stato S 'è impostato su GOTO (G) e il ciclo si ripete con lo stesso token T. Se il parser è un parser SLR, c'è al massimo una regola di riduzione per il stato e quindi l'azione di riduzione può essere eseguita alla cieca senza cercare di vedere quale riduzione si applica. E 'utile per un parser SLR di sapere se v'èuna riduzione o meno; questo è facile da capire se ogni stato registra esplicitamente il numero di riduzioni ad esso associate, e quel conteggio è comunque necessario per le versioni L (AL) R in pratica.
  • ERRORE: se né SHIFT né REDUCE è possibile, viene dichiarato un errore di sintassi.

Quindi, se usano tutti lo stesso macchinario, qual è il punto?

Il valore presunto in SLR è la sua semplicità di implementazione; non è necessario scorrere le possibili riduzioni controllando i set di lookahead perché ce n'è al massimo uno, e questa è l'unica azione praticabile se non ci sono uscite SHIFT dallo stato. Quale riduzione si applica può essere attribuita specificamente allo stato, quindi il macchinario di analisi SLR non deve cercarla. In pratica, i parser L (AL) R gestiscono un insieme di linguaggi utilmente più ampio, ed è così poco lavoro extra da implementare che nessuno implementa SLR se non come esercizio accademico.

La differenza tra LALR e LR ha a che fare con il generatore di tabelle. I generatori di parser LR tengono traccia di tutte le possibili riduzioni da stati specifici e del loro preciso set di lookahead; si finisce con stati in cui ogni riduzione è associata al suo esatto lookahead impostato dal contesto sinistro. Questo tende a costruire insiemi di stati piuttosto ampi. I generatori di parser LALR sono disposti a combinare stati se le tabelle GOTO e le serie di lookhead per le riduzioni sono compatibili e non sono in conflitto; questo produce un numero di stati notevolmente inferiore, al prezzo di non essere in grado di distinguere certe sequenze di simboli che LR può distinguere. Quindi, i parser LR possono analizzare un insieme più ampio di linguaggi rispetto ai parser LALR, ma hanno tabelle di parser molto più grandi. In pratica, si possono trovare grammatiche LALR che sono abbastanza vicine ai linguaggi di destinazione che vale la pena ottimizzare la dimensione della macchina a stati;

Quindi: tutti e tre usano lo stesso macchinario. La reflex è "facile" nel senso che puoi ignorare un po 'della macchina ma non ne vale la pena. LR analizza un insieme più ampio di linguaggi, ma le tabelle di stato tendono ad essere piuttosto grandi. Ciò lascia LALR come scelta pratica.

Detto questo, vale la pena sapere che i parser GLR possono analizzare qualsiasi linguaggio senza contesto, usando macchinari più complicati ma esattamente le stesse tabelle (inclusa la versione più piccola usata da LALR). Ciò significa che GLR è strettamente più potente di LR, LALR e SLR; più o meno se puoi scrivere una grammatica BNF standard, GLR la analizzerà in base ad essa. La differenza nel meccanismo è che GLR è disposto a provare più analisi in caso di conflitti tra la tabella GOTO e / o gli insiemi di lookahead. (Il modo in cui GLR lo fa in modo efficiente è puro genio [non mio] ma non si adatterà a questo post SO).

Questo per me è un fatto estremamente utile. Costruisco analizzatori di programma e trasformatori e parser di codice sono necessari ma "poco interessanti"; il lavoro interessante è quello che fai con il risultato analizzato e quindi l'attenzione è concentrata sul lavoro di post-analisi. Usare GLR significa che posso costruire relativamente facilmente grammatiche funzionanti, rispetto all'hacking di una grammatica per entrare nella forma utilizzabile LALR. Questo è molto importante quando si cerca di trattare con linguaggi non accademici come C ++ o Fortran, dove hai letteralmente bisogno di migliaia di regole per gestire bene l'intera lingua e non vuoi passare la vita cercando di hackerare le regole grammaticali per soddisfare i limiti di LALR (o anche LR).

Come una sorta di famoso esempio, il C ++ è considerato estremamente difficile da analizzare ... dai ragazzi che eseguono l'analisi LALR. C ++ è semplice da analizzare usando i macchinari GLR usando praticamente le regole fornite nel retro del manuale di riferimento di C ++. (Ho proprio un analizzatore di questo tipo e gestisce non solo vanilla C ++, ma anche una varietà di dialetti del fornitore. Questo è possibile solo in pratica perché stiamo usando un parser GLR, IMHO).

[EDIT novembre 2011: abbiamo esteso il nostro parser per gestire tutto il C ++ 11. GLR lo ha reso molto più facile da fare. EDIT Agosto 2014: ora gestisce tutto il C ++ 17. Niente si è rotto o è peggiorato, GLR è ancora il miagolio del gatto.]


AFAIK C ++ non può essere analizzato con LR perché ha bisogno di uno sguardo infinito. Quindi non riesco a pensare a nessun hack che renderà possibile analizzarlo con LR. Anche i parser LRE sembrano promettenti.
Yakov Galka

5
GCC utilizzato per analizzare C ++ utilizzando Bison == LALR. Puoi sempre aumentare il tuo parser con extra goo per gestire i casi (lookahead, is-this-a-typename) che ti danno il cuore. La domanda è "Quanto è doloroso un hack?" Per GCC è stato piuttosto doloroso, ma l'hanno fatto funzionare. Ciò non significa che sia consigliato, che è il mio punto sull'utilizzo di GLR.
Ira Baxter

Non capisco come l'utilizzo di GLR ti aiuti con C ++. Se non sai se qualcosa è un nome di tipo o meno, allora non sai come eseguire il parser x * y;: in che modo l'uso di GLR aiuta con questo?
user541686

2
Il punto è che il parser GLR produrrà entrambe le analisi (come "sottostrutture ambigue" in un "albero" di analisi integrato (in realtà DAG). Puoi risolvere quale delle sottostrutture vuoi mantenere, in seguito, inserendo altre informazioni sul contesto. Il nostro parser C ++ riguarda notevolmente questo problema: non cerca di risolvere il problema. Ciò significa che non dobbiamo intrecciare la costruzione della tabella dei simboli con l'analisi, quindi sia il nostro parser che la costruzione della tabella dei simboli per C ++ sono singolarmente pulito e di conseguenza molto ciascuna per costruire e mantenere.
Ira Baxter

18

I parser LALR uniscono stati simili all'interno di una grammatica LR per produrre tabelle di stato del parser che hanno esattamente le stesse dimensioni della grammatica SLR equivalente, che di solito sono un ordine di grandezza inferiore rispetto alle tabelle di analisi LR pure. Tuttavia, per le grammatiche LR che sono troppo complesse per essere LALR, questi stati uniti provocano conflitti di parser o producono un parser che non riconosce completamente la grammatica LR originale.

A proposito, menziono alcune cose su questo nel mio algoritmo della tabella di analisi MLR (k) qui .

appendice

La risposta breve è che le tabelle di analisi LALR sono più piccole, ma il meccanismo del parser è lo stesso. Una data grammatica LALR produrrà tabelle di analisi molto più grandi se vengono generati tutti gli stati LR, con molti stati ridondanti (quasi identici).

Le tabelle LALR sono più piccole perché gli stati simili (ridondanti) vengono uniti insieme, eliminando efficacemente le informazioni di contesto / lookahead codificate dagli stati separati. Il vantaggio è che ottieni tabelle di analisi molto più piccole per la stessa grammatica.

Lo svantaggio è che non tutte le grammatiche LR possono essere codificate come tabelle LALR perché grammatiche più complesse hanno lookahead più complicati, risultando in due o più stati invece di un singolo stato unito.

La differenza principale è che l'algoritmo per produrre tabelle LR trasporta più informazioni tra le transizioni da stato a stato mentre l'algoritmo LALR no. Quindi l'algoritmo LALR non può dire se un dato stato unito deve essere lasciato in realtà come due o più stati separati.


3
+1 Mi piace l'idea di Honalee. Il mio generatore di parser G / L (AL) R aveva i semi di qualcosa di simile in esso; produce la macchina LALR minima, quindi stavo per dividere gli stati in cui c'erano conflitti, ma non ho mai portato a termine. Questo sembra un bel modo per produrre un insieme di tabelle di analisi di dimensioni minime "LR". Anche se non aiuterà GLR in termini di ciò che può analizzare, potrebbe ridurre il numero di analisi parallele che GLR deve trasportare e ciò sarebbe utile.
Ira Baxter

12

Ancora un'altra risposta (YAA).

Gli algoritmi di analisi per SLR (1), LALR (1) e LR (1) sono identici come ha detto Ira Baxter,
tuttavia, le tabelle del parser possono essere differenti a causa dell'algoritmo di generazione del parser.

Un generatore di parser SLR crea una macchina a stati LR (0) e calcola i look-ahead dalla grammatica (FIRST e FOLLOW set). Questo è un approccio semplificato e può segnalare conflitti che non esistono realmente nella macchina a stati LR (0).

Un generatore di parser LALR crea una macchina a stati LR (0) e calcola le previsioni dalla macchina a stati LR (0) (tramite le transizioni di terminale). Questo è un approccio corretto, ma occasionalmente riporta conflitti che non esisterebbero in una macchina a stati LR (1).

Un generatore di parser LR Canonical calcola una macchina a stati LR (1) e le previsioni fanno già parte della macchina a stati LR (1). Queste tabelle del parser possono essere molto grandi.

Un generatore di parser LR minimo calcola una macchina a stati LR (1), ma unisce stati compatibili durante il processo, quindi calcola i look-ahead dalla macchina a stati LR (1) minima. Queste tabelle del parser hanno la stessa dimensione o leggermente più grandi delle tabelle del parser LALR, offrendo la soluzione migliore.

LRSTAR 10.0 può generare parser LALR (1), LR (1), CLR (1) o LR (*) in C ++, qualunque cosa sia necessaria per la tua grammatica. Vedi questo diagramma che mostra la differenza tra i parser LR.

[Divulgazione completa: LRSTAR è il mio prodotto]


5

Supponiamo che un parser senza un lookahead analizzi felicemente le stringhe per la tua grammatica.

Usando il tuo esempio dato, si imbatte in una stringa dc, cosa fa? Lo riduce a S, perché dcè una stringa valida prodotta da questa grammatica? O forse stavamo cercando di analizzare bdcperché anche quella è una stringa accettabile?

Come esseri umani sappiamo che la risposta è semplice, dobbiamo solo ricordare se abbiamo appena analizzato bo meno. Ma i computer sono stupidi :)

Poiché un parser SLR (1) aveva il potere aggiuntivo su LR (0) per eseguire un lookahead, sappiamo che qualsiasi quantità di lookahead non può dirci cosa fare in questo caso; invece, dobbiamo guardare indietro nel nostro passato. Così viene in soccorso il canonico LR parser. Ricorda il contesto passato.

Il modo in cui ricorda questo contesto è che si disciplina, che ogni volta che incontra un b, inizierà a camminare su un sentiero verso la lettura bdc, come una possibilità. Quindi, quando vede una d, sa se sta già percorrendo un sentiero. Quindi un parser CLR (1) può fare cose che un parser SLR (1) non può!

Ma ora, poiché dovevamo definire tanti percorsi, gli stati della macchina diventano molto grandi!

Quindi uniamo gli stessi percorsi di ricerca, ma come previsto potrebbe dare origine a problemi di confusione. Tuttavia, siamo disposti a correre il rischio a costo di ridurre le dimensioni.

Questo è il tuo parser LALR (1).


Ora come farlo algoritmicamente.

Quando disegni i set di configurazione per la lingua sopra, vedrai un conflitto di riduzione dello spostamento in due stati. Per rimuoverli potresti prendere in considerazione un SLR (1), che prende decisioni guardando un follow, ma osserveresti che non sarà ancora in grado di farlo. Quindi disegneresti di nuovo i set di configurazione, ma questa volta con una restrizione che ogni volta che calcoli la chiusura, le produzioni aggiuntive che vengono aggiunte devono avere uno o più follower rigorosi. Fai riferimento a qualsiasi libro di testo su cosa dovrebbero essere questi seguenti.


Questo non è accurato

4

La differenza fondamentale tra le tabelle del parser generate con SLR e LR è che le azioni di riduzione si basano sul set di follow per le tabelle SLR. Questo può essere eccessivamente restrittivo, causando in ultima analisi un conflitto di riduzione del turno.

Un parser LR, d'altra parte, riduce le decisioni solo sull'insieme di terminali che possono effettivamente seguire la riduzione del non terminale. Questo insieme di terminali è spesso un sottoinsieme appropriato dell'insieme Follows di un tale non terminale, e quindi ha meno possibilità di entrare in conflitto con le azioni di spostamento.

I parser LR sono più potenti per questo motivo. Tuttavia, le tabelle di analisi LR possono essere estremamente grandi.

Un parser LALR inizia con l'idea di costruire una tabella di analisi LR, ma combina gli stati generati in un modo che si traduce in una dimensione della tabella significativamente inferiore. Lo svantaggio è che per alcune grammatiche sarebbe stata introdotta una piccola possibilità di conflitto che una tabella LR avrebbe altrimenti evitato.

I parser LALR sono leggermente meno potenti dei parser LR, ma comunque più potenti dei parser SLR. YACC e altri generatori di analizzatori simili tendono a utilizzare LALR per questo motivo.

PS Per brevità, SLR, LALR e LR sopra in realtà significano SLR (1), LALR (1) e LR (1), quindi è implicito un token lookahead.


4

I parser SLR riconoscono un sottoinsieme appropriato di grammatiche riconoscibili dai parser LALR (1), che a loro volta riconoscono un sottoinsieme appropriato di grammatiche riconoscibili dai parser LR (1).

Ognuno di questi è costruito come una macchina a stati, con ogni stato che rappresenta un insieme di regole di produzione della grammatica (e la posizione in ciascuna) mentre analizza l'input.

L' esempio di Dragon Book di una grammatica LALR (1) che non è SLR è questo:

S → L = R | R
L → * R | id
R → L

Ecco uno degli stati per questa grammatica:

S → L•= R
R → L•

Il indica la posizione del parser in ciascuna delle possibili produzioni. Non sa in quale delle produzioni si trova effettivamente fino a quando non raggiunge la fine e cerca di ridurre.

Qui, il parser potrebbe spostare un =o ridurre R → L.

Un parser SLR (noto anche come LR (0)) determinerebbe se può ridurre controllando se il simbolo di input successivo è nel seguente insieme di R(cioè, l'insieme di tutti i terminali nella grammatica che può seguire R). Poiché =è anche in questo set, il parser SLR incontra un conflitto di riduzione dello spostamento.

Tuttavia, un parser LALR (1) userebbe l'insieme di tutti i terminali che possono seguire questa particolare produzione di R, che è solo $(cioè, la fine dell'input). Quindi, nessun conflitto.

Come hanno notato i commentatori precedenti, i parser LALR (1) hanno lo stesso numero di stati dei parser SLR. Un algoritmo di propagazione del lookahead viene utilizzato per attaccare i lookahead alle produzioni di stati SLR dai corrispondenti stati LR (1). Il parser LALR (1) risultante può introdurre conflitti di riduzione-riduzione non presenti nel parser LR (1), ma non può introdurre conflitti di riduzione-spostamento.

Nel tuo esempio , il seguente stato LALR (1) causa un conflitto di riduzione dello spostamento in un'implementazione SLR:

S → b d•a / $
A → d• / c

Il simbolo dopo /è il set di seguito per ciascuna produzione nel parser LALR (1). In SLR, follow ( A) include a, che potrebbe anche essere spostato.



-2

Una semplice risposta è che tutte le grammatiche LR (1) sono grammatiche LALR (1). Rispetto a LALR (1), LR (1) ha più stati nella macchina a stati finiti associata (più del doppio degli stati). E questo è il motivo principale per cui le grammatiche LALR (1) richiedono più codice per rilevare errori di sintassi rispetto alle grammatiche LR (1). E un'altra cosa importante da sapere riguardo a queste due grammatiche è che nelle grammatiche LR (1) potremmo avere meno conflitti di riduzione / riduzione. Ma in LALR (1) c'è più possibilità di ridurre / ridurre i conflitti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.