I parser GCC e Clang sono davvero scritti a mano?


90

Sembra che GCC e LLVM-Clang stiano utilizzando parser discendenti ricorsivi scritti a mano e non l'analisi bottom-up generata dalla macchina, basata su Bison-Flex.

Qualcuno qui potrebbe confermare che questo è il caso? E se è così, perché i framework dei compilatori tradizionali utilizzano parser scritti a mano?

Aggiornamento : blog interessante su questo argomento qui


27
Quasi tutti i compilatori tradizionali utilizzano parser scritti a mano. Qual è il problema con quello?
SK-logic,

2
devi farlo (semi) manualmente se hai bisogno di prestazioni.
Gene Bushuyev

15
E non solo prestazioni - messaggi di errore migliori, capacità di ripristino, ecc.
SK-logic

Che mi dici di MS VisualStudio? sebbene non open source, qualcuno di MS potrebbe verificare che anche loro stiano utilizzando un parser di discesa ricorsivo scritto a mano?
OrenIshShalom

3
@GeneBushuyev, dal wiki GCC: "... Sebbene i tempi abbiano mostrato un aumento dell'1,5% , i principali vantaggi sono la facilitazione di miglioramenti futuri ..." questo aumento sembra piuttosto marginale ...
OrenIshShalom

Risposte:


78

Sì:

  • GCC ha utilizzato un yacc (bisonte) parser una volta, ma è stato sostituito con un parser ricorsivo discesa scritto a mano ad un certo punto nella serie 3.x: vedi http://gcc.gnu.org/wiki/New_C_Parser per collegamenti alle richieste di patch pertinenti.

  • Clang usa anche un parser di discesa ricorsivo scritto a mano: vedere la sezione "Un singolo parser unificato per C, Objective C, C ++ e Objective C ++" verso la fine di http://clang.llvm.org/features.html .


3
Significa che ObjC, C e C ++ hanno Grammatiche LL (k)?
Lindemann

47
No: anche il C, il più semplice dei tre, ha una grammatica ambigua. Ad esempio, foo * bar;potrebbe essere analizzato come espressione di moltiplicazione (con il risultato inutilizzato) o come dichiarazione di una variabile barcon tipo pointer-to- foo. Quale sia corretto dipende dal fatto che un typedeffor foosia in ambito in quel momento, il che non è qualcosa che può essere determinato con una certa quantità di lookahead. Ma questo significa solo che il parser di discesa ricorsivo necessita di qualche brutto meccanismo extra aggiunto per gestirlo.
Matthew Slattery

9
Posso confermare da prove empiriche, che C ++ 11, C e Objective C hanno grammatiche libere dal contesto che un parser GLR può gestire.
Ira Baxter

2
Per quanto riguarda la sensibilità al contesto, questa risposta non afferma né l'uno né l'altro: che l'analisi di queste lingue è probabilmente Turing completa.
Ioannis Filippidis

106

C'è un teorema popolare che dice che C è difficile da analizzare e C ++ essenzialmente impossibile.

Non è vero.

Ciò che è vero è che C e C ++ sono piuttosto difficili da analizzare usando i parser LALR (1) senza hackerare il meccanismo di analisi e aggrovigliarsi nei dati della tabella dei simboli. GCC infatti usava analizzarli, usando YACC e hackery aggiuntivi come questo, e sì, era brutto. Ora GCC utilizza parser scritti a mano, ma ancora con l'hackery della tabella dei simboli. La gente di Clang non ha mai provato a usare generatori di parser automatizzati; Per quanto ne so, il parser Clang è sempre stato una discesa ricorsiva codificata a mano.

Ciò che è vero, è che C e C ++ sono relativamente facili da analizzare con parser generati automaticamente più potenti, ad esempio parser GLR , e non è necessario alcun hack. Il parser Elsa C ++ è un esempio di questo. Il nostro C ++ Front End è un altro (come tutti i nostri front-end "compilatori", GLR è una tecnologia di analisi davvero meravigliosa).

