Ejemplo en Java - Analizador Lexico Sintactico
Ejemplo en Java - Analizador Lexico Sintactico
Ejemplo en Java - Analizador Lexico Sintactico
JLEX
JLex es un generador de analizadores léxicos, escrito en Java, para Java. JLex fue desarrollado
por Elliot Berk en la Universidad de Princeton. Para más información visitar la página oficial
de JLex.
La principal tarea de un analizador léxico es leer los caracteres de entrada del programa
patrón para un token y que el analizador léxico identifica como una instancia de este
tóken.
Un patrón es una descripción de la forma que pueden tomar los lexemas de un token.
En JLex se definen los patrones de los diferentes tokens que se desean reconocer, estos
patrones pueden definirse a través de expresiones regulares. Además JLex cuenta con
múltiples opciones, una muy importante es su capacidad para integrarse con generadores
El analizador sintáctico obtiene una cadena de tokens del analizador léxico y verifica que
dicha cadena pueda generase con la gramática para el lenguaje fuente. Una gramática
Pre-requisitos
Para este ejemplo hace falta que tengamos instalado:
Java Development Kit (JDK)
Netbeans
Debemos asegurarnos que la carpeta bin del JDK haya sido agregada a nuestra variable de
entorno Path, para ello vamos a la configuración de dicha variable de entorno (Clic derecho
en This PC → Properties → Advanced system settings → Environment Variables → Variable
Path → Edit) y si no existe agregamos la ruta a la carpeta bin del JDK, que en mi caso es:
C:\Program Files\Java\jdk1.8.0_152\bin
Descargar JLex
Lo primero que haremos será descargar JLex, para ello vamos a la página oficial y
Descargar Cup
Para descargar Cup, vamos a la página oficial y descargamos la última versión del software:
relacionado con el analizador léxico y sintáctico (Clic derecho en Source Packages → New
→ Java Package).
Para ello creamos una carpeta lib dentro de la carpeta de nuestro proyecto.
Para importar el archivo jar, vamos a Netbeans y damos clic derecho en la pestaña Libraries
de nuestro proyecto → Add JAR/Folder… luego buscamos el archivo jar en la carpeta lib
En el archivo “Lexico” incluiremos todo el código que le indicará a Jlex lo que debe hacer. El
%%
%class Lexico
%public
%line
%char
%cup
%unicode
%ignorecase
%init{
yyline = 1;
yychar = 1;
%init}
BLANCOS=[ \r\t]+
D=[0-9]+
DD=[0-9]+("."[ |0-9]+)?
%%
\n {yychar=1;}
{BLANCOS} {}
{D} {return new Symbol(sym.ENTERO,yyline,yychar, yytext());}
.{
System.out.println("Este es un error lexico: "+yytext()+
", en la linea: "+yyline+", en la columna: "+yychar);
}
En las primeras líneas indicamos a Jlex que la clase estará en el paquete analizadores y que
package analizadores;
import java_cup.runtime.Symbol;
El analizador no será case sensitive, es decir, no le importa si las letras son mayúsculas
o minúsculas
%%
%class Lexico
%public
%line
%char
%cup
%unicode
%ignorecase
decir, lo que va dentro del constructor del analizador léxico.En este caso indicamos dentro
La variable yyline, que lleva la cuenta del número de linea por el que va el analizador
valdrá inicialmente 1.
La variable yychar, que lleva la cuenta del número de carácter por el que va el
%init{
yyline = 1;
yychar = 1;
%init}
Luego se escriben algunas expresiones regulares que son almacenadas en macros, que
básicamente son variables que almacenan los patrones, en este caso se definen las macros:
BLANCOS, D y DD. Los patrones para cada una son los siguientes:
BLANCOS: Expresión regular que reconoce uno o muchos espacios en blanco, retornos
de carro o tabuladores.
BLANCOS=[ \r\t]+
D=[0-9]+
DD=[0-9]+("."[ |0-9]+)?
%%
Por último se definen todas las reglas léxicas, en las que indicamos el patrón que reconocerá
y dentro de llaves lo que debe hacer cuando lo reconozca. En la mayoría de los casos se
retorna un objeto de tipo Symbol, que vendría siendo un token, este se instancia con el tipo,
la fila en la que se encontró, la columna en la que se encontró y el lexema en específico que
se reconoció, este se obtiene mediante yytext(). Dentro de las llaves podríamos incluir el
código java que quisiéramos. Vemos que al reconocer el patrón BLANCOS no se hace nada
\n {yychar=1;}
{BLANCOS} {}
{D} {return new Symbol(sym.ENTERO,yyline,yychar, yytext());}
{DD} {return new Symbol(sym.DECIMAL,yyline,yychar, yytext());}
.{
System.out.println("Este es un error lexico: "+yytext()+
", en la linea: "+yyline+", en la columna: "+yychar);
En el archivo “Sintáctico” incluiremos todo el código que le indicará a Cup lo que debe
package analizadores;
import java_cup.runtime.*;
parser code
{:
/**
* Método al que se llama automáticamente ante algún error sintactico.
**/
Ing. Juan Abel Callupe Cueva 11
public void syntax_error(Symbol s){
System.out.println("Error Sintáctico en la Línea " + (s.left) +
" Columna "+s.right+ ". No se esperaba este componente: " +s.value+".");
}
/**
* Método al que se llama automáticamente ante algún error sintáctico
* en el que ya no es posible una recuperación de errores.
**/
public void unrecovered_syntax_error(Symbol s) throws java.lang.Exception{
System.out.println("Error síntactico irrecuperable en la Línea " +
(s.left)+ " Columna "+s.right+". Componente " + s.value +
" no reconocido.");
}
:}
ini::=instrucciones;
instrucciones ::=
instruccion instrucciones
| instruccion
| error instrucciones
;
instruccion ::=
REVALUAR CORIZQ expresion:a CORDER PTCOMA{:System.out.println("El valor de la expresión
expresion ::=
MENOS expresion:a {:RESULT=a*-1;:}%prec UMENOS
| expresion:a MAS expresion:b {:RESULT=a+b;:}
| expresion:a MENOS expresion:b {:RESULT=a-b;:}
| expresion:a POR expresion:b {:RESULT=a*b;:}
| expresion:a DIVIDIDO expresion:b {:RESULT=a/b;:}
| ENTERO:a {:RESULT=new Double(a);:}
| DECIMAL:a {:RESULT=new Double(a);:}
| PARIZQ expresion:a PARDER {:RESULT=a;:}
En las primeras líneas indicamos a Cup que la clase estará en el paquete analizadores y que
package analizadores;
import java_cup.runtime.*;
Luego viene la sección “parser code”, en la que se programan acciones propias del parser o
analizador sintáctico que se va a generar, en este caso se programa lo que se debe hacer
ante un error sintáctico y ante un error sintáctico irrecuperable.
parser code
{:
/**
* Método al que se llama automáticamente ante algún error sintactico.
**/
public void syntax_error(Symbol s){
System.out.println("Error Sintáctico en la Línea " + (s.left) +
" Columna "+s.right+ ". No se esperaba este componente: " +s.value+".");
}
/**
* Método al que se llama automáticamente ante algún error sintáctico
Luego se definen los terminales, a estos se les puede indicar un tipo, en este caso todos son
de tipo String, si no se indicara un tipo, los terminales serían por defecto de tipo Object.
terminal String PTCOMA,PARIZQ,PARDER,CORIZQ,CORDER;
terminal String MAS,MENOS,POR,DIVIDIDO;
terminal String ENTERO;
terminal String DECIMAL;
terminal String UMENOS;
terminal String REVALUAR;
Existe un terminal por cada tipo de token que el analizador léxico devuelve. Todos estos
tipos estarán definidos en la clase “sym”, que se genera automáticamente y de la que se
hablará más adelante.
Luego viene la declaración de los no terminales, a los que también se les puede indicar un
tipo específico, si no se les indica un tipo, estos son por defecto de tipo Object.
non terminal ini;
non terminal instrucciones;
non terminal instruccion;
non terminal Double expresion;
RESULT puede ser cualquier objeto, por ejemplo si quisiéramos que RESULT almacenara
varios números enteros hacemos una clase Nodo que contenga muchas variables de tipo
entero y declaramos los no terminales para que sean de tipo Nodo, entonces el RESULT que
sintetizarán dichos no terminales serán de tipo Nodo.
ini::=instrucciones;
instrucciones ::=
instruccion instrucciones
| instruccion
| error instrucciones
;
instruccion ::=
REVALUAR CORIZQ expresion:a CORDER PTCOMA{:System.out.println("El valor de la expresión
es: "+a);:}
;
expresion ::=
MENOS expresion:a {:RESULT=a*-1;:}%prec UMENOS
| expresion:a MAS expresion:b {:RESULT=a+b;:}
| expresion:a MENOS expresion:b {:RESULT=a-b;:}
| expresion:a POR expresion:b {:RESULT=a*b;:}
| expresion:a DIVIDIDO expresion:b {:RESULT=a/b;:}
| ENTERO:a {:RESULT=new Double(a);:}
| DECIMAL:a {:RESULT=new Double(a);:}
| PARIZQ expresion:a PARDER {:RESULT=a;:}
;
Creamos un paquete llamado JLex dentro del paquete analizadores, este almacenará la
En la carpeta del paquete JLex copiamos el archivo Main.java que descargamos de la página
oficial de JLex.
Creamos un paquete llamado Cup dentro del paquete analizadores, este almacenará la
para poder disponer de la herramienta para generar nuestro analizador léxico, la segunda le
indica a la herramienta Jlex que debe generar un analizador léxico en base al código fuente
que se encuentra en el archivo “Lexico”, la tercera indica a Cup que la clase que debe generar
para el analizador sintáctico se llamará “Sintactico” y que debe generarse en base al código
javac JLex/Main.java
java JLex.Main Lexico
java -jar Cup/java-cup-11b.jar -parser Sintactico Sintactico
Para ejecutarlo solo vamos a Netbeans, damos clic derecho sobre el archivo y seleccionamos
la opción Run. Al finalizar la ejecución del archivo, veremos en la consola de Netbeans una
cd
'C:/Users/erick/OneDrive/Documentos/NetBeansProjects/ProyectoCupJlexWindows/src/analizadores'
C:/Users/erick/OneDrive/Documentos/NetBeansProjects/ProyectoCupJlexWindows/src/analizadores/c
ompilar.bat
C:\Users\erick\OneDrive\Documentos\NetBeansProjects\ProyectoCupJlexWindows\src\analizadores>ja
vac JLex/Main.java
C:\Users\erick\OneDrive\Documentos\NetBeansProjects\ProyectoCupJlexWindows\src\analizadores>ja
va JLex.Main Lexico
C:\Users\erick\OneDrive\Documentos\NetBeansProjects\ProyectoCupJlexWindows\src\analizadores>ja
------- CUP v0.11b 20160615 (GIT 4ac7450) Parser Generation Summary -------
sintáctico también. Veremos que se han creado tres nuevos archivos en el paquete
La clase sym.java, sirve como puente entre la clase Lexico.java y Sintactico.java, por ejemplo,
Symbol e indica que es de tipo número entero por medio de la constante “sym.ENTERO”,
que se genera dentro de la clase sym.java y esta constante se genera porque en el archivo
de entrada para Cup se indicó que existe un terminal llamado ENTERO. Entonces tanto el
analizador léxico como el sintáctico hacen referencia a los tokens de tipo número entero con
la constante “sym.ENTERO”. Básicamente eso es sym.java, una clase con muchas constantes
estáticas a las que acceden ambos analizadores para poder integrarse y ejecutar sus tareas
exitosamente.
Dentro de la carpeta del proyecto crearé un archivo de entrada llamado “entrada.txt”. Que
Clase principal
Dentro de la clase principal solo tenemos el método main y el método interpretar que lee el
contenido del archivo que se encuentra en el path que se le indica y ejecuta análisis léxico y
análisis sintáctico, en el transcurso del analisis sintáctico se mandan a imprimir en consola
los resultados de las expresiones aritméticas analizadas, por lo que al final del análisis
tendremos todos los resultados de las operaciones en consola. A continuación se muestra el
código de la clase principal.
/*
* Ejemplo desarrollado por Erick Navarro
* Blog: e-navarro.blogspot.com
* Julio - 2018
*/
package proyectocupjlexwindows;
import java.io.FileInputStream;
/**
* @param args argumentos de la linea de comando
*/
public static void main(String[] args) {
interpretar("entrada.txt");
}
/**
* Método que interpreta el contenido del archivo que se encuentra en
el path
* que recibe como parámentro
* @param path ruta del archivo a interpretar
*/
private static void interpretar(String path) {
analizadores.Sintactico pars;
try {
pars=new analizadores.Sintactico(new analizadores.Lexico(new
FileInputStream(path)));
pars.parse();
} catch (Exception ex) {
System.out.println("Error fatal en compilación de entrada.");
System.out.println("Causa: "+ex.getCause());
}
}
Fuentes consultadas: