Czy parsery GCC i Clang są naprawdę napisane odręcznie?

90

Wygląda na to, że GCC i LLVM-Clang używają ręcznie zapisywanych rekurencyjnych parserów zejścia , a nie generowanego maszynowo, opartego na Bison-Flex, analizowania oddolnego.

Czy ktoś tutaj mógłby potwierdzić, że tak jest? A jeśli tak, to dlaczego główne platformy kompilatorów używają odręcznych parserów?

Aktualizacja : interesujący blog na ten temat tutaj

JCLL
źródło
27
Prawie wszystkie główne kompilatory używają odręcznych parserów. Jaki jest z tym problem?
SK-logic,
2
musisz to zrobić (pół) ręcznie, jeśli potrzebujesz wydajności.
Gene Bushuyev
15
I nie tylko wydajność - lepsze komunikaty o błędach, możliwość odzyskiwania itp.
SK-logic
A co z MS VisualStudio? choć nie jest to oprogramowanie typu open source, czy ktoś z MS mógłby sprawdzić, czy on także używa rekurencyjnego parsera z odręcznym zapisem?
OrenIshShalom
3
@GeneBushuyev, z wiki GCC: "... Chociaż czasy pokazały 1,5% przyspieszenie , główne korzyści to ułatwienie przyszłych ulepszeń ..." to przyspieszenie wydaje się raczej marginalne ...
OrenIshShalom

Odpowiedzi:

78

Tak:

  • GCC używał kiedyś parsera yacc (bison), ale został on zastąpiony przez ręcznie napisany rekursywny parser zejścia w pewnym momencie serii 3.x: patrz http://gcc.gnu.org/wiki/New_C_Parser dla linki do odpowiednich zgłoszeń poprawek.

  • Clang używa również ręcznie napisanego rekurencyjnego parsera zejścia: zobacz sekcję „Pojedynczy zunifikowany parser dla C, Objective C, C ++ i Objective C ++” pod koniec http://clang.llvm.org/features.html .

Matthew Slattery
źródło
3
Czy to oznacza, że ​​ObjC, C i C ++ mają gramatyki LL (k)?
Lindemann
47
Nie: nawet C, najprostszy z trzech, ma niejednoznaczną gramatykę. Na przykład foo * bar;może analizować jako wyrażenie mnożenia (z nieużywanym wynikiem) lub deklarację zmiennej barz typem wskaźnik-to- foo. To, które z nich jest poprawne, zależy od tego, czy typedeffor foojest w danym momencie w zakresie, co nie jest czymś, co można określić przy dowolnej ilości wyprzedzenia. Ale to po prostu oznacza, że ​​parser zejścia rekurencyjnego potrzebuje dodatkowych brzydkich dodatkowych maszyn, aby to obsłużyć.
Matthew Slattery,
9
Mogę potwierdzić na podstawie dowodów empirycznych, że C ++ 11, C i Objective C mają gramatykę bezkontekstową, którą może obsłużyć parser GLR.
Ira Baxter
2
Jeśli chodzi o wrażliwość na kontekst, ta odpowiedź nie twierdzi ani: że analiza tych języków jest prawdopodobnie kompletna w Turingu.
Ioannis Filippidis
106

Istnieje twierdzenie ludowe, które mówi, że C jest trudne do przeanalizowania, a C ++ w zasadzie niemożliwe.

To nieprawda.

Prawdą jest, że C i C ++ są dość trudne do przeanalizowania przy użyciu parserów LALR (1) bez hakowania maszyn parsujących i plątania się w danych tabeli symboli. W rzeczywistości GCC parsował je, używając YACC i dodatkowych hackerów, i tak, to było brzydkie. Teraz GCC używa odręcznych parserów, ale nadal z hackerem tablicy symboli. Ludzie z Clang nigdy nie próbowali używać automatycznych generatorów parserów; AFAIK parser Clang zawsze był ręcznie kodowany rekursywnie.

Prawdą jest, że C i C ++ są stosunkowo łatwe do przeanalizowania z silniejszymi automatycznie generowanymi parserami, np. Parserami GLR , i nie potrzebujesz żadnych hacków. W Elsy C ++ parser jest jednym z przykładów. Nasz interfejs C ++ jest kolejnym (podobnie jak wszystkie nasze interfejsy „kompilatorów”, GLR jest całkiem wspaniałą technologią parsowania).

