Entzifferer (Linux-Magazin, April 2006)

Lexer und Parser in Perl zu schreiben, ist keineswegs langbärtigen Gurus vorbehalten. Anhand eines mathematischen Formelparsers erläutert der Snapshot Konzepte und Algorithmen zur Syntaxanalyse und Übersetzung.

Für Compilerbauer und Erfinder neuer Programmiersprachen sind sie das tägliche Brot: Lexer und Parser prüfen beliebig komplexe Ausdrücke auf syntaktische Korrektheit und helfen, sie in Zielsprachen zu übersetzen.

Da Daten heute meist mit XML formatiert sind und einfach zu bedienende XML-Parser zur Verfügung stehen, ist die Kunst des echten Parserschreibens fast in Vergessenheit geraten. Wer allerdings beispielsweise vom Benutzer eingegebene Formeln analysieren und auswerten muss, kommt um einen Parser nicht herum.

Abbildung 1: Der Lexer zerlegt den Sting in Tokens, der Parser erzeugt den Parse-Tree. Der Übersetzer überführt ihn in die Reverse Polish Notation (RPN) und rechnet mit einem einfachen Algorithmus das Ergebnis aus.

Lex mich!

Um einen Ausdruck wie ``5+4*3'' auszuwerten, muss man ihn zunächst in Operatoren und Operanden zu zerlegen. Wie Abbildung 1 zeigt, extrahiert ein sogenannter Lexer zunächst die Symbole ``5'', ``+'', ``4'', ``*'' und ``3'' aus dem String. Diese auch Tokens genannten Textstücke werden später an den Parser gefüttert, der prüft, ob die eingegebene Formel auch mathematischen Sinn ergibt. Hierzu erzeugt der Parser typischerweise einen Parse-Baum, der sicherstellt, dass der Ausdruck einer vorher aufgestellten Grammatik gehorcht. Diese legt Dinge wie Präzedenz der Operatoren (z.B. Punkt vor Strich) und die Reihenfolge der Auswertung (Assoziativität, links nach rechts oder umgekehrt) fest. Steht so die exakte Bedeutung des Ausdrucks fest, kann er in Programmierschritte übersetzt werden, um ihn mit einem simplen Verfahren auf dem Computer auszuwerten. Abbildung 1 zeigt als Beispiel einen RPN-(Reverse Polish Notation)-Rechner, eine virtuelle Maschine, die entweder Zahlen oder Operatoren oben auf den Stack wirft und anschließend versucht, Operand-Operand-Operator-Kombinationen zu einem Einzelwert reduzieren. So wird aus 4 3 * der Wert 12, und aus der dann auf dem Stack liegenden Kombination 5 12 + das Ergebnis der Berechnung: 17. Zwar könnte man eine String wie ``5+4*3'' einfach an Perls Funktion eval weitergeben, die ihn dann nach Perls Mathematikregeln auswerten würde. Doch falls Variablen auftauchen, Operatoren, von denen Perl nichts weiß, oder gar if-else-Konstrukte erlaubt sind, also letztendlich eine Mini-Programmiersprache entsteht, dann muss ein ausgewachsener Parser ran.

Zurück zum Lexer: Auch Leerzeichen sollen im auzuwertenden String keine Rolle spielen, der Ausdruck ``5 +4 *3'' soll dieselben Symbole liefern wie ``5+ 4*3''. Das Lexen ist allerdings nicht immer ganz so trivial wie im vorgestellten Beispiel. Als Operand könnte beispielsweise auch eine reelle Zahl wie 1.23E40 auftauchen oder eine Funktion wie ``sin(x)'', die in die Tokens ``sin('', ``x'' und ``)'' zerlegt werden müsste. Für solche komplexen Lexer gibt es auf dem CPAN das Modul Parse::Lex. Bei der Installation ist zu beachten, dass es mindestens Version 0.37 des Moduls Parse::Template benötigt.

