Analisi di grammatiche arbitrarie senza contesto, per lo più brevi frammenti


20

Voglio analizzare lingue specifiche del dominio definite dall'utente. Queste lingue sono in genere vicine alle notazioni matematiche (non sto analizzando un linguaggio naturale). Gli utenti definiscono il loro DSL in una notazione BNF, in questo modo:

expr ::= LiteralInteger
       | ( expr )
       | expr + expr
       | expr * expr

Input like 1 + ( 2 * 3 )deve essere accettato, mentre input like 1 +deve essere rifiutato come errato e input like 1 + 2 * 3deve essere rifiutato come ambiguo.

Una difficoltà centrale qui è far fronte a grammatiche ambigue in modo facile da usare. Limitare la grammatica in modo non ambiguo non è un'opzione: è così che la lingua è: l'idea è che gli scrittori preferiscono omettere le parentesi quando non sono necessarie per evitare ambiguità. Finché un'espressione non è ambigua, devo analizzarla e, in caso contrario, devo rifiutarla.

Il mio parser deve funzionare su qualsiasi grammatica senza contesto, anche ambigua, e deve accettare tutti gli input inequivocabili. Ho bisogno dell'albero di analisi per tutti gli input accettati. Per un input non valido o ambiguo, idealmente voglio buoni messaggi di errore, ma per cominciare prenderò quello che posso ottenere.

In genere invoco il parser su input relativamente brevi, con input più lunghi occasionali. Quindi l'algoritmo asintoticamente più veloce potrebbe non essere la scelta migliore. Vorrei ottimizzare per una distribuzione di circa l'80% di input lunghi meno di 20 simboli, il 19% tra 20 e 50 simboli e l'1% di input più rari più lunghi. La velocità per input non validi non è una delle maggiori preoccupazioni. Inoltre, mi aspetto una modifica del DSL circa ogni 1000-100000 ingressi; Posso passare un paio di secondi a elaborare la mia grammatica, non un paio di minuti.

Quali algoritmi di analisi dovrei esaminare, date le mie dimensioni di input tipiche? La segnalazione degli errori dovrebbe essere un fattore nella mia selezione o dovrei concentrarmi sull'analisi di input non ambigui ed eventualmente eseguire un parser completamente separato e più lento per fornire un feedback sugli errori?

(Nel progetto in cui ne avevo bisogno (qualche tempo fa), ho usato CYK , che non era troppo difficile da implementare e ha funzionato adeguatamente per le mie dimensioni di input ma non ha prodotto errori molto belli.)


Rapporti di errori particolarmente buoni sembrano difficili da realizzare. Potresti avere più di una modifica locale che porta a un input accettato in caso di grammatiche ambigue.
Raffaello

Ho appena risposto di seguito. È un po 'imbarazzante rispondere a una modifica di una vecchia domanda che ha già una risposta ben accolta. Ovviamente non dovrei rispondere in modo simile, ma gli utenti leggeranno entrambe le risposte come se stessero rispondendo alla stessa domanda.
babou,

In realtà ti aspetti un messaggio di errore per input ambiguo, se un utente scrive x+y+z.
babou,

@babou Non ho modificato la domanda, ho solo aggiunto i chiarimenti richiesti nei commenti (ora eliminati). Per la piccola grammatica fornita qui, non ho specificato associatività per +, quindi x+y+zè davvero ambigua e quindi errata.
Gilles 'SO- smetti di essere malvagio' il

Bene, è la tua ultima frase, appena aggiunta, anche se tra parentesi. Sembra che tu abbia detto: alla fine l'ho fatto con CYK, ma per alcuni motivi non è più adeguato. E mi chiedo quali possano essere i motivi precisi ... Ora sei la persona con più esperienza con il tuo tipo di problema e la soluzione che usi, quindi ci si aspetterebbe da te più informazioni se dovessero essere date ulteriori risposte.
babou,

Risposte:


19