Nasz interfejs C ++ nie jest tak szybki jak GCC iz pewnością wolniejszy niż Elsa; włożyliśmy niewiele energii w jego dokładne dostrojenie, ponieważ mamy inne, bardziej palące problemy (niemniej jednak zostało ono użyte w milionach wierszy kodu C ++). Elsa jest prawdopodobnie wolniejsza niż GCC tylko dlatego, że jest bardziej ogólna. Biorąc pod uwagę dzisiejsze prędkości procesora, różnice te mogą nie mieć większego znaczenia w praktyce.

Ale "prawdziwe kompilatory", które są dziś szeroko rozpowszechniane, mają swoje korzenie w kompilatorach sprzed 10, 20 lub więcej lat. Nieskuteczność liczyła się wtedy znacznie bardziej i nikt nie słyszał o parserach GLR, więc ludzie robili to, co umieli. Clang jest z pewnością nowszy, ale twierdzenia ludowe przez długi czas zachowują swoją „zdolność przekonywania”.

Nie musisz już tego robić w ten sposób. Możesz bardzo rozsądnie użyć GLR i innych podobnych parserów jako front-endów, z ulepszeniem w utrzymywalności kompilatora.

Co to jest prawdą, jest to, że coraz gramatyki, który odpowiada zachowaniu przyjaznego sąsiedztwa kompilator jest trudne. Podczas gdy praktycznie wszystkie kompilatory C ++ implementują (większość) oryginalnego standardu, zwykle mają również wiele rozszerzeń z ciemnymi rogami, np. Specyfikacje DLL w kompilatorach MS itp. Jeśli masz silny silnik parsujący, możesz spędzić czas próbując uzyskać ostateczna gramatyka, aby pasowała do rzeczywistości, zamiast próbować naginać gramatykę, aby dopasować ją do ograniczeń generatora parsera.

EDYCJA Listopad 2012: Od czasu napisania tej odpowiedzi ulepszyliśmy nasz interfejs C ++, aby obsługiwał pełny C ++ 11, w tym dialekty ANSI, GNU i MS. Chociaż było wiele dodatkowych rzeczy, nie musimy zmieniać naszego silnika parsującego; właśnie zmieniliśmy zasady gramatyczne. My nie musimy zmieniać analizę semantyczną; C ++ 11 jest semantycznie bardzo skomplikowany i ta praca utrudnia uruchomienie parsera.

EDYCJA Luty 2015: ... teraz obsługuje pełne C ++ 14. (Zobacz pobieranie czytelnego dla człowieka kodu AST z kodu c ++, aby zapoznać się z analizami GLR prostego fragmentu kodu i niesławną C ++ „najbardziej irytującą analizą”).

EDYCJA Kwiecień 2017: Teraz obsługuje (wersję roboczą) C ++ 17.

Ira Baxter
źródło
6
PostScript: Tak jak trudniej jest dopasować gramatykę do tego, co naprawdę robią dostawcy, uzyskanie rozdzielczości nazw i typów w celu dopasowania interpretacji podręcznika C ++ 11 przez różnych dostawców jest jeszcze trudniejsze, ponieważ jedynym dowodem, jaki masz, są programy, które nieznacznie się kompilują inaczej, jeśli możesz je znaleźć. W sierpniu 2013 r. Mamy już dużo do czynienia z właściwym językiem C ++ 11, ale trochę rozpaczam z powodu komitetu C ++, który wydaje się być piekielnie nastawiony na stworzenie jeszcze większego (iz doświadczenia, bardziej zagmatwanego) standardu w postaci C ++ 1r.
Ira Baxter,
5
Naprawdę chciałbym wiedzieć: jak sobie foo * bar;radzisz z tą dwuznacznością?
Martin
14
@Martin: nasz parser analizuje to w obie strony, tworząc drzewo zawierające specjalne „węzły niejednoznaczności”, których dzieci są alternatywnymi analizami; dzieci maksymalnie dzielą się swoimi dziećmi, więc zamiast drzewa otrzymujemy DAG. Po zakończeniu analizowania uruchamiamy oceniający gramatykę atrybutów (AGE) nad DAG (fantazyjna nazwa dla „chodź po drzewie i rób rzeczy”, jeśli go nie znasz), który oblicza typy wszystkich zadeklarowanych identyfikatorów. ...
Ira Baxter
12
... Niejednoznaczne dzieci nie mogą jednocześnie być zgodne z typem; WIEK po odkryciu dwuznacznego dziecka, którego nie można rozsądnie wpisać, po prostu je usuwa. Pozostają dobrze wpisane dzieci; w ten sposób ustaliliśmy, która z parsowania "foo bar"; jest poprawne. Ta sztuczka sprawdza się w przypadku wszelkiego rodzaju szalonych niejednoznaczności występujących w prawdziwych gramatykach, które budujemy dla prawdziwych dialektów C ++ 11, i * całkowicie oddziela parsowanie od analizy semantycznej nazw. Ta czysta separacja oznacza dużo mniej pracy inżynierskiej do wykonania (brak splotów do debugowania). Więcej dyskusji znajdziesz na stackoverflow.com/a/1004737/120163 .
Ira Baxter
3
@TimCas: Właściwie, jestem z tobą na balustradzie pozornej głupoty projektowania składni języka (i semantyki), które są tak skomplikowane, że tak trudno jest to zrobić dobrze (tak, język C ++ tutaj bardzo cierpi). Chciałbym, aby komitety ds. Projektowania języków opracowywały składnię tak, aby działały prostsze technologie analizowania i wyraźnie definiowały semantykę języka i sprawdzały ją za pomocą narzędzi do analizy semantycznej. Niestety, świat nie wygląda na taki. Tak więc uważam, że budujesz to, co musisz zbudować, tak dobrze, jak możesz, i kontynuujesz życie, pomimo niezręczności.
Ira Baxter
31

