it-swarm.com.de

Warum kann C++ nicht mit einem LR (1) -Parser analysiert werden?

Ich las über Parser und Parsergeneratoren und fand diese Aussage in der LR-Parsing-Seite von Wikipedia:

Viele Programmiersprachen können mit einigen Variationen eines LR-Parsers analysiert werden. Eine bemerkenswerte Ausnahme ist C++.

Wieso ist es so? Welche besondere Eigenschaft von C++ macht es unmöglich, mit LR-Parsern zu analysieren?

Mit google habe ich nur festgestellt, dass C mit LR (1) perfekt analysiert werden kann, aber C++ erfordert LR (∞). 

141
Cheery

Auf Lambda the Ultimate gibt es einen interessanten Thread, der die LALR-Grammatik für C++ behandelt. 

Es enthält einen Link zu einer Dissertation , die eine Diskussion des C++ - Parsing beinhaltet, die Folgendes besagt:

Msgstr "C++ - Grammatik ist mehrdeutig, Kontextabhängig und möglicherweise Erfordert unendlichen Lookahead, um Einige Mehrdeutigkeiten aufzulösen.".

Im Folgenden werden einige Beispiele angeführt (siehe Seite 147 des PDF).

Das Beispiel ist:

int(x), y, *const z;

bedeutung

int x;
int y;
int *const z;

Vergleichen mit:

int(x), y, new int;

bedeutung

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

(ein durch Kommas getrennter Ausdruck).

Die beiden Tokensequenzen haben die gleiche anfängliche Untersequenz, jedoch unterschiedliche Parsing-Bäume, die vom letzten Element abhängen. Es können beliebig viele Marken vor dem eindeutigen sein.

87
Rob Walker

LR-Parser können nicht mit mehrdeutigen Grammatikregeln umgehen. (Die Theorie wurde in den 70er Jahren leichter gemacht, als die Ideen ausgearbeitet wurden).

C und C++ erlauben beide die folgende Anweisung:

x * y ;

Es hat zwei verschiedene Parses:

  1. Es kann die Deklaration von y sein, als Zeiger auf den Typ x
  2. Es kann eine Multiplikation von x und y sein, die Antwort wegwerfen.

Nun könnten Sie denken, dass letzteres dumm ist und ignoriert werden sollte. Die meisten würden Ihnen zustimmen. Es gibt jedoch Fälle, in denen es eine Nebenwirkung haben kann (z. B. wenn Multiplikation überladen ist). Aber das ist nicht der Punkt. Der Punkt ist, dass are zwei unterschiedliche Parses sind, und daher kann ein Programm verschiedene Dinge bedeuten, abhängig davon, wie dieses sollte gewesen ist analysiert.

Der Compiler muss unter den geeigneten Umständen die entsprechende akzeptieren und, wenn keine anderen Informationen vorliegen (z. B. Kenntnis des Typs von x), beide sammeln, um später entscheiden zu können, was zu tun ist. Also muss eine Grammatik das zulassen. Und das macht die Grammatik mehrdeutig.

Daher kann reines LR-Parsen dies nicht verarbeiten. Viele andere weit verbreitete Parser-Generatoren wie Antlr, JavaCC, YACC oder traditionelle Bison-Parser oder gar PEG-Parser, die auf "reine" Weise verwendet werden, können auch nicht.

Es gibt viele kompliziertere Fälle (die Syntaxanalyse von Schablonen erfordert einen beliebigen Lookahead, wohingegen LALR (k) die meisten k Token vorausschauen kann), aber nur ein Gegenbeispiel benötigt, um pure LR (oder die andere) Parsing.

Die meisten echten C/C++ - Parser behandeln dieses Beispiel, indem sie einen bestimmten deterministischen Parser vom Typ Mit einem zusätzlichen Hack verwenden: Sie verflechten das Parsing mit der Symboltabelle Collection ..., sodass zum Zeitpunkt "x" .__ Der Parser weiß, ob x ein Typ ist oder nicht, und kann somit zwischen den beiden möglichen Parses wählen. Aber ein Parser, der dies tut, ist nicht kontextfrei, und LR-Parser. (Die reinen usw.) sind (im besten Fall) kontextfrei.

Man kann betrügen und semantische Überprüfungen pro Regel für die Reduzierungszeit in LR-Parsern hinzufügen, um diese Disambiguierung durchzuführen. (Dieser Code ist oft nicht einfach). Die meisten anderen Parser-Typen Verfügen über einige Methoden, um semantische Prüfungen an verschiedenen Punkten hinzuzufügen in der Parsing-Funktion, die dazu verwendet werden können.

Und wenn Sie genug betrügen, können Sie LR-Parser für C und C++ einsetzen. Die GCC-Leute machten es eine Weile, gaben es aber für handcodiertes Parsing auf, denke ich, weil sie eine bessere Fehlerdiagnose wollten.

Es gibt jedoch einen anderen Ansatz: Nice and clean Und analysiert C und C++ ohne Symboltabelle Hackery: GLR-Parser . Dies sind vollständige kontextfreie Parser (die praktisch unendlich sind.) Schau voraus). GLR-Parser akzeptieren einfach beide Parses, Erzeugen einen "Baum" (eigentlich ein gerichteter azyklischer Graph, der meist baumähnlich ist) , Der die mehrdeutige Analyse darstellt. Ein Durchgang nach der Analyse kann die Mehrdeutigkeiten auflösen.

