Come devo specificare una grammatica per un parser?


12

Sto programmando da molti anni, ma un compito che mi impiega ancora troppo tempo è specificare una grammatica per un parser, e anche dopo questo sforzo eccessivo, non sono mai sicuro che la grammatica che ho escogitato sia buona ( con ogni ragionevole misura di "buono").

Non mi aspetto che esista un algoritmo per automatizzare il processo di specifica di una grammatica, ma spero che ci siano modi per strutturare il problema che elimini gran parte delle congetture e prove ed errori del mio approccio attuale.

Il mio primo pensiero è stato quello di leggere sui parser, e ne ho fatto un po ', ma tutto ciò che ho letto su questo argomento prende la grammatica come un dato (o abbastanza banale da poterlo specificare tramite ispezione), e mi concentro su il problema di tradurre questa grammatica in un parser. Sono interessato al problema immediatamente prima: come specificare la grammatica in primo luogo.

Sono principalmente interessato al problema di specificare una grammatica che rappresenti formalmente una raccolta di esempi concreti (positivi e negativi). Ciò è diverso dal problema di progettazione di una nuova sintassi . Grazie a Macneil per aver sottolineato questa distinzione.

Non avevo mai davvero apprezzato la distinzione tra una grammatica e una sintassi, ma ora che sto iniziando a vederlo, ho potuto affinare il mio primo chiarimento dicendo che sono principalmente interessato al problema di specificare una grammatica che imporrà un sintassi predefinita: succede che nel mio caso la base di questa sintassi sia di solito una raccolta di esempi positivi e negativi.

Come viene specificata la grammatica per un parser? Esiste un libro o un riferimento là fuori che è lo standard di fatto per descrivere le migliori pratiche, le metodologie di progettazione e altre informazioni utili su come specificare una grammatica per un parser? Su quali punti, quando leggo della grammatica del parser, dovrei concentrarmi?


1
Ho modificato un po 'la tua domanda per concentrarmi sul tuo vero problema. Questo sito è esattamente il tipo di posto in cui è possibile porre domande su grammatiche e parser e ottenere risposte di esperti. Se ci sono risorse esterne che vale la pena guardare, emergeranno naturalmente nelle risposte che ti aiuteranno direttamente.
Adam Lear

8
@kjo È sfortunato. Se tutto ciò che stai chiedendo è un elenco di riferimenti, non stai utilizzando Stack Exchange al massimo delle sue potenzialità. Il tuo meta-problema non sta usando il sito come previsto. Le domande dell'elenco sono quasi universalmente scoraggiate su Stack Exchange perché non si adattano molto bene al modello di domande e risposte. Consiglio vivamente di spostare la tua mentalità verso porre domande che abbiano risposte, non elementi, idee o opinioni .
Adam Lear

3
@kjo È certamente una domanda, ma non è la domanda giusta da porre su Stack Exchange . SE non è qui per creare elenchi di riferimenti. È qui per essere il riferimento. Si prega di leggere il meta post che ho collegato nel mio commento per una spiegazione più dettagliata.
Adam Lear