Probabilmente l'algoritmo ideale per le tue esigenze è l' analisi LL generalizzata o GLL. Questo è un algoritmo nuovissimo (l'articolo è stato pubblicato nel 2010). In un certo senso, è l'algoritmo di Earley aumentato con uno stack strutturato (GSS) grafico e utilizzando lookahead LL (1).

L'algoritmo è abbastanza simile al vecchio LL (1), tranne per il fatto che non rifiuta le grammatiche se non sono LL (1): prova semplicemente tutti i possibili analisi LL (1). Utilizza un grafico diretto per ogni punto dell'analisi, il che significa che se si incontra uno stato di analisi che è stato trattato in precedenza, unisce semplicemente questi due vertici. Ciò lo rende adatto anche a grammatiche ricorsive a sinistra, a differenza di LL. Per i dettagli esatti sui suoi meccanismi interni, leggi l'articolo (è un documento piuttosto leggibile, sebbene la zuppa di etichette richieda una certa perseveranza).

L'algoritmo presenta una serie di chiari vantaggi rilevanti per le tue esigenze rispetto agli altri algoritmi di analisi generali (di cui sono a conoscenza). Innanzitutto, l'implementazione è molto semplice: penso che solo Earley sia più facile da implementare. In secondo luogo, le prestazioni sono abbastanza buone: in effetti, diventano altrettanto veloci di LL (1) nelle grammatiche che sono LL (1). In terzo luogo, il recupero dell'analisi è abbastanza semplice e anche la verifica della presenza di più di un'analisi può essere eseguita.

Il vantaggio principale che GLL ha è che si basa su LL (1) ed è quindi molto facile da capire e da debug, durante l'implementazione, durante la progettazione di grammatiche e durante l'analisi degli input. Inoltre, semplifica anche la gestione degli errori: sai esattamente dove sono stati analizzati i possibili analisi e come potrebbero essere continuati. Puoi facilmente dare i possibili analisi al punto dell'errore e, diciamo, gli ultimi 3 punti in cui gli analisi sono bloccati. Potresti invece optare per provare a recuperare dall'errore e contrassegnare la produzione su cui l'analisi che ha ottenuto il più lontano stava lavorando come "completa" per quell'analisi e vedere se l'analisi può continuare dopo quella (diciamo che qualcuno ha dimenticato una parentesi). Potresti anche farlo per, diciamo, i 5 analisi che hanno ottenuto il risultato più lontano.

L'unico aspetto negativo dell'algoritmo è che è nuovo, il che significa che non ci sono implementazioni consolidate prontamente disponibili. Questo potrebbe non essere un problema per te: ho implementato l'algoritmo da solo ed è stato abbastanza facile da fare.


Bello imparare qualcosa di nuovo. Quando ne avevo bisogno (qualche anno fa, in un progetto che mi piacerebbe rivivere un giorno), ho usato CYK, soprattutto perché era il primo algoritmo che ho trovato. In che modo GLL gestisce input ambigui? L'articolo non sembra discuterne, ma l'ho solo scremato.
Gilles 'SO-smetti di essere malvagio' il

@Gilles: crea uno stack strutturato graficamente e tutte le analisi (potenzialmente esponenzialmente molte) sono rappresentate in modo compatto in questo grafico, in modo simile a come funziona GLR. Se ricordo bene, l'articolo menzionato in cstheory.stackexchange.com/questions/7374/… tratta di questo.
Alex ten Brink,

@Gilles Questo parser 2010 sembra dover essere programmato a mano dalla grammatica, non troppo adeguato se si dispone di più lingue o se si modifica spesso la lingua. Le tecniche per la generazione automatica dalla grammatica di un parser generale seguendo qualsiasi strategia scelta (LL, LR o altre) e producendo una foresta di tutti gli analisi sono note da circa 40 anni. Tuttavia ci sono problemi nascosti riguardanti la complessità e l'organizzazione del grafico che rappresenta gli analisi. Il numero di analisi potrebbe essere peggiore dell'esponenziale: infinito. Il recupero degli errori può utilizzare tecniche più sistematiche e indipendenti dal parser.
babou,

In che modo GLL si riferisce a LL (*) trovato in ANTLR?
Raffaello

6

La mia azienda (Semantic Designs) ha utilizzato con successo i parser GLR per fare esattamente ciò che OP suggerisce nell'analisi di entrambi i linguaggi specifici di dominio e nell'analisi dei linguaggi di programmazione "classici", con il nostro DMS Software Reengineering Toolkit. Questo supporta trasformazioni di programma da sorgente a sorgente utilizzate per la ristrutturazione di programmi su larga scala / reverse engineering / generazione di codici forward. Ciò include la riparazione automatica degli errori di sintassi in un modo abbastanza pratico. Usando GLR come base e alcune altre modifiche (predicati semantici, input di set di token anziché solo input di token, ...) siamo riusciti a creare parser per circa 40 lingue.

Importante quanto la capacità di analizzare le istanze di lingue complete, GLR si è dimostrato estremamente utile anche nell'analisi delle regole di riscrittura da fonte a fonte . Questi sono frammenti di programma con un contesto molto inferiore rispetto a un programma completo e quindi generalmente hanno più ambiguità. Utilizziamo annotazioni speciali (ad esempio, insistendo sul fatto che una frase corrisponde a una grammatica specifica non terminale) per aiutare a risolvere tali ambiguità durante / dopo l'analisi delle regole. Organizzando il macchinario di analisi GLR e gli strumenti che lo circondano, otteniamo parser per riscrivere le regole "gratis" una volta che abbiamo un parser per il suo linguaggio. Il motore DMS ha un applicatore di regole di riscrittura incorporato che può quindi essere utilizzato per applicare queste regole per eseguire le modifiche desiderate al codice.

Probabilmente il nostro risultato più spettacolare è la capacità di analizzare il C ++ 14 completo , nonostante tutte le ambiguità, usando una grammatica senza contesto come base. Noto che tutti i classici compilatori C ++ (GCC, Clang) hanno rinunciato alla possibilità di farlo e di utilizzare parser scritti a mano (che IMHO li rende molto più difficili da mantenere, ma poi non sono un mio problema). Abbiamo utilizzato questo macchinario per apportare enormi cambiamenti all'architettura dei grandi sistemi C ++.

Per quanto riguarda le prestazioni, i nostri parser GLR sono ragionevolmente veloci: decine di migliaia di righe al secondo. Questo è ben al di sotto dello stato dell'arte, ma non abbiamo fatto alcun serio tentativo di ottimizzarlo e alcuni dei colli di bottiglia sono nell'elaborazione del flusso di caratteri (Unicode completo). Per costruire tali parser, pre-elaboriamo le grammatiche libere dal contesto usando qualcosa di molto vicino a un generatore di parser LR (1); normalmente funziona su una moderna workstation in dieci secondi con grammatiche grandi come C ++. Sorprendentemente, per linguaggi molto complessi come il moderno COBOL e il C ++, la generazione di lexer risulta impiegare circa un minuto; alcuni dei DFA definiti su Unicode diventano piuttosto pelosi. Ho appena fatto Ruby (con un subgrammar completo per le sue incredibili regexps) come esercizio per le dita; DMS può elaborare il suo lexer e la grammatica insieme in circa 8 secondi.


@Raphael: il collegamento "enormi cambiamenti" punta a una serie di documenti tecnici di tipo accademico, tra cui alcuni sulla reingegnerizzazione dell'architettura C ++, uno sul motore DMS stesso (piuttosto vecchio ma descrive bene le basi) e uno sui argomento esotico di acquisizione e riutilizzo del design, che era la motivazione originale per DMS (purtroppo ancora irrisolto, ma DMS si è rivelato comunque piuttosto utile).
Ira Baxter,

1

Esistono molti parser generici senza contesto che possono analizzare frasi ambigue (secondo una grammatica ambigua). Vengono sotto vari nomi, in particolare la programmazione dinamica o gli analizzatori di grafici. Il più noto, e accanto al più semplice, è probabilmente il parser CYK che stai usando. Questa generalità è necessaria poiché devi gestire analisi multiple e potresti non sapere fino alla fine se hai a che fare con un'ambiguità o meno.

Da quello che dici, penso che CYK non sia una cattiva scelta. Probabilmente non hai molto da guadagnare aggiungendo predittività (LL o LR) e potrebbe effettivamente avere un costo discriminando i calcoli che dovrebbero essere uniti piuttosto che discriminati (specialmente nel caso LR). Possono anche avere un costo corrispondente nella dimensione della foresta di analisi prodotta (che può avere un ruolo negli errori di ambiguità). In realtà, mentre non sono sicuro di come confrontare formalmente l'adeguatezza degli algoritmi più sofisticati, so che CYK offre una buona computazione computazionale.

Ora, non credo che ci sia molta letteratura sui parser CF generali per grammatiche ambigue che dovrebbe accettare solo input non ambigui. Non ricordo di aver visto nessuno, probabilmente perché anche per i documenti tecnici, o anche i linguaggi di programmazione, l'ambiguità sintattica è accettabile purché possa essere risolta con altri mezzi (ad esempio l'ambiguità nelle espressioni ADA).

