Perché il C ++ non può essere analizzato con un parser LR (1)?


153

Stavo leggendo di parser e generatori di parser e ho trovato questa affermazione nella pagina di analisi LR di wikipedia:

Molti linguaggi di programmazione possono essere analizzati usando alcune varianti di un parser LR. Un'eccezione notevole è C ++.

Perché è così? Quale particolare proprietà del C ++ rende impossibile analizzare i parser LR?

Usando google, ho scoperto solo che C può essere perfettamente analizzato con LR (1) ma C ++ richiede LR (∞).


7
Proprio come: devi capire la ricorsione per imparare la ricorsione ;-).
Toon Krijthe

5
"Capirai i parser una volta analizzata questa frase."
ilya n.

Risposte:


92

C'è un thread interessante su Lambda the Ultimate che discute la grammatica LALR per C ++ .

Include un collegamento a una tesi di dottorato che include una discussione sull'analisi del C ++, in cui si afferma che:

"La grammatica C ++ è ambigua, dipendente dal contesto e potenzialmente richiede uno sguardo infinito per risolvere alcune ambiguità".

Segue una serie di esempi (vedi pagina 147 del pdf).

L'esempio è:

int(x), y, *const z;

senso

int x;
int y;
int *const z;

Confrontare con:

int(x), y, new int;

senso

(int(x)), (y), (new int));

(un'espressione separata da virgola).

Le due sequenze di token hanno la stessa sottosequenza iniziale ma alberi di analisi diversi, che dipendono dall'ultimo elemento. Ci possono essere arbitrariamente molti token prima di quello non ambiguo.


29
Sarebbe bello avere un riassunto della pagina 147 in questa pagina. Leggerò quella pagina però. (+1)
Allegro,

11
L'esempio è: int (x), y, * const z; // significato: int x; int y; int * const z; (una sequenza di dichiarazioni) int (x), y, new int; // significato: (int (x)), (y), (new int)); (un'espressione separata da virgola) Le due sequenze di token hanno la stessa sottosequenza iniziale ma alberi di analisi diversi, che dipendono dall'ultimo elemento. Ci possono essere arbitrariamente molti token prima di quello non ambiguo.
Blaisorblade,

6
Bene, in quel contesto ∞ significa "arbitrariamente molti" perché il lookahead sarà sempre limitato dalla lunghezza dell'input.
MauganRa,

1
Sono abbastanza perplesso dalla citazione estratta da una tesi di dottorato. Se c'è un'ambiguità, allora, per definizione, NESSUN lookahead può mai "risolvere" l'ambiguità (cioè decidere quale analisi è la sequenza corretta, poiché almeno 2 analisi sono considerate corrette dalla grammatica). Inoltre, la citazione menziona l'ambiguità di C ma la spiegazione non mostra un'ambiguità, ma solo un esempio non ambiguo in cui la decisione di analisi può essere presa solo dopo una lungimirante prospettiva arbitraria.
dodecaplex,

231

