Der Scanner

Der Scanner erkennt einzelne Wörter und Symbole und gibt diese in Form von Rückgabewerten an den Parser. Daneben liefert er jedoch auch Werte für Bezeichner und Konstanten, die später weiterverarbeitet werden. In dem Beispiel sind diese Werte bereits als C++-Typen deklariert, weswegen solche C++-Definitionen in den Scannercode eingebunden werden müssen. Da der Scannergenerator den eingebetteten C-Code nicht weiter überprüft, sondern lediglich kopiert, sollten hier keinerlei Probleme auftreten. Flex und einige andere Lex-Derivate verfügen über eine Kommandozeilenoption, mit der sich der Scanner in Form von C++-Klassen generieren läßt. Es mag verwundern, aber diese Option wird hier nicht benötigt. Statt dessen wird mit der üblichen (yylex)-Funktion gearbeitet, wie sie auch für C generiert wird. Vorteil ist, dass diese Funktion direkt vom Parser genutzt werden kann, und es steht einer Verwendung von C++-Befehlen innerhalb dieser Funktion nichts entgegen. (yylex) gibt im Falle eines erkannten Symbols ein sog. Token an den Parser zurück. Hierbei stehen all jene Werte zur Auswahl, die man in der Grammatik explizit deklariert hat. Diese Deklarationsdatei (in diesem Fall easypars.hpp) wird vom Parsergenerator bei entsprechend gesetzter Kommandozeilenoption erzeugt und muß in den Scanner eingebunden werden. Bei der Rückgabe einfacher Token gibt es im Zusammenhang mit C++ wenig zu beachten. Eine Aktion wie

while {return WHILE;}

die beim Erkennen des Schlüsselwortes „while“ die Konstante „WHILE“ zurückgibt, könnte genau so auch in einem C-Programm stehen. Etwas anders gestaltet sich die Rückgabe von Attributen wie Bezeichnernamen und Zahlenwerten. Solche Attribute werden über die Variable (yylval.xxx) zurückgegeben, wobei (xxx) für den Typen steht, der dem Token zugeordnet ist. Implementiert wird dies über eine (union), und somit lassen sich Objekte nicht direkt als Attribute verwenden, wohl aber als Zeiger auf solche. Es ist möglich und äußerst sinnvoll, den Syntaxbaum in Form von Objektstrukturen aufzubauen. Als Rückgabe- und Attributwerte im Scanner und im Parser sollte man jedoch konsequent Zeiger auf diese Objekte verwenden. Dieses Vorgehen ermöglicht es, den Speicherplatz für neue Objekte auf dem Heap anzulegen. Ein Beispiel ist die Rückgabe eines Bezeichnernamens:

// Wenn ein Identifier erkannt wurde

[A-Za-z]+ {

// Erzeuge ein neues string-Objekt.

string* s = new string (yytext);

// Wandle alle Zeichen in Kleinbuchstaben um.

transform (s->begin(),s->end(), s->begin(), tolower);

// Liefere den Zeiger auf den neuen string als Attributwert zurück

yylval.pstr = s;

// und gib das Token NAME an den Parser.

return NAME;}

Das (return) beendet die Funktion (yylex), und die Werte aller auf dem lokalen Stack erzeugten Objekte werden ungültig, weshalb globaler Speicher reserviert werden muß. Das obige Beispiel zeigt auch, dass sich typische STL-Aufrufe problemlos in Scanneraktionen einbinden lassen.