You are on page 1of 6

DESARROLLO Perl

Creamos un analizador sintctico con Perl

ARTISTA DE LA COMPILACIN
L
os lexers y parsers son herramientas cotidianas para los diseadores de compiladores e inventores de nuevos lenguajes de programacin. Ambos analizan expresiones arbitrariamente complejas para validarlas sintcticamente y ayudan a traducir estas expresiones

Los analizadores lxicos y sintcticos no son slo para barbudos gurs. Este mes mostramos cmo podemos crear un parser para nuestras propias aplicaciones. POR MICHAEL SCHILLI
complejas desde un formato comprensible para las personas a formato de lenguaje mquina. La verdad es que hoy da es raro que tengamos que escribir nuestro propio parser, ya que la informacin est frecuentemente en formato XML, existiendo muchos parsers sencillos de utilizar capaces de manejar esta informacin XML. Pero si necesitamos analizar y evaluar frmulas tecleadas por los usuarios, no existe ms remedio que crear nuestro propio parser.

Anlisis final
Si tenemos que evaluar una expresin como 5+4*3 , en primer lugar ser necesario aislar los operadores de los operandos. Como muestra la Figura 1, el llamado lexer extrae primero los smbolos 5 , + , 4 , * y 3 de la cadena. Estas cadenas, a las que llamamos tokens, alimentan al parser, que verifica si tienen sentido desde el punto de vista matemtico. Para ello, el parser crea generalmente una estructura de rbol que luego usa para verificar si la expresin pasada sigue las reglas de una gramtica previamente definida. La gramtica especifica tambin cosas como la precedencia de los operadores (por ejemplo, PEMDAS: parntesis, exponentes, multiplicacin, divisin, adicin, substraccin) o la asociatividad (de izquierda a derecha, o viceversa). Tras averiguar el significado exacto de la expresin, el ordenador ya puede evaluarla. La parte inferior

44

Nmero 19

WWW.LINUX- MAGAZINE.ES

Perl DESARROLLO

Figura 1: El lexer convierte la cadena a tokens, y el parser crea el rbol de parseo. El traductor convierte esto a Notacin Polaca Inversa (RPN) y calcula el resultado aplicando un sencillo algoritmo.

de la Figura 1 muestra un ejemplo de procesador RPN (RPN: Notacin Polaca Inversa). La mquina virtual acumula tanto nmeros como operadores en la pila, y luego intenta reducir las combinaciones operandooperando-operador a valores. En la Figura 1, en primer lugar 4 3 * se reduce a 12, y la combinacin de la parte superior de la pila, 5 12 + , resulta 17, que es el resultado correcto de clculo original 5+4*3 . Por supuesto, nada nos impide pasarle una cadena como 5+4*3 a la funcin eval de Perl, que aplicar las reglas matemticas de ste para calcular las expresiones. Pero si la expresin contiene variables, operadores que no entiende Perl, o incluso construcciones if-else, es decir, si manejamos un lenguaje de programacin en miniatura, no tendremos alternativa a un verdadero parser. Volviendo al lexer: tenemos que ignorar los espacios en blanco de la cadena que estamos evaluando. Es decir, de la expresin 5 +4 *3 tiene que resultar 5+ 4*3 . Sin embargo, el anlisis lxico no es siempre tan trivial como este ejemplo. El operando puede ser un nmero real como 1.23E40 , o incluso una funcin como sin(x) , la cual tendramos que separar en sin , ( , x y ) . CPAN tiene el mdulo Parse::Lex para anlisis

lxicos como ste. Cuando se instale el mdulo, debe tenerse en cuenta que se requiere al menos la versin 0.37 del mdulo Parse::Template . El script mathlexer (Listado 1) muestra un ejemplo. Aguarda una expresin arbitrariamente compleja como entrada y se la pasa al lexer. El lexer devuelve el tipo de token y el contenido del token, que se pasan como salida para propsitos de prueba. El mdulo que usa mathlexer , MathLexer.pm , define la clase MathLexer, que proporciona el constructor new para aceptar una cadena para el anlisis lxico (Listado 2). Luego pasa a verificar si la cadena coincide con una serie de expresiones regulares guardadas en el array @tokens . Para cada lexema que encuentra el mtodo next , el lexer devuelve dos valores (un lexema es una secuencia de caracteres encontrado por el lexer a partir del cual se genera un token). El primer elemento de la referencia al array devuelto es el nombre del token que ha encontrado el lexer (por ejemplo, NUM, OPADD o RIGHTP). El segundo elemento contiene el valor realmente encontrado en el texto analizado (por ejemplo, 4.27e-14, + o )). La figura 2 muestra la salida de prueba, que se usar para alimentar el parser en una situacin real.