Parser Clanga to odręcznie napisany parser rekurencyjny, podobnie jak kilka innych otwartych i komercyjnych interfejsów C i C ++.

Clang używa parsera zstępującego rekurencyjnego z kilku powodów:

  • Wydajność : ręcznie napisany parser pozwala nam napisać szybki parser, optymalizując gorące ścieżki w razie potrzeby, i zawsze mamy kontrolę nad tą wydajnością. Posiadanie szybkiego parsera umożliwiło użycie Clanga w innych narzędziach programistycznych, w których zazwyczaj nie używa się "prawdziwych" parserów, np. Podświetlanie składni i uzupełnianie kodu w środowisku IDE.
  • Diagnostyka i naprawianie błędów : ponieważ masz pełną kontrolę dzięki ręcznie napisanemu parserowi zejścia rekurencyjnego, łatwo jest dodać specjalne przypadki, które wykrywają typowe problemy i zapewniają doskonałą diagnostykę i usuwanie błędów (np. Patrz http: //clang.llvm .org / features.html # expressivediags ) Dzięki automatycznie generowanym parserom jesteś ograniczony do możliwości generatora.
  • Prostota : parsery zstępujące rekurencyjnie są łatwe do napisania, zrozumienia i debugowania. Nie musisz być ekspertem od analizowania ani uczyć się nowego narzędzia do rozszerzania / ulepszania parsera (co jest szczególnie ważne w przypadku projektów typu open source), ale nadal możesz uzyskać świetne wyniki.

Ogólnie rzecz biorąc, dla kompilatora C ++ nie ma to większego znaczenia: część parsująca C ++ jest nietrywialna, ale nadal jest jedną z łatwiejszych części, więc opłaca się zachować prostotę. Analiza semantyczna - w szczególności wyszukiwanie nazw, inicjalizacja, rozwiązywanie przeciążeń i tworzenie instancji szablonów - jest o rząd wielkości bardziej skomplikowana niż analiza. Jeśli potrzebujesz dowodu, sprawdź dystrybucję kodu i zatwierdzeń w komponencie "Sema" Clanga (do analizy semantycznej) w porównaniu z komponentem "Parse" (do analizowania).

Doug
źródło
4
Tak, analiza semantyczna jest o wiele trudniejsza. Mamy około 4000 linii reguł gramatycznych, które składają się na naszą gramatykę C ++ 11 i około 180 000 linii kodu gramatyki atrybutów do „analiz semantycznych” wymienionych powyżej list Doub, a kolejne 100 000 linii kodu pomocniczego. Parsowanie naprawdę nie stanowi problemu, chociaż jest wystarczająco trudne, jeśli zaczynasz od złej stopy.
Ira Baxter,
1
Nie jestem pewien, czy ręczne parsery są koniecznie lepsze do raportowania / odzyskiwania błędów. Wydaje się, że w praktyce ludzie włożyli więcej energii w takie parsery niż na ulepszanie parserów wytwarzanych przez automatyczne generatory parserów. Wydaje się, że istnieją całkiem dobre badania na ten temat; ten konkretny artykuł naprawdę przyciągnął moją uwagę: MG Burke, 1983, Praktyczna metoda diagnostyki i odzyskiwania błędów składniowych LR i LL, praca doktorska, Wydział Informatyki Uniwersytetu Nowojorskiego, patrz archive.org/details/practicalmethodf00burk
Ira Baxter
1
... kontynuując ten ciąg myśli: jeśli chcesz zmodyfikować / rozszerzyć / dostosować swój ręcznie zbudowany parser w celu sprawdzenia specjalnych przypadków w celu lepszej diagnozy, powinieneś być skłonny w równym stopniu zainwestować w lepsze diagnozy generowanego mechanicznie parsera. Dla każdej specjalnej parsowania, którą możesz zakodować dla ręcznej, możesz również zakodować czek dla mechanicznej (a dla parserów (G) LR, możesz to zrobić w zasadzie jako semantyczne kontrole redukcji). Do tego stopnia, że ​​wydaje się to nieapetyczne, jest się po prostu leniwym, ale to nie jest oskarżenie mechanicznie generowanych parserów IMHO.
Ira Baxter
8

parser gcc jest napisany odręcznie. . Podejrzewam, że to samo dotyczy brzęku. Dzieje się tak prawdopodobnie z kilku powodów:

  • Wydajność : coś, co zostało ręcznie zoptymalizowane pod kątem konkretnego zadania, prawie zawsze będzie działać lepiej niż ogólne rozwiązanie. Abstrakcja zwykle ma hit wydajnościowy
  • Czas : przynajmniej w przypadku GCC, GCC poprzedza wiele bezpłatnych narzędzi programistycznych (pojawiło się w 1987 roku). W tamtym czasie nie było darmowej wersji yacc itp., Co, jak sobie wyobrażam, byłoby priorytetem dla ludzi z FSF.

Prawdopodobnie nie jest to przypadek syndromu „nie wynaleziono tutaj”, ale raczej „nie było nic zoptymalizowanego specjalnie do tego, czego potrzebowaliśmy, więc napisaliśmy własny”.

Rafe Kettler
źródło
15
Brak darmowej wersji yacc w 1987 roku? Myślę, że były darmowe wersje, kiedy yacc został po raz pierwszy dostarczony pod Uniksem w latach 70-tych. I IIRC (inny plakat wydaje takie same), GCC używane mieć parsera YACC oparte. Słyszałem, że pretekstem do zmiany było lepsze raportowanie błędów.
Ira Baxter
7
Chciałbym dodać, że generowanie dobrych komunikatów o błędach z odręcznego parsera jest często łatwiejsze.
Dietrich Epp
1
Twój punkt widzenia na temat synchronizacji jest niedokładny. GCC posiadało parser oparty na YACC, ale później został on zastąpiony odręcznym rekursywnym parserem zstępującym.
Tommy Andersen
7

Tam dziwne odpowiedzi!

Gramatyki C / C ++ nie są bezkontekstowe. Są wrażliwe na kontekst ze względu na pasek Foo *; Dwuznaczność. Musimy zbudować listę typedef, aby wiedzieć, czy Foo jest typem, czy nie.

Ira Baxter: Nie widzę sensu w twoim GLR. Po co budować drzewo parsowania, które zawiera niejednoznaczności. Parsowanie oznacza rozwiązywanie niejednoznaczności, budowanie drzewa składni. Rozwiązujesz te dwuznaczności w drugim przejściu, więc nie jest to mniej brzydkie. Dla mnie jest o wiele brzydszy ...

Yacc jest generatorem parsera LR (1) (lub LALR (1)), ale można go łatwo zmodyfikować, aby był zależny od kontekstu. I nie ma w tym nic brzydkiego. Yacc / Bison został stworzony, aby pomóc w parsowaniu języka C, więc prawdopodobnie nie jest to najbrzydsze narzędzie do generowania parsera C ...

Aż do GCC 3.x parser C jest generowany przez yacc / bison, z tablicą typedefs budowaną podczas parsowania. Dzięki budowaniu tabel typu "in parse", gramatyka języka C staje się lokalnie bezkontekstowa, a ponadto "lokalnie LR (1)".

Teraz, w Gcc 4.x, jest to parser rekurencyjny. Jest to dokładnie ten sam parser co w Gcc 3.x, wciąż jest to LR (1) i ma te same reguły gramatyczne. Różnica polega na tym, że parser yacc został przepisany ręcznie, przesunięcie / zmniejszenie jest teraz ukryte w stosie wywołań i nie ma "state454: if (nextsym == '(') goto state398" jak w gcc 3.x yacc's parser, więc łatwiej jest łatać, obsługiwać błędy i drukować ładniejsze komunikaty, a także wykonywać niektóre z następnych kroków kompilacji podczas parsowania Za cenę znacznie mniej „łatwego do odczytania” kodu dla nooba gcc.

Dlaczego przeszli z yacc na zejście rekurencyjne? Ponieważ konieczne jest unikanie yacc do parsowania C ++ i ponieważ GCC marzy o byciu kompilatorem wielojęzycznym, tj. Dzieleniu maksymalnego kodu między różnymi językami, które może kompilować. Dlatego parser C ++ i C są napisane w ten sam sposób.

C ++ jest trudniejszy do przeanalizowania niż C, ponieważ nie jest „lokalnie” LR (1) jak C, nie jest nawet LR (k). Spójrz, func<4 > 2>która funkcja szablonu, której wystąpienie ma wartość 4> 2, czyli func<4 > 2> musi być odczytana jako func<1>. To zdecydowanie nie jest LR (1). Teraz rozważyć func<4 > 2 > 1 > 3 > 3 > 8 > 9 > 8 > 7 > 8>. Tutaj rekursywne zejście może łatwo rozwiązać niejednoznaczność za cenę kilku dodatkowych wywołań funkcji (parametr_parse_template_parameter to niejednoznaczna funkcja parsera. to działa).

Nie wiem, dlaczego nie byłoby możliwe dodanie do rekurencyjnych pod gramatyki yacc / bison, może to będzie następny krok w rozwoju parsera gcc / GNU?

reuns
źródło
9
„dla mnie jest o wiele brzydszy”. Mogę wam powiedzieć, że inżynieria parsera jakości produkcji przy użyciu GLR i rozwiązywania niejednoznaczności opóźnienia jest praktyczna w przypadku naprawdę małego zespołu. Wszystkie inne rozwiązania, które widziałem, obejmowały lata publicznego zgrzytania zębami przez backflipy i hacki wymagane, aby działał z LR, rekursywnym zejściem. Możesz postulować wiele innych fajnych nowych technologii analizowania, ale o ile wiem, w tym momencie jest to po prostu więcej zgrzytania zębami. Pomysły są tanie; egzekucja jest droga.
Ira Baxter
@IraBaxter: Szczury! citeseerx.ist.psu.edu/viewdoc/…
Fizz
@Fizz: Interesujący artykuł na temat analizowania Fortress, złożonego naukowego języka programowania. Powiedzieli kilka ważnych rzeczy: a) klasyczne generatory parserów (LL (k), LALR (1)) nie radzą sobie z trudnymi gramatykami, b) wypróbowali GLR, mieli problemy ze skalą, ale programiści byli niedoświadczeni, więc nie mieli complete [to nie jest wina GLR] oraz c) użyli wycofującego się (transakcyjnego) parsera Packrat i włożyli w to wiele wysiłku, włączając w to pracę nad lepszymi komunikatami o błędach. Jeśli chodzi o ich przykład parsowania „{| x || x ← mySet, 3 | x}”, uważam, że GLR zrobiłby to dobrze i nie potrzebuje spacji.
Ira Baxter
0

