GENERADOR DE ANALIZADOR SINTÁCTICO YACC
Yacc es un programa para generar analizadores sintácticos. Las siglas del nombre significan Yet Another Compiler-Compiler, es decir, "Otro generador de compiladores más". Genera un analizador sintáctico (la parte de un compilador que comprueba que la estructura del código fuente se ajusta a la especificación sintáctica del lenguaje) basado en una gramática analítica escrita en una notación similar a la BNF. Yacc genera el código para el analizador sintáctico en el Lenguaje de programación C. Fue desarrollado por Stephen C. Johnson en AT&T para el sistema operativo Unix. Después se escribieron programas compatibles, por ejemplo Berkeley Yacc, GNU bison, MKS yacc y Abraxas yacc (una versión actualizada de la versión original de AT&T que también es software libre como parte del proyecto de OpenSolaris de Sun). Cada una ofrece mejoras leves y características adicionales sobre el Yacc original, pero el concepto ha seguido siendo igual. Yacc también se ha reescrito para otros lenguajes, incluyendo Ratfor, EFL, ML, Ada, Java, y Limbo. Puesto que el analizador sintáctico generado por Yacc requiere un analizador léxico, se utiliza a menudo conjuntamente con un generador de analizador léxico, en la mayoría de los casos lex o Flex, alternativa del software libre. El estándar de IEEE POSIX P1003.2 define la funcionalidad y los requisitos a Lex y Yacc. La versión Yacc de AT&T se convirtió en software libre; el código fuente está disponible con las distribuciones estándares del Plan 9 y de OpenSolaris. Es correcto decir que yacc es una herramienta que sirve para generar un programa, capaz de analizar gramaticalmente una entrada dada por lex, a partir de una especificación. Esta especificación, debe contener los tokens reconocidos y Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
2
los tipos de datos de los mismos si es que se ocupan para realizar operaciones sobre ellos, y una especificación de gramática en un formato similar a BNF (Backus Naus Form), que va desde el símbolo no terminal más general a cada una de las opciones terminales.
Ejemplo: Numero + Numero - Numero En el ejemplo podemos ver que es un símbolo no terminal que está compuesto por un "Numero" terminal seguido de un símbolo '+' o '-' terminales seguido por un no terminal, que a su vez puede ser otro número u otra expresión más compleja. Es importante notar que esta especificación es recursiva sobre pero no es ambigua, es decir, siempre se llegara a un terminal. Una especificación yacc se divide en tres secciones diferentes de manera similar a lex, la de definiciones, la de reglas, y la de subrutinas, que van igualmente separadas por un '%%', mismas que pueden incluir código de C encerrado entre un %{ y un %}.
Ejemplo 1.1 (Mini calculadora) A continuación, vamos a analizar un ejemplo sencillo de una verdadera especificación de yacc, que es la gramática para una calculadora sencilla que permite hacer operaciones como suma, resta, multiplicación, división y exponente. %{ #include %} %union{ double dval; Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
3
} %token %token %token %token
NUMBER PLUS MINUS TIMES DIVIDE POWER LEFT_PARENTHESIS RIGHT_PARENTHESIS END
%left PLUS MINUS %left TIMES DIVIDE %left NEG %right POWER %type Expression %start Input %% Input: Line | Input Line ; Line: END | Expression END ; Expression: NUMBER
{ printf("Result: %f\n",$1); } { $$=$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(); } Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
4
Definiciones En esta primera sección, al igual que en lex, incluimos las librerías que usaremos en el programa, definiciones de los tokens, tipos de datos y precedencia de la gramática.
%union Esta definición, se traduce a una unión 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 unión se definen miembros cuyos correspondientes tipos de datos serán usados para dar el tipo de dato a los tokens como se explicara en la siguiente sección. %union se traduce de la siguiente forma: En yacc : %union{ double dval; } En C : typedef union { double dval; } YYSTYPE; Con esta definición, yacc declara algunas uniones de este tipo, de las cuales la más importante es: YYSTYPE yylval; que será usada en la especificación 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 qué tipo es cada token devuelto por yylex(), se usan las definiciones %token y %type. Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
5
%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 símbolos terminales, lo cual veremos mejor reflejado en la sección de reglas, estos también tienen el objetivo de servir como etiquetas que yylex() regresa a yacc para identificar el token que se ha leído recientemente. Su uso es como sigue: %token [] ETIQUETA1 [ETIQUETA2 ... ETIQUETAn] Donde todo lo que esta entre [ y ] es opcional. : Indica el miembro al que serán mapeados los tokens en la union yylval dentro de lex.
ETIQUETAS: Estos son los nombres con los que se identificaran los tokens mismos, que serán traducidos en C como números en instrucciones #define del preprocesador de C. %type es análogo a %token, solo que este define el tipo de dato para símbolos no terminales de nuestra gramática, 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 Expression Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
6
La primera línea indica que el token NUMERO será del tipo de miembro de dval, es decir, un double. Las siguientes tres líneas, son para definir algunos tokens mas que serán usados en la gramática, pero no necesitan un tipo de dato ni un miembro en yylval asociado. En la última línea definimos el tipo de dato que usara nuestro no terminal Expression.
%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 sí misma, y la agrupación de los operadores.
Precedencia La precedencia es asignada en orden inverso al que aparecen, es decir, el último 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)))
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
7
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 ambigüedades en el lenguaje generado. %start En algunos casos es conveniente indicarle a yacc cuál es el símbolo (no terminal) inicial a la hora de hacer el paseo, es decir, el símbolo que se trata de reducir, si esta opción no es especificada, yacc toma al primer símbolo de la sección de reglas como símbolo inicial. En nuestro ejemplo, se presentan ambos casos, nuestro símbolo inicial "Input" se encuentra al inicio del archivo y también está declarado como símbolo inicial. %start Input
Reglas En esta parte finalmente llegamos a la definición de la gramática, acá podemos observar que cada símbolo se define con su nombre, seguido de dos puntos ":" seguidos de varios símbolos que conformaran su composición gramatical que en caso de tener varias opciones, son separados por "|" (or) indicando que se tienen varias opciones para reducir ese símbolo y para terminar cada regla, un ";".
Ejemplo: Si tomamos la gramática que definimos al principio de esta sección:
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
8
Numero + Numero - Numero Y la transformamos a una regla de yacc, se vería 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 símbolos de la gramática una vez que son resueltos en la parte de código de C. En este ejemplo, Expresion es el símbolo no terminal que se está 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 símbolo no terminal, $1, $2 y $3 son variables que son mapeadas al valor de los símbolos de cada línea 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 $2 y Expresion (la parte recursiva) equivale a $3
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
9
Todo esto claro, en la parte de acciones en C para cada línea de la regla en yacc. En el ejemplo también 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 más alta precedencia a la operación "menos" cuando está en un contexto unario, que a la mayoría de los operadores excepto el POWER (exponente): . . . | MINUS Expression %prec NEG
{ $$=-$2; }
. .
Reducción 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 diseño de gramáticas 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 pequeño no hay ningún problema pero para casos donde la reducción es compleja, puede desbordar la pila ya que cuando la recursión 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 recursión 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 recursión por la derecha, un análogo recurrente por la izquierda seri como este: Expresion: Expresion '+' NUMERO {$$ = $1 + $3;} | Expresion '-' NUMERO {$$ = $1 - $3;} | NUMERO {$$ = $1;} ; Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
10
Especificación de Lex para el ejemplo1.1 Para que el Ejemplo1.1 pueda funcionar, al igual que cualquier otro programa en yacc,
necesita
un
tokenizer,
y
a
continuación
tenemos
su
tokenizer
correspondiente escrito en lex. %{ #include "y.tab.h" #include #include %} white digit integer
[ \t]+ [0-9] {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); %%
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
11
Acerca de la sección de definiciones de este lexer, lo unico relevante que podemos mencionar es la línea de C que dice:
#include "y.tab.h" Esta línea incluye al archivo y.tab.h que contiene algunas de las definiciones de yacc que lex necesita para poder interactuar con él, entre las más importantes se encuentran definidas todas las etiquetas de los tokens, como PLUS, MINUS, NUMBER, etcétera. Estas constantes son los valores que yylex() regresara a yyparse() (la función del parser de yacc) para identificar el tipo de token que recién se ha leído. En la sección de reglas, en la parte del código, podemos ver como al final de cada regla, se hace un return especificando la etiqueta que fue declarada como %token o cómo %left/%rigth en la especificación 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.1 25*5-5 Result: 120.000000 5^2*2 Result: 50.000000 5^(2*2) Result: 625.000000 bye Terminando programa $
Subrutinas En esta última sección, es posible re implementar, siguiendo la misma idea de lex, algunas funciones que pueden ser útiles en algún momento dado o declarar nuevas funciones para usar dentro de nuestro código o nuestras reglas, no hay mucho que reimplementat a este nivel (yacc) a menos que sea con propósitos Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
12
realmente específicos. Las funciones más comúnmente implementadas son main() e yyerror(), la primera se usa para personalizar el programa con mensajes antes o después del parser, o para llamarlo varias veces en el código y la segunda la ocupa yyparse() cada vez que encuentra un error de sintaxis, en este ejemplo, se incluyen ambas con su contenido mínimo.
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
13
GENERADOR DE ANALIZADOR SINTÁCTICO JAVACC
JavaCC (Java Compiler Compiler) es un generador de analizadores sintácticos de código abierto para el lenguaje de programación Java. JavaCC es similar a Yacc en que genera un parser para una gramática presentada en notación BNF, con la diferencia de que la salida es en código Java. A diferencia de Yacc, JavaCC genera analizadores descendentes (top-down), lo que lo limita a la clase de gramáticas LL(K) (en particular, la recursión desde izquierda no se puede usar). El constructor de árboles que lo acompaña, 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 compañía llamada Metamata y cambiaron el nombre Jack a JavaCC. Metamata se convirtió en WebGain. Después de queWebGain finalizara sus operaciones, JavaCC se trasladó a su ubicación actual. JavaCC integra las funciones de análisis léxico y análisis sintáctico en una sola herramienta, obteniendo a la salida código java –a diferencia de lex/yacc cuya salida es código C-. Antes que nada veamos un ejemplo sobre el cual se va a ir haciendo un seguimiento de cada uno de los distintos puntos que vayamos comentando. En él se muestra una gramática reconocedora de una calculadora. Más adelante, en Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
14
este mismo capítulo, lo modificaremos con el objetivo de mostrar mayores funcionalidades que aporta JavaCC. La principal diferencia con lex/yacc es que los analizadores sintácticos generados por JavaCC son analizadores sintácticos descendentes de tipo LL(k), mientras que los analizadores sintácticos generados por yacc son de tipo ascendente LALR. Otra diferencia bastante importante es que las especificaciones léxicas pueden estar incluidas dentro de la especificación de la gramática. Por ejemplo, podemos escribir algo como esto: sentenciaIf : {} { "if" "(" expresion() ")" sentencia() } . y automáticamente se ocupa de generar tokens para “if” y para “(“.
También es necesario reseñar que el análisis léxico en JavaCC genera combinaciones de autómatas finitos deterministas (AFD) y autómatas finitos no deterministas (AFND); por contra, lex siempre genera autómatas finitos deterministas.
ESTRUCTURA GENERAL DE UN PROGRAMA EN JAVACC El fichero de entrada comienza con una lista de opciones, la cual es opcional. Seguidamente nos encontramos la unidad de compilación java la cual se encuentra encerrada entre PARSER_BEGIN(nombre) y PARSER_END(nombre). Después nos encontramos con una lista de reglas de producción (cada una de estas partes son claramente distinguibles en el ejemplo 2.1.). El nombre que sigue a PARSER_BEGIN y a PARSER_END debe ser el mismo, y éste identifica al analizador sintáctico que va a ser generado con posterioridad. Por ejemplo si nuestro nombre es Gramática, se generan los siguientes archivos:
Gramatica.java: El analizador sintáctico.
GramaticaTokenManager.java: gestor de tokens para el analizador léxico. Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
15
GramaticaConstants.java: El analizador léxico no maneja los tokens por el
Opciones_javacc “PARSER_BEGIN” “(“”)” Unidad de compilación java “PARSER_END” “(“”)” (reglas de produccion)* Nombre que especificamos nosotros en el código, sino que a cada uno de ellos le asigna un número. En este archivo se encuentra cada uno de los tokens junto con el número que le ha sido asignado por el analizador léxico. OPCIONES 1 La sección options, si está presente, comienza con la palabra reservada “options” seguida de una lista de una o más opciones entre llaves. La misma opción no debe establecerse más de una vez. En el ejemplo 2.1. Sólo se ha usado una de las opciones ( LOOKAHEAD=1). Las opciones pueden ser especificadas tanto en el archivo de la gramática como en la línea de comandos. En caso de estar presentes en el archivo, éstas deben aparecer al principio precedidas por la palabra “options”. Y en el caso en que se especifiquen en la línea de comandos, éstas tienen mayor prioridad que las especificadas en el código. opciones::= [ “options” “{“ (valores_de_las_opciones)* “}”]
Los nombres de las opciones van en mayúsculas.
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
16
A continuación se detallan brevemente las opciones más importantes y las funciones que realizan:
LOOKAHEAD: Indica la cantidad de tokens que son tenidos en cuenta por el parser antes de tomar una decisión de parsing. El valor por defecto es 1, con lo cual se trabaja con parser LL(1), aunque el número puede variar para alguna/s producción/es en particular. Cuanto más pequeño sea el número de tokens del lookahead, más veloz será el parser.
Valor por defecto: 1
CHOICE_AMBIGUITY_CHECK: Es el número de tokens que se consideran en las elecciones de chequeo por ambigüedad, de la forma "A | B | ...". Por ejemplo: si existen dos tokens prefijos comunes para A y B, pero no tres; entonces por medio de esta opción, JavaCC permite usar un lookahead de 3 con propósitos de desambiguación. La utilización de esta opción provee mayor información y flexibilidad ante las ambigüedades, pero a costo de un mayor tiempo de procesamiento.
Valor por defecto: 2
STATIC: Si el valor es true, todos los métodos y clases variables se especifican como estáticas en el parser generado y en el token manager. Esto permite la presencia de un único objeto parser, pero mejora la actuación del procedimiento de parsing. Para ejecutar múltiples parsers durante la ejecución de un programa Java, debe invocarse el método ReInit() para reinicializar el parser, si es que había sido definido como estático. Si el parser no es estático, puede utilizarse el operador "new" para construir los parsers que sean necesarios; los mismos, podrían ser usados simultáneamente en threads diferentes.
Valor por defecto: true
DEBUG_PARSER: Esta opción se utiliza para obtener información de debugging desde el parser generado. Si el valor de la opción es true, el parser genera un trace de sus acciones. Dichos traces pueden deshabilitarse por medio de la invocación del método disable_tracing() en la Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
17
clase del parser generado, y, recíprocamente, pueden habilitarse con el método enable_tracing(). Valor por defecto: false
IGNORE_CASE: Si su valor es true, el token manager generado no efectúa diferencia entre letras mayúsculas y minúsculas en las especificaciones de tokens y los archivos de entrada. Es muy usado para escribir gramáticas para lenguajes como HTML.
Valor por defecto: false.
FORCE_LA_CHECK: Esta opción determina los controles de chequeo de ambigüedad de lookahead ejecutados por JavaCC. Cuando el valor es false, el chequeo de ambigüedad del lookahead se ejecuta para todos los puntos de elección donde se usa el lookahead de 1 por defecto. El chequeo de ambigüedad de lookahead no se ejecuta en los puntos donde existe una especificación explícita de lookahead, o si la opción LOOKAHEAD tiene un valor distinto de 1.
Valor por defecto: false.
COMMON_TOKEN_ACTION: Si el valor de esta opción es true, todas las invocaciones del método "getNextToken()" del token manager, causan una llamada al método definido "CommonTokenAction()" después de que el token haya sido leído por el token manager. El usuario debe definir este método en la sección TOKEN_MGR_DECLS. La signatura de este método es:
Valor por defecto: false.
OUTPUT_DIRECTORY: Es una opción cuyo valor por defecto es el void CommonTokenAction(Token t)
directorio actual. Controla dónde son
generados los archivos de salida. Valor por defecto: directorio actual.
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
18
REGLAS DE PRODUCCIÓN Existen cuatro tipos de reglas de producción en JavaCC: las reglas de producción javacode y BNF se usan para definir la gramática a partir de la cual va a generarse el analizador sintáctico. Las Reglas de producción mediante expresiones regulares, se usan para definir los tokens de la gramática – el token manager es generado a partir de esta información-. La sección token manager decls se usa para introducir declaraciones en el analizador sintáctico que se generará con posterioridad.
Reglas de producción Javacode Las reglas de producción JAVACODE son una forma de escribir código Java para una regla de producción en lugar de las típicas expansiones EBNF. Esto suele ser útil cuando tenemos la necesidad de reconocer algo que no es de contexto libre o algo para lo cual es difícil escribir una gramática que lo reconozca. Para ello nos serviremos de los métodos que el propio parser nos proporciona con tal fin, a saber: getToken(), jj_concume_token(), getNextToken(),etc. A continuación se muestra un ejemplo de uso de JAVACODE. En el ejemplo 2.3., el no termina l “skipToMatchingBrace” consume tokens de la entrada hasta que encuentra una “}”. JAVACODE void skipToMatchingBrace(){ Token tok; int nesting=1; while(true){ tok=getToken(1); if(tok.kind==LBRACE) nesting++; if(tok.kind==RBRACE){ nesting--; if(nesting==0) Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
19
break; } tok=getNextToken(); } } Cuando usamos este tipo de reglas de producción tenemos que tener muchísimo cuidado ya que es muy fácil especificar bastante más de lo que realmente se quiere. Este tipo de reglas son problemáticas cuando las utilizamos en lugares donde el analizador sintáctico tiene que hacer una elección. Supongamos que la regla de producción anterior es referenciada desde ésta: Reglas de producción BNF Las reglas de producción BNF son la forma estándar de especificar gramáticas en JavaCC. Sobre ella diferenciaremos cada uno de los puntos que a continuación se exponen: void unario() : {} { }
|
elemento() elemento()
Cada regla de producción BNF consta de una parte izquierda la cual consiste en la especificación de un no terminal. Las reglas de producción BNF definen este no terminal en función de expansiones BNF que se encuentran en la parte derecha. El no terminal se escribe exactamente de la misma forma en que se declara un método en Java, (en el ejemplo 2.6. “void unario():”) . Ya que c ada no terminal se traduce a
un método en el analizador sintáctico, la forma de escribir el no terminal hace que esta asociación sea obvia. El nombre del no terminal es el nombre del método, y Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
20
los parámetros y el valor de retorno son los medios para pasar valores arriba y abajo dentro del árbol sintáctico. – parse tree- (Esto se mostrará más adelante). La parte derecha de una regla de producción BNF tiene dos partes: La primera parte es un conjunto de declaraciones y código Java ( bloque Java). Este código es generado al principio del método relacionado con el no terminal. Por lo tanto, cada vez que el no terminal es usado en el proceso de análisis, este código es ejecutado. La segunda parte está formada por las expansiones BNF. bnf_production::= java_return _type java_identifier “(“ java_parameter_list “)” “:” java_block “{“ expansion_choices “}”
Reglas de producción mediante expresiones regulares Las expresiones regulares se usan para definir entidades léxicas las cuales van a ser procesadas por el token manager. Una regla de producción de este tipo comienza con la especificación de los estados léxicos a los cuales es aplicable. regular_expr_production::= [lista_de_estados_lexicos] regexpr_kind[ “[“ ”IGNORE CASE” “]” ] ”:” “{“ regexpr_spec ( “|” regexpr_spec )* “}”
Existe un estado léxico estándar llamado “DEFAULT”. Si la lista de estados léxicos se omite, la expresión regular se aplica al estado “DEFAULT”.
lista_de_estados_lexicos::= “<” ”*” ”>” | “<” identificador_java (“,” identificador_java)* “>”
La lista de estados léxicos describe el conjunto de estados léxicos, para los cuales la expresión regular es aplicable. Si ésta es “<” “*” “>”, entonces la expresión Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
21
regular se puede aplicar a todos los estados léxicos. En otro caso se aplicará a todos los estados léxicos cuyo identificador se encuentre en la lista de identificadores del interior de los paréntesis angulares. A continuación se especifica el tipo de regla de producción. Existen cuatro tipos basadas en expresiones regulares:
o
TOKEN: La expresión regular de esta regla de producción describe tokens de la gramática. El token manager crea un objeto de tipo Token para cada una de las entradas que se ajusten a dicha expresión regular, y luego lo retorna al analizador sintáctico.
o
SPECIAL_TOKEN: La expresión regular de esta regla de producción describe tokens especiales.
Los tokens especiales son iguales que los
tokens sólo que no tienen importancia durante el análisis sintáctico. Son muy útiles cuando hay que procesar entidades léxicas,
del tipo de
comentarios, las cuales no tienen ninguna importancia para el análisis sintáctico, pero son una parte muy importante del fichero de entrada. o
SKIP: Cualquier entrada que se ajuste a la expresión regular especificada en esta sección será ignorada por el token manager.
o
MORE: A veces es necesario ir construyendo el token que vamos a pasar al analizador sintáctico de forma gradual. Las entradas que se ajustan a la expresión regular son guardadas en un buffer hasta que llega el siguiente
o
TOKEN o SPECIAL_TOKEN. Después todos los valores almacenados en el buffer son concatenados para formar un solo TOKEN o SPECIAL_TOKEN y es entonces cuando se envía al analizador sintáctico. En el caso en que una entrada encajara con una expresión regular especificada en la sección SKIP, el contenido del buffer es desechado.
VERSIONES:
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
22
Updated Size Title + Description + Tags
Actions
Date
beta
about 1 year ago
Beta / Nightly Versions Tags: beta nightly javacc
contrib
over 4 years ago
Contributions, including grammars Tags: --
oldversions
over 4 years ago
Older versions of JavaCC Tags: --
releases
almost 2 years ago
JavaCC releases Tags: --
javacc-5.0.tar.gz
over 4 years ago
461.6 KB
over 4 years ago
627.3 KB
over 4 years ago
791.1 KB
over 4 years ago
1.1 MB
about 1 year ago
726.7 KB
about 1 year ago
1.2 MB
javacc-5.0.tar.gz Tags: --
javacc-5.0.zip javacc-5.0.zip Tags: --
javacc-5.0src.tar.gz javacc-5.0src.tar.gz Tags: --
javacc-5.0src.zip javacc-5.0src.zip Tags: --
javacc-6.0.zip This is the binary distribution of JavaCC 6.0.1 - with few bug fixes. Tags: javacc 6.0 c++ code parser lexer 6.0.1
JavacCC-6.0src.zip JavaCC 6.0 source distribution Tags: javacc c++ parser lexer genertor
Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
23
Conclusión Como hemos visto un generador de analizadores sintácticos es un programa que toma como su entrada una especificación de la sintaxis de un lenguaje en alguna forma, y produce como su salida un procedimiento de análisis sintáctico para ese lenguaje, también sabemos ahora que un generador de analizadores sintácticos ampliamente utilizado que incorpora el algoritmo de análisis sintáctico LR(1) se conoce como Yacc, este no es directamente un analizador sino un generador de analizadores. A partir de un fichero fuente en yacc, se genera un fichero fuente en C que contiene el analizador sintáctico. Por otra parte el generador de analizadores sintácticos JavaCC integra las funciones de análisis léxico y análisis sintáctico en una sola herramienta, obteniendo a la salida código java –a diferencia de lex/yacc cuya salida es código C. La principal diferencia con lex/yacc es que los analizadores sintácticos generados por JavaCC son analizadores sintácticos descendentes de tipo LL(k), mientras que los analizadores sintácticos generados por yacc son de tipo ascendente LALR. Otra diferencia bastante importante es que las especificaciones léxicas pueden estar incluidas dentro de la especificación de la gramática. De manera general JavaCC es similar a Yacc en que genera un parser para una gramática presentada en notación BNF, con la diferencia de que la salida es en código Java. A diferencia de Yacc, JavaCC genera analizadores descendentes (top-down), lo que lo limita a la clase de gramáticas LL(K) (en particular, la recursión desde izquierda no se puede usar). Otra diferencia bastante importante es que las especificaciones léxicas pueden estar incluidas dentro de la especificación de la gramática. JavaCC es quizá el generador de parsers usado con aplicaciones Java más popular.
Referencias: http://www.lcc.uma.es/~galvez/theme/IntroduccionAJavaCC.pdf http://www.udb.edu.sv/udb/archivo/guia/informatica-ingenieria/compiladores/2014/ii/guia-5.pdf http://es.wikipedia.org/wiki/JavaCC http://es.wikipedia.org/wiki/Yacc Jose Alfredo Jimenez Cornejo Romeo H. Vazquez Velazquez
03/06/2015
24