Quando utilizzare un combinatore parser? Quando utilizzare un generatore di parser?


59

Recentemente ho fatto un tuffo nel mondo dei parser, volendo creare il mio linguaggio di programmazione.

Tuttavia, ho scoperto che esistono due approcci in qualche modo diversi nella scrittura di parser: Parser Generators e Parser Combinators.

È interessante notare che non sono stato in grado di trovare alcuna risorsa che spiegasse in quali casi quale approccio fosse migliore; Piuttosto, molte risorse (e persone) ho interrogati su questo argomento non conosceva l'altro approccio, solo spiegando il loro approccio, come il metodo e non menzionare l'altra a tutti:

Panoramica semplice:

Generatore di parser

Un generatore di parser prende un file scritto in un DSL che è un dialetto del modulo Extended Backus-Naur e lo trasforma in codice sorgente che può quindi (una volta compilato) diventare un parser per il linguaggio di input descritto in questo DSL.

Ciò significa che il processo di compilazione viene eseguito in due passaggi separati. È interessante notare che anche gli stessi generatori di parser sono compilatori (e molti di essi sono effettivamente self-hosting ).

Combinatore parser

Un Combinatore di parser descrive semplici funzioni chiamate parser che prendono tutti un input come parametro e provano a togliere i primi caratteri di questo input se corrispondono. Restituiscono una tupla (result, rest_of_input), dove resultpotrebbe essere vuota (ad esempio nilo Nothing) se il parser non fosse in grado di analizzare nulla da questo input. Un esempio potrebbe essere un digitparser. Ovviamente altri parser possono prendere i parser come primi argomenti (l'argomento finale rimane ancora la stringa di input) per combinarli : ad esempio, many1tenta di abbinare un altro parser il maggior numero di volte possibile (ma almeno una volta, oppure non riesce).

Ora puoi ovviamente combinare (comporre) digite many1, per creare un nuovo parser, diciamo integer.

Inoltre, è choicepossibile scrivere un parser di livello superiore che accetta un elenco di parser, provando ciascuno di essi a turno.

In questo modo è possibile creare lexer / parser molto complessi. Nelle lingue che supportano il sovraccarico dell'operatore, questo assomiglia molto all'EBNF, anche se è ancora scritto direttamente nella lingua di destinazione (e puoi usare tutte le funzionalità della lingua di destinazione che desideri).

Differenze semplici

Linguaggio:

  • I generatori di parser sono scritti in una combinazione del DSL EBNF-ish e del codice a cui queste istruzioni dovrebbero generare quando corrispondono.
  • I combinatori parser sono scritti direttamente nella lingua di destinazione.

Lexing / Analisi:

  • I generatori di parser hanno una differenza molto distinta tra il "lexer" (che divide una stringa in token che potrebbero essere taggati per mostrare con che tipo di valore abbiamo a che fare) e il "parser" (che prende l'elenco di output dei token dal lexer e tenta di combinarli, formando un albero di sintassi astratto).
  • Combinatori parser non hanno / necessitano di questa distinzione; di solito, i parser semplici eseguono il lavoro del "lexer" e i parser di più alto livello chiamano questi più semplici per decidere quale tipo di nodo AST creare.

Domanda

Tuttavia, anche date queste differenze (e questo elenco di differenze è probabilmente lungi dall'essere completo!), Non posso fare una scelta istruita su quando usare quale. Non riesco a vedere quali sono le implicazioni / conseguenze di queste differenze.

Quali proprietà del problema indicherebbero che un problema sarebbe meglio risolto utilizzando un generatore di parser? Quali proprietà del problema indicherebbero che un problema sarebbe meglio risolto usando e un Parser Combinator?


4
Esistono almeno altri due modi per implementare i parser che non hai menzionato: gli interpreti di parser (simili ai generatori di parser, tranne che invece di compilare il linguaggio parser per es. C o Java, il linguaggio parser viene eseguito direttamente) e semplicemente scrivere il parser a mano. Scrivere il parser a mano è la forma preferita di implementazione per molte implementazioni linguistiche moderne di forza industriale (ad esempio GCC, Clang javac, Scala). Ti dà il massimo controllo sullo stato interno del parser, che aiuta a generare buoni messaggi di errore (che negli ultimi anni ...
Jörg W Mittag

3
... è diventata una priorità molto alta per gli implementatori di lingue). Inoltre, molti generatori / interpreti / combinatori di parser esistenti non sono realmente progettati per far fronte alla grande varietà di esigenze che le moderne implementazioni linguistiche devono soddisfare. Ad esempio, molte implementazioni di linguaggio moderno usano lo stesso codice per la compilazione batch, la compilazione di background IDE, l'evidenziazione della sintassi, il refactoring automatizzato, il completamento intelligente del codice, la generazione automatica della documentazione, la creazione automatica di diagrammi, ecc. Scala utilizza persino il compilatore per la riflessione di runtime e il suo sistema macro . Molti parser esistenti ...
Jörg W Mittag,

1
... i framework non sono abbastanza flessibili da gestirli. Si noti inoltre che esistono framework di parser che non sono basati su EBNF. Es . Parser packrat per Parsing Expression Grammars .
Jörg W Mittag,

2
Penso che dipenda fortemente dalla lingua che stai cercando di compilare. Di che tipo è (LR, ...)?
qwerty_so,

1
La tua ipotesi sopra è basata su BNF, che di solito è semplicemente compilato con la combinazione parser lexer / LR. Ma le lingue non sono necessariamente basate su grammatiche LR. Quindi qual è il tuo che stai pianificando di compilare?
qwerty_so,

Risposte:


59

Ho fatto molte ricerche negli ultimi giorni, per capire meglio perché esistono queste tecnologie separate e quali sono i loro punti di forza e di debolezza.

Alcune delle risposte già esistenti hanno accennato ad alcune delle loro differenze, ma non hanno fornito il quadro completo e sembrano essere in qualche modo supponente, motivo per cui questa risposta è stata scritta.

Questa esposizione è lunga, ma importante. abbi pazienza con me (o se sei impaziente, scorri fino alla fine per vedere un diagramma di flusso).


Per comprendere le differenze tra Parser Combinator e Parser Generators, è necessario innanzitutto comprendere la differenza tra i vari tipi di analisi esistenti.

parsing

L'analisi è il processo di analisi di una serie di simboli secondo una grammatica formale. (In Informatica), l'analisi viene utilizzata per consentire a un computer di comprendere il testo scritto in una lingua, in genere creando un albero di analisi che rappresenta il testo scritto, memorizzando il significato delle diverse parti scritte in ciascun nodo dell'albero. Questo albero di analisi può quindi essere utilizzato per una varietà di scopi diversi, come tradurlo in un'altra lingua (utilizzata in molti compilatori), interpretando le istruzioni scritte direttamente in qualche modo (SQL, HTML), consentendo a strumenti come Linters di fare il loro lavoro , ecc. A volte, un albero di analisi non è esplicitamentegenerato, ma piuttosto l'azione che deve essere eseguita su ciascun tipo di nodo nella struttura viene eseguita direttamente. Ciò aumenta l'efficienza, ma sott'acqua esiste ancora un albero di analisi implicito.

L'analisi è un problema difficile dal punto di vista computazionale. Ci sono stati oltre cinquant'anni di ricerche su questo argomento, ma c'è ancora molto da imparare.

In parole povere, ci sono quattro algoritmi generali per consentire a un computer di analizzare l'input:

  • Analisi LL. (Analisi senza contesto, analisi dall'alto verso il basso.)
  • Analisi LR. (Analisi senza contesto, analisi dal basso.)
  • Analisi PEG + Packrat.
  • Earley Parsing.

Si noti che questi tipi di analisi sono descrizioni teoriche molto generali. Esistono diversi modi per implementare ciascuno di questi algoritmi su macchine fisiche, con diversi compromessi.

LL e LR possono solo guardare le grammatiche senza contesto (vale a dire; il contesto attorno ai token scritti non è importante per capire come vengono utilizzati).

L'analisi PEG / Packrat e l'analisi Earley sono usate molto meno: Earley-parsing è bello in quanto può gestire molte più grammatiche (comprese quelle che non sono necessariamente senza contesto) ma è meno efficiente (come affermato dal drago libro (sezione 4.1.1); non sono sicuro che queste affermazioni siano ancora accurate). Parsing Expression Grammar + Packrat-parsing è un metodo che è relativamente efficiente e può anche gestire più grammatiche sia di LL che di LR, ma nasconde ambiguità, come verrà rapidamente toccato di seguito.

LL (derivazione da sinistra a destra, estrema sinistra)

Questo è probabilmente il modo più naturale di pensare all'analisi. L'idea è quella di guardare il token successivo nella stringa di input e quindi decidere quale delle possibili chiamate ricorsive multiple multiple dovrebbe essere presa per generare una struttura ad albero.

Questo albero è costruito "dall'alto verso il basso", il che significa che iniziamo dalla radice dell'albero e viaggiamo le regole grammaticali nello stesso modo in cui viaggiamo attraverso la stringa di input. Può anche essere visto come la costruzione di un equivalente "postfix" per il flusso di token "infix" che viene letto.

I parser che eseguono l'analisi in stile LL possono essere scritti in modo molto simile alla grammatica originale che è stata specificata. Ciò rende relativamente semplice la comprensione, il debug e il miglioramento. I combinatori di parser classici non sono altro che "pezzi lego" che possono essere messi insieme per costruire un parser in stile LL.

LR (derivazione da sinistra a destra, estrema destra)

L'analisi LR viaggia dall'altra parte, dal basso verso l'alto: ad ogni passo, gli elementi superiori nello stack vengono confrontati con l'elenco della grammatica, per vedere se potevano essere ridotti a una regola di livello superiore nella grammatica. In caso contrario, il token successivo dal flusso di input viene spostato e posizionato in cima allo stack.

Un programma è corretto se alla fine finiamo con un singolo nodo nello stack che rappresenta la regola di partenza dalla nostra grammatica.

Guarda avanti

In uno di questi due sistemi, a volte è necessario sbirciare più token dall'input prima di poter decidere quale scelta fare. Questo è il (0), (1), (k)o (*)-syntax vedete dopo i nomi di questi due algoritmi generali, come LR(1) o LL(k). kdi solito sta per "tutto ciò di cui ha bisogno la tua grammatica", mentre di *solito sta per "questo parser esegue backtracking", che è più potente / facile da implementare, ma ha una memoria e un tempo di utilizzo molto più elevati rispetto a un parser che può semplicemente continuare ad analizzare linearmente.

Si noti che i parser in stile LR hanno già molti token nello stack quando potrebbero decidere di "guardare avanti", quindi hanno già più informazioni su cui inviare. Ciò significa che spesso hanno bisogno di meno "lookahead" di un parser in stile LL per la stessa grammatica.

LL vs. LR: ambiguità

Leggendo le due descrizioni precedenti, ci si potrebbe chiedere perché esiste l'analisi in stile LR, poiché l'analisi in stile LL sembra molto più naturale.

Tuttavia, l'analisi in stile LL presenta un problema: ricorsione sinistra .

È molto naturale scrivere una grammatica come:

expr ::= expr '+' expr | term term ::= integer | float

Ma un parser in stile LL rimarrà bloccato in un ciclo ricorsivo infinito quando analizza questa grammatica: quando prova la possibilità più a sinistra della exprregola, ricorre nuovamente a questa regola senza consumare alcun input.

Esistono modi per risolvere questo problema. Il più semplice è riscrivere la grammatica in modo che questo tipo di ricorsione non avvenga più:

expr ::= term expr_rest expr_rest ::= '+' expr | ϵ term ::= integer | float (Qui, ϵ sta per 'stringa vuota')

Questa grammatica ora è ricorsiva. Si noti che è immediatamente molto più difficile da leggere.

In pratica, la ricorsione a sinistra potrebbe avvenire indirettamente con molti altri passaggi nel mezzo. Questo rende un problema difficile da cercare. Ma cercare di risolverlo rende la tua grammatica più difficile da leggere.

Come afferma la Sezione 2.5 del Dragon Book:

Sembra che abbiamo un conflitto: da un lato abbiamo bisogno di una grammatica che faciliti la traduzione, dall'altro abbiamo bisogno di una grammatica significativamente diversa che faciliti l'analisi. La soluzione è iniziare con la grammatica per una facile traduzione e trasformarla attentamente per facilitare l'analisi. Eliminando la ricorsione sinistra possiamo ottenere una grammatica adatta all'uso in un traduttore predittivo di discesa ricorsiva.

I parser in stile LR non hanno il problema di questa ricorsione a sinistra, poiché costruiscono l'albero dal basso verso l'alto. Tuttavia , la traduzione mentale di una grammatica come sopra in un parser in stile LR (che è spesso implementato come un automa a stati finiti )
è molto difficile (e soggetta a errori), poiché spesso ci sono centinaia o migliaia di stati + transizioni di stato da considerare. Questo è il motivo per cui i parser in stile LR sono generalmente generati da un generatore di parser, noto anche come "compilatore del compilatore".

Come risolvere le ambiguità

Abbiamo visto due metodi per risolvere le ambiguità della ricorsione a sinistra sopra: 1) riscrivere la sintassi 2) usare un parser LR.

Ma ci sono altri tipi di ambiguità che sono più difficili da risolvere: cosa succede se due diverse regole sono ugualmente applicabili contemporaneamente?

Alcuni esempi comuni sono:

Sia i parser in stile LL che quelli in stile LR hanno problemi con questi. I problemi con l'analisi delle espressioni aritmetiche possono essere risolti introducendo la precedenza dell'operatore. In modo simile, altri problemi come il Dangling Else possono essere risolti, scegliendo un comportamento di precedenza e attenendosi ad esso. (In C / C ++, ad esempio, il penzolante appartiene sempre al "se" più vicino).

Un'altra "soluzione" a questo consiste nell'utilizzare Parser Expression Grammar (PEG): è simile alla grammatica BNF usata in precedenza, ma nel caso di un'ambiguità, scegli sempre "la prima". Naturalmente, questo non "risolve" realmente il problema, ma piuttosto nasconde l'esistenza di un'ambiguità: gli utenti finali potrebbero non sapere quale scelta fa il parser e questo potrebbe portare a risultati inaspettati.

Ulteriori informazioni che sono molto più approfondite di questo post, incluso il motivo per cui è impossibile in generale sapere se la tua grammatica non ha alcuna ambiguità e le implicazioni di ciò è il meraviglioso articolo di blog LL e LR nel contesto: Perché analizzare gli strumenti sono difficili . Lo consiglio vivamente; mi ha aiutato molto a capire tutte le cose di cui sto parlando in questo momento.

50 anni di ricerca

Ma la vita va avanti. Si è scoperto che i parser "normali" in stile LR implementati come automi a stati finiti spesso necessitavano di migliaia di stati + transizioni, il che rappresentava un problema nella dimensione del programma. Quindi, sono state scritte varianti come Simple LR (SLR) e LALR (Look-ahead LR) che combinano altre tecniche per ridurre l'automa, riducendo il footprint di memoria e del disco dei programmi parser.

Inoltre, un altro modo per risolvere le ambiguità sopra elencate consiste nell'utilizzare tecniche generalizzate in cui, nel caso di un'ambiguità, entrambe le possibilità sono mantenute e analizzate: l'una o l'altra potrebbe non riuscire ad analizzare la linea (nel qual caso l'altra possibilità è la 'corretto'), oltre a restituire entrambi (e in questo modo mostrando l'esistenza di un'ambiguità) nel caso entrambi siano corretti.

È interessante notare che, dopo la descrizione dell'algoritmo LR generalizzato , si è scoperto che un approccio simile poteva essere utilizzato per implementare parser LL generalizzati , che è altrettanto veloce ($ O (n ^ 3) $ complessità temporale per grammatiche ambigue, $ O (n) $ per grammatiche completamente inequivocabili, sebbene con più contabilità di un semplice parser LR (LA), il che significa un fattore costante più elevato) ma consente ancora una volta che un parser sia scritto in uno stile di discesa ricorsivo (dall'alto verso il basso) che è molto più naturale scrivere e debug.

Combinatori di parser, generatori di parser

Quindi, con questa lunga esposizione, stiamo arrivando al centro della domanda:

Qual è la differenza tra Parser Combinator e Parser Generator e quando si dovrebbe usare uno rispetto all'altro?

Sono davvero diversi tipi di animali:

I Parser Combinator furono creati perché le persone scrivevano parser top-down e si resero conto che molti di questi avevano molto in comune .

I generatori di parser sono stati creati perché le persone stavano cercando di costruire parser che non presentassero i problemi dei parser di tipo LL (ovvero i parser di tipo LR), che si sono rivelati molto difficili da eseguire manualmente. Quelli comuni includono Yacc / Bison, che implementa (LA) LR).

È interessante notare che al giorno d'oggi il paesaggio è un po 'confuso:

  • È possibile scrivere Parser Combinator che funzionano con l' algoritmo GLL , risolvendo i problemi di ambiguità che avevano i classici parser in stile LL, pur essendo leggibili / comprensibili come tutti i tipi di analisi top-down.

  • I generatori di parser possono anche essere scritti per parser in stile LL. ANTLR fa esattamente questo e usa altre euristiche (Adaptive LL (*)) per risolvere le ambiguità che avevano i classici parser in stile LL.

In generale, la creazione di un generatore di parser LR e il debug dell'output di un generatore di parser in stile LR (LA) in esecuzione sulla grammatica sono difficili, a causa della traduzione della grammatica originale nel modulo LR "rovesciato". D'altra parte, strumenti come Yacc / Bison hanno avuto molti anni di ottimizzazioni e hanno visto molto uso in natura, il che significa che molte persone ora lo considerano come il modo di eseguire l'analisi e sono scettici nei confronti di nuovi approcci.

Quale dovresti usare dipende da quanto è dura la tua grammatica e da quanto deve essere veloce il parser. A seconda della grammatica, una di queste tecniche (/ implementazioni delle diverse tecniche) potrebbe essere più veloce, avere un footprint di memoria più piccolo, avere un footprint del disco più piccolo o essere più estensibile o più facile da debug rispetto alle altre. Il tuo chilometraggio può variare .

Nota a margine: sull'argomento dell'analisi lessicale.

L'analisi lessicale può essere utilizzata sia per i combinatori parser che per i generatori parser. L'idea è di avere un parser "stupido" che sia molto facile da implementare (e quindi veloce) che esegua un primo passaggio sul codice sorgente, rimuovendo ad esempio la ripetizione di spazi bianchi, commenti, ecc. E possibilmente "tokenizzazione" in un modo molto in modo approssimativo i diversi elementi che compongono la tua lingua.

Il vantaggio principale è che questo primo passo rende il vero parser molto più semplice (e quindi probabilmente più veloce). Lo svantaggio principale è che hai una fase di traduzione separata e, ad esempio, la segnalazione degli errori con i numeri di riga e colonna diventa più difficile a causa della rimozione dello spazio bianco.

Un lexer alla fine è "solo" un altro parser e può essere implementato usando una delle tecniche sopra. Per la sua semplicità, spesso vengono utilizzate altre tecniche rispetto al parser principale, e per esempio esistono "generatori di lexer" extra.


Tl; Dr:

Ecco un diagramma di flusso applicabile alla maggior parte dei casi: inserisci qui la descrizione dell'immagine


@Sjoerd È davvero un sacco di testo, in quanto si è rivelato un problema molto difficile. Se conosci un modo in cui posso rendere più chiaro l'ultimo paragrafo, sono tutto orecchi: "Quale dovresti usare, dipende da quanto è dura la tua grammatica e da quanto deve essere veloce il parser. A seconda della grammatica, una di queste tecniche (/ implementazioni delle diverse tecniche) potrebbe essere più veloce, avere un footprint di memoria più piccolo, avere un footprint del disco più piccolo o essere più estensibile o più facile da eseguire il debug degli altri. Il tuo chilometraggio può variare ".
Qqwy,

1
Le altre risposte sono entrambe molto più brevi e molto più chiare e svolgono un lavoro molto migliore nel rispondere.
Sjoerd,

1
@Sjoerd il motivo per cui ho scritto questa risposta era perché le altre risposte o stavano semplificando eccessivamente il problema, presentando una risposta parziale come risposta completa e / o cadendo nella trappola dell'errore aneddotico . La risposta sopra è l'incarnazione della discussione che Jörg W Mittag, Thomas Killian e io abbiamo avuto nei commenti della domanda dopo aver capito di cosa stavano parlando e presentandola senza assumere conoscenze preliminari.
Qqwy,

In ogni caso, ho aggiunto un diagramma di flusso tl; dr alla domanda. Ti soddisfa, @Sjoerd?
Qqwy,

2
I combinatori parser non riescono davvero a risolvere il problema quando non li si utilizza effettivamente. Ci sono più combinatori che semplici |, questo è il punto. La corretta riscrittura exprè ancora più concisa expr = term 'sepBy' "+"(in cui le virgolette singole sostituiscono i backtick per trasformare una funzione infix, poiché il mini-markdown non ha caratteri di escape). Nel caso più generale c'è anche il chainBycombinatore. Mi rendo conto che è difficile trovare un semplice compito di analisi come esempio che non si adatta bene ai PC, ma è davvero un argomento forte a loro favore.
Steven Armstrong,

8

Per input che sono garantiti privi di errori di sintassi, o in cui un corretto passaggio / fallimento sulla correttezza sintattica va bene, i combinatori parser sono molto più semplici da lavorare, specialmente nei linguaggi di programmazione funzionale. Queste sono situazioni come programmare puzzle, leggere file di dati, ecc.

La caratteristica che ti fa desiderare di aggiungere la complessità dei generatori di parser sono i messaggi di errore. Volete messaggi di errore che indirizzano l'utente a una riga e una colonna e, si spera, siano anche comprensibili da un essere umano. Ci vuole un sacco di codice per farlo correttamente, e i migliori generatori di parser come antlr possono aiutarti.

Tuttavia, la generazione automatica può arrivare solo lontano, e la maggior parte dei compilatori open source commerciali e di lunga durata finiscono per scrivere manualmente i loro parser. Presumo che se ti sentissi a tuo agio nel fare questo, non avresti fatto questa domanda, quindi consiglierei di andare con il generatore di parser.


2
La ringrazio per la risposta! Perché sarebbe più semplice creare messaggi di errore leggibili usando un generatore di parser rispetto a un combinatore di parser? (Indipendentemente da quello dell'attuazione di cui stiamo parlando, in particolare) Ad esempio, so che sia Parsec e Spirito contengono funzionalità per stampare i messaggi di errore comprese le informazioni linea + colonna, così sembra sicuramente possibile fare questo in Parser combinatori pure.
Qqwy

Non è che non è possibile stampare messaggi di errore con i combinatori di parser, è che i loro vantaggi sono meno evidenti quando si lanciano messaggi di errore nel mix. Fai una grammatica relativamente complessa usando entrambi i metodi e vedrai cosa intendo.
Karl Bielefeldt,

Con un Parser Combinator, per definizione, tutto ciò che è possibile ottenere in una condizione di errore è "A partire da questo punto, non è stato trovato alcun input legale". Questo non ti dice davvero cosa non andava. In teoria, i singoli parser chiamati a quel punto potrebbero dirti cosa si aspettava e DIDN'T find, ma tutto ciò che potevi fare è stampare tutto ciò, creando un messaggio di errore troppo lungo.
John R. Strohm,

1
Ad ogni modo, i generatori di parser non sono esattamente noti per i loro buoni messaggi di errore.
Miles Rout il

Non di default, no, ma hanno ganci più convenienti per l'aggiunta di buoni messaggi di errore.
Karl Bielefeldt,

4

Sam Harwell, uno dei manutentori del generatore di parser ANTLR, ha recentemente scritto :

Ho trovato [combinatori] non soddisfano le mie esigenze:

  1. ANTLR mi offre strumenti per gestire cose come le ambiguità. Durante lo sviluppo ci sono strumenti che possono mostrarmi risultati di analisi ambigui in modo da poter eliminare quelle ambiguità nella grammatica. In fase di esecuzione posso sfruttare l'ambiguità risultante da un input incompleto nell'IDE per produrre risultati più accurati in funzionalità come il completamento del codice.
  2. In pratica, ho scoperto che i combinatori parser non erano adatti a raggiungere i miei obiettivi di performance. Parte di questo risale
  3. Quando i risultati dell'analisi vengono utilizzati per funzioni come il contorno, il completamento del codice e il rientro intelligente, è facile che sottili modifiche alla grammatica influiscano sull'accuratezza di tali risultati. ANTLR fornisce strumenti che possono trasformare queste discrepanze in errori di compilazione, anche nei casi in cui i tipi verrebbero altrimenti compilati. Posso con sicurezza prototipare una nuova funzionalità linguistica che influisce sulla grammatica sapendo che tutto il codice aggiuntivo che costituisce l'IDE fornirà un'esperienza completa per la nuova funzionalità fin dall'inizio. Il mio fork di ANTLR 4 (su cui si basa il target C #) è l'unico strumento che conosco che tenta persino di fornire questa funzionalità.

In sostanza, i combinatori di parser sono un bel giocattolo con cui giocare, ma semplicemente non sono tagliati per fare un lavoro serio.


3

Come menziona Karl, i generatori di parser tendono ad avere una migliore segnalazione degli errori. Inoltre:

  • tendono ad essere più veloci, poiché il codice generato può essere specializzato per la sintassi e generare tabelle di salto per il lookahead.
  • tendono ad avere strumenti migliori, per identificare la sintassi ambigua, rimuovere la ricorsione a sinistra, riempire i rami di errore, ecc.
  • tendono a gestire meglio le definizioni ricorsive.
  • tendono ad essere più robusti, poiché i generatori sono stati più a lungo e fanno più della piastra della caldaia per te, riducendo la possibilità che tu lo rovini.

D'altra parte, i combinatori hanno i loro vantaggi:

  • sono in codice, quindi se la sintassi varia in fase di esecuzione, è possibile mutare più facilmente le cose.
  • tendono ad essere più facili da legare e consumare effettivamente (l'output dei generatori di parser tende ad essere molto generico e scomodo da usare).
  • sono in codice, quindi tendono ad essere un po 'più facili da eseguire il debug quando la grammatica non fa ciò che ti aspetti.
  • tendono ad avere una curva di apprendimento più superficiale poiché funzionano come qualsiasi altro codice. I generatori parser tendono ad avere le proprie stranezze per imparare a far funzionare le cose.

I generatori di parser tendono ad avere orribili segnalazioni di errori rispetto ai parser LL scritti a mano ricorsivi di discesa utilizzati nel mondo reale. Generatori di parser raramente offrono hook di transizione della tabella di stato necessari per aggiungere una diagnostica di qualità. Questo è il motivo per cui quasi tutti i compilatori reali non usano combinatori di parser o generatori di parser. I parser LL ricorsivi decenti sono banali da costruire, anche se non come PC / PG "puliti", sono più utili.
dhchdhd
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.