Il nostro front-end C ++ non è veloce come quello di GCC, e certamente più lento di Elsa; abbiamo investito poca energia nel metterlo a punto attentamente perché abbiamo altri problemi più urgenti (tuttavia è stato utilizzato su milioni di righe di codice C ++). Elsa è probabilmente più lenta di GCC semplicemente perché è più generale. Date le velocità del processore in questi giorni, queste differenze potrebbero non avere molta importanza nella pratica.

Ma i "veri compilatori" che sono ampiamente distribuiti oggi hanno le loro radici in compilatori di 10 o 20 anni fa o più. Le inefficienze allora contavano molto di più e nessuno aveva sentito parlare di parser GLR, quindi le persone facevano quello che sapevano fare. Il clang è certamente più recente, ma i teoremi popolari conservano la loro "capacità di persuasione" per molto tempo.

Non devi più farlo in quel modo. Puoi ragionevolmente usare GLR e altri analoghi parser come front-end, con un miglioramento nella manutenibilità del compilatore.

Che cosa è vero, è che ottenere una grammatica che corrisponda al comportamento del tuo amichevole compilatore di quartiere è difficile. Sebbene praticamente tutti i compilatori C ++ implementino (la maggior parte) dello standard originale, tendono anche ad avere molte estensioni degli angoli oscuri, ad esempio, le specifiche DLL nei compilatori MS, ecc. Se hai un potente motore di analisi, puoi passare il tuo tempo a cercare di ottenere la grammatica finale per abbinare la realtà, piuttosto che cercare di piegare la tua grammatica per soddisfare i limiti del tuo generatore di parser.

EDIT Novembre 2012: da quando abbiamo scritto questa risposta, abbiamo migliorato il nostro front-end C ++ per gestire il C ++ 11 completo, inclusi dialetti varianti ANSI, GNU e MS. Anche se c'erano molte cose extra, non dobbiamo cambiare il nostro motore di analisi; abbiamo appena rivisto le regole grammaticali. Noi fatto dovuto cambiare l'analisi semantica; C ++ 11 è semanticamente molto complicato e questo lavoro sommerge lo sforzo per far funzionare il parser.

EDIT Febbraio 2015: ... ora gestisce la versione completa di C ++ 14. (Vedi ottenere AST leggibile dall'uomo dal codice c ++ per l'analisi GLR di un semplice bit di codice e la famigerata "analisi più irritante" di C ++).

EDIT Aprile 2017: ora gestisce (bozza) C ++ 17.


6
PostScript: proprio come è più difficile ottenere che la grammatica corrisponda a ciò che fanno davvero i fornitori, ottenere la risoluzione del nome e del tipo per corrispondere all'interpretazione del manuale C ++ 11 da parte del diverso fornitore è ancora più difficile, perché le uniche prove che hai sono programmi che si compilano leggermente diversamente, se riesci a trovarli. Siamo in gran parte superati a partire da agosto 2013 per C ++ 11 vero e proprio, ma mi dispero un po 'al comitato C ++ che sembra intenzionato a produrre uno standard ancora più grande (e per esperienza, più confuso) sotto forma di C ++ 1y.
Ira Baxter

5
Mi piacerebbe davvero sapere: come gestisci questa foo * bar;ambiguità?
Martin

14
@ Martin: il nostro parser lo analizza in entrambi i modi, producendo un albero contenente speciali "nodi di ambiguità" i cui figli sono le analisi alternative; i bambini condividono al massimo i loro figli così finiamo con un DAG invece di un albero. Dopo il completamento dell'analisi, eseguiamo un analizzatore di grammatica degli attributi (AGE) sul DAG (nome di fantasia per "cammina sull'albero e fai cose" se non lo conosci) che calcola i tipi di tutti gli identificatori dichiarati. ...
Ira Baxter