5
@kjo: per favore non scoraggiarti! Le modifiche di Anna hanno mantenuto il nucleo e il cuore della tua domanda e ti ha aiutato rendendola più del modulo che ci aspettiamo su Programmers.SE. Non conosco riferimenti definitivi che cerchi, ma sono stato in grado di fornire una risposta. [OTOH, se avessi saputo un riferimento del genere, l'avrei sicuramente incluso.] Vogliamo incoraggiare più risposte come la mia perché, in questo caso specifico, non credo che ci sia un riferimento per quello che cerchi, solo esperienza dal parlare con gli altri.
Macneil,

4
@kjo Ho eseguito il rollback alle modifiche di Anna e ho cercato di incorporare una chiamata specifica per un riferimento canonico basato sulla nostra guida per i consigli sui libri : ci sono molte buone informazioni nelle risposte fornite e per invalidarle rendendo l'ambito del chiedersi solo di trovare un libro sarebbe uno spreco. Ora, se tutti potessimo semplicemente smettere con la guerra di modifica, sarebbe fantastico.

Risposte:


12

Dai file di esempio dovrai prendere decisioni in base a quanto vuoi generalizzare da quegli esempi. Supponiamo di avere i seguenti tre esempi: (ognuno è un file separato)

f() {}
f(a,b) {b+a}
int x = 5;

È possibile specificare banalmente due grammatiche che accetteranno questi esempi:

Trivial Grammar One:

start ::= f() {} | f(a,b) {b+a} | int x = 5;

Trivial Grammar Two:

start ::= tokens
tokens ::= token tokens | <empty>
token ::= identifier | literal | { | } | ( | ) | , | + | = | ;

Il primo è banale perché accetta solo i tre campioni. Il secondo è banale perché accetta tutto ciò che potrebbe eventualmente usare quei tipi di token. [Per questa discussione suppongo che non ti preoccupi molto del design del tokenizer: è semplice assumere identificatori, numeri e punteggiatura come token, e potresti prendere in prestito qualsiasi set di token da qualsiasi linguaggio di script che avresti come comunque.]

Quindi, la procedura che dovrai seguire è quella di iniziare ad alto livello e decidere "quante di ciascuna istanza voglio consentire?" Se un costrutto sintattico può avere senso ripetere un numero qualsiasi di volte, come ad esempio methoduna classe, vorrai una regola con questo modulo:

methods ::= method methods | empty

Che è meglio affermato in EBNF come:

methods ::= {method}

Sarà probabilmente ovvio quando vuoi solo zero o una istanza (il che significa che il costrutto è facoltativo, come con la extendsclausola per una classe Java), o quando vuoi consentire una o più istanze (come con un inizializzatore variabile in una dichiarazione ). Dovrai prestare attenzione a problemi come richiedere un separatore tra gli elementi (come ,nel caso in un elenco di argomenti), richiedere un terminatore dopo ogni elemento (come nel caso ;delle istruzioni separate) o non richiedere separatori o terminatori (come nel caso con metodi in una classe).

Se la tua lingua utilizza espressioni aritmetiche, sarebbe facile per te copiare dalle regole di precedenza di una lingua esistente. È meglio attenersi a qualcosa di ben noto, come le regole delle espressioni di C, piuttosto che cercare qualcosa di esotico, ma a condizione che tutto il resto sia uguale.

Oltre ai problemi di precedenza (che cosa viene analizzato l'uno con l'altro) e ai problemi di ripetizione (quanti di ciascun elemento dovrebbero verificarsi, come sono separati?), Dovrai anche pensare all'ordine: qualcosa deve sempre apparire prima di un'altra cosa? Se una cosa è inclusa, un'altra dovrebbe essere esclusa?

A questo punto, potresti essere tentato di applicare grammaticalmente alcune regole, una regola come se Personsi specifica un'età che non si desidera consentire anche alla loro data di nascita. Mentre puoi costruire la tua grammatica per farlo, potresti trovare più semplice imporre questo con un passaggio di "controllo semantico" dopo che tutto è stato analizzato. Ciò semplifica la grammatica e, a mio avviso, rende migliori i messaggi di errore per la violazione della regola.


1
+1 per messaggi di errore migliori. La maggior parte degli utenti della tua lingua non saranno esperti, indipendentemente dal fatto che ce ne siano 10 o 10 milioni. La teoria dell'analisi ha trascurato questo aspetto troppo a lungo.
Salterio,

10

Dove posso imparare come specificare la grammatica per un parser?

Per la maggior parte i generatori di parser, di solito è qualche variante di Backus-Naur s' <nonterminal> ::= expressionformato. Partirò dal presupposto che stai usando qualcosa del genere e non stai provando a costruire i tuoi parser a mano. Se riesci a produrre un parser per un formato in cui ti è stata data la sintassi (ho incluso un problema di esempio di seguito), specificare le grammatiche non è il tuo problema.

Quello che penso tu sia contrario è dividere la sintassi da un set di campioni, che è davvero più riconoscimento di pattern di quanto non stia analizzando. Se devi ricorrere a questo, significa che chiunque fornisce i tuoi dati non può darti la sua sintassi perché non ha una buona gestione del suo formato. Se hai la possibilità di respingere e dire loro di darti una definizione formale, fallo. Non è giusto che ti diano un vago problema se potresti essere ritenuto responsabile delle conseguenze di un parser basato su una sintassi dedotta che accetta input errati o rifiuta input positivi.

... Non sono mai sicuro che la grammatica che ho escogitato sia buona (con ogni ragionevole misura di "buono").

"Buono" nella tua situazione dovrebbe significare "analizza gli aspetti positivi e rifiuta gli aspetti negativi". Senza altre specifiche formali della sintassi del file di input, i campioni sono i tuoi unici casi di test e non puoi fare di meglio. Potresti abbassare il piede e dire che solo gli esempi positivi sono buoni e rifiutano qualsiasi altra cosa, ma probabilmente non è nello spirito di ciò che stai cercando di realizzare.

In circostanze più sanitarie, testare una grammatica è come testare qualsiasi altra cosa: devi inventare abbastanza casi di test per esercitare tutte le varianti dei non-terminali (e dei terminali, se generati da un lexer).


Problema di esempio

Scrivi una grammatica che analizzerà i file di testo contenenti un elenco come definito dalle regole seguenti:

  • Un elenco è composto da zero o più cose .
  • Una cosa è costituita da un identificatore , un controvento aperto, un elenco di elementi e un controvento di chiusura.
  • Una _item_list_ è composta da zero o più elementi .
  • Un oggetto è costituito da un identificatore , un segno di uguale, un altro identificatore e un punto e virgola.
  • Un identificatore è una sequenza di uno o più caratteri AZ, az, 0-9 o il trattino basso.
  • Lo spazio bianco viene ignorato.

Esempio di input (tutti validi):

clank { foo = bar; baz = bear; }
clunk {
    quux =bletch;
    281_apple = OU812;
    He_Eats=Asparagus ; }

2
E assicurati di usare "alcune varianti di Backus-Naur" e non BNF stesso. BNF può esprimere una grammatica, ma rende molti concetti molto comuni, come gli elenchi, molto più complicati di quanto debbano essere. Esistono varie versioni migliorate, come EBNF, che migliorano su questi problemi.
Mason Wheeler,

7

Le risposte di Macneil e Blrfl sono fantastiche. Voglio solo aggiungere alcuni commenti sull'inizio del processo.

Una sintassi è solo un modo per rappresentare un programma . Quindi la sintassi della tua lingua dovrebbe essere determinata dalla tua risposta a questa domanda: che cos'è un programma?

Si potrebbe dire che un programma è una raccolta di classi. Ok, questo ci da

program ::= class*

come punto di partenza. Oppure potresti doverlo scrivere

program ::= ε
         |  class program

Ora, cos'è una classe? Ha un nome; una specifica per superclasse opzionale; e un sacco di dichiarazioni di costruttore, metodo e campo. Inoltre, è necessario un modo per raggruppare una classe in una singola unità (non ambigua) e ciò dovrebbe comportare alcune concessioni all'usabilità (ad esempio, contrassegnarlo con la parola riservata class). Va bene:

class ::= "class" identifier extends-clause? "{" class-member-decl * "}"

Questa è una notazione ("sintassi concreta") che potresti scegliere. Oppure potresti decidere altrettanto facilmente su questo:

class ::= "(" "class" identifier extends-clause "(" class-member-decl* ")" ")"

o

class ::= "class" identifier "=" "CLASS" extends-clause? class-member-decl* "END"

Probabilmente hai già preso questa decisione implicitamente, specialmente se hai esempi, ma voglio solo rafforzare il punto: la struttura della sintassi è determinata dalla struttura dei programmi che rappresenta. Questo è ciò che ti fa superare le "banali grammatiche" dalla risposta di Macneil. I programmi di esempio sono comunque molto importanti. Servono a due scopi. In primo luogo, ti aiutano a capire, a livello astratto, cos'è un programma. In secondo luogo, ti aiutano a decidere quale sintassi concreta dovresti usare per rappresentare la struttura della tua lingua.

Una volta abbattuta la struttura, dovresti tornare indietro e affrontare problemi come consentire spazi bianchi e commenti, correggere ambiguità, ecc. Questi sono importanti, ma sono secondari al design generale e dipendono fortemente dal tecnologia di analisi che stai utilizzando.

Infine, non cercare di rappresentare tutto ciò che riguarda la tua lingua nella grammatica. Ad esempio, potresti voler vietare determinati tipi di codice non raggiungibile (ad esempio, un'istruzione dopo un return, come in Java). Probabilmente non dovresti provare a inserirli nella grammatica, perché o ti mancheranno cose (whoops, what if returnis in parentesi graffe, o cosa succede se ifritornano entrambi i rami di un'istruzione?) O renderai la tua grammatica troppo complicata gestire. È un vincolo sensibile al contesto ; scrivilo come un passaggio separato. Un altro esempio molto comune di un vincolo sensibile al contesto è un sistema di tipi. Potresti rifiutare espressioni come 1 + "a"nella grammatica, se hai lavorato abbastanza duramente, ma non puoi rifiutare 1 + x(dove xha la stringa di tipo). Cosìevitare le restrizioni cotte nella grammatica e farle correttamente come un passaggio separato.

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.