Ntese que Parse::Lex aguarda expresiones regulares como cadenas en el array @token . Esto significa que necesitamos evitar las barras \\ si no queremos que smbolos como * se interpreten como metacaracteres de expresiones regulares. Como las expresiones tal que \\*\\* son difciles de descifrar, MathLexer usa una expresin regular idntica, pero con un aspecto algo extrao: [*][*], para la definicin del primer token. No es nada sencillo formular una expresin regular que cubra las diferentes maneras de representar un nmero real (por ejemplo, 1.23E40 , .37 , 7 , 1e10 ). Afortunadamente, el mdulo de CPAN Regexp::Common tiene expresiones preconstruidas para muchas tareas, incluyendo una para nmeros reales con todo tipo de detalles. Tras realizar la llamada use Regexp::Common en el programa, podemos usar un hash global para aprovechar estas perlas de la sabidura en expresiones regulares. La expresin para nmeros reales puede recuperarse con un simple $RE{num}{real} . Por cierto, esta expresin tambin permite un signo menos opcional delante del nmero real. Pero debido al orden elegido de los lexemas detectados en @tokens , el lexer supondr que un signo menos precedente es un OP . Sin embargo, si el signo menos est en el exponente del nmero real, el lexer lo toma como parte del lexema NUM . Adicionalmente, el mtodo skip llamado en la lnea 32 del Listado 2 asegura que el lexer ignora los espacios y caracteres de nueva lnea. Sin embargo, si el mtodo skip se topa con una secuencia de caracteres que no reconoce (como con }), se usa el pseudo-token ERROR de la lnea 19. Este token define una rutina de manejo de errores, que usa el comando die para indicarle al lexer que finalice.

Tokens Por Favor!


El parser verifica entonces la validez sintctica de una expresin. 4+*3 sera invlida. Queremos que el parser reporte un error en este caso y cancele el procesamiento. En muchos casos, los parsers no slo

WWW.LINUX- MAGAZINE.ES

Nmero 19

45

DESARROLLO Perl

verifican la sintaxis de una expresin, sino que tambin controlan el trabajo de traduccin. Despus de todo, por qu no dejar al parser que calcule el resultado mientras estudia una expresin aritmtica. El Listado 3, AddMult.yp , define una gramtica para el parser. Especifica cmo combina el parser los tokens que salen del lexer en estructuras predefinidas. La primera produccin, expr: add | mult | NUM , especifica que la tarea global del parser es reducir la secuencia de todos los tokens a una construccin de tipo expr . Si esto no es posible, los tokens no obedecen la gramtica: ha ocurrido un error y el parser finaliza. Las producciones como la del Listado 3 tienen un smbolo de no terminal a la izquierda. El objetivo del parser es hacer coincidir de alguna manera la salida del lexer con la parte derecha de una produccin y entonces reducirla al no terminal de su izquierda. En la parte derecha, una produccin puede listar tokens que ya pasaron el lexer (tambin conocidos como terminales) pero tambin otros no terminales, que son resueltos por otras producciones. En nuestro ejemplo, expr puede ser tres cosas, con las alternativas separadas por

tuberas | a la derecha de los dos puntos. add (suma), mult (multiplicacin) o un NUM terminal, un nmero real que llega del lexer. Los no terminales add y mult se definen en las siguientes producciones en AddMult.yp . add: expr OPADD expr especifica que el no terminal add comprende dos expr no terminales enlazados por el operador +. Como ya sabamos expr puede contener adiciones, multiplicaciones o nmeros sencillos. El archivo con la gramtica, AddMult.yp , proporciona una descripcin abstracta de un parser al mdulo Parse::Yapp disponible en CPAN. AddMult.yp se divide en tres secciones separadas por la cadena %% . La cabecera est al comienzo, puede contener instrucciones del parser o cdigo Perl. Las producciones que pertenecen a la gramtica estn en el centro y le sigue el pie de pgina, donde se puede definir ms cdigo Perl, aunque est vaco en el Listado 3. Para implementar el parser, AddMult.yp se convierte a un

