Tecnolgico Nacional de MxicoInstituto Tecnolgico de Pachuca
Ingeniera en Sistemas Computacionales
Materia: Lenguaje Y Autmatas I
Alumno: Miguel Castillo LpezNo. De control: 14200855
Unidad 6: Investigacin sobre el generador de analizador
sintctico YACC y JavaCC
GENERADOR DE ANALIZADOR SINTCTICO YACC
Yacc es un programa para generar analizadores sintcticos. Las
siglas del nombre significan Yet Another Compiler-Compiler, es
decir, "Otro generador de compiladores ms". Genera un analizador
sintctico (la parte de un compilador que comprueba que la
estructura del cdigo fuente se ajusta a la especificacin sintctica
del lenguaje) basado en una gramtica analtica escrita en una
notacin similar a la BNF. Yacc genera el cdigo para el analizador
sintctico en el Lenguaje de programacin C.
Fue desarrollado por Stephen C. Johnson en AT&T para el
sistema operativo Unix. Despus se escribieron programas
compatibles, por ejemplo Berkeley Yacc, GNU visn, MKS yacc y
Abraxas yacc (una versin actualizada de la versin original de
AT&T que tambin es software libre como parte del proyecto de
OpenSolaris de Sun). Cada una ofrece mejoras leves y caractersticas
adicionales sobre el Yacc original, pero el concepto ha seguido
siendo igual. Yacc tambin se ha reescrito para otros lenguajes,
incluyendo Ratfor, EFL, ML, Ada, Java, y Limbo.
Puesto que el analizador sintctico generado por Yacc requiere un
analizador lxico, se utiliza a menudo conjuntamente con un
generador de analizador lxico, en la mayora de los casos lex o
Flex, alternativa del software libre. El estndar de IEEE POSIX
P1003.2 define la funcionalidad y los requisitos a Lex y Yacc.
La versin Yacc de AT&T se convirti en software libre;
VERSIONES
Versin Yacc de AT&T ML-Yacc, una versin de yacc para el
lenguaje Standard ML. Yecc, una versin de yacc para Erlang.
MODO DE USO
Construccin de un traductor usando Yacc:
Partes de las que consta un programa fuente en Yacc:
{declaraciones}%%{reglas}%%{rutinas de apoyo en C}
-Declaraciones
1) Declaraciones ordinarias en C, delimitadas por %{ y %}.2)
Declaraciones de los componentes lxicos de la gramtica (esto se
explica ms adelante).
Podra estar vaca
-Reglas de traduccin
Cada una de ellas consta de una produccin de la gramtica y la
accin semntica asociada.
Es decir,
| | ... |
pasara a ser en Yacc:
: {accin semntica 1} | {accin semntica 2} ... | {accin semntica
n} ;
Un carcter simple entre comillas c se considera como el smbolo
terminal c. Las cadenas sin comillas de letras y dgitos no
declaradas como componentes lxicos se consideran no terminales. El
primer lado izquierdo se considera como smbolo inicial por defecto,
o bien se declara : %start smbolo Una accin semntica es una
secuencia de proposiciones en C, dnde $$ se refiere al valor del
atributo asociado con el no terminal del lado izquierdo, mientras
que $i se refiere al valor asociado con el i-simo smbolo gramatical
del lado derecho. Se ejecuta siempre que se reduzca por la
produccin asociada. Por defecto es {$$=$1;}. Una accin semntica no
tiene por qu venir al final de su regla. Yacc permite que una accin
sea escrita en mitad de una regla. Esta regla devuelve un valor,
accesible de forma normal por las acciones que estn a su derecha,
que pueden acceder a los valores devueltos por los smbolos a su
izquierda
Ejemplo: A:B {$$=1;} C {x=$2; y=$3;} El efecto es poner x a 1 e
y al valor devuelto por C.
Las acciones que no terminan una regla son manejadas por Yacc
como si fueran un nuevo nombre de no-terminal, con una nueva regla
para l con el String vaco en su parte derecha, y, como accin de esa
regla, ella misma.
Es decir, el ejemplo anterior lo maneja como si se hubiera
escrito as:
$ACT : /* empty */ {$$=1;}; A : B $ACT C {x=$2; y=$3;};
NOTA: Puede haber conflictos cuando ocurre una accin interior en
una regla antes de que el parser pueda estar seguro de qu regla est
siendo reducida.En muchas aplicaciones, la salida no viene dada
directamente con las acciones; en su lugar, una estructura de
datos, tal como un rbol de anlisis, se construye en memoria y se
aplican transformaciones en l antes de que la salida sea generada.
Los rboles de anlisis son particularmente fciles de construir, si
se tienen rutinas para realizar y mantener la estructura de rbol
deseada. Ejemplo.- Tenemos una funcin C que se llama nodo, de forma
que
nodo(L,n1,n2)
crea un nodo con etiqueta L y descendientes n1 y n2, y devuelve
el ndice del nuevo nodo. El rbol de anlisis podra ser realizado
suministrando acciones en la especificacin como
expr : expr '+' expr {$$=nodo('+',$1,$3);}
Pueden definirse tambin otras variables para ser usadas por las
acciones. Tanto las declaraciones como las definiciones deben
aparecer en la seccin de declaraciones, entre %{ y %}. Tienen mbito
global. Deben evitarse los nombres de variables que empiecen por
yy, pues los nombres de variables internas de Yacc comienzan de
esta forma todos.
EJEMPLOS
Mini calculadora
A continuacin, vamos a analizar un ejemplo sencillo de una
verdadera especificacin de yacc, que es la gramtica para una
calculadora sencilla que permite hacer operaciones como suma,
resta, multiplicacin, divisin y exponente.
%{#include %}
%union{ double dval;}
%token NUMBER %token PLUS MINUS TIMES DIVIDE POWER%token
LEFT_PARENTHESIS RIGHT_PARENTHESIS%token END
%left PLUS MINUS%left TIMES DIVIDE%left NEG%right POWER
%type Expression%start Input
%%
Input:Line| Input Line ;
Line:END | Expression END { printf("Result: %f\n",$1); } ;
Expression:NUMBER { $$=$1; }
| Expression PLUS Expression { $$=$1+$3; } | Expression MINUS
Expression { $$=$1-$3; } | Expression TIMES Expression { $$=$1*$3;
} | Expression DIVIDE Expression { $$=$1/$3; } | MINUS Expression
%prec NEG { $$=-$2; } | Expression POWER Expression {
$$=pow($1,$3); } | LEFT_PARENTHESIS Expression RIGHT_PARENTHESIS {
$$=$2; } ;
%%int yyerror(char *s) { printf("%s\n",s);}
int main(void) { yyparse();}
Definiciones
En esta primera seccin, al igual que en lex, incluimos las
libreras que usaremos en el programa, definiciones de los tokens,
tipos de datos y precedencia de la gramtica.
%union
Esta definicin, se traduce a una union de C que a su vez dar el
tipo de dato a una variable global de nombre yylval que ser de
donde yacc tomara los datos a procesar, en la union se definen
miembros cuyos correspondientes tipos de datos sern usados para dar
el tipo de dato a los tokens como se explicara en la siguiente
seccin. %union se traduce de la siguiente forma :
En yacc :
%union{ double dval;}
En C :
typedef union{ double dval;} YYSTYPE;
Con esta definicin, yacc declara algunas uniones de este tipo,
de las cuales la mas importante es :
YYSTYPE yylval;que ser usada en la especificacin de lex, del
mismo programa para asignarle valor a los tokens que yacc usara
para realizar operaciones. Esta estructura puede llegar a ser muy
compleja, y para saber de que tipo es cada token devuelto por
yylex(), se usan las definiciones %token y %type.
%token y %type
%token sirve para definir los tokens que hay, y si es necesario,
el tipo de dato que usan, todos los tokens son tomados como smbolos
terminales, lo cual veremos mejor reflejado en la seccin de reglas,
estos tambin tienen el objetivo de servir como etiquetas que
yylex() regresa a yacc para identificar el token que se ha ledo
recientemente.Su uso es como sigue :
%token [] ETIQUETA1 [ETIQUETA2 ... ETIQUETAn]
Donde todo lo que esta entre [ y ] es opcional.
: Indica el miembro al que sern mapeados los tokens en la union
yylval dentro de lex.
ETIQUETAS : Estos son los nombres con los que se identificaran
los tokens mismos, que sern traducidos en C como nmeros en
instrucciones #define del preprocesador de C.
%type es anlogo a %token, solo que este define el tipo de dato
para smbolos no terminales de nuestra gramtica, la nica diferencia
es que el tipo de dato a usar es obligatorio.En nuestro ejemplo
:
%token NUMBER %token PLUS MINUS TIMES DIVIDE POWER%token
LEFT_PARENTHESIS RIGHT_PARENTHESIS%token END
...
%type ExpresinLa primera lnea indica que el token NUMERO ser del
tipo de miembro de dval, es decir, un doubl.Las siguientes tres
lneas, son para definir algunos tokens mas que sern usados en la
gramtica, pero no necesitan un tipo de dato ni un miembro en yylval
asociado.En la ultima lnea definimos el tipo de dato que usara
nuestro no terminal Expresin.%left y %right
El siguiente paso, es definir el tipo de precedencia de nuestros
tokens operadores, en este punto tenemos dos factores, la
precedencia por si misma, y la agrupacin de los
operadores.Precedencia
La precedencia es asignada en orden inverso al que aparecen, es
decir, el ultimo operador declarado, tiene mayor precedencia que el
anterior y as sucesivamente.Asociatividad
%left y %right indican si el operador se agrupa a la derecha o a
la izquierda, por ejemplo, en el caso de POWER (Exponente) debe
asociarse a la derecha, por que buscamos que se resuelva de ese
modo, de derecha a izquierda, por ejemplo :Buscamos que
4^3^5^2^9
sea evaluado as :
4^(3^(5^(2^9)))
Por lo contrario, las sumas y restas queremos resolverlas de
izquierda a derecha:Buscamos que
4-3+5+2-9
sea evaluado as :
(((4-3)+5)+2)-9
Usar este tipo de declaraciones es importante para disminuir la
posibilidad de ambigedades en el lenguaje generado.
%start
En algunos casos es conveniente indicarle a yacc cual es el
smbolo (no terminal) inicial a la hora de hacer el parseo, es
decir, el smbolo que se trata de reducir, si esta opcin no es
especificada, yacc toma al primer smbolo de la seccin de reglas
como smbolo inicial. En nuestro ejemplo, se presentan ambos casos,
nuestro smbolo inicial "Input" se encuentra al inicio del archivo y
tambin esta declarado como smbolo inicial.
%start Input
Reglas
En esta parte finalmente llegamos a la definicin de la gramtica,
ac podemos observar que cada smbolo se define con su nombre,
seguido de dos puntos ":" seguidos de varios smbolos que
conformaran su composicin gramatical que en caso de tener varias
opciones, son separados por "|" (or) indicando que se tienen varias
opciones para reducir ese smbolo y para terminar cada regla, un
";".
Ejemplo :Si tomamos la gramtica que definimos al principio de
esta seccin :
Numero + Numero - Numero
Y la transformamos a una regla de yacc, se vera como esto:
Expresion: NUMERO '+' Expresion{$$ = $1 + $3;}| NUMERO '-'
Expresion{$$ = $1 - $3;}| NUMERO{$$ = $1;};
en el ejemplo ya transformado a una regla gramatical de yacc,
podemos ver que ya se especifica que hacer con los smbolos de la
gramtica una vez que son resueltos en la parte de cdigo de C. En
este ejemplo, Expresion es el smbolo no terminal que se esta
definiendo de manera recursiva, el valor que tomara una vez
resuelto es el valor asignado a la variable $$, que traducida a C
es una variable mapeada al smbolo no terminal, $1, $2 y $3 son
variables que son mapeadas al valor de los smbolos de cada lnea de
cada regla, estas son numeradas de izquierda a derecha.
Ejemplo :En este segmento de regla :
Expresion: NUMERO '+' Expresion
Expresion equivale a $$NUMERO equivale a $1'+' equivale a
$2yExpresion (la parte recursiva) equivale a $3
todo esto claro, en la parte de acciones en C para cada lnea de
la regla en yacc.En el ejemplo tambin podemos encontrar el uso de
%prec que sirve para cambiar la precedencia de un operador
dependiendo del contexto en el ejemplo, le estamos dando una ms
alta precedencia a la operacin "menos" cuando esta en un contexto
unario, que a la mayora de los operadores excepto el POWER
(exponente) :...| MINUS Expression %prec NEG { $$=-$2;
}...Reduccin
Yacc reduce sus reglas generando un parse tree (no
literalmente), y va resolviendo cada regla completa tan pronto como
puede, lo cual nos trae un detalle de diseo de gramaticas en yacc,
y es la diferencia entre especificar la recursividad por la derecha
o por la izquierda, para expresiones muy sencillas que generen un
parse tree pequeo no hay ningn problema pero para casos donde la
reduccin es compleja, puede desbordar la pila ya que cuando la
recursin es derecha, para resolverla, tiene que guardar los datos
de la izquierda, y si estos son demasiados, no puede manejarlos.
Por lo contrario, cuando la recursin es izquierda, no tiene que
guardar datos que no va a utilizar por que recorre el rbol de
izquierda a derecha y resuelve las reglas tan pronto como puede. En
el ejemplo anterior tenemos la recursin por la derecha, un anlogo
recurrente por la izquierda seri como este :
Expresion: Expresion '+' NUMERO {$$ = $1 + $3;}| Expresion '-'
NUMERO {$$ = $1 - $3;}| NUMERO{$$ = $1;};
Especificacin de Lex para el ejemplo
Para que el Ejemplo pueda funcionar, al igual que cualquier otro
programa en yacc, necesita un tokenizer, y a continuacin tenemos su
tokenizer correspondiente escrito en lex.
%{#include "y.tab.h"#include #include %}
white [ \t]+digit [0-9]integer {digit}+
%%
{white} { /* Ignoramos espacios en blanco */
}"exit"|"quit"|"bye"{printf("Terminando
programa\n");exit(0);}{integer}{ yylval.dval=atof(yytext);
return(NUMBER); }
"+" return(PLUS);"-" return(MINUS);"*" return(TIMES);"/"
return(DIVIDE);"^" return(POWER);"(" return(LEFT_PARENTHESIS);")"
return(RIGHT_PARENTHESIS);"\n" return(END);
%%
Acerca de la seccin de definiciones de este lexer, lo nico
relevante que podemos mencionar es la lnea de C que dice :
#include "y.tab.h"
esta lnea incluye al archivo y.tab.h que contiene algunas de las
definiciones de yacc que lex necesita para poder interactuar con
el, entre las mas importantes se encuentran definidas todas las
etiquetas de los tokens, como PLUS, MINUS, NUMBER, etctera. Estas
constantes son los valores que yylex() regresara a yyparse() (la
funcin del parser de yacc) para identificar el tipo de token que
recin se ha ledo. En la seccin de reglas, en la parte del cdigo,
podemos ver como al final de cada regla, se hace un return
especificando la etiqueta que fue declarada como %token o como
%left/%rigth en la especificacin yacc.Para compilar y correr este
ejemplo en sistemas UNIX o similares :
$ lex ejem1.1.l$ yacc -d ejem1.1.y$ cc -o ejem1.1 lex.yy.c
y.tab.c -ly -ll -lm$ ejem1.125*5-5Result: 120.0000005^2*2Result:
50.0000005^(2*2)Result: 625.000000byeTerminando
programa$Subrutinas
En esta ultima seccin, es posible reimplementar, siguiendo la
misma idea de lex, algunas funciones que pueden ser tiles en algn
momento dado o declarar nuevas funciones para usar dentro de
nuestro cdigo o nuestras reglas, no hay mucho que reimplementat a
este nivel (yacc) a menos que sea con propsitos realmente
especficos. Las funciones mas comnmente implementadas son main() e
yyerror(), la primera se usa para personalizar el programa con
mensajes antes o despus del parser, o para llamarlo varias veces en
el cdigo y la segunda la ocupa yyparse() cada vez ue encuentra un
error de sintaxis, en este ejemplo, se incluyen ambas con su
contenido mnimo.
GENERADOR DE ANALIZADOR SINTCTICO JAVACC
JavaCC (Java Compiler Compiler) es un generador de analizadores
sintcticos de cdigo abierto para el lenguaje de programacin Java.
JavaCC es similar a Yacc en que genera un parser para una gramtica
presentada en notacin BNF, con la diferencia de que la salida es en
cdigo Java. A diferencia de Yacc, JavaCC genera analizadores
descendentes (top-down), lo que lo limita a la clase de gramticas
LL(K) (en particular, la recursin desde izquierda no se puede
usar). El constructor de rboles que lo acompaa, JJTree, construye
rboles de abajo hacia arriba (bottom-up).JavaCC est licenciado bajo
una licencia BSD.En 1996, Sun Microsystems liber un parser llamado
Jack. Los desarrolladores responsables de Jack crearon su propia
compaa llamada Metamata y cambiaron el nombre Jack a JavaCC.
Metamata se convirti en WebGain. Despus de que WebGain finalizara
sus operaciones, JavaCC se traslad a su ubicacin actual.
VERSIONES
todas las modificaciones que han tenido lugar desdeLA LIBERACIN
DE LA VERSIN 0.5 DE OCTUBRE DE 1996 .
Durante la transicin de 0,5 a 6,0 , se han producidoLAS
SIGUIENTES VERSIONES INTERMEDIOS :
0.6. - 100.6. - 90.6. - 80.6 ( Beta1 )0.6 ( Beta 2
)0.60.6.10.7pre10.7pre20.7pre30.7pre40.7pre50.7pre60.7pre70.70.7.10.8pre10.8pre21.01.22.02.13.03.13.24.04.14.26.0
MODO DE USO
ESTRUCTURA GENERAL DE UN PROGRAMA EN JAVACCCualquier cdigo
escrito para JavaCC obedece a la siguiente estructura:
El fichero de entrada comienza con una lista de opciones, la
cual es opcional.Seguidamente nos encontramos la unidad de
compilacin java la cual se encuentra encerrada entre
PARSER_BEGIN(nombre) y PARSER_END(nombre). Despus nos encontramos
con una lista de reglas de produccin (cada una de estas partes son
claramente distinguibles en el ejemplo).
El nombre que sigue a PARSER_BEGIN y a PARSER_END debe ser el
mismo, yste identifica al analizador sintctico que va a ser
generado con posterioridad. Por ejemplo si nuestro nombre es
Gramtica, se generan los siguientes archivos: Gramatica.java: El
analizador sintctico. GramaticaTokenManager.java: gestor de tokens
para el analizador lxico. GramaticaConstants.java: El analizador
lxico no maneja los tokens por el nombre que especificamos nosotros
en el cdigo, sino que a cada uno de ellos le asigna un nmero. En
este archivo se encuentra cada uno de los tokens junto con el nmero
que le ha sido asignado por el analizador lxico.
Adems de stos, tambin se generan otros ficheros (Token.java y
ParseException.java, ASCII_CharStream.java, TokenMgrError.java),
pero estos ficheros son los mismos para cualquier gramtica y pueden
conservarse de una para otra.
Entre PARSER_BEGIN y PARSER_END, se encuentra una unidad de
compilacin Java (el contenido completo de un archivo escrito en
Java). sta puede ser cualquier cdigo Java siempre que contenga una
declaracin de clase cuyo nombre sea el mismo que el analizador
sintctico que va a ser generado.(Gramtica en el ejemplo anterior).
Esta parte del archivo de gramtica tiene esta forma:
JavaCC no realiza un chequeo minucioso de la unidad de
compilacin por lo que es posible que un fichero de gramtica pase el
test de JavaCC y genere los archivos correspondientes y luego
produzca errores al ser compilados.El archivo correspondiente al
analizador sintctico que es generado por JavaCCcontiene la unidad
de compilacin Java en su totalidad, adems del cdigo generado por el
analizador sintctico, el cual se incluye al final de su clase. En
el caso del ejemplo la siguiente forma:
El cdigo del analizador sintctico incluye la declaracin de un
mtodo pblico correspondiente a cada no terminal de la gramtica. La
realizacin del anlisis sintctico a partir de un no terminal se
consigue invocando al mtodo correspondiente a ese no terminal.
EJEMPLOS
Construir un analizador lxico con JavaCC.
En la programacin, un analizador lxico es la parte de un
compilador o analizador (parser) que trata de descomponer el
lenguaje proporcionado como entrada en tokens.Un token es la unidad
mnima con significado. Tokens habituales son los identificadores,
enteros, flotantes, constantes, etc.Para el desarrollo,
utilizaremos una herramienta increblemente til, JavaCC. Mediante
expresiones regulares podemos definir cmodamente los tokens de
nuestro lenguaje.
Caso prctico, construir analizador lxico para un determinado
lenguaje de programacin:
Las especificaciones de nuestro lenguaje son:Tokens:Constantes:
Cadenas: Caracteres entrecomillados, ejemplo: "cadena" Enteros:
Nmeros positivos, ejemplo: 234 o 0 Lgicas: TRUE y
FALSEIdentificadores: Todos los identificadores son una secuencia
de letras (a-zA-Z) y nmeros que obligatoriamente deben comenzar con
una letra (y no un nmero).Los identificadores que se refieran a
cadenas terminarn en "$".Palabras reservadas:En el lenguaje hay
palabras reservadas que darn vida al lenguaje, estas sern: "not,
if, end, let, call, then, case, else, input, print, select y
static".Adems el lenguaje ser "case insensitive" o lo que es lo
mismo, un identificador llamado "id" apuntar al mismo lugar que
"Id", "iD" o "ID".De igual forma ser para las palabras
reservadas.
Cdigo JavaCC (exparser.jj):
options { IGNORE_CASE = true;}PARSER_BEGIN(ExampleParser)
public class ExampleParser { //Ejecucin del analizador public
static void main ( String args [ ] ) {
//Inicializacin del analizador ExampleParser parser;
if(args.length == 0){ System.out.println ("ExampleParser:
Leyendo de la entrada estandar ..."); parser = new
ExampleParser(System.in); } else if(args.length == 1){
System.out.println ("ExampleParser: Leyendo de fichero " + args[0]
+ " ..." ); try { parser = new ExampleParser(new
java.io.FileInputStream(args[0])); }
catch(java.io.FileNotFoundException e) { System.out.println
("ExampleParser: El fichero " + args[0] + " no ha sido
encontrado."); return; } } else { System.out.println
("ExampleParser: Debes utilizarlo de una de las siguientes
formas:"); System.out.println (" java ExampleParser < fichero");
System.out.println ("Or"); System.out.println (" java ExampleParser
fichero"); return ; } try { compilador.Start(); System.out.println
("ExampleParser: La entrada ha sido leida con xito."); }
catch(ParseException e){ System.out.println ("ExampleParser: Ha
ocurrido un error durante el anlisis."); System.out.println
(e.getMessage()); } catch(TokenMgrError e){ System.out.println
("ExampleParser: Ha ocurrido un error."); System.out.println
(e.getMessage()); } } }PARSER_END(ExampleParser)
//ESTRUCTURAS Y CARACTERES DE ESCAPESKIP : { " "| "\t"| "\n"|
"\r"| }//TOKENS ESTTICOSTOKEN : { | | | }//PALABRAS RESERVADASTOKEN
: { | | | | | | | | | | | | }//TOKEN IDENTIFICADORTOKEN : { | }
//UNIDAD PRINCIPALvoid Start () : {}{ ( INTEGER_CONSTANT |
STRING_CONSTANT | LOGIC_CONSTANT | NOT | IF | END | SUB | LET |
CALL | THEN | CASE | ELSE | INPUT | PRINT | SELECT | STATIC |
IDENTIFIER )* }
Para compilar este fichero, se debe hacer con "javacc" y
posteriormente con "javac":
javacc exparser.jjjavac *.java
Para ejecutar el programa:
java ExampleParser fichero
CONCLUSIN
El analizador sintctico ya se a YACC o JavaCC recibe los tokens
o palabras generadas por el analizador lxico y determina si la
secuencia u orden que presentan es correcta y permitida por el
lenguaje. La salida que generar ser el rbol sintctico. Gracias a
estas excelentes herramientas, nosotros como programadores no
tenemos que reinventar la rueda cuando de crear un analizador
sintctico se trate, las numerosas opciones que nos brindan los
generadores hacen que esta tarea sea mas fcil.
REFERENCIAShttp://es.wikipedia.org/wiki/Yacchttp://www.lcc.uma.es/~galvez/theme/IntroduccionAJavaCC.pdfhttp://kiwwito.com/construir-un-analizador-lexico-con-javacc