Sind GCC- und Clang-Parser wirklich handgeschrieben?

88

Es scheint, dass GCC und LLVM-Clang handgeschriebene Parser für rekursiven Abstieg verwenden und keine maschinengenerierte, Bison-Flex-basierte Bottom-up-Analyse.

Könnte hier jemand bitte bestätigen, dass dies der Fall ist? Und wenn ja, warum verwenden Mainstream-Compiler-Frameworks handgeschriebene Parser?

Update : interessanter Blog zu diesem Thema hier

JCLL
quelle
27
Fast alle Mainstream-Compiler verwenden handgeschriebene Parser. Was ist ein Problem damit?
SK-Logik
2
Sie müssen dies (halb-) manuell tun, wenn Sie Leistung benötigen.
Gene Bushuyev
15
Und nicht nur Leistung - bessere Fehlermeldungen, Wiederherstellungsfähigkeit usw.
SK-Logik
Was ist mit MS VisualStudio? Könnte jemand von MS überprüfen, ob auch er einen handgeschriebenen Parser für rekursiven Abstieg verwendet, obwohl er nicht aus Open-Source-Quellen stammt?
OrenIshShalom
3
@GeneBushuyev, aus dem GCC-Wiki: "... Obwohl das Timing eine Beschleunigung von 1,5% aufwies , erleichtern die Hauptvorteile zukünftige Verbesserungen ..." Diese Beschleunigung scheint eher marginal zu sein ...
OrenIshShalom

Antworten:

75

Ja:

  • GCC verwendete einst einen Yacc (Bison) -Parser, der jedoch irgendwann in der 3.x-Reihe durch einen handgeschriebenen Parser für rekursiven Abstieg ersetzt wurde: siehe http://gcc.gnu.org/wiki/New_C_Parser für Links zu relevanten Patch-Einsendungen.

  • Clang verwendet auch einen handgeschriebenen Parser für rekursiven Abstieg: siehe Abschnitt "Ein einzelner einheitlicher Parser für C, Objective C, C ++ und Objective C ++" am Ende von http://clang.llvm.org/features.html .

Matthew Slattery
quelle
3
Bedeutet das, dass ObjC, C und C ++ LL (k) Grammatiken haben?
Lindemann
47
Nein: Selbst C, das einfachste der drei, hat eine mehrdeutige Grammatik. Zum Beispiel foo * bar;könnte parsen entweder als ein Multiplikationsausdruck (mit dem Ergebnis , nicht verwendet), oder eine Erklärung einer Variablen barmit Typ pointer-to foo. Welches richtig ist, hängt davon ab, ob ein typedeffor foozu diesem Zeitpunkt im Geltungsbereich ist, was nicht mit einer beliebigen Menge an Lookahead bestimmt werden kann. Dies bedeutet jedoch nur, dass der Parser für rekursiven Abstieg einige hässliche zusätzliche Maschinen benötigt, um dies zu handhaben.
Matthew Slattery
9
Ich kann anhand empirischer Daten bestätigen, dass C ++ 11, C und Objective C kontextfreie Grammatiken haben, die ein GLR-Parser verarbeiten kann.
Ira Baxter
2
In Bezug auf die Kontextsensitivität behauptet diese Antwort auch nicht: Das Parsen dieser Sprachen ist wahrscheinlich vollständig.
Ioannis Filippidis
104

Es gibt einen Folk-Satz, der besagt, dass C schwer zu analysieren ist und C ++ im Wesentlichen unmöglich ist.

Es ist nicht wahr.

Was wahr ist, ist, dass C und C ++ mit LALR (1) -Parsern ziemlich schwer zu analysieren sind, ohne die Parsing-Maschinerie zu hacken und sich in Symboltabellendaten zu verwickeln. GCC hat sie tatsächlich mit YACC und zusätzlichem Hackery wie diesem analysiert, und ja, es war hässlich. Jetzt verwendet GCC handgeschriebene Parser, aber immer noch mit der Symboltabelle Hackery. Die Clang-Leute haben nie versucht, automatisierte Parser-Generatoren zu verwenden. AFAIK the Clang Parser war schon immer eine handcodierte rekursive Abstammung.

