Análisis sintáctico con ANTLR Antlr (ANother Tool for Language Recognition) Recognition) genera genera analizadores analizadores sintácticos sintácticos LL(k) LL(k) con predicados sintácticos y semánticos.
Estructura de un analizador sintáctico Los analizadores sintácticos o parsers reconocen estructuras sintácticas desde secuencias de tokens. La estructura general de un analizador sintáctico antlr es:
header { ... } //Código que se situará en la cabecera del parser (opcional) // Suele usarse para decidir la ubicación del analizador tras ser // compilado (pe. header {package X; }) class Anasint extends Parser; options { ... } // opciones destinadas a particularizar particulariz ar el parser (opcional) tokens {...}
//definición de tokens (opcional)
{ //clases internas necesarias para la implementación del parser } (opcional) // definición de reglas léxicas
Ejemplo: class Anasint extends Parser;
instrucciones: (expresion ";" ";")* )* ; expresion : exp_base (("+" (("+"||"-" "-")) exp_base)* ; exp_base : NUMERO | "(" "(" expresion expresion ")" ;
Uso del analizador sintáctico El procesamiento de un parser como el anterior produce una clase Anasint con métodos que representan las reglas reglas gramaticales. El analisis sintáctico sintáctico se apoya sobre un análisis léxico, es decir, las categorías sintácticas son generadas desde un flujo de tokens producidos desde un analizador léxico. Ejemplo de uso del parser suponiendo un analizador léxico denominado Analex: import java.io.*; import antlr.collections.AST;
import antlr.ANTLRException; public class expre { public static void main(String args[]){ try{ FileInputStream fis= new FileInputStream("entrada.txt"); 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"); } } }
Construcción de Reconocedores por Separado Antlr ofrece la posibilidad de desarrollar lexers y parsers por separado. Por defecto, todo parser exporta su vocabulario (conjunto de tokens usados en la definición del parser). Antlr implementa esta idea mediante dos ficheros, uno de texto y otro Java implementando una interfaz. Supongamos un fichero Anasint.g conteniendo el siguiente texto: class Anasint extends Parser;
instrucciones : (expresion ";")* ; expresion : exp_mult (("+"|"-") exp_mult)* ; exp_mult : exp_base (("*"|"/") exp_base)* ; exp_base : NUMERO | "(" expresion ")" ;
Al compilarlo se producen tres ficheros: • •
Anasint.java (el parser java), AnasintTokenTypes.java (la interfaz con la definición del conjunto de tokens utilizados en el parser) y
•
AnasintTokenTypes.txt (el fichero de texto con la definición del conjunto de tokens utilizados en el parser).
AnasintTokenTypes.java contiene: public interface AnasintTokenTypes { int EOF = 1; int NULL_TREE_LOOKAHEAD = 3; // ";" = 4 // "+" = 5 // "-" = 6 // "*" = 7 // "/" = 8 int NUMERO = 9; // "(" = 10 // ")" = 11 }
AnasintTokenTypes.txt contiene: ";"=4 "+"=5 "-"=6 "*"=7 "/"=8 NUMERO=9 "("=10 ")"=11
Estos ficheros son necesarios para conectar el parser con el lexer. La forma de hacerlo es incluir en el lexer una importación del vocabulario correspondiente: class Analex extends Lexer; options{ importVocab = Anasint; // Importación del conjunto de tokens } BLANCO : (' '|'\t'|"\r\n") {$setType(Token.SKIP);}; NUMERO : ('0'..'9')+('.'('0'..'9')+)?; OPERADOR : '+'|'-'|'*'|'/'; PARENTESIS : '('|')'; SEPARADOR : ';';
Predicados sintácticos: Antlr permite construir parsers LL(k) (con k arbitrario) haciendo uso de predicados sintácticos. Se trata de construcciones de la forma ( lookahead ) => regla gramatical
El siguiente ejemplo es un típico de gramática no-LL(1): instruccion : asignacion | llamada ... ; asignacion : IDENT ":=" expr ";" ; llamada : IDENT "(" expr ")" ";" ;
La forma de resolverlo con predicados sintácticos es: instruccion : (IDENT ":=") => asignacion | (IDENT "(") => llamada ... ;
Gramáticas Atribuidas en Antlr El reconocimiento descendente permite asociar a cada símbolo no terminales de una gramática argumentos de entrada, resultados de salida y variables locales. La idea que subyace detrás de implementar reglas sintácticas como procedimientos en un lenguaje de programación. Las variables locales son simuladas mediante declaraciones entre llaves en la parte izquierda de la regla: expr_1 returns [int res=0;] {int e1,e2;} : e1=expr_2 {res=e1;} (("+" e2=expr_2 {res=res+e2;}) | ( "-" e2=expr_2 {res=res-e2;}))* ;
En este ejemplo, se definen dos variables locales a la regla {int e1,e2;} para almacenar los atributos sintetizados asociados a las ocurrencias de los símbolos no terminales expr_2 (e1=expr_2 {res=e1;} y e2=expr_2 {res=res+e2;}) Los atributos sintetizados se simulan mediante resultados devueltos por los símbolos no terminales. La forma de hacerlo es mediante la definición de cláusulas returns en la parte izquierda de la regla: expr_1 returns [int res=0;] {int e1,e2;} : e1=expr_2 {res=e1;} (("+" e2=expr_2 {res=res+e2;}) | ( "-" e2=expr_2 {res=res-e2;}))* ;
En este ejemplo, el reconocimiento del símbolo no terminal expr_1 tiene asociado la construcción de un valor entero res.
Los atributos heredados se simulan mediante argumentos asociados a los símbolos no terminales. La forma de hacerlo es mediante la definición de argumentos entre corchetes en la parte izquierda de la regla: expr_1 [java.util.HashMap h] returns [int res=0;] {int e1,e2;} : e1=expr_2[h] {res=e1;} (("+" e2=expr_2[h] {res=res+e2;}) | ("-" e2=expr_2[h] {res=res-e2;}))* ;
En este ejemplo, el símbolo no terminal expr_1 tiene asociado el argumento h de tipo java.util.HashMap . Es importante destacar que el uso de objetos como atributos heredados, también permite la modificación de sus respectivos estados convirtiéndose en atributos heredado y sintetizados de forma simultánea. El siguiente ejemplo muestra una gramática con atributos heredados ( h ) y sintetizados ( res ): class Anasint extends Parser; options{ k=2; }
prog { java.util.HashMap h = new java.util.HashMap();} : ( declaracion [h] )* ; declaracion [java.util.HashMap h] { int e;}: e=expr_1[h] ";" {System.out.println("Expresion => "+e);} | i:IDENT ":=" e=expr_1[h] ";" { h.put(i.getText(), new Integer(e)); System.out.println("Asignacion ("+i.getText()+ ")=> "+e);} ; expr_1 [java.util.HashMap h] returns [int res=0;] {int e1,e2;} : e1=expr_2[h] {res=e1;} (("+" e2=expr_2[h] {res=res+e2;}) |("-" e2=expr_2[h] {res=res-e2;}))* ; expr_2 [java.util.HashMap h] returns [int res=0;] {int e1,e2;} : e1=expr_3[h] {res=e1;} (("*" e2=expr_3[h] {res=res*e2;}) | ("/" e2=expr_3[h] {res=res/e2;}))* ; expr_3 [java.util.HashMap h] returns [int res=0;] {int e;} : i:IDENT {Object aux=h.get(i.getText()); if (aux!=null) res=((Integer)aux).intValue(); } | n:NUMERO {res=new Integer(n.getText()).intValue();} | "(" e=expr_1[h] ")" {res=e;}
;
Opciones Muchos aspectos en el comportamiento de Antlr puede programarse mediante opciones. Las opciones principales en relación con el análisis sintáctico son:
k: número de tokens lookahead. Ejemplo: class Anasint extends Parser; options { k=2; } ...
importVocab: permite importar un vocabulario de tokens. Ejemplo: class Anasint extends Parser; options{ importVocab = Analex; // Importación del conjunto de tokens }
buildAST: indica si el analizador es genera o no representaciones intermedias en forma de áboles de sintaxis abstracta (ASA) Ejemplo: class Anasint extends Parser; options{ buildAST = true; //construcción automática de AST }
instrucciones : (expresion ";")*; expresion : exp_mult (("+"|"-") exp_mult)*; exp_mult : exp_base (("*"|"/") exp_base)*; exp_base : NUMERO | "(" expresion ")" ;
Análisis Sintáctico en Antlr. Ejemplos En esta sección, mostramos en 5 ejemplos los recursos de Antlr para realizar análisis sintáctico.
Ejemplo 1: Ejemplo de analizador sintáctico. Analizador léxico-sintáctico Antlr (Anasint.g) : class Anasint extends Parser;
instrucciones: (expresion ";")* {System.out.println("instrucciones reconocidas");} ; expresion : exp_base (("+"|"-") exp_base)* ; exp_base : NUMERO | "(" expresion ")" ; class Analex extends Lexer;
BLANCO : (' '|'\t'|"\r\n") {$setType(Token.SKIP);}; NUMERO : ('0'..'9')+('.'('0'..'9')+)?; OPERADOR : '+'|'-'|'*'|'/'; PARENTESIS : '('|')'; SEPARADOR : ';';
Programa que utiliza el analizador anterior (Prog.java): import java.io.*; import antlr.collections.AST; import antlr.ANTLRException; public class Prog { public static void main(String args[]){ try{ FileInputStream fis= new FileInputStream("entrada.txt"); 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"); } } }
Flujo de entrada (entrada.txt): 1+2; (3-1)+7+5; 33;
Resultado ejecución programa Prog sobre entrada.txt:
instrucciones reconocidas
Ejemplo 2: Ejemplo de analizadores construidos por separado. Analizador sintáctico Antlr (Anasint.g) : class Anasint extends Parser;
instrucciones : (expresion ";")* {System.out.println("instrucciones reconocidas");} ; expresion : exp_mult (("+"|"-") exp_mult)* ; exp_mult : exp_base (("*"|"/") exp_base)* ; exp_base : NUMERO | "(" expresion ")" ;
Analizador léxico Antlr (Analex.g) : class Analex extends Lexer; options{ importVocab = Anasint; // Importación del conjunto de tokens } BLANCO : (' '|'\t'|"\r\n") {$setType(Token.SKIP);};
NUMERO : ('0'..'9')+('.'('0'..'9')+)?; OPERADOR : '+'|'-'|'*'|'/'; PARENTESIS : '('|')'; SEPARADOR : ';';
Programa que utiliza el analizador anterior (Prog.java): import java.io.*; import antlr.collections.AST; import antlr.ANTLRException; public class Prog { public static void main(String args[]){ try{ FileInputStream fis= new FileInputStream("entrada.txt"); 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"); } } }
Flujo de entrada (entrada.txt): 1+2; (3-1)+7+5; 33;
Resultado ejecución programa Prog sobre entrada.txt:
instrucciones reconocidas
Ejemplo 3: Ejemplo de uso de predicados sintácticos para resolver indeterminismo. Analizador sintáctico Antlr (Anasint.g) con indeterminsmo en la regla instruccion : class Anasint extends Parser;
prog : (instruccion)* {System.out.println("programa reconocido");} ; instruccion : asignacion | llamada ; asignacion : IDENT ":=" expresion ";" ; llamada : IDENT "(" expresion ")" ";" ; expresion : exp_mult (("+"|"-") exp_mult)* ; exp_mult : exp_base (("*"|"/") exp_base)* ; exp_base : NUMERO | IDENT | "(" expresion ")" ;
Analizador léxico Antlr (Analex.g) : class Analex extends Lexer; options{ importVocab = Anasint; // Importación del conjunto de tokens } BLANCO : (' '|'\t'|"\r\n") {$setType(Token.SKIP);}; protected DIGITO : ('0'..'9'); protected LETRA : ('a'..'z')|('A'..'Z');
OPERADOR : '+'|'-'|'*'|'/'; PARENTESIS : '('|')'; SEPARADOR : ';'; ASIG : ":=";
NUMERO : (DIGITO)+('.'(DIGITO)+)?; IDENT : LETRA (LETRA | DIGITO)* ;
Programa que utiliza el analizador anterior (Prog.java): import java.io.*; import antlr.collections.AST; import antlr.ANTLRException; public class Prog { public static void main(String args[]){ try{ FileInputStream fis= new FileInputStream("entrada.txt"); Analex analex = new Analex(fis); Anasint anasint = new Anasint(analex); anasint.prog(); }catch(ANTLRException ae){ System.err.println(ae.getMessage()); } catch (FileNotFoundException fnfe){ System.err.println("No se encontró el fichero"); } } }
Flujo de entrada (entrada.txt): a:=3+5; f(a);
Resultado ejecución programa Prog sobre entrada.txt: line 1:13: expecting ":=", found '(' line 1:15: expecting ":=", found ')' programa reconocido
Analizador sintáctico Antlr (Anasint.g) sin indeterminismo: class Anasint extends Parser;
prog : (instruccion)* {System.out.println("programa reconocido");} ; instruccion : (IDENT ":=") => asignacion | (IDENT "(") => llamada ; asignacion : IDENT ":=" expresion ";" ; llamada : IDENT "(" expresion ")" ";" ; expresion : exp_mult (("+"|"-") exp_mult)* ; exp_mult : exp_base (("*"|"/") exp_base)* ; exp_base : NUMERO | IDENT | "(" expresion ")" ;
Flujo de entrada (entrada.txt): a:=3+5; f(a);
Resultado ejecución programa Prog sobre entrada.txt: programa reconocido
Ejemplo 4: Ejemplo de atribución Analizador sintáctico Antlr (Anasint.g): class Anasint extends Parser; options{ k=2; }
prog {java.util.HashMap h = new java.util.HashMap();} : ( declaracion [h] )* ; declaracion [java.util.HashMap h] {int e;}: e=expr_1[h] ";" {System.out.println("Expresion => "+e);} | i:IDENT ":=" e=expr_1[h] ";" { h.put(i.getText(), new Integer(e)); System.out.println("Asignacion ("+i.getText()+ ")=> "+e);} ; expr_1 [java.util.HashMap h] returns [int res=0;] {int e1,e2;} : e1=expr_2[h] {res=e1;} (("+" e2=expr_2[h] {res=res+e2;}) |("-" e2=expr_2[h] {res=res-e2;}))* ; expr_2 [java.util.HashMap h] returns [int res=0;] {int e1,e2;} : e1=expr_3[h] {res=e1;} (("*" e2=expr_3[h] {res=res*e2;}) | ("/" e2=expr_3[h] {res=res/e2;}))* ; expr_3 [java.util.HashMap h] returns [int res=0;] {int e;} : i:IDENT {Object aux=h.get(i.getText()); if (aux!=null) res=((Integer)aux).intValue(); } | n:NUMERO {res=new Integer(n.getText()).intValue();} | "(" e=expr_1[h] ")" {res=e;} ;
Analizador léxico Antlr (Analex.g) : class Analex extends Lexer; options{ importVocab = Anasint; // Importación del conjunto de tokens } BLANCO : (' '|'\t'|"\r\n") {$setType(Token.SKIP);}; protected DIGITO : ('0'..'9'); protected LETRA : ('a'..'z')|('A'..'Z');
OPERADOR : '+'|'-'|'*'|'/';
PARENTESIS : '('|')'; SEPARADOR : ';'; ASIG : ":="; NUMERO : (DIGITO)+('.'(DIGITO)+)?; IDENT : LETRA (LETRA | DIGITO)* ;
Programa que utiliza el analizador anterior (Prog.java): import java.io.*; import antlr.collections.AST; import antlr.ANTLRException; public class Prog { public static void main(String args[]){ try{ FileInputStream fis= new FileInputStream("entrada.txt"); Analex analex = new Analex(fis); Anasint anasint = new Anasint(analex); anasint.prog(); }catch(ANTLRException ae){ System.err.println(ae.getMessage()); } catch (FileNotFoundException fnfe){ System.err.println("No se encontró el fichero"); } } }
Flujo de entrada (entrada.txt): 3+5; a := 3+5; b := a +2; b:=b+1; 1;
Resultado ejecución programa Prog sobre entrada.txt: Expresion => 8 Asignacion (a)=> 8 Asignacion (b)=> 10 Asignacion (b)=> 11 Expresion => 1
Ejemplo 4: Ejemplo de Generación Automática de Árboles de Sintaxis Abstracta Analizador sintáctico Antlr (Anasint.g): class Anasint extends Parser; options{ k=2; buildAST = true; }
prog {java.util.HashMap h = new java.util.HashMap();} : ( declaracion [h] )* ; declaracion [java.util.HashMap h] {int e;}: e=expr_1[h] ";" {System.out.println("Expresion => "+e);} | i:IDENT ":=" e=expr_1[h] ";" { h.put(i.getText(), new Integer(e)); System.out.println("Asignacion ("+i.getText()+ ")=> "+e);} ; expr_1 [java.util.HashMap h] returns [int res=0;] {int e1,e2;} : e1=expr_2[h] {res=e1;} (("+" e2=expr_2[h] {res=res+e2;}) |("-" e2=expr_2[h] {res=res-e2;}))* ; expr_2 [java.util.HashMap h] returns [int res=0;] {int e1,e2;} : e1=expr_3[h] {res=e1;} (("*" e2=expr_3[h] {res=res*e2;}) | ("/" e2=expr_3[h] {res=res/e2;}))* ; expr_3 [java.util.HashMap h] returns [int res=0;] {int e;} : i:IDENT {Object aux=h.get(i.getText()); if (aux!=null) res=((Integer)aux).intValue(); } | n:NUMERO {res=new Integer(n.getText()).intValue();} | "(" e=expr_1[h] ")" {res=e;} ;
Analizador léxico Antlr (Analex.g) : class Analex extends Lexer; options{ importVocab = Anasint; // Importación del conjunto de tokens } BLANCO : (' '|'\t'|"\r\n") {$setType(Token.SKIP);}; protected DIGITO : ('0'..'9'); protected LETRA : ('a'..'z')|('A'..'Z');
OPERADOR : '+'|'-'|'*'|'/'; PARENTESIS : '('|')'; SEPARADOR : ';'; ASIG : ":="; NUMERO : (DIGITO)+('.'(DIGITO)+)?; IDENT : LETRA (LETRA | DIGITO)* ;
Programa que utiliza el analizador anterior (Prog.java): import java.io.*; import antlr.collections.AST; import antlr.*; public class Prog { public static void main(String args[]){ try{ FileInputStream fis= new FileInputStream("entrada.txt"); Analex analex = new Analex(fis); Anasint anasint = new Anasint(analex); anasint.prog(); CommonAST a = (CommonAST)anasint.getAST(); System.out.println("Resultado ASA: "+a.toStringList()); }catch(ANTLRException ae){ System.err.println(ae.getMessage()); } catch (FileNotFoundException fnfe){ System.err.println("No se encontró el fichero"); } } }
Flujo de entrada (entrada.txt): 3+5; a := 3+5; b := a +2; b:=b+1; 1;
Resultado ejecución programa Prog sobre entrada.txt: Expresion => 8 Asignacion (a)=> 8 Asignacion (b)=> 10 Asignacion (b)=> 11 Expresion => 1 Resultado ASA: 3 + 5 ; a : = 3 + 5 ; b : = a + 2 ; b := b + 1 ; 1 ;