Wir verwenden diese Technik in den C- und C++ - Frontends für unsere DMS Software Reengineering Tookit (Stand Juni 2017 Diese behandeln volle C++ 17 in MS und GNU - Dialekte) ..__ wurden verwendet, um Millionen von Zeilen großer C- und C++ - Systeme zu verarbeiten, mit vollständigen und genauen Parses, die ASTs mit vollständigen Details des Quellcodes erstellen. (Siehe die AST für C++ ärgerlichsten Parser. )

223
Ira Baxter

Das Problem wird nie so definiert, obwohl es interessant sein sollte:

was sind die kleinsten Änderungen an der C++ - Grammatik, die erforderlich wären, damit diese neue Grammatik von einem "nicht kontextfreien" Yacc-Parser perfekt analysiert werden kann? (Verwenden Sie nur einen 'Hack': die Disambiguierung von Typname/Identifier, den Parser, der den Lexer über alle typedef/class/struct informiert)

Ich sehe ein paar:

  1. Type Type; ist verboten. Ein als Typenname deklarierter Bezeichner kann nicht zu einem Nicht-Typennamen-Bezeichner werden (beachten Sie, dass struct Type Type nicht mehrdeutig ist und weiterhin zulässig sein kann).

    Es gibt 3 Arten von names tokens:

    • types: Eingebauter Typ oder aufgrund eines typedef/class/struct
    • template-Funktionen
    • bezeichner: Funktionen/Methoden und Variablen/Objekte

    Die Berücksichtigung von Template-Funktionen als unterschiedliche Token löst die Mehrdeutigkeit von func<. Wenn func ein Template-Funktionsname ist, muss < der Anfang einer Template-Parameterliste sein, andernfalls ist func ein Funktionszeiger und < der Vergleichsoperator.

  2. Type a(2); ist eine Objektinstanziierung. Type a(); und Type a(int) sind Funktionsprototypen.

  3. int (k); ist völlig verboten, sollte geschrieben werden int k;

  4. typedef int func_type(); und typedef int (func_type)(); sind verboten.

    Eine Funktion typedef muss ein Funktionszeiger typedef sein: typedef int (*func_ptr_type)();

  5. die Vorlagenrekursion ist auf 1024 begrenzt, andernfalls kann ein erhöhtes Maximum optional an den Compiler übergeben werden.

  6. int a,b,c[9],*d,(*f)(), (*g)()[9], h(char); könnte auch verboten sein, ersetzt durch int a,b,c[9],*d;int (*f)();

    int (*g)()[9];

    int h(char);

    eine Zeile pro Funktionsprototyp oder Funktionszeigerdeklaration.

    Eine sehr bevorzugte Alternative wäre die Änderung der Zeigersyntax für schreckliche Funktionen.

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

    resyntaxiert werden als:

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

    dies steht im Einklang mit dem Cast-Operator (int (MyClass::*)(char*))

  7. typedef int type, *type_ptr; könnte auch verboten sein: eine Zeile pro typedef. So würde es werden

    typedef int type;

    typedef int *type_ptr;

  8. sizeof int, sizeof char, sizeof long long und Co. könnte in jeder Quelldatei deklariert werden. Daher sollte jede Quelldatei, die den Typ int verwendet, mit beginnen

    #type int : signed_integer(4)

    und unsigned_integer(4) wäre außerhalb dieser #type Direktive verboten, dies wäre ein großer Schritt in die dumme sizeof int Mehrdeutigkeit, die in so vielen C++ Headern vorhanden ist

Der Compiler, der das resyntaxierte C++ implementiert, verschiebt source.cpp zu einem ambiguous_syntax -Ordner und erstellt vor dem Kompilieren automatisch einen eindeutigen übersetzten source.cpp.

Bitte fügen Sie Ihre mehrdeutigen C++ - Syntax hinzu, wenn Sie einige kennen!

14
reuns

Wie Sie in meiner answer hier sehen können, enthält C++ eine Syntax, die von einem LL- oder LR-Parser nicht deterministisch analysiert werden kann, da die Phase der Typauflösung (normalerweise nach dem Parsing) die Reihenfolge der Operationenändert. und daher die fundamentale Form von AST (normalerweise erwartet, dass sie durch eine erste Analyse bereitgestellt wird).

8
Sam Harwell

Ich denke, Sie sind der Antwort ziemlich nahe. 

LR (1) bedeutet, dass für das Parsing von links nach rechts nur ein Token erforderlich ist, um den Kontext vorauszusehen, während LR (∞) ein unendliches Voraussehen bedeutet. Das heißt, der Parser müsste alles wissen, was kommen würde, um herauszufinden, wo es jetzt ist.

5
casademora

Das "typedef" -Problem in C++ kann mit einem LALR (1) -Parser analysiert werden, der beim Parsen eine Symboltabelle erstellt (kein reiner LALR-Parser). Das "Template" -Problem kann mit dieser Methode wahrscheinlich nicht gelöst werden. Der Vorteil dieser Art von LALR (1) -Parser ist, dass die Grammatik (siehe unten) eine LALR (1) -Grammatik ist (keine Mehrdeutigkeit). 

/* 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. */

Die folgende Eingabe kann problemlos analysiert werden:

 typedef int x;
 x * y;

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

Der LRSTAR-Parser-Generator liest die obige Grammatiknotation und generiert einen Parser, der das "typedef" -Problem ohne Mehrdeutigkeit im Parser-Tree oder AST behandelt. (Offenlegung: Ich bin der Typ, der LRSTAR erstellt hat.)

0
Paul B Mann