introduccion al analisis sintactico con antlr

Upload: badleo02

Post on 10-Oct-2015

11 views

Category:

Documents


0 download

TRANSCRIPT

  • Introduccin al anlisis sintctico con ANTLR

    En esta prctica trabajaremos con ANTLR a nivel sintctico. En las prcticas anteriores ya hemos visto los elementos bsicos de la notacin. En sta, nos centraremos fundamentalmente en la manera en la que se comunican analizadores lxicos y sintcticos, la forma en la que deben escribirse las gramticas y la resolucin de cierto tipo de ambigedades sintcticas.

    Un ejemplo simple

    He aqu un analizador sintctico simple, es capaz de reconocer expresiones aritmticas con nmeros enteros y reales, se incluye adems el analizador lxico en el mismo fuente:

    /////////////////////////////// // Analizador lxico /////////////////////////////// class Analex extends Lexer; BLANCO : (' '|'\t'|"\r\n") {$setType(Token.SKIP);}; protected DIGITO : '0'..'9'; NUMERO : (DIGITO)+('.'(DIGITO)+)?; OPERADOR : '+'|'-'|'/'|'*'; PARENTESIS : '('|')' ; SEPARADOR : ';' ; /////////////////////////////// // Analizador sintctico /////////////////////////////// class Anasint extends Parser; instrucciones : (expresion ";")* ; expresion : exp_mult (("+"|"-") exp_mult)* ; exp_mult : exp_base (("*"|"/") exp_base)* ; exp_base : NUMERO | "(" expresion ")" ;

    El analizador sintctico generado se implementar a travs de la clase Anasint (tal y como hemos indicado en el fuente) que ser una subclase de LLkParser que es la que implementa los aspectos genricos de los reconocedores sintcticos. La clase Anasint ofrece un mtodo por cada smbolo terminal de la gramtica, dichos mtodos sern los encargados de reconocer el lenguaje asociado a los smbolos correspondientes.

    Probando el analizador sintctico Para poder probar el analizador sintctico utilizaremos una clase principal que cree

    los analizadores lxico y sintctico, y que llame al mtodo encargado de reconocer el smbolo principal de la gramtica (anasint.instrucciones()):

  • ///////////////////////////////////// // Procesador.java (clase principal) /////////////////////////////////////

    import java.io.*; import antlr.*; public class Procesador {

    public static void main(String args[]) { try {

    FileInputStream fis =new FileInputStream(args[0]); Analex analex = new Analex(fis); Anasint anasint = new Anasint(analex); anasint.instrucciones();

    }catch(ANTLRException ae) { System.err.println(ae.getMessage());

    }catch(FileNotFoundException fnfe) { System.err.println("No se encontr el fichero");

    } }

    }

    Anlisis lxico y sintctico en fuentes separados. Exportacin de vocabularios Una de las ventajas de ANTLR es que permite especificar todos los analizadores en

    un nico fuente o en fuentes separados. Esto permite que adaptemos nuestra especificacin al tamao del problema. Si el lenguaje es muy simple podremos incluir todos los analizadores en un fuente y no tendremos que preocuparnos de comunicarlos. Si por el contrario el lenguaje es complicado podremos especificar los analizadores por separados y comunicarlos a travs de una interfaz.

    El hecho de generar una implementacin orientada a objetos hace que las interfaces

    sean muy simples y claras. El analizador sintctico invocar al mtodo nextToken del analizador lxico cada vez que requiera un token y recibir como respuesta un objeto de la clase Token (de la subclase CommonToken o de otra que le indiquemos tal y como vimos en la prctica dedicada al anlisis lxico).

    La informacin ms importante de un objeto de la clase Token viene dada por su atributo type. Este atributo sirve para identificar el tipo de token, y es de tipo entero para optimizar las mltiples comparaciones que ha de hacer el analizador sintctico al procesar los tokens recibidos desde el analizador lxico. Se denomina vocabulario al conjunto de tokens, junto con su codificacin, presentes en un fichero de gramtica.

    ANTLR codifica los vocabularios a travs de dos ficheros: una interfaz java y un fichero de texto. La interfaz java contiene un atributo entero para cada token inicializado con el valor que a la postre servir de codificacin del tipo de token. Esta interfaz es el elemento clave en la coordinacin de dos analizadores ya que si ambos la implementan compartirn la informacin codificada en ella. La interfaz para el analizador sintctico del ejemplo anterior sera:

  • // $ANTLR 2.7.2: "Anasint.g" -> "Anasint.java"$ public interface AnasintTokenTypes { int EOF = 1; int NULL_TREE_LOOKAHEAD = 3; // ";" = 4 // "+" = 5 // "-" = 6 // "*" = 7 // "/" = 8 int NUMERO = 9; // "(" = 10 // ")" = 11 }

    Tal y como se indica en la primera lnea del fichero esta interfaz se ha obtenido a partir de la compilacin del fichero Anasint.g (que contiene slo la especificacin del analizador sintctico). El nombre de la interfaz se obtiene aadiendo el prefijo TokenTypes al nombre del analizador.

    El otro fichero utilizado en la codificacin de vocabulario tiene el mismo nombre

    que el de la interfaz pero su extensin es ".txt". En l adems del token y del nmero que lo representa se incluye tambin una cadena de caracteres con el nombre del token (siempre que lo tenga). Esta ltima informacin se utiliza en el informe de errores cuando adems del tipo de token se necesita conocer tambin su nombre. En nuestro caso el fichero AnasintTokenTypes.txt tendra el siguiente contenido:

    // $ANTLR 2.7.2: Anasint.g -> AnasintTokenTypes.txt$ Anasint // output token vocab name ";"=4 "+"=5 "-"=6 "*"=7 "/"=8 NUMERO=9 "("=10 ")"=11

    Si especificamos los analizadores lxico y sintctico en un mismo fuente ANTLR se encarga de generar un vocabulario comn para los dos. Sin embargo, si los analizadores estn en fuentes distintos tenemos que preocuparnos de que ambos trabajen con el mismo vocabulario. Esto se consigue con la opcin importVocab que permite aadir tokens de otro vocabulario a los definidos en un analizador. Dado que los tokens que realmente nos interesan son los especificados en el analizador sintctico importaremos el vocabulario desde el anlisis sintctico hacia el lxico:

  • /////////////////////////////// // Analex.g: Analizador lxico /////////////////////////////// class Analex extends Lexer; options { importVocab = Anasint; } // Reglas

    De esta forma el vocabulario comn a los dos analizadores ser el del sintctico, contenido en la interfaz AnasintTokenTypes.java.

    Cmo escribir buenas gramticas?

    Los tres esquemas de reglas ms usuales en la descripcin de lenguajes, son el esquema lista, el esquema agregado y el esquema eleccin. La descripcin de estos esquemas es la siguiente:

    Esquema lista: se aplica cuando la entrada que se quiere reconocer consta de una secuencia de elementos de una misma categora. Por ejemplo una lista de nmeros, una lista de instrucciones, una lista de identificadores. En ANTLR se dispone de los operadores + y * para especificar listas de uno o ms elementos y listas de cero o ms elementos. Un ejemplo de lista sera:

    lista : (elemento)*;

    Esquema agregado: se aplica cuando la entrada que se quiere reconocer consta siempre de un nmero fijo de elementos. Por ejemplo una instruccin de asignacin o un programa compuesto por tres secciones. Las reglas que definen este tipo de construcciones sintcticas son del tipo:

    agregado : componente1 componente2 componente3;

    con tantos componentes como sean necesarios.

    Esquema eleccin: se aplica para agrupar distintas opciones en la definicin de la estructura sintctica de una determinada entrada. Por ejemplo los distintos tipos de instrucciones en un lenguaje de programacin o los distintos tipos de datos en la declaracin de una variable. Las reglas que definen una eleccin son del tipo:

    eleccion : opcion1 | opcion2 | opcion3 ;

    El proceso de escritura de una gramtica consiste en identificar a qu tipo de esquema obedece un determinado elemento del lenguaje y especificarlo con las reglas correspondientes. Lo aconsejable es aplicar estos esquemas paso a paso, lo que dar lugar a una gramtica ms clara y legible. En este sentido hay que ser cuidadoso con las facilidades

  • (propias de la notacin EBNF) que ANTLR nos proporciona para escribir gramticas compactas ya que su abuso puede dar lugar a gramticas excesivamente crpticas. Por ejemplo:

    entrada : ((IDENT ("," IDENT)* "=")? NUMERO

    ("," NUMERO)* ";");

    La anterior genera el mismo lenguaje que la siguente, que es mucho ms clara y fcil de escribir y comprender:

    entrada : (asignacion)*; asignacion : (idents)? numeros ";"; idents : IDENT ("," IDENT)* "="; numeros : NUMERO ("," NUMERO)*;

    Predicados sintcticos Para los problemas que pueden aparecer cuando una gramtica no cumple la

    condicin LL(k) las soluciones sern similares que se aplicaron en la especificacin del analizador lxico cuando haba tokens con prefijos comunes:

    Ampliar el nmero de smbolos de anticipacin con la opcin k, utilizar predicados sintcticos para resolver localmente las reglas que

    provocan conflictos.

    Tomemos como ejemplo el tpico problema no-LL(1) que supone determinar si el primer identificador de una instruccin se corresponde con un nombre de funcin (en una llamada) o un nombre de variable (en una asignacin). La gramtica de partida sera:

    instruccion : asignacion

    | llamada ...

    ;

    asignacion : IDENT ":=" expr ";";

    llamada : IDENT "(" expr ")" ";";

    La solucin pasa por aprovechar el hecho de que los dos primeros smbolos de una instruccin son siempre IDENT ":=", y se expresara de la siguiente forma con un predicado sintctico asociado a la regla conflictiva:

    instruccion : (IDENT ":=") => asignacion | llamada ...

    ;

  • ANTLR utiliza el predicado para realizar una simulacin del anlisis previa a la toma de la decisin. De esta forma el analizador predictivo puede contar, siempre que as lo indiquemos, con ms informacin para determinar la regla que tiene que aplicar.

    Precedencia y asociatividad Uno de los pocos lenguajes para los que no es fcil escribir una gramtica aplicando

    la metodologa vista en el apartado anterior es el de las expresiones (aritmticas, lgicas, regulares, etc.). Esto se debe fundamentalmente a la posibilidad de utilizar operadores infijos (es decir, que se mezclan entre los operandos) que adems pueden estar encuadrados en distintos niveles de prioridad. Si escribimos una gramtica para un lenguaje de expresiones y no tenemos en cuenta este tipo de cuestiones seguramente obtendremos como resultado una gramtica ambigua. Para las expresiones con sumas y multiplicaciones la siguiente gramtica adolece de este problema:

    expresion : expresion "+" expresion

    | expresion "*" expresion | NUMERO ;

    Por ejemplo la expresin 2+2*2 podra ser analizada como (2+2)*2 como 2+(2*2). Veamos cmo obtener a partir de esta gramtica otra que resuelva esta ambigedad y adems pueda ser procesada sin problemas por un reconocedor descendente. Para empezar hay que escribir la gramtica de forma que no aparezca ninguna recursin por la izquierda. Para ello basta con pensar que una expresin no es ms que una lista de nmeros en la que se utiliza como separadores + *, visto as una posible solucin sera:

    expresion : NUMERO (("+"|"*") NUMERO)*;

    Esta gramtica ya resuelve la cuestin de la recursin por la izquierda, aunque todava no es una solucin definitiva ya que no tiene en cuenta para nada la prioridad de los operadores. No es que sea ambigua, en realidad lo que ocurre es que utiliza un mecanismo demasiado simple para determinar en qu orden han de aplicarse las operaciones: aplica primero las que estn ms a la izquierda. Si se construye el rbol de derivacin para la entrada 2+2*2+2*2, se observar que la aplicacin de las operaciones propuesta por la gramtica es ((((2+2)*2)+2)*), totalmente distinta a la que nos interesara que es ((2+(2*2))+(2*2)). De todas formas la idea no es del todo mala ya que resuelve bien las situaciones en las que hay que aplicar varias veces el mismo operador, como 2-2-2 que debe ser interpretada de izquierda a derecha, o sea ((2-2)-2) en lugar de (2-(2-2)).

    En realidad lo que le falta a la gramtica anterior es estratificar la aplicacin de

    operaciones, de manera que se obligue a aplicar unas antes que otras. As, si consideramos que una expresin es una lista de sumandos, donde cada sumando es el resultado de multiplicar una lista de nmeros la gramtica quedara como sigue:

    expresion: sumando ("+" sumando)*;

    sumando: NUMERO ("*" NUMERO)*;

  • Esta gramtica s puede ser considerada ya una solucin ya que no es ambigua, aplica antes las multiplicaciones y despus las sumas, y adems en caso de encontrar varios operadores iguales los asocia de izquierda a derecha. Podemos aprovechar los dos niveles establecidos por las reglas expresion y sumando para incluir operadores que tengan una prioridad similar (la divisin igual que la multiplicacin y la resta igual que la suma):

    expresion: sumando (("+"|"-") sumando)*;

    sumando: NUMERO (("*"|"/") NUMERO)*;

    Si queremos incluir parntesis tan slo tenemos que establecer un nivel nuevo para que no coincida con la prioridad de los niveles anteriores. Cambiaremos tambin los nombres de los smbolos porque a medida que aumentamos el nmero de niveles es ms difcil encontrar nombres significativos:

    expr : expr_mult (("+"|"-") expr_mult)*;

    expr_mult : expr_base (("*"|"/") expr_base)*;

    expr_base : NUMERO | "(" expr ")" ;

    Ejercicios

    1. Compilar y probar el analizador lxico-sintctico que aparece en el enunciado. 2. Especificar los analizadores lxico y sintctico del ejercicio anterior en fuentes

    separados. Compilarlos y probar el funcionamiento.

    3. Ampliar el ejercicio anterior para que se admitan identificadores, asignaciones, el operador de cambio de signo, los operadores lgicos &&, || y !, y los operadores relacionales >,=,

  • Aplicar el reconocedor a la siguiente entrada errnea:

    while (a=b) b=2;

    a=a+1; } {

    Se informa del error? Para solucionar este problema hay que emplear de manera explcita el token EOF en la gramtica.

    5. Extender el ejercicio anterior con los elementos presentes en el siguiente ejemplo:

    void main(void) { int a, b; scanf("%d",&b); a=1; while (a=b) { printf("punto medio %d\n",a); break;

    } a++;

    } }

    6. Ampliar el ejercicio anterior para que el reconocedor procese tambin los tipos char y float, los literales carcter y real (por ejemplo 'a' y 1.09) en las expresiones, la declaracin y llamada de funciones, los operadores del apartado 3 y el condicional aritmtico (por ejemplo a>3?2:1 ).

    7. Escribir un analizador sintctico para los elementos de XML presentes en el

    siguiente ejemplo:

    La isla del tesoro Robert L. Stevenson Juventud Yo que he servido al Rey de Inglaterra Bohumil Hrabal Destino