Was stimmt, ist, dass C und C ++ mit einfacheren automatisch generierten Parsern, z. B. GLR-Parsern , relativ einfach zu analysieren sind und Sie keine Hacks benötigen. Der Elsa C ++ - Parser ist ein Beispiel dafür. Unser C ++ - Frontend ist ein anderes (wie alle unsere "Compiler" -Frontends ist GLR eine wunderbare Parsing-Technologie).

Unser C ++ - Frontend ist nicht so schnell wie das von GCC und sicherlich langsamer als Elsa. Wir haben wenig Energie in die sorgfältige Optimierung gesteckt, da wir andere dringlichere Probleme haben (dennoch wurde es in Millionen von Zeilen C ++ - Code verwendet). Elsa ist wahrscheinlich langsamer als GCC, nur weil es allgemeiner ist. Angesichts der heutigen Prozessorgeschwindigkeit sind diese Unterschiede in der Praxis möglicherweise nicht sehr wichtig.

Aber die heute weit verbreiteten "echten Compiler" haben ihre Wurzeln in Compilern von vor 10 oder 20 Jahren oder mehr. Ineffizienzen waren dann viel wichtiger, und niemand hatte von GLR-Parsern gehört, also taten die Leute, was sie zu tun wussten. Clang ist sicherlich jünger, aber dann behalten Volkstheoreme ihre "Überzeugungskraft" für lange Zeit.

Sie müssen es nicht mehr so ​​machen. Sie können GLR und andere solche Parser sehr vernünftig als Frontends verwenden, um die Wartbarkeit des Compilers zu verbessern.

Was ist wahr, das ist eine Grammatik erhalten , die Ihre freundliche Nachbarschaft Compiler Verhalten entspricht hart ist. Während praktisch alle C ++ - Compiler (die meisten) des ursprünglichen Standards implementieren, haben sie auch viele Erweiterungen für dunkle Ecken, z. B. DLL-Spezifikationen in MS-Compilern usw. Wenn Sie eine starke Parsing-Engine haben, können Sie Ihre Zeit damit verbringen, diese zu erhalten Die endgültige Grammatik, die der Realität entspricht, anstatt zu versuchen, Ihre Grammatik so zu biegen, dass sie den Einschränkungen Ihres Parser-Generators entspricht.

BEARBEITEN November 2012: Seit wir diese Antwort geschrieben haben, haben wir unser C ++ - Frontend verbessert, um volles C ++ 11 zu verarbeiten, einschließlich ANSI-, GNU- und MS-Dialektvarianten. Obwohl es viele zusätzliche Dinge gab, müssen wir unsere Parsing-Engine nicht ändern. Wir haben gerade die Grammatikregeln überarbeitet. Wir mussten die semantische Analyse ändern; C ++ 11 ist semantisch sehr kompliziert, und diese Arbeit überflutet den Aufwand, den Parser zum Laufen zu bringen.