mdulo Perl por la utilidad yapp incluida en Parse::Yapp . El mdulo que se crea en este proceso, AddMult.pm , implementa un parser bottom-up. Este tipo de parser lee un flujo de tokens desde el lexer e intenta crear el rbol mostrado en la Figura 1, desde abajo hacia arriba. Para ello, combina las unidades ledas para crear construcciones de mayor nivel desde tokens y construcciones de menor nivel. Se contina este proceso, basado en las reglas de la gramtica, hasta que los resultados coinciden con la parte izquierda de la primera produccin. En cada paso, el parser hace una de estas dos cosas: desplazar o reducir. Al desplazar se le indica al parser que tome el siguiente token del flujo de entrada y lo ponga encima de la pila. Al reducir se le est indicando que combine los terminales y los no terminales de la pila para crear no terminales de nivel superior, basndose en las reglas de la gramtica, y por tanto reduciendo la altura de la pila. Si la

Listado 2: MathLexer.pm
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 ############################## package MathLexer; ############################## use strict; use Regexp::Common; use Parse::Lex; my @token = OPPOW => OPSUB => OPADD => OPMULT => OPDIV => FUNC => ID => LEFTP => RIGHTP => NUM => ERROR => 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; 28

Listado 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 07 08 my $lex = MathLexer->new($str); 09 10 while(1) { 11 12 13 14 } my($tok, $val) = $lex->next(); last unless defined $tok; printf %8s %s\n, $tok, $val; $str\n\n;