12
... I figli ambigui non possono essere entrambi coerenti con il tipo; l'ETA quando si scopre un bambino ambiguo che non può essere digitato in modo sensato lo elimina semplicemente. Ciò che resta sono i bambini ben dattiloscritti; quindi, abbiamo determinato quale parse di "foo bar;" è corretta. Questo trucco funziona per tutti i tipi di folli ambiguità che si trovano nelle grammatiche reali che costruiamo per i veri dialetti di C ++ 11 e * separa completamente l' analisi dall'analisi semantica per i nomi. Questa separazione netta significa molto meno lavoro di ingegneria da fare (nessun groviglio per il debug). Vedi stackoverflow.com/a/1004737/120163 per ulteriori discussioni.
Ira Baxter

3
@ TimCas: In realtà, sono con te a inveire contro l'apparente stupidità di progettare la sintassi del linguaggio (e la semantica) che sono così complicate che è così difficile farlo bene (sì, il linguaggio C ++ soffre gravemente qui). Vorrei che i comitati di progettazione del linguaggio progettassero la sintassi in modo che le tecnologie di analisi più semplici funzionassero e definissero esplicitamente la semantica del linguaggio e la controllassero con alcuni strumenti di analisi semantica. Ahimè, il mondo non sembra essere così. Quindi, ritengo che tu costruisca ciò che devi costruire nel miglior modo possibile, e vai avanti con la vita, nonostante l'imbarazzo.
Ira Baxter

31

Il parser di Clang è un parser a discesa ricorsiva scritto a mano, così come molti altri front-end C e C ++ open source e commerciali.

Clang utilizza un parser a discesa ricorsiva per diversi motivi:

  • Prestazioni : un parser scritto a mano ci consente di scrivere un parser veloce, ottimizzando i percorsi attivi in ​​base alle esigenze e abbiamo sempre il controllo di tali prestazioni. Avere un parser veloce ha permesso a Clang di essere utilizzato in altri strumenti di sviluppo in cui non vengono usati i parser "reali", ad esempio l'evidenziazione della sintassi e il completamento del codice in un IDE.
  • Diagnostica e ripristino degli errori : poiché hai il pieno controllo con un parser di discesa ricorsivo scritto a mano, è facile aggiungere casi speciali che rilevano problemi comuni e forniscono un'ottima diagnostica e ripristino degli errori (ad esempio, vedi http: //clang.llvm .org / features.html # expressivediags ) Con i parser generati automaticamente, sei limitato alle capacità del generatore.
  • Semplicità : i parser a discesa ricorsiva sono facili da scrivere, capire e correggere. Non è necessario essere un esperto di analisi o imparare un nuovo strumento per estendere / migliorare il parser (che è particolarmente importante per un progetto open-source), ma puoi comunque ottenere ottimi risultati.