Das kurze Skript mathlexer nimmt einen verschachtelten mathematischen Ausdruck, gibt ihn an den Lexer weiter und lässt diesen die Kombinationen aus Tokentyp und Tokeninhalt zurückliefern, die anschließend zu Testzwecken ausgegeben werden.

Das von mathlexer verwendete Listing MathLexer.pm definiert die Klasse MathLexer, die über den Konstruktor new einen String zu Analysezwecken entgegennimmt und gemäß der in der Variablen @tokens abgelegten regulären Ausdrücke durchforstet. Für jedes gefundene Lexem (die Sequenz der gefundenen Zeichen, aus denen der Lexer einen Token zusammenstellt), reicht der mit der Methode next() weitergeschubste Lexer zwei Werte zurück. Das erste Element des als Referenz zurückgereichten Arrays ist der Name des gefundenen Tokens (z.B. ``NUM'', ``OPADD'', ``RIGHTP''). Im zweiten Element folgt dann die tatsächlich gefundene Zeichenkette (z.B. ``4.27e-14'', ``+'', ``)''). Abbildung 2 zeigt die Ausgabe zu Testzwecken, die im wirklichen Leben als Parserfutter dient.

Listing 1: mathlexer

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use MathLexer;
    04 
    05 my $str = "5*sin(x*-4.27e-14)**4*(e-pi)";
    06 print "  $str\n\n";
    07 
    08 my $lex = MathLexer->new($str);
    09 
    10 while(1) {
    11     my($tok, $val) = $lex->next();
    12     last unless defined $tok;
    13     printf "%8s %s\n", $tok, $val;
    14 }

Abbildung 2: Ein von MathParser.pm gelexter mathematischer Ausdruck