BEARBEITEN Februar 2015: ... behandelt jetzt volles C ++ 14. (Siehe GLS-Analyse eines einfachen Code-Bits und C ++ 's berüchtigte "ärgerlichste Analyse" unter " Vom Menschen lesbaren AST aus C ++ - Code abrufen").

BEARBEITEN April 2017: Behandelt jetzt (Entwurf) C ++ 17.

Ira Baxter
quelle
6
PostScript: Genauso wie es schwieriger ist, die Grammatik an das anzupassen, was die Anbieter wirklich tun, ist es noch schwieriger, die Auflösung von Namen und Typ an die Interpretation des C ++ 11-Handbuchs durch die verschiedenen Anbieter anzupassen, da Sie nur Programme haben, die leicht kompiliert werden können anders, wenn Sie sie finden können. Wir haben das ab August 2013 für C ++ 11 weitgehend hinter uns gelassen, aber ich verzweifle ein wenig am C ++ - Komitee, das verdammt darauf aus ist, einen noch größeren (und erfahrungsgemäß verwirrenderen) Standard in Form von C zu produzieren ++ 1y.
Ira Baxter
5
Ich würde wirklich gerne wissen: Wie gehen Sie mit dieser foo * bar;Mehrdeutigkeit um?
Martin
14
@Martin: Unser Parser analysiert es in beide Richtungen und erzeugt einen Baum mit speziellen "Mehrdeutigkeitsknoten", deren untergeordnete Elemente die alternativen Parses sind. Die Kinder teilen ihre Kinder maximal, so dass wir am Ende eine DAG anstelle eines Baumes haben. Nach Abschluss der Analyse führen wir einen Attribut-Grammatik-Evaluator (AGE) über die DAG aus (ausgefallener Name für "Gehen Sie durch den Baum und erledigen Sie Dinge", wenn Sie ihn nicht kennen), der die Typen aller deklarierten Bezeichner berechnet. ...
Ira Baxter
12
... Die mehrdeutigen Kinder können nicht beide typkonsistent sein. Das ALTER bei der Entdeckung eines mehrdeutigen Kindes, das nicht sinnvoll getippt werden kann, löscht es einfach. Was bleibt, sind die gut getippten Kinder; So haben wir festgestellt, welche Analyse von "foo bar"; ist richtig. Dieser Trick funktioniert für alle Arten von verrückten Zweideutigkeiten, die in den realen Grammatiken zu finden sind, die wir für die realen Dialekte von C ++ 11 erstellen, und * trennt das Parsen vollständig von der semantischen Analyse für Namen. Diese saubere Trennung bedeutet viel weniger Engineering-Arbeit (keine Verwicklungen zum Debuggen). Weitere Informationen finden Sie unter stackoverflow.com/a/1004737/120163 .
Ira Baxter
3
@TimCas: Eigentlich bin ich mit Ihnen auf dem Geländer über die scheinbare Dummheit, Sprachsyntax (und Semantik) zu entwerfen, die so kompliziert ist, dass es so schwer ist, sie richtig zu machen (ja, die C ++ - Sprache leidet hier sehr). Ich wünschte, Sprachdesign-Komitees würden die Syntax so entwerfen, dass einfachere Parsing-Technologien funktionieren, und die Sprachsemantik explizit definieren und mit einigen semantischen Analysewerkzeugen überprüfen. Leider scheint die Welt nicht so zu sein. Ich bin also der Ansicht, dass Sie das bauen, was Sie bauen müssen, so gut Sie können, und trotz der Unbeholfenheit mit dem Leben weitermachen.
Ira Baxter
31

Clangs Parser ist ein handgeschriebener Parser mit rekursivem Abstieg, ebenso wie mehrere andere Open-Source- und kommerzielle C- und C ++ - Frontends.

Clang verwendet aus mehreren Gründen einen Parser für rekursiven Abstieg:

  • Leistung : Mit einem handgeschriebenen Parser können wir einen schnellen Parser schreiben und die Hot Paths nach Bedarf optimieren. Wir haben immer die Kontrolle über diese Leistung. Mit einem schnellen Parser konnte Clang in anderen Entwicklungstools verwendet werden, in denen "echte" Parser normalerweise nicht verwendet werden, z. B. Syntaxhervorhebung und Code-Vervollständigung in einer IDE.
  • Diagnose und Fehlerbehebung : Da Sie mit einem handgeschriebenen Parser für rekursiven Abstieg die volle Kontrolle haben, können Sie ganz einfach Sonderfälle hinzufügen, die häufig auftretende Probleme erkennen und eine hervorragende Diagnose und Fehlerbehebung bieten (siehe z. B. http: //clang.llvm) .org / features.html # expressivediags ) Mit automatisch generierten Parsern sind Sie auf die Funktionen des Generators beschränkt.
  • Einfachheit : Parser mit rekursivem Abstieg sind einfach zu schreiben, zu verstehen und zu debuggen. Sie müssen kein Parsing-Experte sein oder ein neues Tool zum Erweitern / Verbessern des Parsers erlernen (was besonders für ein Open-Source-Projekt wichtig ist), aber Sie können trotzdem großartige Ergebnisse erzielen.

Insgesamt spielt es für einen C ++ - Compiler keine Rolle: Der Parsing-Teil von C ++ ist nicht trivial, aber immer noch einer der einfacheren Teile. Es lohnt sich also, ihn einfach zu halten. Die semantische Analyse - insbesondere die Suche nach Namen, die Initialisierung, die Überlastungsauflösung und die Instanziierung von Vorlagen - ist um Größenordnungen komplizierter als das Parsen. Wenn Sie einen Beweis wünschen, überprüfen Sie die Verteilung von Code und Commits in Clangs "Sema" -Komponente (für die semantische Analyse) im Vergleich zu ihrer "Parse" -Komponente (für die Analyse).

Doug
quelle
4
Ja, die semantische Analyse ist um einiges schwieriger. Wir haben ungefähr 4000 Zeilen Grammatikregeln, aus denen unsere C ++ 11-Grammatik besteht, und ungefähr 180.000 Zeilen Attribut-Grammatikcode für die oben genannten Doub-Listen "semantische Analysen", mit weiteren 100.000 Zeilen unterstützendem Code. Das Parsen ist wirklich nicht das Problem, obwohl es schwierig genug ist, wenn Sie mit dem falschen Fuß beginnen.
Ira Baxter
1
Ich bin mir nicht sicher, ob handgeschriebene Parser für die Fehlerberichterstattung / -behebung unbedingt besser sind . Es scheint, dass die Leute mehr Energie in solche Parser gesteckt haben als in die Verbesserung von Parsern, die in der Praxis von automatischen Parser-Generatoren erzeugt werden. Es scheint ziemlich gute Forschung zu diesem Thema zu geben; Dieses spezielle Papier ist mir wirklich aufgefallen: MG Burke, 1983, Eine praktische Methode zur Diagnose und Wiederherstellung syntaktischer LR- und LL-Fehler, Doktorarbeit, Institut für Informatik, Universität New York, siehe archive.org/details/practicalmethodf00burk
Ira Baxter
1
... diesen Gedankengang fortsetzen: Wenn Sie bereit sind, Ihren handgefertigten Parser zu ändern / zu erweitern / anzupassen, um nach Sonderfällen für eine bessere Diagnose zu suchen, sollten Sie bereit sein, gleichermaßen in bessere Diagnosen eines mechanisch erzeugten Parsers zu investieren. Für jede spezielle Analyse, die Sie für die manuelle Analyse codieren können, können Sie auch eine Prüfung für die mechanische codieren (und für (G) LR-Parser können Sie dies so ziemlich als semantische Überprüfung von Reduzierungen tun). In dem Maße, wie es unappetitlich erscheint, ist man nur faul, aber das ist meiner Meinung nach keine Anklage gegen die mechanisch erzeugten Parser.
Ira Baxter
8

Der Parser von gcc ist handgeschrieben. . Ich vermute dasselbe für Klirren. Dies hat wahrscheinlich einige Gründe:

  • Leistung : Etwas, das Sie von Hand für Ihre spezielle Aufgabe optimiert haben, ist fast immer besser als eine allgemeine Lösung. Abstraktion hat normalerweise einen Leistungseinbruch
  • Timing : Zumindest im Fall von GCC ist GCC älter als viele kostenlose Entwicklertools (erschien 1987). Zu dieser Zeit gab es keine kostenlose Version von Yacc usw., von der ich mir vorstellen würde, dass sie für die Leute bei der FSF eine Priorität gewesen wäre.

Dies ist wahrscheinlich kein Fall von "hier nicht erfunden" -Syndrom, sondern eher im Sinne von "Es wurde nichts speziell für das optimiert, was wir brauchten, also haben wir unser eigenes geschrieben".

Rafe Kettler
quelle
15
Keine kostenlose Version von yacc im Jahr 1987? Ich denke, es gab kostenlose Versionen, als yacc in den 70er Jahren erstmals unter Unix ausgeliefert wurde. Und IIRC (anderes Poster scheint dasselbe zu sein), GCC hatte früher einen YACC-basierten Parser. Ich hörte die Entschuldigung für die Änderung darin, eine bessere Fehlerberichterstattung zu erhalten.
Ira Baxter
7
Ich möchte hinzufügen, dass es oft einfacher ist, gute Fehlermeldungen aus einem handgeschriebenen Parser zu generieren.
Dietrich Epp
1
Ihr Punkt zum Timing ist ungenau. GCC hatte früher einen YACC-basierten Parser, der jedoch später durch einen handgeschriebenen Parser für rekursiven Abstieg ersetzt wurde.
Tommy Andersen
7

Seltsame Antworten da!

C / C ++ - Grammatiken sind nicht kontextfrei. Sie sind aufgrund der Foo * -Leiste kontextsensitiv. Mehrdeutigkeit. Wir müssen eine Liste von Typedefs erstellen, um zu wissen, ob Foo ein Typ ist oder nicht.

Ira Baxter: Ich verstehe den Punkt mit Ihrer GLR-Sache nicht. Warum einen Analysebaum erstellen, der Mehrdeutigkeiten enthält? Parsen bedeutet, Mehrdeutigkeiten zu lösen und den Syntaxbaum zu erstellen. Sie lösen diese Unklarheiten in einem zweiten Durchgang, sodass dies nicht weniger hässlich ist. Für mich ist es viel hässlicher ...

Yacc ist ein LR (1) -Parsergenerator (oder LALR (1)), kann jedoch leicht so geändert werden, dass er kontextsensitiv ist. Und da ist nichts Hässliches drin. Yacc / Bison wurde entwickelt, um das Parsen der C-Sprache zu erleichtern. Daher ist es wahrscheinlich nicht das hässlichste Tool, um einen C-Parser zu generieren ...

Bis GCC 3.x wird der C-Parser von yacc / bison generiert, wobei die typedefs-Tabelle während des Parsens erstellt wird. Mit der Tabellenerstellung "in parse" typedefs wird die C-Grammatik lokal kontextfrei und darüber hinaus "lokal LR (1)".

In Gcc 4.x handelt es sich nun um einen Parser für rekursiven Abstieg. Es ist genau der gleiche Parser wie in Gcc 3.x, es ist immer noch LR (1) und hat die gleichen Grammatikregeln. Der Unterschied besteht darin, dass der yacc-Parser von Hand neu geschrieben wurde, die Verschiebung / Reduzierung jetzt im Aufrufstapel versteckt ist und es kein "state454: if (nextsym == '(') goto state398" wie in gcc 3.x yacc's gibt Parser, so ist es einfacher zu patchen, Fehler zu behandeln und schönere Nachrichten zu drucken und einige der nächsten Kompilierungsschritte während des Parsens auszuführen. Zum Preis von viel weniger "leicht lesbarem" Code für einen gcc noob.

Warum wechselten sie von Yacc zu rekursivem Abstieg? Weil es durchaus notwendig ist, yacc zu vermeiden, um C ++ zu analysieren, und weil GCC davon träumt, ein mehrsprachiger Compiler zu sein, dh maximal Code zwischen den verschiedenen Sprachen zu teilen, die es kompilieren kann. Aus diesem Grund werden C ++ und C-Parser auf dieselbe Weise geschrieben.

C ++ ist schwieriger zu analysieren als C, da es nicht "lokal" LR (1) als C ist, sondern nicht einmal LR (k). Schauen Sie sich an, func<4 > 2>welche Vorlagenfunktion mit 4> 2 instanziiert ist, d. H.func<4 > 2> wie gelesen werden muss func<1>. Dies ist definitiv nicht LR (1). Nun überlegen Sie , func<4 > 2 > 1 > 3 > 3 > 8 > 9 > 8 > 7 > 8>. Hier kann ein rekursiver Abstieg Mehrdeutigkeiten leicht lösen, und zwar zum Preis einiger weiterer Funktionsaufrufe (parse_template_parameter ist die mehrdeutige Parserfunktion. Wenn parse_template_parameter (17tokens) fehlgeschlagen ist, versuchen Sie es erneut mit parse_template_parameter (15tokens), parse_template_parameter (13tokens) ... bis Es klappt).

Ich weiß nicht, warum es nicht möglich wäre, rekursive Subgrammatiken zu yacc / bison hinzuzufügen. Vielleicht ist dies der nächste Schritt in der Entwicklung von gcc / GNU-Parsern?

reuns
quelle
9
"Für mich ist es viel hässlicher". Was ich Ihnen sagen kann, ist, dass das Engineering eines Parsers für Produktionsqualität unter Verwendung von GLR und der Auflösung von Verzögerungsmehrdeutigkeiten mit einem wirklich kleinen Team praktisch ist. Alle anderen Lösungen, die ich gesehen habe, beinhalteten jahrelanges Zähneknirschen in der Öffentlichkeit über die Backflips und Hacks, die erforderlich sind, damit es mit LR funktioniert, rekursiver Abstieg, wie Sie es nennen. Sie können viele andere coole neue Parsing-Technologien postulieren, aber soweit ich das beurteilen kann, ist das an dieser Stelle nur mehr Zähneknirschen. Ideen sind billig; Hinrichtung ist teuer.
Ira Baxter
@IraBaxter: Ratten! citeseerx.ist.psu.edu/viewdoc/…
Fizz
@Fizz: Interessantes Papier zum Parsen von Fortress, einer komplexen wissenschaftlichen Programmiersprache. Sie sagten einige wichtige Dinge: a) Klassische Parser-Generatoren (LL (k), LALR (1)) können nicht mit harten Grammatiken umgehen, b) sie versuchten GLR, hatten Probleme mit der Skalierung, aber die Entwickler waren unerfahren, also taten sie es nicht vollständig [das ist nicht die Schuld von GLR] und c) sie haben einen Backrating (Transaktions-) Packrat-Parser verwendet und viel Aufwand in ihn gesteckt, einschließlich der Arbeit, um bessere Fehlermeldungen zu erzeugen. In Bezug auf ihr Beispiel für das Parsen von "{| x || x ← mySet, 3 | x}" glaube ich, dass GLR es gut machen würde und keine Leerzeichen benötigt.
Ira Baxter
0

