Programmiersprachen und Übersetzer

Werbung
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");
}
Herunterladen