( [*][*], [-], [+], [*], [/], [a-zA-Z]\\w*\\(, [a-zA-Z]\\w*, \\(, \\), $RE{num}{real}, .*, sub { die qq(Cant lex $_[1]) }, 21 ); 22 23 ############################## 24 sub new { 25 ############################## 26 my($class, $string) = @_; 27

46

Nmero 19

WWW.LINUX- MAGAZINE.ES

Perl DESARROLLO

cola de entrada est vaca, y si la ltima reduccin acaba de salir del parser con la parte izquierda de la produccin inicial, significa que el parser se ha ejecutado satisfactoriamente. La Tabla 1 muestra un parser bottom-up, implementado en base a la gramtica de AddMult.yp , que procesa los tokens extrados de la cadena de entrada 5+4*3 paso a paso. En el paso 0, los tokens [NUM, 5] , [OPADD, +] , [NUM, 4] , [OPMULT, *] y [NUM, 3] estn disponibles en la cola de entrada. En el paso 1, el parser pone el 5 (que es un token NUM) encima de la pila (desplazando). En el paso 2, reduce el terminal NUM a expr basndose en la tercera alternativa de la primera produccin de la gramtica de AddMult.yp . El parser procesa entonces los tokens [OPADD, +] y [NUM, 4] desde la entrada, los desplaza hasta la pila, y luego reduce el 4 a expr . Y ahora qu? El parser podra reducir expr OPADD expr de la pila a expr , siguiendo la segunda produccin de la gramtica. Por otro lado, podra traerse [OPMULT, *] desde la entrada y esperar a encontrar otra expr ms tarde para reducir expr OPMULT expr (tercera produccin).

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

Asociatividad y Precedencia
El generador del parser yapp tambin detecta que la gramtica es ambigua. Aqu vemos cmo el generador yapp crea el mdulo del parser AddMult.pm a partir del archivo AddMult.yp :
$ yapp -m AddMult AddMult.yp 4 shift/reduce conflicts

Las dos primeras lneas del Listado 3 resuelven el conflicto en la gramtica:


%left OPADD %left OPMULT

Conflicto
Este tipo de problema es comn. Las gramticas son a menudo ambiguas. Si no tuviramos la tradicional regla

Figura 2: Expresin matemtica procesada por el lexer MathParser.pm.

PEMDAS en matemticas, el parser estara completamente desconcertado por los conflictos de desplazamiento-reduccin causados por la expresin 5+4*3. El hecho de que estos operadores algebraicos tengan precedencia, no obstante, evita el conflicto. El parser tiene que esperar antes de reducir 5+4, y necesita desplazar el token * hasta la pila, ya que un * es un enlace ms fuerte entre operandos que el +, ms dbil. Si se presentan los mismos operadores varias veces en sucesin, como en 5-3-2 , todas las operaciones tienen la misma precedencia, y aparece otro tipo de conflicto. Si el parser decide reducir, tras parsear 5-3 , evala los operadores de izquierda a derecha, de acuerdo a las reglas del lgebra. Un desplazamiento, por otro lado, evaluara la expresin como 5-(3-2) , y esta expresin conducira a un sorprendente resultado de 6, en lugar del 0 que esperbamos. Es por esto que el operador menos se considera como asociativo por la izquierda. Necesitamos indicarle esto al parser para que pueda resolver tambin este tipo de conflicto. Por cierto, en el caso del operador de potencia ( en Perl), el lgebra dicta un mtodo contrario: 4 3 2 (4 elevado a 3 elevado a 2) se calcula como 4**(3**2) . El operador potencia es asociativo por la derecha! Esto puede verificarse fcilmente en Perl: perl -le print 4**3**2 devuelve 262144 (4 9) y no 4096 (64 2).

Estas sentencias estipulan que tanto el operador + como el * son asociativos por la izquierda, y, lo que es ms importante: OPMULT tiene prioridad sobre OPADD , dado que %left OPMULT aparece ms tarde que %left OPADD en la definicin del parser. Si el parser fuese a definir una operacin OPMINUS usando el operador -, sera importante insertar

Listado 4: addmult
01 #!/usr/bin/perl 02 ############################## ############# 03 # addmult 04 # 2005, Mike Schilli <cpan@perlmeister.com> 05 ############################## ############# 06 use strict; 07 use warnings; 08 09 use MathParser; 10 use AddMult; 11 12 my $mp = MathParser->new(AddMult->new() ); 13 14 for (qw( 5+2*3 5+2+3 5*2*3 5*2+3)) { 15 print $_: , $mp->parse($_), \n; 16 }

WWW.LINUX- MAGAZINE.ES

Nmero 19

47

DESARROLLO Perl

Listado 5: MathParser.pm
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 ############################## package MathParser; ############################## use MathLexer; use strict; use warnings; ############################## sub new { ############################## my($class, $parser) = @_; my $self = { parser => $parser }; bless $self, $class; } ############################## 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;

%left OPMINUS antes de la definicin de OPMULT. Si la cabecera del archivo yp tiene una entrada %right OPMINUS en lugar de %left OPMINUS , el parser evaluara expresiones como 5-3-2 de derecha a izquierda. Y esto sera desastroso, ya que 5-(3-2) es 6 , en lugar de 5-3-2 , que nos da un valor de 0. Para indicarle al parser cmo elevar nmeros a potencias, vamos a necesitar un operador de potencias asociativo por la derecha, %right OPPOW , situado despus de la definicin de OPMULT debido a la alta prioridad de la operacin potencia y a su asociatividad por la derecha. Estos trucos le permiten al parser terminar como se muestra en la Tabla 2. Adems de la gramtica, AddMult.yp define cierto cdigo ejecutable en Perl, adjunto a las producciones. Por ejemplo:

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

produccin (esto es, expr ). Partiendo de la norma, el contador no comienza en 0, ya que $_[0] de la produccin de Parse::Yapp es siempre una referencia para el parser. Si una produccin contiene mltiples alternativas separadas por |, cada alternativa puede definir su propio bloque de cdigo. Ntese que un bloque de cdigo slo se refiere a la alternativa a la que est adjunta. Antes de que pueda usarse el parser, slo un paso intermedio ms: la interfaz del parser yapp es algo extica, y como vamos a usar nuestro lexer MathLexer previamente definido, se puede definir una interfaz ms sencilla en el Listado 5. El mtodo parse() de MathParser simplemente acepta la cadena a parsear y devuelve el resultado aritmtico. Si surge un error, el parser se dirige a la subrutina annima definida en la lnea 35 y finaliza. El listado mathparser muestra una sencilla aplicacin que usa MathParser.pm para parsear y evaluar cuatro expresiones diferentes:
5+4*3: 5+4+3: 5*4*3: 5*4+3: 17 12 60 23

estipula que el valor devuelto de la produccin (que acompaa al no terminal en la parte izquierda) es el producto de los valores devueltos de las expresiones expr . Esto significa que el parser va a continuar acumulando arriba el resultado de la expresin aritmtica que est evaluando hasta que alcance la produccin inicial, y el resultado puede devolverse a quien lo llam desde el parser. Ello proporciona automticamente al verificador de sintaxis la posibilidad de calcular frmulas. Las Tablas 1 y 2 muestran los valores devueltos en la reduccin en curso, en la columna Return. Ntese que $_[1] en los segmentos de cdigo refieren a la primera expresin en el lado derecho de la

Esto muestra que el parser hace honor a las reglas de precedencia y evala expresiones como 5+4*3 y 5*4+3 de manera correcta.

Listado 6: UnAmb.yp
01 ############################## 02 # UnAmb.yp - Unambiguous +/* grammar 03 ############################## 04 %% 05 expr: expr OPADD term { 06 return $_[1] + $_[3]; 07 } 08 | term { 09 return $_[1]; 10 }; 11 12 term: term OPMULT NUM { 13 return $_[1] * $_[3]; 14 } 15 | NUM { 16 return $_[1]; 17 }; 18 %%

Tabla 1: Pasos del Parser


Paso 0 1 2 3 4 5 Regla Devuelve Pila NUM expr expr OPADD expr OPADD NUM expr OPADD expr DESPLAZA REDUCE expr: NUM 5 DESPLAZA DESPLAZA REDUCE expr:NUM 4 *Conflicto: Desplazamiento/Reduccin? Entrada 5+4*3 +4*3 +4*3 4*3 *3 *3

48

Nmero 19

WWW.LINUX- MAGAZINE.ES

Perl DESARROLLO

Tabla 2: Pasos Finales de la Ejecucin del Parser


Paso 6 7 8 9 10 Regla DESPLAZA DESPLAZA REDUCE expr: NUM REDUCE expr: expr OPMULT expr REDUCE expr: expr OPADD expr Devuelve Pila expr OPADD expr OPMULT expr OPADD expr OPMULT NUM expr OPADD expr OPMULT expr expr OPADD expr expr Entrada 3

12 17

Existe otra manera de resolver conflictos de precedencia. Si formulamos una gramtica como la del Listado 6, la mayor precedencia del operador * deriva de las relaciones entre las producciones. Una multiplicacin se reduce primero al no terminal term , antes de que se haga alguna reduccin. Este mtodo nos permite implementar el comportamiento de los parntesis, si se permiten en la cadena de entrada. Para forzar (5+4)*3, por ejemplo. Para hacer esto, simplemente redefinimos la produccin term y aadimos otra produccin para force, que salta ante cualquier parntesis y reduce inmediatamente las expresiones de su interior:
term: term OPMULT force { ... } | force force: LEFTP expr RIGHTP

{ return $_[2]; } | NUM

En lugar de evaluar la expresin aritmtica directamente, tiene ms sentido convertirla a un formato que sea ms sencillo de computar, como RPN. El Listado 7 muestra la gramtica para hacer esto. Hemos cambiado slo los segmentos de produccin de cdigo, que, en lugar de pasar sobre los valores calculados, ahora escribe los nmeros y las operaciones en un array, que se pasa como referencia, para llegar finalmente donde fue llamado el parser. rpn es el script al que se llama. Como es de suponer, produce conversiones completamente diferentes de 5+4*3 y 5+4+3 :
5+4+3: [5, 4, +, 3, +, ] 5+4*3: [5, 4, 3, *, +, ]

Listado 7: RPN.yp
01 02 03 04 05 06 07 %left OPADD %left OPMULT %% expr: add | mult | NUM { return [ $_[1] ]; };

En la expresin superior, el traductor simplemente procesa la expresin de izquierda a derecha y

Listado 8: rpn
01 02 03 04 05 06 07 08 #!/usr/bin/perl use strict; use warnings; use MathParser; use RPN; my $mp = MathParser->new(RPN->new());

aade los valores individuales, primero sumando 5 y 4, y luego sumando 3 al resultado. En la expresin de abajo, 5+4 no puede reducirse de manera directa debido a las reglas PEMDAS. En vez de esto, el traductor acumula el siguiente nmero, 3 , encima de la pila RPN, y luego realiza la multiplicacin. Slo entonces suma el resultado, 12, al 5 ubicado en la parte inferior de la pila. Existen numerosos libros acerca de la materia. El Dragon Book [2] es uno de los clsicos. Puede que no sea demasiado sencillo de leer, pero es indispensable. Adems del generador de parser bottom-up, Parse::Yapp , que est basado en tcnicas usadas por las herramientas de Unix lex y yacc ([5]), CPAN tiene tambin un generador de parser topdown, Parse::RecDescent . Parse::RecDescent tiene caractersticas completamente diferentes debido a las tecnologas de parseo utilizadas. [4] ofrece algunos ejemplos sobre cmo usar Parse::Yapp y Parse::RecDescent . Por ltimo, podemos escribir parsers a mano. Esta opcin es particularmente efectiva en programacin funcional, como se I describe en [3] y [6].

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 %%

RECURSOS
[1] Listados de este artculo: http://www. linux-magazine.es/Magazine/ Downloads/19 [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

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 }

WWW.LINUX- MAGAZINE.ES

Nmero 19

49

You might also like