Es scheint, dass GCC und LLVM-Clang handgeschriebene Parser für rekursiven Abstieg verwenden und keine maschinengenerierte, Bison-Flex-basierte Bottom-up-Analyse.

Insbesondere Bison kann meiner Meinung nach nicht mit der Grammatik umgehen, ohne einige Dinge mehrdeutig zu analysieren und später einen zweiten Durchgang durchzuführen.

Ich weiß, dass Haskell's Happy monadische (dh zustandsabhängige) Parser zulässt, die das spezielle Problem mit der C-Syntax lösen können, aber ich kenne keine C-Parser-Generatoren, die eine vom Benutzer bereitgestellte Statusmonade zulassen.

Theoretisch wäre die Fehlerbehebung ein Punkt für einen handgeschriebenen Parser, aber meine Erfahrung mit GCC / Clang hat gezeigt, dass die Fehlermeldungen nicht besonders gut sind.

In Bezug auf die Leistung scheinen einige der Behauptungen unbegründet zu sein. Das Generieren einer großen Zustandsmaschine mit einem Parser-Generator sollte zu etwas führen, O(n)und ich bezweifle , dass das Parsen der Engpass bei vielen Werkzeugen ist.

Vanessa McHale
quelle
3
Diese Frage hat bereits eine sehr hochwertige Antwort. Was möchten Sie hinzufügen?
tod