Nel complesso, per un compilatore C ++, non importa molto: la parte di analisi di C ++ non è banale, ma è comunque una delle parti più facili, quindi vale la pena mantenerla semplice. L'analisi semantica, in particolare la ricerca del nome, l'inizializzazione, la risoluzione del sovraccarico e la creazione di istanze dei modelli, sono ordini di grandezza più complicati dell'analisi. Se vuoi una prova, controlla la distribuzione del codice e dei commit nel componente "Sema" di Clang (per l'analisi semantica) rispetto al suo componente "Parse" (per l'analisi).


4
Sì, l'analisi semantica è molto più difficile. Abbiamo circa 4000 righe di regole grammaticali che comprendono la nostra grammatica C ++ 11, e circa 180.000 righe di codice grammaticale per attributi per le "analisi semantiche" Doub elencate sopra, con altre 100.000 righe di codice di supporto. L'analisi non è davvero il problema, anche se è già abbastanza difficile se inizi con il piede sbagliato.
Ira Baxter

1
Non sono così sicuro che i parser scritti a mano siano necessariamente migliori per la segnalazione / il ripristino degli errori. Sembra che le persone abbiano messo più energia in tali parser che nel potenziamento dei parser prodotti dai generatori di parser automatici nella pratica. Sembra esserci una ricerca abbastanza buona sull'argomento; questo particolare articolo ha davvero catturato la mia attenzione: MG Burke, 1983, Un metodo pratico per la diagnosi e il recupero degli errori sintattici LR e LL, Tesi di dottorato, Dipartimento di Informatica, New York University, Vedi archive.org/details/practicalmethodf00burk
Ira Baxter

1
... continuando questo corso di pensiero: se sei disposto a modificare / estendere / personalizzare il tuo parser costruito a mano per verificare casi speciali per una migliore diagnosi, allora dovresti essere disposto a fare lo stesso investimento in diagnosi migliori di un parser generato meccanicamente. Per qualsiasi analisi speciale che puoi codificare per quella manuale, puoi anche codificare un controllo per quella meccanica (e per i parser (G) LR, puoi farlo praticamente come controlli semantici sulle riduzioni). Nella misura in cui sembra poco appetibile, si è solo pigri ma non è un'accusa nei confronti dei parser generati meccanicamente IMHO.
Ira Baxter

8

Il parser di gcc è scritto a mano. . Sospetto lo stesso per il clang. Questo è probabilmente per alcuni motivi:

  • Prestazioni : qualcosa che hai ottimizzato manualmente per il tuo compito particolare funzionerà quasi sempre meglio di una soluzione generale. L'astrazione di solito ha un impatto sulle prestazioni
  • Tempistica : almeno nel caso di GCC, GCC precede molti strumenti di sviluppo gratuiti (usciti nel 1987). All'epoca non esisteva una versione gratuita di yacc, ecc., Che immagino sarebbe stata una priorità per le persone alla FSF.

Questo probabilmente non è un caso di sindrome del "non inventato qui", ma più sulla falsariga di "non c'era nulla di ottimizzato specificamente per ciò di cui avevamo bisogno, quindi abbiamo scritto il nostro".


15
Nessuna versione gratuita di yacc nel 1987? Penso che esistessero versioni gratuite quando yacc fu distribuito per la prima volta sotto Unix negli anni '70. E IIRC (altro poster sembra lo stesso), GCC usata per avere un parser YACC-based. Ho sentito che la scusa per cambiarlo era per ottenere una migliore segnalazione degli errori.
Ira Baxter

7
Vorrei aggiungere che spesso è più facile generare buoni messaggi di errore da un parser scritto a mano.
Dietrich Epp

1
Il tuo punto di vista sui tempi è impreciso. GCC aveva un parser basato su YACC, ma questo è stato sostituito con un parser discendente ricorsivo scritto a mano, in seguito.
Tommy Andersen

7

Strane risposte lì!

Le grammatiche C / C ++ non sono libere dal contesto. Sono sensibili al contesto a causa della barra Foo *; ambiguità. Dobbiamo costruire un elenco di typedef per sapere se Foo è un tipo o meno.

Ira Baxter: Non vedo il punto con la tua cosa GLR. Perché costruire un albero di analisi che comprende ambiguità. Analizzare significa risolvere ambiguità, costruire l'albero della sintassi. Risolvi queste ambiguità in un secondo passaggio, quindi non è meno brutto. Per me è molto più brutto ...

Yacc è un generatore di parser LR (1) (o LALR (1)), ma può essere facilmente modificato per essere sensibile al contesto. E non c'è niente di brutto in esso. Yacc / Bison è stato creato per aiutare nell'analisi del linguaggio C, quindi probabilmente non è lo strumento più brutto per generare un parser C ...

Fino a GCC 3.x il parser C è generato da yacc / bison, con la tabella typedefs costruita durante l'analisi. Con la creazione di tabelle typedef "in parse", la grammatica C diventa localmente libera dal contesto e inoltre "localmente LR (1)".

Ora, in Gcc 4.x, è un parser discendente ricorsivo. È esattamente lo stesso parser di Gcc 3.x, è ancora LR (1) e ha le stesse regole grammaticali. La differenza è che il parser yacc è stato riscritto a mano, lo shift / reduce sono ora nascosti nello stack di chiamate e non c'è "state454: if (nextsym == '(') goto state398" come in gcc 3.x yacc's parser, quindi è più facile patchare, gestire errori e stampare messaggi più belli, ed eseguire alcuni dei passaggi successivi di compilazione durante l'analisi, al prezzo di un codice molto meno "facile da leggere" per un noob di gcc.

Perché sono passati da yacc a discendenza ricorsiva? Perché è abbastanza necessario evitare che yacc parli C ++, e poiché GCC sogna di essere un compilatore multilingue, cioè condividere il massimo del codice tra i diversi linguaggi che può compilare. Questo è il motivo per cui il C ++ e il parser C vengono scritti nello stesso modo.

C ++ è più difficile da analizzare rispetto a C perché non è "localmente" LR (1) come C, non è nemmeno LR (k). Guarda func<4 > 2>quale è una funzione modello istanziata con 4> 2, cioè func<4 > 2> deve essere letta come func<1>. Questo non è sicuramente LR (1). Ora considera,func<4 > 2 > 1 > 3 > 3 > 8 > 9 > 8 > 7 > 8> . È qui che una discesa ricorsiva può facilmente risolvere l'ambiguità, al prezzo di qualche altra chiamata di funzione (parse_template_parameter è la funzione parser ambigua. Se parse_template_parameter (17tokens) fallisce, riprova parse_template_parameter (15tokens), parse_template_parameter (13tokens) ... Funziona).

Non so perché non sarebbe possibile aggiungere sotto grammatiche ricorsive yacc / bison, forse questo sarà il prossimo passo nello sviluppo del parser gcc / GNU?


9
"per me è molto più brutto". Quello che posso dirti è che l'ingegneria di un parser di qualità di produzione che utilizza GLR e la risoluzione dell'ambiguità del ritardo è pratica con un team davvero piccolo. Tutte le altre soluzioni che ho visto hanno coinvolto anni di digrignare i denti in pubblico per i salti mortali e gli hack necessari per farlo funzionare con LR, discesa ricorsiva, lo chiami. Puoi postulare molte altre fantastiche nuove tecnologie di analisi, ma per quanto posso dire, a questo punto è solo più digrignare i denti. Le idee costano poco; l'esecuzione è cara.
Ira Baxter


@Fizz: articolo interessante sull'analisi di Fortress, un complesso linguaggio di programmazione scientifica. Hanno detto diverse cose degne di nota: a) i generatori di parser classici (LL (k), LALR (1)) non possono gestire grammatiche difficili, b) hanno provato GLR, hanno avuto problemi con la scala ma gli sviluppatori erano inesperti quindi non l'hanno fatto complete [non è colpa di GLR] ec) hanno usato un parser Packrat di backtracking (transazionale) e ci hanno messo molto impegno, incluso il lavoro per produrre messaggi di errore migliori. Per quanto riguarda il loro esempio di analisi di "{| x || x ← mySet, 3 | x}", credo che GLR lo farebbe bene e non ha bisogno di spazi.
Ira Baxter

0

Sembra che GCC e LLVM-Clang stiano utilizzando parser discendenti ricorsivi scritti a mano e non analisi bottom-up generate dalla macchina, basate su Bison-Flex.

Bison in particolare non credo possa gestire la grammatica senza analizzare alcune cose in modo ambiguo e fare un secondo passaggio più tardi.

So che Happy di Haskell consente parser monadici (cioè dipendenti dallo stato) che possono risolvere il problema particolare con la sintassi C, ma non conosco generatori di parser C che consentono una monade di stato fornita dall'utente.

In teoria, il ripristino degli errori sarebbe un punto a favore di un parser scritto a mano, ma la mia esperienza con GCC / Clang è stata che i messaggi di errore non sono particolarmente buoni.

Per quanto riguarda le prestazioni, alcune delle affermazioni sembrano infondate. La generazione di una grande macchina a stati utilizzando un generatore di parser dovrebbe risultare in qualcosa che è O(n)e dubito che l'analisi sia il collo di bottiglia in molti strumenti.


3
Questa domanda ha già una risposta di altissima qualità, cosa stai cercando di aggiungere?
tod
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.