In realtà mi chiedo perché vuoi cambiare il tuo algoritmo, piuttosto che attenersi a quello che hai. Questo potrebbe aiutarmi a capire quale tipo di cambiamento potrebbe aiutarti meglio. È un problema di velocità, è la rappresentazione di analisi o è il rilevamento e il recupero degli errori?

Il modo migliore per rappresentare più analisi è con una foresta condivisa, che è semplicemente una grammatica senza contesto che genera solo il tuo input, ma con esattamente tutti gli stessi alberi di analisi della grammatica DSL. Ciò semplifica la comprensione e l'elaborazione. Per maggiori dettagli, ti suggerisco di guardare questa risposta che ho dato sul sito linguistico. Capisco che non sei interessato a ottenere una foresta di analisi, ma una corretta rappresentazione della foresta di analisi può aiutarti a fornire messaggi migliori su quale sia l'ambiguità problen. Potrebbe anche aiutarti a decidere che l'ambiguità non importa in alcuni casi (associatività) se lo desideri.

Menzionate i vincoli di tempo di elaborazione della vostra grammatica DSL, ma non date indicazioni sulla sua dimensione (il che non significa che potrei rispondere con cifre che avete fatto).

Alcune elaborazioni degli errori possono essere integrate in questi algoritmi CF generali in modo semplice. Ma avrei bisogno di capire che tipo di elaborazione degli errori ti aspetti essere più affermativa. Vorresti qualche esempio.

Sono un po 'a disagio a dire di più, perché non capisco quali siano le tue motivazioni e i tuoi vincoli. Sulla base di quello che dici, mi atterrei a CYK (e conosco gli altri algoritmi e alcune delle loro proprietà).

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.