Programmiersprachen und Übersetzer Sommersemester 2010 17. Mai 2010 Implementation eines Top-Down-Parsers nach der Methode des rekursiven Abstiegs (recursive descent parser) Idee: Man benutzt Syntaxgraphen zur Darstellung der Grammatik der Programmiersprache und interpretiert diese Syntaxgraphen als Ablaufpläne für rekursive Prozeduren. Konstruktion von Syntaxgraphen aus einer Grammatik Gegeben: eine kontextfreie Grammatik G = (N, T , P, S). 1. Für jedes A ∈ N wird ein Syntaxgraph mit Namen A erzeugt. Jeder Syntaxgraph hat eine Eingangskante und eine Ausgangskante. 2. Sind A → α1 | α2 | . . . | αn alle A-Produktionen, dann hat der Graph A das folgende Aussehen: ✲ α1 ✲ α2 A ✲ .. . ✲ αn wobei die Teilgraphen αi wie folgt bestimmt werden: 3. Ist αi = β1 . . . βk mit βi ∈ N ∪ T , dann hat der Teilgraph das Aussehen ✲ β1 wobei βi ✲ ... ✲ βk ✲ die Form B hat, falls βi = B ist und B ∈ N gilt oder die Form x♥hat, falls βi = x ist und x ∈ T gilt. Ist αi = ε, so hat der Teilgraph die Form ✲ Ist αi = δ1 {γ} δ2 , so hat der Teilgraph die Form ✲ δ1 wobei die Teilgraphen werden. ✲ ✻ δ1 , δ2 γ ✛ und γ δ2 ✲ wie in 3. bestimmt Ist αi = δ1 [γ] δ2 , so hat der Teilgraph die Form ✲ δ1 wobei die Teilgraphen werden. δ1 ✲ γ , δ2 ✻ und γ ✲ δ2 ✲ wie in 3. bestimmt Vereinfachung der Syntaxgraphen 1. Ein in einem Graphen auftretender Teilgraph B mit B ∈ N kann durch den Syntaxgraphen für B ersetzt werden (Substitutionsregel). 2. Zwei identische Teilgraphen mit gemeinsamem Ausgang bzw. Eingang können zusammengefaßt werden: ☛ ✲ α ✡ ☛ ✲ α ✡ ✟ ✠ ✟ ✠ ☛ ✲ α ✡ ☛ ✲ α ✡ ✲ ✟ ✠ ✟ ✠ =⇒ ✲ ✲ =⇒ ☛ ✟ ✲ α ✲ ✡ ✠ ☛ ✟ ✲ α ✡ ✠ ✲ ✲ 3. Tritt am Ausgang eines Syntaxgraphen für B der Knoten B auf, so kann man diesen durch eine Kante auf den Eingang des Syntaxgraphens ersetzen. B ✲ ✲ ··· ✲ ✲B ··· ✲ =⇒ ✲ B ❄✲ · · · ✲ ✲ ··· (Ersetzen einer Endrekursion durch eine Iteration) ✲ Beispiel Wir benutzen wieder die Grammatik G2 aus Beispiel 2.10. Die Syntaxgraphen für diese Grammatik sind: S ✲ a❥ (❥ R S )❥ ✲ R R ✲ +❥ ∗❥ S S ✲ Die Vereinfachung ergibt: S ✲ a❥ (❥ S )❥ ✲ +❥ ∗❥ ❄ S ✻ ✲ Definition Syntaxgraphen heißen deterministisch, wenn für jede Verzweigung gilt, daß die Mengen der ersten terminalen Zeichen, die auf jedem Ast der Verzweigung zu erreichen sind, paarweise disjunkt sind. Bemerkung Aus LL(1)-Grammatiken konstruierte Syntaxgraphen sind deterministisch. Konstruktion eines Parsers aus deterministischen Syntaxgraphen � Gegeben sei ein Unterprogramm getToken(), das bei jedem Aufruf das nächste Token aus der Eingabe bestimmt und die Tokenklasse in der globalen Variablen token abspeichert. � Der Tokenwert wird, sofern vorhanden, in der globalen Variable tokenValue gespeichert. � Außerdem wird die Existenz einer Fehlerprozedur error() vorausgesetzt, die nach Ausgabe einer Fehlermeldung den Parsingprozeß abbricht. Für jeden erstellten und vereinfachten Syntaxgraphen erzeuge man ein Unterprogramm ohne Parameter entsprechend den folgenden Regeln: 1. Einem Graphen ✲ A ✲ wird ein Aufruf des Unterprogramms A, A( ); zugeordnet. 2. Einem Graphen ✲ x♥ ✲ wird ein Programmstück if (token == x) getToken(); else error(); zugeordnet. 3. Einer Sequenz ✲ S1 ✲ ... ✲ Sk ✲ wird eine Folge von Programmstücken { P(S1 ); . . . ; P(Sk ); } zugeordnet, wobei P(Si ) das Si zugeordnete Programmstück ist. 4. Einer Verzweigung L1 ✲ S1 L2 ✲ S2 .. . Ln ✲ ✲ Sn wird das Programmstück switch (token) { case L1 : P(S1 ); break; . . . case Ln : P(Sn ); break; default: error(); } zugeordnet. Die Li sind dabei die Mengen der ersten terminalen Zeichen, die man über den entsprechenden Ast erreichen kann. 5. Einem Graphen der Form ✻ S2 ✛ wird das Programmstück while (true) { P(S1 ); if (token �= L) break; P(S2 ); } zugeordnet. ✲ S1 L Die so erhaltenen Unterprogramme werden vereinfacht (Entfernen überflüssiger Abfragen usw.). Der eigentliche Parser hat dann die Form: getToken(); S(); /* S ist das Startsymbol der Grammatik */ if (token == ’$’) printf ("Wort gehoert zur Sprache L(G).\n"); else error(); Konstruktion eines Parsers für eine einfache Programmiersprache“ ” Als Variablennamen sind nur a, b, . . . , z erlaubt; als Zahlen treten nur Integerzahlen auf. Der Lexical Scanner arbeitet folgendermaßen: Er erkennt die Schlüsselwörter begin end print und liefert dann das Token begin, und liefert dann das Token end und und liefert dann das Token print. Bei Variablen liefert er das Paar (ident, index), wobei der Index des i-ten Buchstabens im Alphabet i ist, und bei Zahlen liefert er das Paar (number, Zahlenwert). Am Ende der Eingabe, die hier wieder durch das Zeichen “$” markiert sei, gibt der Lexical Scanner das spezielle Token eof zurück. Alle anderen Zeichen werden direkt übergeben. Die Rückgabewerte des Lexical Scanners stehen in den Variablen token bzw. tokenValue. Bei Erkennen eines syntaktischen Fehlers wird wieder die Prozedur error("...") aufgerufen. Beispielgrammatik (siehe Anhang im Skript) <program> <statementlist> <statement> <expression> <sign> <termlist> → → → → → → <term> <factorlist> → → <factor> → begin <statementlist> end <statement> | <statement>; <statementlist> ident = <expression> | print <expression> <sign><termlist> +|-|ε <term> | <term> + <termlist> | <term> - <termlist> <factorlist> <factor> | <factor> * <factorlist> | <factor> / <factorlist> ident | number | ( <expression> ) program ✗✔ ✻ ✖✕ begin statement ;✐ ✗✔ ✖✕ end ✲ void program() { if (token != begin) error("program: begin expected"); do { getToken(); statement();} while (token == ’;’); if (token != end) error("program: end expected"); getToken(); } statement ✗✔ expression =✐ ident ✖✕ ✻ ✗✔ ✲ ✖✕ print void statement() { if (token == ident) { getToken(); if (token != ’=’) error(statement: = expected"); } else if (token != print) error(statement: print expected"); getToken(); expression(); } expression +✐ -✐ ❄ ✻ ✻ ✛ ✛ term +✐ ✲ -✐ void expression() { if ((token == ’-’) || (token == ’+’)) getToken(); do { term(); if ((token != ’-’) && (token != ’+’)) break; getToken();} while (true); } term ✻ ✛ ✛ factor *✐ ✲ /✐ void term() { do { factor(); if ((token != ’*’) && (token != ’/’)) break; getToken();} while (true); } ✓✏ factor ✒✑ ✗✔ ident ✖✕ number ❤ ( expression ✻ ✲ ✲ ❤✲ ) void factor() { switch (token) { case ident: case number: getToken(); break; case ’(’: getToken(); expression(); if (token != ’)’) error("factor: ) expected"); getToken(); break; default: error ("factor: identifier, number or ( expected"); break; } } Das Hauptprogramm könnte dann etwa folgendermaßen aussehen: void main() { getToken(); program(); if (token == eof) printf ("Wort gehoert zur Sprache\n"); else error("main: eof expected"); }