Listing 2: MathLexer.pm

    01 ###########################################
    02 package MathLexer;
    03 ###########################################
    04 use strict;
    05 use Regexp::Common;
    06 use Parse::Lex;
    07 
    08 my @token = (
    09   OPPOW  => "[*][*]",
    10   OPSUB  => "[-]",
    11   OPADD  => "[+]",
    12   OPMULT => "[*]",
    13   OPDIV  => "[/]",
    14   FUNC   => "[a-zA-Z]\\w*\\(",
    15   ID     => "[a-zA-Z]\\w*",
    16   LEFTP  => "\\(",
    17   RIGHTP => "\\)",
    18   NUM    => "$RE{num}{real}",
    19   ERROR  => ".*", sub { 
    20                die qq(Can't lex "$_[1]") },
    21 );
    22 
    23 ###########################################
    24 sub new {
    25 ###########################################
    26     my($class, $string) = @_;
    27 
    28     my $lexer = Parse::Lex->new(@token);
    29     $lexer->skip("[\\s]");
    30     $lexer->from($string);
    31 
    32     my $self = { 
    33         lexer  => $lexer,
    34     };
    35 
    36     bless $self, $class;
    37 }
    38 
    39 ###########################################
    40 sub next {
    41 ###########################################
    42     my($self) = @_;
    43 
    44     my $tok = $self->{lexer}->next();
    45     return undef if $self->{lexer}->eoi();
    46 
    47     return $tok->name(), $tok->text();
    48 }
    49 
    50 1;

Zu beachten ist, dass Parse::Lex die regulären Ausdrücke als Strings entgegennimmt. Darum müssen Backslashes als \\ maskiert werden, wenn ein Symbol wie * nicht als Regex-Metazeichen genutzt wird. Da Ausdrücke wie \\*\\* schwer zu entziffern sind, verwendet MathLexer in der ersten Tokendefinition den etwas merkwürdig formulierten aber identischen Regex "[*][*]".

Ein regulärer Ausdruck, der die vielfältigen Darstellungen von reellen Zahlen abdeckt (zum Beispiel 1.23E40, .37, 7, 1e10), ist gar nicht so einfach von Hand zu erstellen. Glücklicherweise bietet das Modul Regexp::Common vom CPAN bereits vorgekochte Regexen für allerlei Zwecke, darunter auch einen für reelle Zahlen mit allerlei Schnickschnack. Nach einem use Regexp::Common im Programm greift man über einen globalen Hash auf diese Perlen der Regexkunst zu. Der Ausdruck für reelle Zahlen liegt in $RE{num}{real}.

Übrigens lässt der Regex $RE{num}{real} auch ein optionales Minuszeichen vor der reellen Zahl zu. Wegen der gewählten Reihenfolge der erkannten Lexeme in @tokens wird der Lexer ein vorangestelltes Minuszeichen aber immer als OP erkennen. Taucht allerdings ein Minuszeichen im Exponent der reellen Zahl auf, schnappt der Lexer es als Teil des Lexems NUM auf.

Weiter sorgt Methode skip in Zeile 28 von MathLexer.pm dafür, dass der Lexer Leerzeichen und Zeilenumbrüche ignoriert. Trifft er allerdings auf ein Zeichenkonstrukt wie zum Beispiel "}", kommt die in Zeile 18 definierte ERROR-Pseudo-Token zum Einsatz, das zur Fehlerbehandlung eine Routine definiert, die einfach mit einem die-Kommando den Lexer abbricht.

Syntaxkontrolle, die Tokens bitte!

Die syntaktische Gültigkeit eines Ausdrucks untersucht anschließend ein Parser. ``4+*3'' wäre ungültig, der Parser sollte in solchen Fällen einen Fehler melden und das Programm abbrechen. Parser prüfen aber nicht nur die Syntax eines Ausdrucks sondern halten oft auch als Übersetzer her. Während der Parser einen arithmetischen Ausdruck durchkämmt, kann er auch gleich ausrechnen, was dabei herauskommt.

Listing AddMult.yp definiert eine Grammatik für den Parser. Sie bestimmt, wie der Parser die aus dem Lexer herausströmenden Tokens zu vorgegebenen Strukturen zusammenfasst. Die erste sogenannte Produktion

    expr: add | mult | NUM;

bestimmt, dass der Parser die Sequenz aller Tokens am Ende zu einem Konstrukt vom Typ expr zusammenfassen muss. Gelingt dies, war der Parser erfolgreich. Ist dies unmöglich, genügen die Tokens des untersuchten Ausdrucks nicht der Grammatik. Es liegt ein Syntaxfehler vor und der Parse-Vorgang wird abgebrochen.

Listing 3: AddMult.yp

    01 ###########################################
    02 %left OPADD
    03 %left OPMULT
    04 
    05 %%
    06 expr: add | mult | NUM;
    07 
    08 add:  expr OPADD expr { 
    09         return $_[1] + $_[3] 
    10       };
    11 mult: expr OPMULT expr { 
    12         return $_[1] * $_[3] 
    13       };
    14 %%

Produktionen wie die oben gezeigte zeigen auf der linken Seite ein sogenanntes Non-Terminal. Dies ist ein Ausdruck, der sich aus gelexten Tokens (auch Terminals genannt) zusammensetzt, aber auch weitere Non- Terminals enthalten kann. expr oben kann zum Beispiel dreierlei sein, wie die mit ``|'' getrennten Alternativen auf der rechten Seite des Doppelpunkts zeigen: add (eine Addition), mult (eine Multiplikation) oder ein Terminal NUM, eine reelle Zahl, die vom Lexer kommt.

Die Non-Terminals add und mult werden in den nächsten Produktionen in AddMult.yp definiert. add: expr OPADD expr bestimmt, dass ein Non-Terminal add sich aus zwei mit dem '+'-Operator verknüpften expr-Non-Terminals zusammensetzt, die, wie vorher definiert, sich wiederum aus Additionen, Multiplikationen oder einfachen Zahlen zusammensetzen lassen.

Die Grammatikdatei AddMult.yp dient dem Modul Parse::Yapp als abstrakte Beschreibung eines Parsers. Sie teilt sich in drei Teile, die jeweils durch die Zeichensequenz %% voneinander getrennt sind. Oben steht der Header, der Parserinstruktionen oder Perl-Code beinhalten kann. In der Mitte stehen die Produktionen der Grammatik und unten schließlich folgt der Footer, der weiteren Perl-Code definieren kann, in Listing AddMult.yp jedoch leergelassen wurde. AddMult.yp wird mit der dem Parse::Yapp beiliegenden Utilty yapp in ein Perl-Modul umgewandelt, das den Parser implementiert.

Das so entstehende Modul AddMult.pm implementiert einen sogenannten Bottom-Up-Parser. Dieser liest den Token-Strom aus dem Lexer und versucht, den in Abbildung 1 gezeigten Parse-Baum von unten nach oben aufzubauen. Hierzu fasst er gelesene Einheiten gemäß der Grammatik zu übergeordneten Konstrukten zusammen, solange, bis am Ende die linke Seite der Startproduktion herauskommt.

Mit jedem Schritt führt der Parser eine von zwei Aktionen aus: Shift oder Reduce. Mit Shift holt der Parser den nächsten Token aus dem Eingabestrom. Mit Reduce fasst der Parser die auf dem Stack liegenden Terminals und Non-Terminals entsprechend der Grammatik zu weiteren Non-Terminals zusammen und 'reduziert' damit die Höhe des Stacks. Ist die Eingabeschlange leer und die letzte Reduktion lässt nur die linke Seite der Start-Produktion auf dem Stack liegen, war der Parse-Vorgang erfolgreich.

Tabelle 1 zeigt, wie ein entsprechend der Grammatik in AddMult.yp implementierter Bottom-Up-Parser den in Tokens zerlegten Eingabestring 5+4*3 Schritt für Schritt abarbeitet.

    Tabelle 1:
    +-----+---------------------------+------+---------------+------+
    | Step| Rule                      |Return|Stack          |Input |
    +-----+---------------------------+------+---------------+------+
    | 0   |                           |      |               |5+4*3 |
    | 1   | SHIFT                     |      |NUM            | +4*3 |
    | 2   | REDUCE expr: NUM          |5     |expr           | +4*3 |
    | 3   | SHIFT                     |      |expr OPADD     |  4*3 |
    | 4   | SHIFT                     |      |expr OPADD NUM |   *3 |
    | 5   | REDUCE expr: NUM          |4     |expr OPADD expr|   *3 |
    |     | ***Conflict: Shift/Reduce?|      |               |      |
    +-----+---------------------------+------+---------------+------+

In Schritt 0 liegen in der Eingabe die Tokens [NUM, ``5''], [OPADD, ``+''], [NUM, ``4''], [OPMULT, ``*''] und [NUM, ``3''] vor. In Schritt 1 holt der Parser die 5 auf den Stack (Shift) und reduziert das NUM-Terminal in Schritt 2 gemäß der Grammatik (dritte Alternative der ersten Produktion) zu expr. Anschließend holt der Parser die Tokens [OPADD, ``+''] und [NUM, ``4''] aus der Eingabe und reduziert letzteren ebenfalls wieder zu expr. Dann stellt sich die Frage: Was tun? Der Parser könnte expr OPADD expr gemäß der Grammatik zu expr reduzieren (zweite Produktion). Andererseits könnte er auch [OPMULT, ``*''] aus der Eingabe holen und hoffen, später einmal expr OPMULT expr zu reduzieren (dritte Produktion).

Im Konflikt

Grammatiken sind oft nicht eindeutig. Gäbe es beispielsweise nicht die uralte Mathematikregel ``Punkt vor Strich'', stünde der Parser bei einem Ausdruck wie ``5+4*3'' vor dem eben erläuterten Shift-Reduce- Konflikt. Was tun? Im Fall von ``5+4*3'' umschifft die Präzedenz der Algebra-Operatoren den Konflikt. Der Parser muss mit dem Reduzieren warten und den Operator * mit shift auf den Stack hieven, denn * bindet seine Operanden stärker als das schwache +.

Kommen gleiche Operatoren mehrmals hintereinander zum Einsatz, wie bei 5-3-2, sind alle Operationen gleichberechtigt und der Shift-Reduce-Konflikt stellt sich ebenfalls ein. Entscheidet sich der Parser nach dem Lesen von 5-3 für ein Reduce, wertet er die Operatoren von links nach rechts aus und verhält sich damit gemäß den Regeln der Algebra. Ein Shift hätte den Ausdruck stattdessen wie c<5-(3-2)> ausgewertet, was statt dem mathematisch erwarteten Wert 0 das überraschende Ergebnis 6 gebracht hätte. Der Minus-Operator nennt man deswegen links-assoziativ.

Im Fall des Potenzoperators (** in Perl) erwartet die Algebra übrigens genau das umgekehrte Verhalten: ``4**3**2'' (``4 hoch 3 hoch 2'') wird tatsächlich wie 4**(3**2) ausgerechnet. Das lässt sich leicht in Perl nachprüfen: perl -le 'print 4**3**2' ergibt 262144 (4**9) und nicht 4096 (64**2).

Assoziativität und Präzedenz

Diese Mehrdeutigkeit der Grammatik fällt auch dem Parser-Generator yapp auf, der aus der Datei AddMult.yp das Parser-Modul AddMult.pm erzeugt:

    $ yapp -m AddMult AddMult.yp
    4 shift/reduce conflicts

Die ersten zwei Zeilen in Listing AddMult.yp lösen das Problem:

    %left OPADD
    %left OPMULT

bestimmen, dass sowohl der '+' als auch der '*'-Operator links-assoziativ arbeiten (also Operationen von links nach rechts vorgenommen werden) und, wichtiger, dass OPMULT eine höhere Priorität als OPADD besitzt, da %left OPMULT in einer Zeile nach %left OPADD steht.

Definierte der Parser auch eine OPMINUS Operation mit dem '-'-Operator, wäre es dringend notwendig, auch %left '-' (und zwar vor der Definition von OPMULT) einzufügen. Stünde statt %left '-' im Header der yp-Datei hingegen %right '-', würde der Parser Ausdrücke wie 5-3-2 von rechts nach links auswerten. Das hätte fatale Folgen, denn 5-(3-2) ergibt 6, nicht wie 5-3-2 den Wert 0. Um dem Parser beizubringen, Zahlen zur Potenz zu erheben, wäre allerdings ein rechts-assoziativer Potenzoperator %right OPPOW notwendig, und zwar wegen der hohen Priorität der Potenzoperation nach der Definition on OPMULT.

Mit diesen Tricks läuft der Parser gemäß Tabelle 2 bis zum Ende durch.

    Tabelle 2:
    +-----+------------------------------+------+---------------------------+------+
    | Step| Rule                         |Return|Stack                      |Input |
    +-----+------------------------------+------+---------------------------+------+
    |     | ...                          |      |                           |      |
    |  6  | SHIFT                        |      |expr OPADD expr OPMULT     |3     |
    |  7  | SHIFT                        |      |expr OPADD expr OPMULT NUM |      |
    |  8  | REDUCE expr: NUM             |      |expr OPADD expr OPMULT expr|      |
    |  9  | REDUCE expr: expr OPMULT expr|12    |expr OPADD expr            |      |
    | 10  | REDUCE expr: expr OPADD expr |17    |expr                       |      |
    +-----+------------------------------+------+---------------------------+------+

Und auch das yapp-Kommando erzeugt die Datei AddMult.pm jetzt ohne zu meckern. Außer der Grammatik definiert AddMult.yp neben manchen Produktionen auch noch ausführbaren Perl-Code. So legt

    mult: expr OPMULT expr {                  
           return $_[1] * $_[3]             
          };

zum Beispiel fest, dass der Rückgabewert der Produktion (zusätzlich zum erzeugten Non-Terminal) das Produkt aus den Rückgabewerten der beiden expr-Ausdrücke ist. So schleift der Parser das Ergebnis des untersuchten arithmetischen Ausdrucks immer weiter nach oben durch, bis es schließlich der Startproduktion beiliegt und vom Parser an den Aufrufer zurückgegeben wird. So wird aus dem Syntaxprüfer automatisch auch ein Formelberechner. Die Tabellen 1 und 2 zeigen in der Spalte ``Return'' jeweils den Rückgabewert einer gerade ausgeführten Reduktion.

Zu beachten ist, dass $_[1] sich in den Codestücken auf den ersten Ausdruck auf der rechten Seite der Produktion bezieht (also expr). Die sonst übliche Zählung von 0 gilt hier per Definition nicht, da $_[0] in Parse::Yapp-Produktionen grundsätzlich eine Referenz auf den Parser enthält. Enthält eine Produktion mehrere durch | getrennte Alternativen, kann jede Alternative einen eigenen Codeblock definieren. Zu beachten ist, dass sich ein Codeblock immer nur auf die Alternative bezieht, neben der er steht.

Bevor die Parser zum Einsatz kommt, noch ein Zwischenschritt: Da das Interface zu yapp-Parsern etwas exotisch ist und unser MathLexer-Lexer zum Einsatz kommen soll, wird in Listing MathParser.pm eine einfachere Schnittstelle definiert. Die Methode parse() von MathParser nimmt so einfach den zu parsenden String entgegen und liefert das arithmetische Ergebnis zurück. Im Falle eines Fehlers springt der Parser die in Zeile 28 in MathParser.pm definierte Routine an und steigt aus.

Listing 4: addmult

    01 #!/usr/bin/perl
    02 use strict;
    03 use warnings;
    04 
    05 use MathParser;
    06 use AddMult;
    07 
    08 my $mp = MathParser->new(AddMult->new());
    09 
    10 for (qw( 5+2*3 5+2+3 5*2*3 5*2+3)) {
    11     print "$_: ", $mp->parse($_), "\n";
    12 }

Listing 5: MathParser.pm

    01 ###########################################
    02 package MathParser;
    03 ###########################################
    04 use MathLexer;
    05 use strict;
    06 use warnings;
    07 
    08 ###########################################
    09 sub new {
    10 ###########################################
    11     my($class, $parser) = @_;
    12 
    13     my $self = {
    14         parser => $parser
    15     };
    16 
    17     bless $self, $class;
    18 }
    19 
    20 ###########################################
    21 sub parse {
    22 ###########################################
    23   my($self, $str, $debug) = @_;
    24 
    25   my $lexer = MathLexer->new($str);
    26 
    27   my $result = $self->{parser}->YYParse(
    28     yylex   => sub { $lexer->next(); },
    29     yyerror => sub { die "Error" },
    30     yydebug => $debug ? 0x1F : undef,
    31   );
    32 }
    33 
    34 1;

Listing mathparser zeigt eine einfache Anwendung, die MathParser.pm nutzt und vier verschiedene Ausdrücke parst und auswertet:

    5+2*3: 11
    5+2+3: 10
    5*2*3: 30
    5*2+3: 13

Es zeigt sich, dass der Parser die Präzedenzregeln beachtet und die Ausdrücke korrekt wie 5+(2*3) und (5*2)+3 ausführt.

Der Präzedenzkonflikt lässt sich auch anders lösen: Wird die Grammatik wie in UnAmb.yp umformuliert, ergibt sich die höhere Präzedenz des '*'-Operators einfach aus den Zusammenhängen zwischen den Produktionen. Eine Multiplikation wird zunächst in dem Non-Terminal term zusammengefasst, bevor eventuell bestehende Additions-Reduktionen ausgeführt werden.

Listing 6: UnAmb.yp

    01 %%
    02 expr: expr OPADD term {
    03         return $_[1] + $_[3];
    04       }
    05       | term {
    06         return $_[1];
    07       };
    08 
    09 term: term OPMULT NUM {
    10         return $_[1] * $_[3];
    11       }
    12       | NUM { 
    13         return $_[1];
    14       };
    15 %%

Mit diesem Verfahren lässt sich auch das Verhalten von im Eingabestring zugelassenen Klammern implementieren, um zum Beispiel ``(5+4)*3'' zu erzwingen. Hierzu wird einfach die term-Produktion umdefiniert und eine weitere Produktion force hinzugefügt:

    term:  term OPMULT force 
             { ... }
           | force
    force: LEFTP expr RIGHTP 
             { return $_[2]; }
           | NUM

Nun genießen geklammerte Ausdrücke höchste Priorität und werden noch vor den Multiplikationen ausgeführt.

Statt den arithmetischen Ausdruck direkt auszuwerten, bietet es sich an, ihn in eine leicht berechenbare Form wie RPN zu überführen. Listing RPN.yp zeigt die zugehörige Grammatik. Nur die Codestücke an den Produktionen wurden verändert. Sie geben nun nicht mehr berechnete Werte weiter, sondern schieben Zahlen und Operationen nach RPN-manier in einen Array, der als Referenz Stufe um Stufe zurückgereicht wird und schließlich beim Aufrufer des Parsers ankommt.

Listing 7: RPN.yp

    01 %left OPADD
    02 %left OPMULT
    03 
    04 %%
    05 expr: add 
    06       | mult
    07       | NUM { return [ $_[1] ]; };
    08 
    09 add:  expr OPADD expr { 
    10           return [ 
    11               @{$_[1]}, @{$_[3]}, $_[2] 
    12           ];
    13       };
    14 
    15 mult: expr OPMULT expr {
    16           return [ 
    17               @{$_[1]}, @{$_[3]}, $_[2] 
    18           ];
    19       };
    20 %%

Listing 8: rpn

    01 #!/usr/bin/perl
    02 use strict;
    03 use warnings;
    04 
    05 use MathParser;
    06 use RPN;
    07 
    08 my $mp = MathParser->new(RPN->new());
    09 
    10 for my $string (qw(5+4+3 5+4*3)) {
    11   print "$string: [";
    12   for (@{ $mp->parse($string) }) {
    13     print "$_, ";
    14   }
    15   print "]\n";
    16 }

rpn ist das zugehörige aufrufende Skript und erzeugt -- wie erwartet -- grundsätzlich unterschiedliche Umwandlungen für 5+4*3 und 5+4+3:

    5+4+3: [5, 4, +, 3, +, ]
    5+4*3: [5, 4, 3, *, +, ]

Im oberen Ausdruck geht der Übersetzer einfach stur von links nach rechts durch und addiert die Einzelwerte nacheinander auf. Zuerst werden 5 und 4 addiert, dann zum Ergebnis 3 hinzugezählt.

Im unteren Ausdruck dürfen 5+4 jedoch wegen der ``Punkt-vor-Strich''-Regelung nicht sofort addiert werden. Vielmehr schiebt der Übersetzer zunächst die nächste Zahl 3 auf den Stack, führt die dann verlangte Multiplikation aus und addiert das Ergebnis erst danch zur weiter unten im Stack stehenden 5.

Zum Thema Parsen gibt es reichlich Bücher, das Drachenbuch [2] ist der Klassiker unter ihnen. Es ist nicht ganz einfach zu lesen, aber unverzichtbar. Neben dem besprochenen Bottom-Up-Parser-Generator Parse::Yapp, der an die Unix-Tools lex und yacc ([5]) angelehnt ist, führt das CPAN auch noch den Top-Down-Parser-Generator Parse::RecDescent, der dank der unterschiedlichen Parse-Technologie völlig der andere Charakteristiken aufweist und in [4] (ebenso wie Parse::Yapp) angerissen wird. Und letztlich kann man Parser auch von Hand schreiben. Besonders mit funktionaler Programmierung geht das sehr effektiv, wie [3] und [6] beschreiben.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2006/04/Perl

[2]
``Compilers'', Aho, Sethi, Ullman, Addison Wesley, 1986

[3]
``Higher Order Perl'', Mark Jason Dominus, Morgan Kaufmann, 2005

[4]
``Pro Perl Parsing'', Christopher M. Frenz, Apress, 2005

[5]
``lex & yacc'', Levine, Mason & Brown, O'Reilly, 1990

[6]
``Parser Combinators in Perl'', Frank Antonsen, theperlreview.com, Summer 2005

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.