Wygląda na to, że GCC i LLVM-Clang używają ręcznie zapisywanych rekurencyjnych parserów zejścia, a nie generowanego maszynowo, opartego na Bison-Flex, oddolnego parsowania.

Zwłaszcza żubr nie wydaje mi się, aby poradził sobie z gramatyką bez niejednoznacznego analizowania niektórych rzeczy i wykonywania drugiego przejścia później.

Wiem, że Haskell's Happy pozwala na monadyczne (tj. Zależne od stanu) parsery, które mogą rozwiązać konkretny problem ze składnią C, ale nie znam żadnych generatorów parsera C, które pozwalają na monadę stanu dostarczaną przez użytkownika.

Teoretycznie naprawianie błędów byłoby punktem na korzyść ręcznego parsera, ale moje doświadczenie z GCC / Clang jest takie, że komunikaty o błędach nie są szczególnie dobre.

Jeśli chodzi o wydajność - niektóre twierdzenia wydają się bezpodstawne. Generowanie dużej maszyny stanu za pomocą generatora parsera powinno skutkować czymś, co O(n)i wątpię, że parsowanie jest wąskim gardłem w wielu narzędziach.

Vanessa McHale
źródło
3
To pytanie ma już bardzo dobrą odpowiedź, co próbujesz dodać?
tod