I parser LR non possono gestire regole grammaticali ambigue, in base alla progettazione. (Ha reso la teoria più semplice negli anni '70, quando le idee venivano elaborate).

C e C ++ consentono entrambi la seguente dichiarazione:

x * y ;

Ha due analisi diverse:

  1. Può essere la dichiarazione di y, come puntatore per digitare x
  2. Può essere una moltiplicazione di xey, gettando via la risposta.

Ora, potresti pensare che quest'ultimo sia stupido e debba essere ignorato. La maggior parte sarebbe d'accordo con te; tuttavia, ci sono casi in cui potrebbe avere un effetto collaterale (ad esempio, se la moltiplicazione è sovraccarica). ma non è questo il punto. Il punto è che ci sono due analisi diverse, e quindi un programma può significare cose diverse a seconda di come dovrebbe essere analizzato.

Il compilatore deve accettare quello appropriato nelle circostanze appropriate e in assenza di altre informazioni (ad esempio, conoscenza del tipo di x) deve raccogliere entrambi per decidere in seguito cosa fare. Quindi una grammatica deve permetterlo. E questo rende la grammatica ambigua.

Quindi l'analisi pura di LR non può gestirlo. Né molti altri generatori di parser ampiamente disponibili, come Antlr, JavaCC, YACC, o Bison tradizionale, o persino parser in stile PEG, possono essere usati in modo "puro".

Ci sono molti casi più complicati (l'analisi della sintassi del modello richiede un lookahead arbitrario, mentre LALR (k) può guardare avanti alla maggior parte dei token k), ma solo un solo controesempio richiede di abbattere puro l'analisi LR (o gli altri).

La maggior parte dei veri parser C / C ++ gestisce questo esempio usando un qualche tipo di parser deterministico con un hack aggiuntivo: si intrecciano con l'analisi della raccolta di tabelle di simboli ... in modo che quando si incontra "x", il parser sa se x è un tipo oppure no, e può quindi scegliere tra i due potenziali analisi. Ma un parser che fa questo non è privo di contesto e i parser LR (quelli puri, ecc.) Sono (nella migliore delle ipotesi) liberi dal contesto.

Si può imbrogliare e aggiungere controlli semantici di riduzione del tempo per regola nei parser LR per fare questo chiarimento. (Questo codice spesso non è semplice). La maggior parte degli altri tipi di parser ha alcuni mezzi per aggiungere controlli semantici in vari punti dell'analisi, che possono essere usati per fare questo.

E se trucchi abbastanza, puoi far funzionare i parser LR per C e C ++. I ragazzi del GCC lo hanno fatto per un po ', ma hanno rinunciato all'analisi codificata a mano, penso perché volevano una migliore diagnostica degli errori.

C'è un altro approccio, tuttavia, che è bello e pulito e analizza C e C ++ bene senza alcun hackery nella tabella dei simboli: i parser GLR . Questi sono parser senza contesto completo (con lookahead effettivamente infinito). I parser GLR accettano semplicemente entrambe le analisi, producendo un "albero" (in realtà un grafico aciclico diretto che è per lo più simile ad un albero) che rappresenta l'analisi ambigua. Un passaggio post-analisi può risolvere le ambiguità.

Usiamo questa tecnica nei front-end C e C ++ per il nostro Tookit di reingegnerizzazione del software DMS (a giugno 2017 gestiscono il C ++ 17 completo nei dialetti MS e GNU). Sono stati usati per elaborare milioni di linee di grandi sistemi C e C ++, con analisi complete e precise che producono AST con dettagli completi del codice sorgente. (Vedi l'AST per l'analisi più irritante del C ++. )


11
Mentre l'esempio 'x * y' è interessante, lo stesso può accadere in C ('y' può essere un typedef o una variabile). Ma C può essere analizzato da un parser LR (1), quindi qual è la differenza con C ++?
Martin Cote,

12
La mia risposta aveva già osservato che C aveva lo stesso problema, penso che te lo sei perso. No, non può essere analizzato da LR (1), per lo stesso motivo. Ehm, cosa intendi con 'y' può essere un errore di battitura? Forse intendevi "x"? Questo non cambia nulla.
Ira Baxter,

6
Parse 2 non è necessariamente stupido in C ++, poiché * potrebbe essere ignorato per avere effetti collaterali.
Dour High Arch,

8
Ho guardato x * ye ridacchiato: è incredibile come si pensi alle piccole piccole ambiguità come questa.
nuovo123456,

51
@altie Sicuramente nessuno sovraccaricherebbe un operatore bit-shift per farlo scrivere la maggior parte dei tipi variabili in un flusso, giusto?
Troy Daniels,

16

Il problema non viene mai definito in questo modo, mentre dovrebbe essere interessante:

qual è la più piccola serie di modifiche alla grammatica C ++ che sarebbe necessaria affinché questa nuova grammatica potesse essere perfettamente analizzata da un parser yacc "senza contesto"? (facendo uso di un solo 'hack': la disambiguazione del nome / identificatore, il parser che informa il lexer di ogni tipo / classe / struttura)

Ne vedo alcuni:

  1. Type Type;è vietato. Un identificatore dichiarato come nome tipografico non può diventare un identificatore non tipografico (notare chestruct Type Type non è ambiguo e potrebbe essere ancora consentito).

    Esistono 3 tipi di names tokens:

    • types : tipo incorporato o a causa di un typedef / class / struct
    • template-funzioni
    • identificatori: funzioni / metodi e variabili / oggetti

    Considerare le funzioni del modello come token diversi risolve l' func<ambiguità. Se funcè un nome di funzione modello, <deve essere l'inizio di un elenco di parametri modello, altrimenti funcè un puntatore a funzione ed <è l'operatore di confronto.

  2. Type a(2);è un'istanza di oggetto. Type a();e Type a(int)sono prototipi di funzioni.

  3. int (k); è completamente proibito, dovrebbe essere scritto int k;

  4. typedef int func_type(); e typedef int (func_type)();sono vietati.

    Una funzione typedef deve essere un puntatore a funzione typedef: typedef int (*func_ptr_type)();

  5. la ricorsione del modello è limitata a 1024, altrimenti un massimo maggiore potrebbe essere passato come opzione al compilatore.

  6. int a,b,c[9],*d,(*f)(), (*g)()[9], h(char); potrebbe essere vietato anche, sostituito da int a,b,c[9],*d; int (*f)();

    int (*g)()[9];

    int h(char);

    una riga per prototipo di funzione o dichiarazione del puntatore di funzione.

    Un'alternativa altamente preferita sarebbe quella di cambiare la terribile sintassi del puntatore a funzione,

    int (MyClass::*MethodPtr)(char*);

    essere resintassato come:

    int (MyClass::*)(char*) MethodPtr;

    questo essendo coerente con l'operatore del cast (int (MyClass::*)(char*))

  7. typedef int type, *type_ptr; potrebbe anche essere vietato: una riga per errore di battitura. Così sarebbe diventato

    typedef int type;

    typedef int *type_ptr;

  8. sizeof int, sizeof char, sizeof long longE co. potrebbe essere dichiarato in ciascun file sorgente. Pertanto, ogni file sorgente che utilizza il tipo intdovrebbe iniziare con

    #type int : signed_integer(4)

    e unsigned_integer(4)sarebbe vietato al di fuori di quella #type direttiva questo sarebbe un grande passo verso la stupida sizeof intambiguità presente in così tante intestazioni C ++

Il compilatore che implementa il C ++ resintassato, se incontra una sorgente C ++ usando una sintassi ambigua, sposta source.cppanche una ambiguous_syntaxcartella e creerebbe automaticamente una traduzione non ambiguasource.cpp prima di compilarla.

Aggiungi le tue sintassi C ++ ambigue se ne conosci alcune!


3
Il C ++ è troppo ben radicato. Nessuno lo farà in pratica. Quelle persone (come noi) che costruiscono front end mordono semplicemente il proiettile e fanno l'ingegneria per far funzionare i parser. E, fintanto che esistono modelli nella lingua, non otterrai un parser puro privo di contesto.
Ira Baxter,

9

Come puoi vedere nella mia risposta qui , C ++ contiene una sintassi che non può essere analizzata in modo deterministico da un parser LL o LR a causa della fase di risoluzione del tipo (in genere post-analisi) che modifica l' ordine delle operazioni e quindi la forma fondamentale dell'AST ( in genere dovrebbe essere fornito da un'analisi del primo stadio).


3
La tecnologia di analisi che gestisce l'ambiguità produce semplicemente entrambe le varianti AST mentre analizzano ed elimina semplicemente quella errata in base alle informazioni sul tipo.
Ira Baxter,

@Ira: Sì, è corretto. Il vantaggio particolare è che consente di mantenere la separazione dell'analisi del primo stadio. Mentre è più comunemente noto nel parser GLR, non c'è alcun motivo particolare per cui vedo che non puoi colpire C ++ con un "GLL?" anche parser.
Sam Harwell,

"GLL"? Bene, certo, ma dovrai andare a capire la teoria e scrivere un documento per il resto da usare. Più probabilmente, è possibile utilizzare un parser con codifica manuale dall'alto verso il basso o un parser LALR () che esegue il backtracking (ma mantenere l'analisi "rifiutata") oppure eseguire un parser Earley. GLR ha il vantaggio di essere una soluzione dannatamente buona, è ben documentata e ormai ben dimostrata. Una tecnologia GLL dovrebbe avere alcuni vantaggi piuttosto significativi per visualizzare GLR.
Ira Baxter,

Il progetto Rascal (Paesi Bassi) sostiene che stanno costruendo un parser GLL senza scanner. Lavori in corso, potrebbe essere difficile trovare informazioni online. en.wikipedia.org/wiki/RascalMPL
Ira Baxter,

@IraBaxter Sembra che ci siano nuovi sviluppi su GLL: vedi questo documento del 2010 su GLL dotat.at/tmp/gll.pdf
Sjoerd

6

Penso che tu sia abbastanza vicino alla risposta.

LR (1) significa che l'analisi da sinistra a destra richiede solo un token per guardare avanti al contesto, mentre LR (∞) significa uno sguardo infinito. Cioè, il parser avrebbe dovuto sapere tutto ciò che stava per capire dove si trova ora.


4
Ricordo dalla mia classe di compilatori che LR (n) per n> 0 è matematicamente riducibile a LR (1). Non è vero per n = infinito?
rmeador,

14
No, c'è una montagna invalicabile con una differenza tra n e l'infinito.
effimero

4
La risposta non è: sì, dato un tempo infinito? :)
Steve Fallows,

7
In realtà, secondo il mio vago ricordo di come avviene LR (n) -> LR (1), comporta la creazione di nuovi stati intermedi, quindi il runtime è una funzione non costante di 'n'. La traduzione di LR (inf) -> LR (1) richiederebbe un tempo infinito.
Aaron,

5
"Non è la risposta: sì, dato un tempo infinito?" - No: la frase "dato un tempo infinito" è solo un modo insensato e breve di dire "non si può fare dato un tempo limitato". Quando vedi "infinito", pensa: "non è finito".
ChrisW,

4

Il problema "typedef" in C ++ può essere analizzato con un parser LALR (1) che crea una tabella dei simboli durante l'analisi (non un parser LALR puro). Il problema "template" probabilmente non può essere risolto con questo metodo. Il vantaggio di questo tipo di parser LALR (1) è che la grammatica (mostrata sotto) è una grammatica LALR (1) (nessuna ambiguità).

/* C Typedef Solution. */

/* Terminal Declarations. */

   <identifier> => lookup();  /* Symbol table lookup. */

/* Rules. */

   Goal        -> [Declaration]... <eof>               +> goal_

   Declaration -> Type... VarList ';'                  +> decl_
               -> typedef Type... TypeVarList ';'      +> typedecl_

   VarList     -> Var /','...     
   TypeVarList -> TypeVar /','...

   Var         -> [Ptr]... Identifier 
   TypeVar     -> [Ptr]... TypeIdentifier                               

   Identifier     -> <identifier>       +> identifier_(1)      
   TypeIdentifier -> <identifier>      =+> typedefidentifier_(1,{typedef})

// The above line will assign {typedef} to the <identifier>,  
// because {typedef} is the second argument of the action typeidentifier_(). 
// This handles the context-sensitive feature of the C++ language.

   Ptr          -> '*'                  +> ptr_

   Type         -> char                 +> type_(1)
                -> int                  +> type_(1)
                -> short                +> type_(1)
                -> unsigned             +> type_(1)
                -> {typedef}            +> type_(1)

/* End Of Grammar. */

È possibile analizzare senza problemi il seguente input:

 typedef int x;
 x * y;

 typedef unsigned int uint, *uintptr;
 uint    a, b, c;
 uintptr p, q, r;

Il generatore di parser LRSTAR legge la notazione grammaticale sopra riportata e genera un parser che gestisce il problema "typedef" senza ambiguità nell'albero di analisi o AST. (Rivelazione: sono il ragazzo che ha creato LRSTAR.)


Questo è l'hack standard utilizzato da GCC con il suo precedente parser LR per gestire l'ambiguità di cose come "x * y;" Ahimè, c'è ancora il requisito arbitrariamente grande di lookahead per analizzare altri costrutti, quindi LR (k) non riesce a essere una soluzione qualsiasi k fisso. (GCC è passato alla discesa ricorsiva con più hockery di annunci).
Ira Baxter,
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.