Documento javaCC - L.P.S.I. - Universidad Politécnica de Madrid

Anuncio
Una introducción al generador
JavaCC
José Gabriel Pérez Díez
Departamento L.P.S.I.
Escuela Universitaria de Informática
Universidad Politécnica de Madrid
Enero 2010
1
Contenido
Principios básicos del generador JavaCC
2
Descripción inicial
2
Instalación y documentación
2
Obtención de un analizador léxico-sintáctico
3
Ejemplo de presentación
4
Analizadores generados
5
Forma de una especificación JavaCC
6
Sección de opciones
6
Sección de ejecución
7
Sección de sintaxis
8
Sección de lexicografía
Tareas asociadas a la estructura sintáctica
11
16
Bloque para un símbolo
16
Acciones sintácticas
16
Valor comunicado por un método
18
Comunicación entre los analizadores léxico y sintáctico
22
Tareas asociadas a las piezas sintácticas
25
Acción ligada a una pieza sintáctica
25
Bloque de declaraciones lexicográficas
27
Declaraciones lexicográficas predefinidas
27
Acción lexicográfica común
30
A modo de recapitulación
34
Precisiones sobre el analizador sintáctico generado
36
2
□
Principios básicos del generador JavaCC
Descripción inicial
El generador JavaCC (Java Compiler Compiler) es una herramienta para generar analizadores de lenguajes; acepta como entrada una especificación de un determinado lenguaje y produce como salida un analizador para ese lenguaje; el analizador generado está escrito en Java. La especificación proporcionada al generador JavaCC puede contemplar distintos aspectos del lenguaje para el que se quiere obtener el analizador:
- Características lexicográficas y sintácticas
es la forma más frecuente de uso del generador; la especificación proporcionada define las características sintácticas y lexicográficas de un lenguaje y se genera un analizador léxico-sintáctico del lenguaje
especificado.
- Características lexicográficas
en la especificación proporcionada al generador sólo se definen características lexicográficas del lenguaje; con el código generado se puede obtener un analizador lexicográfico.
- Características lexicográficas y sintácticas y comprobaciones semánticas
también es posible completar una especificación léxico-sintáctica con la inclusión de código Java complementario para que el programa generado (que incorpora adecuadamente ese código auxiliar) pueda
hacer un análisis completo (léxico, sintáctico y semántico) del lenguaje especificado.
◊
Instalación y documentación
Dado que el código generado por JavaCC está escrito en Java, es necesario disponer de una versión del sistema Java (compilador de Java e intérprete de la Máquina Virtual Java). Son programas de libre distribución y fáciles de conseguir.
El generador JavaCC también es un programa de libre distribución; se puede conseguir en:
◊
▫
la página oficial de JavaCC: https://javacc.dev.java.net
la página de la asignatura
(se tiene la versión Java Compiler Compiler, version 5.0)
Se consigue un fichero empaquetado de nombre javacc-5.0.zip; tras desempaquetar (en un determinado directorio, que puede elegirse como convenga) dicho fichero, se tienen instalados, entre otros, los siguientes ficheros (que son los que interesan para el sistema operativo windows):
···· \javacc-5.0\bin\javacc.bat
···· \javacc-5.0\bin\jjdoc.bat
···· \javacc-5.0\bin\jjtree.bat
▫
los nombre de estos ficheros se corresponden con los nombres de los comandos para llamar a las herramientas instaladas
javacc: generador de analizadores
jjdoc: productor de documentación
jjtree: preprocesador de apoyo para tareas semánticas
Para que la llamada a estos comandos pueda realizarse desde cualquier directorio, ha de anotarse el camino
(PATH) que lleve hasta ···· \javacc-5.0\bin. También ha de tenerse en cuenta si la instalación
del sistema Java se tiene preparada para que el compilador (javac) y el intérprete (java) se puedan ejecutar
desde cualquier directorio.
Para comprobar si la instalación del generador se ha realizado adecuadamente, se puede llamar desde la línea de comandos al generador javacc, y aparecerá por pantalla una información sobre el uso de dicho
comando; la primera línea de esa información es:
Java Compiler Compiler Version 5.0 (Parser Generator)
3
Documentación sobre JavaCC
Se dispone de abundante documentación relativa a JavaCC; entre otras, se pueden encontrar:
▫
documentación que acompaña a los ficheros de la versión instalada, se tienen diversos ficheros en
···· \javacc-5.0\doc\*.html
(en javaccgrm.html se tiene una descripción general del generador)
▫
documentación variada en la red
JavaCC Documentation
The JavaCC Tutorial
Introduction to JavaCC
The JavaCC FAQ
etc
Primera prueba con JavaCC
En lo que sigue se expone un “ejemplo de presentación” completo: la generación de un analizador léxicosintáctico para un tipo de expresiones muy sencillo. Se puede empezar el estudio de JavaCC probando su
funcionamiento con ese ejemplo.
◊
•
Obtención de un analizador léxico-sintáctico
Pasos para la generación del analizador
1.- Edición de la especificación (editor de texto plano)
vi | edit |∙ ∙ ∙
NombreFichero.jj
(el nombre del fichero puede tener cualquier extensión; suele usarse .jj)
2.- Ejecución del generador
javacc NombreFichero.jj
Si el nombre elegido para la especificación es NombreDeLaEspecif (más adelante se
indica la manera de dar un nombre a la especificación), como resultado de la generación se
obtiene (además de otros ficheros auxiliares) el fichero
NombreDeLaEspecif.java
3.- Compilación del analizador generado
javac NombreDeLaEspecif.java
Como resultado de la compilación se obtiene (además de otras clases auxiliares) el fichero
NombreDeLaEspecif.class
•
Ejecución del analizador generado
Si el nombre del fichero donde se encuentra el texto fuente (escrito en el lenguaje para el que se ha generado el analizador) que se pretende analizar es Programa.len
java NombreDeLaEspecif < Programa.len
Si se desea que los resultados del análisis, en vez de presentarse por pantalla, queden grabados en un fichero de nombre Salida.dat
java NombreDeLaEspecif < Programa.len > Salida.dat
4
◊
•
Ejemplo de presentación
Descripción del lenguaje
El lenguaje L está formado por las expresiones en las que pueden aparecer:
- variables
- constantes
- operadores + y *
Las variables son nombres formados por una única letra (minúscula o mayúscula); las constantes son números enteros de una o más cifras. El espacio y el tabulador pueden estar presentes, pero no tienen ningún
significado; los finales de línea tampoco son significativos (una expresión puede codificarse ocupando una
o más líneas).
La sintaxis de las expresiones se especifica mediante la siguiente gramática:
<Expresion> ::= <Termino>
{
+ <Termino>
<Termino> ::= <Factor> { * <Factor>
<Factor> ::= variable
| constante
| ( <Expresion> )
•
}
}
Especificación léxico-sintáctica codificada con la notación JavaCC
Una manera de escribir la especificación (para la que se ha elegido el nombre ExprMin) de forma que
sea aceptada por el generador es:
options {
Ignore_Case = true;
}
PARSER_BEGIN (ExprMin)
public class ExprMin {
public static void main (String[] argum) throws ParseException {
ExprMin anLexSint = new ExprMin (System.in);
anLexSint.unaExpresion();
System.out.println("Análisis terminado:");
System.out.println
("no se han hallado errores léxico-sintácticos");
}
}
PARSER_END (ExprMin)
void unaExpresion() :
{ }
{
expresion() <EOF>
}
void expresion() :
{ }
{
termino() ( "+"
}
void termino() :
{ }
{
factor() ( "*"
termino() )*
factor() )*
5
}
void factor() :
{ }
{
<constante>
| <variable>
| "(" expresion() ")"
}
TOKEN:
{
<
variable : ["a"-"z"]
<
constante :
>
}
TOKEN:
{
( ["0"-"9"] ) +
>
}
SKIP:
{ " " | "\t" | "\n" | "\r" }
•
Obtención del analizador
Si la especificación precedente se tiene grabada en un fichero de nombre Ejemplo.jj, para obtener el
analizador:
- se ejecuta el generador: javacc Ejemplo.jj
- se compila el analizador generado: javac ExprMin.java
•
Ejecución del analizador
Si se quiere analizar una expresión grabada en un fichero de nombre PruebaExp.txt:
- se ejecuta el analizador obtenido: java ExprMin < PruebaExp.txt
Analizadores generados
En su funcionamiento más sencillo y habitual, JavaCC genera un analizador sintáctico, complementado
con un analizador lexicográfico, para que, conjuntamente, se pueda realizar un análisis léxico-sintáctico de
un texto de entrada.
◊
El analizador sintáctico obtenido es, en general, LL(k): descendente y determinista con la consulta de k
símbolos por adelantado; si la gramática proporcionada cumple la condición LL(1), se genera un analizador sintáctico descendente-predictivo-recursivo. Más adelante se hacen algunas precisiones sobre esta afirmación.
Si la especificación léxico-sintáctica de un lenguaje codificada en JavaCC tiene dado (como indicativo que
acompaña a las palabras reservadas PARSER_BEGIN y PARSER_END) el nombre EspLexSin y se tiene
grabada en un fichero de nombre Lenguaje.jj, cuando se ejecuta el generador tomando como entrada
ese fichero
javacc Lenguaje.jj
se obtienen los siguientes ficheros (clases) con código Java:
▫
Token.java
descripciones para la comunicación entre los analizadores léxico y sintáctico
6
▫
TokenMgrError.java
tratamiento de errores para el análisis lexicográfico
▫
ParseException.java
tratamiento de errores para el análisis sintáctico
▫
SimpleCharStream.java
componentes para la realización de las tareas de entrada/salida del analizador
▫
EspLexSinConstants.java
definición de la representación interna de las piezas sintácticas
▫
EspLexSinTokenManager.java
analizador lexicográfico
▫
EspLexSin.java
analizador sintáctico
Puede apreciarse que hay dos categorías de nombres de ficheros generados: los cuatro primeros nombres
citados no dependen del nombre de la especificación considerada, los otros nombres de ficheros se forman
a partir del nombre dado a la especificación.
□
Forma de una especificación JavaCC (versión simplificada)
La forma que se describe en lo que sigue es una versión simplificada;
el generador JavaCC admite especificaciones con otras muchas posibilidades no mencionadas en esta introducción.
Una especificación para el generador JavaCC puede considerarse dividida en cuatro secciones:
Sección de opciones
Sección de ejecución
Sección de sintaxis
Sección de lexicografía
Sección de opciones
En esta sección, cuya presencia es optativa, se pueden asignar valores a diversos parámetros (llamados opciones) que sirven para configurar ciertas características del funcionamiento del generador o del analizador
generado. Cada parámetro (opción) tiene un valor por defecto, que es el que toma cuando no se le asigna
explícitamente un valor. Los valores de las opciones también se pueden fijar en la línea de comandos
◊
7
cuando se ejecuta el generador (lo indicado en la línea de comandos tiene prioridad sobre lo especificado
en esta sección de opciones).
Algunas de las opciones para las que se puede fijar un valor son:
Ignore_Case
(valor por defecto: false)
indica si en el texto analizado ha de distinguirse o no entre letras minúsculas y mayúsculas
Build_Parser
(valor por defecto: true)
indica si se genera el analizador sintáctico o no
Build_Token_Manager
(valor por defecto: true)
indica si se genera el analizador lexicográfico o no
Sanity_Check
(valor por defecto: true)
indica si se realizan comprobaciones sobre la gramática sintáctica
Debug_Parser
(valor por defecto: false)
indica si se genera una traza para el análisis léxico-sintáctico
Error_Reporting
(valor por defecto: true)
indica si los mensajes de error emitidos son más o menos explicativos
Static
(valor por defecto: true)
con el valor por defecto, los métodos de los analizadores léxico y sintáctico se generan con el descriptor estático (static)
Los nombres de las opciones pueden escribirse con letras minúsculas o mayúsculas; los valores de las opciones son, en la mayoría de los casos, un valor entero o un valor lógico. Si se incluye la sección, se empieza poniendo la palabra reservada options; en este caso, al menos ha de ponerse una opción.
La forma de esta sección es:
options {
nombreOpcion1 = valorOpcion1;
nombreOpcion2 = valorOpcion2;
∙ ∙ ∙ ∙
nombreOpcionk = valorOpcionk;
}
Sección de ejecución
En esta sección se pone el código Java que contiene la llamada al analizador generado para que se realice
el análisis de un determinado texto de entrada. También se establece aquí el nombre de la especificación,
que es el nombre que se toma para formar los nombres de parte de los ficheros (clases) generados.
◊
La sección está delimitada por dos palabras reservadas, ambas acompañadas por un mismo nombre (puesto
entre paréntesis); ese nombre es el que se da a la especificación. Entre esas dos palabras ha de ponerse una
clase directora para el proceso de análisis; el nombre de esa clase directora ha de coincidir con el nombre
dado a la especificación.
PARSER_BEGIN (NombreDeLaEspecif)
8
∙ ∙ ∙ ∙
public class NombreDeLaEspecif {
∙ ∙ ∙ ∙
}
∙ ∙ ∙ ∙
PARSER_END (NombreDeLaEspecif)
Una posible versión sencilla de la clase directora es:
public class NombreDeLaEspecif {
public static void main (String[] argum) throws ParseException {
NombreDeLaEspecif anLeSi = new NombreDeLaEspecif (System.in);
anLeSi.simboloPrograma();
}
}
▫ se crea el objeto anLeSi para poder aplicar los métodos de análisis,
▫ se asocia la entrada standard ( System.in ) a la entrada para el analizador generado,
▫ se aplica al objeto creado el método de análisis correspondiente al símbolo inicial de la gramática.
Sección de sintaxis
En esta sección se describe la sintaxis del lenguaje para el que se desea generar el analizador, usándose para ello una notación parecida a la BNF. En lo que sigue se expone la forma de las producciones tal y como
se escriben en JavaCC, poniéndolas en comparación con las producciones de la notación BNF-Ampliada.
◊
•
Reglas sintácticas
El conjunto de reglas que definen un símbolo no terminal
<NombreSimb> ::=
|
∙
|
α1
α2
∙
αn
se codifica de la siguiente manera:
void nombreSimb () :
{ }
{
α1-jcc
| α2-jcc
∙
∙
∙
| αn-jcc
}
∙
9
- el nombre del símbolo no terminal (parte izquierda de las reglas) se escribe como el encabezamiento de un método en Java, sin argumentos y sin valor de devolución (método de tipo void); se
sigue, como en Java, la costumbre de escribir los nombres de los métodos empezando con letra minúscula,
- el símbolo dos puntos representa la separación entre las partes izquierda y derechas de las reglas
(en vez del símbolo ::= de la notación BNF),
- el símbolo barra vertical representa la separación entre distintas partes derechas con la misma
parte izquierda (igual que en la notación BNF),
- después del símbolo dos puntos se pone un bloque de código Java; este código se suele dedicar a
la realización de tareas semánticas; si se pretende hacer sólo un análisis sintáctico, el bloque quedará vacío (aunque siempre es obligada su presencia),
- el conjunto de las alternativas (partes derechas de las reglas) está delimitado mediante llaves,
como si constituyese un bloque de código Java,
- αi-jcc representa la codificación de una alternativa (parte derecha), empleándose una notación que se describe a continuación.
•
Símbolos no terminales en las partes derechas
En la notación BNF se pone <NombreSimb>; en la notación JavaCC se escribe
nombreSimb()
esto es, se pone como si fuese una llamada en Java a un método sin argumentos.
•
Símbolos terminales en las partes derechas
Se considera una clasificación en las piezas sintácticas (símbolos terminales de una gramática sintáctica),
distinguiéndose entre piezas sintácticas nominales y anónimas:
▫ son nominales las piezas sintácticas a las que se asocia un nombre en otra parte de la especificación
(más adelante se describe la manera de indicar esta asociación); ese nombre asociado a la pieza es el que
se usa al escribir las partes derechas de las producciones. En la notación JavaCC un símbolo terminal nominal se escribe poniendo el nombre delimitado por los caracteres < y >
<nombreTerminal>
▫ son anónimas las piezas sintácticas que no tienen nombre asociado; no se precisa porque la pieza se representa por su propia secuencia de caracteres (lexema). En la notación JavaCC, un símbolo terminal
anónimo se escribe poniendo su lexema delimitado por comillas; por ejemplo
";"
•
":="
"<"
"<="
"END"
Metasímbolos de opcionalidad y de repetición
En la notación BNF se emplean los corchetes para indicar opcionalidad; en la notación JavaCC se utilizan
también esos mismos símbolos
[ α ]
en ambas notaciones
En la notación BNF se emplean las llaves para indicar una repetición de cero o más veces; en la notación
JavaCC se utilizan los paréntesis y un asterisco detrás del paréntesis de cerrar
•
{ α }
en notación BNF-Ampliada
( α )*
en notación JavaCC
Otros metasímbolos de la notación JavaCC
10
La opcionalidad también se puede indicar empleando el metasímbolo de interrogación
( α )?
Para indicar repetición de una o más veces se utiliza el metasímbolo operador aditivo suma
( α )+
•
Control del final de fichero de entrada
Para controlar el final del fichero en el texto analizado, ha de añadirse una regla sintáctica especial que
tenga (según la notación BNF) la forma
<ProgramaCompleto> ::= <Programa> FF
donde
<ProgramaCompleto> es un nuevo símbolo (no terminal) auxiliar añadido
<Programa> es el símbolo inicial de la gramática sintáctica
FF es una representación del final del fichero
En JavaCC el final de fichero se escribe con la notación <EOF>, siendo EOF una palabra reservada; la
regla sintáctica especial añadida a la especificación quedaría codificada así:
void programaCompleto() :
{ }
{
programa() <EOF>
}
•
Representación de la producción vacía
Las producciones vacías (reglas sintácticas cuya parte derecha es la palabra vacía ε ), no se pueden representar explícitamente en una especificación léxico-sintáctica escrita en JavaCC. Para indicar la presencia
de una producción vacía han de aplicarse las posibilidades de la notación BNF-Ampliada; según esta notación, cuando se indica la opcionalidad de un componente, se está especificando implícitamente la palabra
vacía (en el caso de que el componente no esté presente).
Así pues, si se quisiera escribir en JavaCC la construcción sintáctica expresada por las reglas
<Componente> ::= α
| ε
deberían de considerarse con la notación equivalente
<Componente> ::=
[ α ]
y si hubiera varias alternativas no vacías
<Componente> ::= α
| β
| ε
habría que considerarlas como
<Componente> ::=
[ α | β ]
o también podría verse como
<Componente> ::= α
11
|
[ β ]
Como ejemplo, sea la especificación sintáctica de una llamada a una función que puede tener cero o más
parámetros separados por comas; aunque no haya parámetros, los paréntesis están presentes. Las siguientes reglas definen esta estructura:
<Llamada> ::= id ( <Parametros> )
<Parametros> ::= <Expresion>
|
{
, <Expresion>
}
ε
En JavaCC la definición del símbolo <Parametros> se puede escribir como sigue (nótese que aquí los
paréntesis empleados para aplicar el operador * son metasímbolos)
void parametros() :
{ }
{
[ expresion()
}
( ","
expresion() )*
]
Sección de lexicografía
En esta sección se indica la lexicografía del lenguaje para el que se va a generar el analizador; la notación
de JavaCC que representa la forma de cada una de las piezas sintácticas es una variante de las bien conocidas expresiones regulares.
◊
•
El nombre de las piezas sintácticas nominales
Para asociar un nombre a una pieza sintáctica y para, al mismo tiempo, definir la forma de la pieza se emplea la notación
TOKEN :
{ < nombreTerminal : Expresión-Regular >
}
- TOKEN es una palabra reservada; detrás de ella se pone el separador dos puntos,
- nombreTerminal representa el nombre que se quiere asociar a la pieza sintáctica,
- Expresión-Regular indica una expresión regular que especifica la forma de la pieza,
- el nombre y la expresión se separan mediante el carácter dos puntos y, conjuntamente, se delimitan mediante los caracteres < y >; a su vez, toda la asociación se delimita con llaves.
•
Expresiones regulares en JavaCC
Las expresiones regulares en una especificación JavaCC se escriben de manera parecida a la forma utilizada en otras conocidas notaciones, aunque con ciertas peculiaridades. A continuación se mencionan los aspectos más significativos de las expresiones regulares JavaCC, y algunos ejemplos.
Los operadores de las expresiones son:
|
*
+
?
unión
repetición cero o más veces
repetición una o más veces
opcionalidad
12
Los paréntesis permiten fijar el orden de aplicación de los operadores. Una peculiaridad de las expresiones
regulares JavaCC es que los operadores de repetición y de opcionalidad requieren que el componente al
que se aplican esté en todo caso delimitado por paréntesis.
Se pueden citar las siguientes características básicas de las expresiones regulares en JavaCC.
▫ Se tiene la posibilidad de definir conjuntos de caracteres poniendo entre corchetes la relación de caracteres separados por comas y cada carácter delimitado por comillas. En un conjunto se puede fijar un rango de caracteres poniendo el primero y el último, separados por el símbolo guión (-). A veces resulta
más cómodo indicar los caracteres que no pertenecen al conjunto, para ello se puede definir el complementario de un conjunto poniendo el símbolo ~ delante del corchete inicial.
▫ Para indicar que una secuencia de caracteres colocados consecutivamente constituye por sí misma una
expresión regular (o un componente de una expresión), se escribe la secuencia delimitada por comillas.
▫ El símbolo \ se emplea como metacarácter para indicar que el símbolo que está inmediatamente a su
derecha se considere como carácter del alfabeto, no como posible metacarácter (operador, delimitador).
▫ En una expresión regular se pueden incluir, como si fueran caracteres, las secuencias de escape, con la
misma notación y el mismo significado que en Java o en C.
Nótese que en una producción sintáctica la opcionalidad se puede especificar de dos formas: con corchetes
o con el símbolo de interrogación; sin embargo, en una expresión regular la opcionalidad sólo se puede especificar mediante el símbolo de interrogación (los corchetes en las expresiones regulares son para definir
conjuntos de caracteres).
A continuación se muestran ejemplos de expresiones regulares escritas según la notación JavaCC.
"END"
secuencia de las tres letras mayúsculas que conforman la palabra END
"while"
secuencia de las cinco letras minúsculas que conforman la palabra while
"\n"
carácter representativo del fin de línea (secuencia de escape)
"\r"
carácter representativo del retorno de carro (secuencia de escape)
":"
carácter dos puntos
":="
secuencia de dos caracteres (símbolo de asignación en Pascal)
">"
carácter representativo del operador de relación mayor
("i" | "I")("f" | "F")
palabra “if” escrita con letras minúsculas o mayúsculas (cuatro combinaciones)
"if" | "IF"
palabra “if” escrita sólo con minúsculas o sólo con mayúsculas (dos combinaciones)
13
"u" | "o" | "i" | "e" | "a"
una cualquiera de las vocales minúsculas (pero sólo una de ellas)
[ "A", "E", "I", "O", "U" ]
una cualquiera de las vocales mayúsculas (pero sólo una de ellas)
[ "a" - "z", "ñ" ]
una cualquiera de las letras minúsculas, incluyendo la letra ñ
[ "a" - "z", "A" - "Z", "0" - "9" ]
una cualquiera de las letras o de las cifras decimales
([ "0" - "9" ])+
constante entera (número de una o más cifras)
nótese que es incorrecto poner [ 0" - "9" ]+
( "+" | "-" )?([ "0" - "9" ])+
constante entera, precedida opcionalmente por un signo
nótese que la primera cifra ha de seguir inmediatamente al signo (si existe)
(["+", "-"])?([ "0" - "9" ])+
representa lo mismo que la expresión anterior
[ "a" - "z" ] ([ "a" - "z", "0" - "9" ])*
una letra minúscula, seguida de cero o más letras minúsculas o cifras decimales
~["a" - "z", "ñ", "A" - "Z", "Ñ"]
cualquier carácter que no sea una letra
nótese que la expresión representa un único carácter
~["\n", "\r"]
cualquier carácter (un único carácter) excepto el fin de línea y el retorno de carro
~[ ]
cualquier carácter del alfabeto (un único carácter)
([ "0" - "9" ])+ ("." ([ "0" - "9" ])+)?
constante aritmética, con parte decimal opcional
"Los \"modernos\""
secuencia de catorce caracteres Los "modernos"
nótese el uso de \ para imponer la condición de carácter del símbolo comillas
"\"\\n\""
secuencia de cuatro caracteres
"\n"
nótese que aquí no se representa la secuencia de escape
•
Piezas sintácticas internas (“private”)
14
Es posible definir piezas sintácticas auxiliares, llamadas “internas” porque no se comunican al analizador
sintáctico, sino que sólo sirven para facilitar la escritura de la forma de las piezas sintácticas que sí se comunican; con el uso de las piezas internas es posible evitar repeticiones de escritura y hacer más legible la
especificación de las características lexicográficas. Las piezas internas permiten dar nombre a subexpresiones regulares que, a su vez, pueden emplearse como componentes para escribir expresiones más complejas que incorporan esos nombres. Para indicar que una pieza sintáctica se considere como interna, se
pone el símbolo # delante del nombre asociado a la pieza.
A continuación se muestra un ejemplo típico de utilización de las piezas internas en una especificación lexicográfica en JavaCC. Se quieren definir tres piezas sintácticas que representan un nombre, una constante
entera y una constante decimal; una posible especificación es:
TOKEN:
{ <nombre : ( [ "a" - "z", "A" - "Z", "ñ", "Ñ" ] )+ > }
TOKEN:
{ <cteEntera : ( [ "0" - "9" ] )+ > }
TOKEN:
{ <cteDecimal : ( [ "0" - "9" ] )+ "." ( [ "0" - "9" ] )+ > }
Con la inclusión de tres piezas sintácticas internas auxiliares, esta misma especificación podría escribirse
de la siguiente manera:
TOKEN:
{ < # letra : [ "a" - "z", "A" - "Z", "ñ", "Ñ" ] >
TOKEN:
{ < # cifra : [ "0" - "9" ] >
}
TOKEN:
{ < # numero : ( <cifra> )+ >
}
TOKEN:
{ < nombre : ( <letra> )+ >
TOKEN:
{ < cteEntera : <numero> >
}
}
}
TOKEN:
{ < cteDecimal : <numero> "." <numero> >
}
Los nombres letra, cifra y numero son exclusivos (internos) del analizador lexicográfico y no se
pueden emplear como nombres de piezas en la especificación de las reglas sintácticas; cuando esos nombres se usan en la definición de otras piezas, han de escribirse delimitados por los símbolos < y >.
•
Distinción restringida entre minúsculas y mayúsculas
Se tiene la opción Ignore_Case para determinar si en el análisis del texto de entrada se distingue entre
las letras minúsculas y las mayúsculas; el valor por defecto false indica que sí se diferencian; si se asigna el valor true a esta opción, entonces no se diferencian en ningún caso, con independencia de que en
los patrones que se definan para las piezas sintácticas se pongan minúsculas o mayúsculas.
Pero, a veces, conviene que la diferenciación entre minúsculas y mayúsculas no se aplique de manera general a toda la entrada sino en particular a las secuencias de caracteres que se acoplen a ciertos patrones.
Para este fin, existe la posibilidad de establecer que, en la expresión regular que define el patrón de una
determinada pieza sintáctica, no se diferencie entre minúsculas y mayúsculas; ello se consigue poniendo
15
[ IGNORE_CASE ]
inmediatamente detrás de la palabra TOKEN con la que empieza la especificación de la pieza sintáctica en
la que se pretende la no distinción entre minúsculas y mayúsculas.
Nótese que "ignore_case" tiene dos significados:
- es el nombre de un parámetro de la sección de opciones (puede ponerse en minúsculas o mayúsculas),
- es una palabra reservada que puede aplicarse (escribiéndola entre corchetes y en mayúsculas) a la definición de una pieza sintáctica.
Por ejemplo, sea un lenguaje que, en general, distingue entre minúsculas y mayúsculas, pero en la codificación del exponente de una constante real exponencial puede ponerse la letra e en minúscula o en mayúscula. En la especificación JavaCC para ese lenguaje la opción Ignore_Case ha de tomar el valor
false; en la definición de una constante exponencial, son posibles las siguientes formas equivalentes:
1) sin hacer uso de IGNORE_CASE
TOKEN : {
< # cifras : ( [ "0" - "9" ] )+ >
TOKEN : {
< # signo
: ["+", "-"] >
TOKEN :
{
< constante :
<cifras> ("." <cifras>)?
}
}
}
["e",
"E"]
(<signo>)?
<cifras>
>
2) aplicando IGNORE_CASE
TOKEN : {
< # cifras : ( [ "0" - "9" ] )+ >
TOKEN : {
< # signo
: ["+", "-"] >
TOKEN [IGNORE_CASE] :
{
< constante :
<cifras> ("." <cifras>)? "e"
}
•
}
}
(<signo>)?
<cifras>
>
Secuencias de caracteres sin efecto en la estructura sintáctica
En los lenguajes de programación suele haber secuencias de caracteres que no forman parte de la estructura sintáctica de un programa; esas secuencias son detectadas por el analizador lexicográfico, pero no son
comunicadas al sintáctico, no han de considerarse como piezas, sino que han de saltarse. En JavaCC se
pueden definir estas secuencias mediante la palabra reservada SKIP; la notación de una especificación
con la palabra SKIP de una secuencia que ha de saltarse es la misma que la notación usada para las especificaciones con la palabra TOKEN de las piezas que han de comunicarse.
Los dos casos más habituales de secuencias que no se comunican al analizador sintáctico son:
a) los caracteres separadores que no tienen significado en el texto analizado
en los lenguajes de codificación en formato libre hay caracteres del texto fuente que han de ignorarse;
en la mayoría de los casos, estos caracteres son: el espacio en blanco, el tabulador, el retorno de carro
y el fin de línea; esta característica se especifica
SKIP :
b) los comentarios
{
" " |"\t" | "\r" |"\n"
}
16
en el análisis sintáctico de un programa, los comentarios han de ignorarse; una manera de especificar
esta circunstancia es poniendo en una definición SKIP la expresión regular que denota la forma de
los comentarios del lenguaje; por ejemplo, para los comentarios de una línea de Java se pondría
SKIP :
{
□
<
"//" (~ ["\n", "\r"])*
>
}
Tareas asociadas a la estructura sintáctica
JavaCC genera un analizador sintáctico descendente recursivo constituido por un conjunto de métodos: cada símbolo no terminal de la gramática sintáctica tiene asociado un método de análisis (el método tiene el
mismo nombre que el símbolo correspondiente). Un método de análisis, además de la realización de la
parte del análisis sintáctico que le corresponde, puede llevar a cabo otras tareas complementarias que sirvan
para efectuar un análisis más complejo del texto de entrada.
Bloque para un símbolo
Al describir la forma de las reglas sintácticas de un símbolo no terminal en una especificación JavaCC, ya
se ha visto que, detrás de la definición del nombre del símbolo y antes de empezar con las diferentes alternativas que tiene asociadas, se incluye un bloque de código Java (en el ejemplo de presentación este bloque
está vacío en todos los símbolos no terminales de la gramática). Cada bloque situado en esa posición puede
considerarse ligado al símbolo no terminal en cuya descripción está incluido; el código Java del bloque se
traslada literalmente al principio del método generado correspondiente al símbolo no terminal y, por lo tanto, es lo primero que se considera (se ejecuta) cuando, en el transcurso del proceso de análisis, se realiza
una llamada al método. Así pues, cada símbolo no terminal siempre tiene asociado un único bloque de código Java –bloque de presencia obligada, aunque pueda estar vacío-; en este bloque se pueden poner declaraciones y sentencias.
◊
Acciones sintácticas
Además del bloque asociado a cada uno de los símbolos no terminales de la gramática, se pueden incluir
otros bloques de código Java intercalados en cualquiera de las partes derechas de las reglas sintácticas. Los
bloques incluidos en las partes derechas se denominan “acciones sintácticas” (parser actions) ya que son
trozos de código que pueden considerarse asociados a los puntos de la estructura sintáctica donde están colocados. Una acción sintáctica se puede poner en cualquier posición: al principio, al final o en el medio de
la parte derecha de una regla, precedida o seguida de símbolos terminales o no terminales; el código Java
de una acción sintáctica se traslada literalmente al método asociado al símbolo no terminal de la parte
izquierda de la regla, de manera que se ejecuta cada vez que el analizador transcurre por el punto de la
estructura sintáctica donde se ha incorporado la acción (esto es, las acciones sintácticas se incorporan al
código del analizador sintáctico). No obstante su nombre, en los bloques de las acciones sintácticas suele
ponerse código dedicado a la realización de tareas semánticas.
◊
Si en el bloque asociado a un símbolo no terminal se ponen declaraciones, los nombres ahí definidos son
accesibles en todas las acciones sintácticas intercaladas en las partes derechas de las reglas de ese símbolo;
la justificación es sencilla: el código del bloque asociado al símbolo se incluye al principio de un método y
el código de las acciones pertenece a ese mismo método.
∆ Ejemplo 1
Se considera de nuevo el ejemplo de presentación; ahora se pretende completar el análisis léxico-sintáctico con la realización de las siguientes tareas:
- cada vez que se analiza una expresión, un término o un factor, se graba el nombre del símbolo no
terminal correspondiente, y
17
-
cuando se analiza un término en el que hay operadores multiplicativos, se cuenta su número y se
graba el resultado (nótese que se cuentan los operadores de cada término, pero no se acumula la
cuenta de todos los operadores de la expresión).
Para la realización de estas tareas se añaden bloques de código Java. En la especificación que se describe
a continuación puede apreciarse:
- para los símbolos no terminales expresion, termino y factor se ha incluido en los bloques correspondientes la operación de grabación de su nombre, que se ejecutará cada vez que se efectúe una llamada al método,
- en el bloque asociado a termino se tiene la declaración del contador de operadores (que será una
variable local del método),
- en la parte derecha de la regla que define la estructura de un término se han incluido dos acciones
sintácticas: una para contar la cantidad de operadores (situada detrás de la pieza sintáctica del operador) y otra para grabar la cantidad de operadores del término (situada al final de la parte derecha),
- se ha definido el método grabar para facilitar la escritura del código Java.
options {
Ignore_Case = true;
}
PARSER_BEGIN (ExprMin)
public class ExprMin {
public static void main (String[] argum)
throws ParseException {
ExprMin anLexSint = new ExprMin (System.in);
anLexSint.unaExpresion();
System.out.println("Analisis terminado:");
System.out.println
("no se han hallado errores léxico-sintácticos");
}
private static void grabar(String nombre) {
System.out.println (" -> " + nombre + "\n");
}
}
PARSER_END (ExprMin)
void unaExpresion() :
{ }
{
expresion() <EOF>
}
void expresion() :
{ grabar("Expresion");
}
{
termino() ( "+"
termino() )*
}
void termino() :
{
int nAst = 0;
grabar("Termino");
}
{
18
factor() ( "*" { nAst++; } factor() )*
{ if (nAst > 0)
System.out.println
("\n
Asteriscos: " + nAst + "\n");
}
}
void factor() :
{ grabar("Factor");
}
{
<constante>
| <variable>
| "(" expresion() ")"
}
TOKEN:
{
<
variable : ["a"-"z"]
TOKEN:
{
<
constante :
SKIP:
{
" " | "\t" | "\n" | "\r"
>
}
( ["0"-"9"] ) +
>
}
}
Cuando se incluyen acciones sintácticas, la legibilidad del aspecto sintáctico de la especificación queda
oscurecida; para paliar ese inconveniente, puede añadirse un comentario que muestre la regla sintáctica
escueta, sin las acciones; por ejemplo, podría escribirse:
void termino() :
{
int nAst = 0;
grabar("Termino");
}
{ /*
factor() ( "*"
factor() )*
*/
factor()
(
"*"
{ nAst++; }
factor()
)*
{ if (nAst > 0)
System.out.println
("\n
Asteriscos: " + nAst + "\n");
}
}
Valor comunicado por un método
En todo lo visto hasta ahora, los métodos de análisis sintáctico generados por JavaCC no devuelven valor
alguno al terminar su tarea de análisis, ya que en la especificación siempre se ha declarado el nombre de un
método precedido de void (son métodos de tipo void). Pero, en general, un método de análisis sintáctico puede ser de cualquier tipo (primitivo o no); por ello, un método puede devolver un valor de cualquier
tipo (de cualquier clase) al método desde el que se efectuó la llamada.
◊
Esta posibilidad de devolución de valores tiene una utilidad fundamental en la implementación de analizadores de lenguajes: permite la comunicación de datos (atributos de las entidades analizadas) entre los diferentes métodos que colaboran en el análisis del programa completo.
En una especificación JavaCC, la llamada a un método ocurre cuando en la parte derecha de una regla está
el nombre del símbolo no terminal asociado al método. Cuando un método devuelve un valor (tiene un tipo
distinto de void), es habitual la recepción del valor devuelto en una variable; en la propia especificación
19
se puede indicar el nombre de la variable a la que se asignará el valor devuelto tras la llamada, para ello se
emplea una notación idéntica a la forma que tienen las sentencias de asignación en el lenguaje Java
dato = nombreSimb()
donde dato es el nombre de la variable a la que se asigna el valor devuelto por el método llamado; esa variable ha de estar oportunamente declarada del mismo tipo que dicho método.
∆ Ejemplo 2
En el mismo ejemplo de presentación se desea, además del análisis léxico-sintáctico de la entrada, contar
la cantidad de operadores que hay en la expresión. A continuación se expone una especificación JavaCC
que es una posible solución; sólo se muestran las reglas sintácticas (el resto de la especificación es igual
que en el ejemplo de presentación).
En la especificación que se propone puede apreciarse:
- los símbolos no terminales expresion, termino y factor se han declarado de tipo int:
los métodos correspondientes devuelven un valor de tipo entero,
- el valor devuelto por los métodos sirve para propagar a través de la estructura sintáctica la cuenta de
los operadores que hay en el texto analizado,
- por tratarse de métodos que devuelven un valor, es preciso incluir las correspondientes sentencias de
devolución return.
void unaExpresion() :
{ int nOper; }
{
nOper = expresion() <EOF>
{ System.out.println
("Cantidad de operadores = " + nOper); }
}
int expresion() :
{ int n, i; }
{
n = termino()
( "+" { n++; } i = termino() { n = n + i; } )*
{ return n; }
}
int termino() :
{ int n, i; }
{
n = factor()
( "*" { n++; } i = factor() { n = n + i; } )*
{ return n; }
}
int factor() :
{ int n; }
{
<constante>
| <variable>
| "(" n = expresion() ")"
}
{ return 0; }
{ return 0; }
{ return n; }
La especificación de un factor también podría escribirse de esta otra manera:
20
int factor() :
{ int n = 0; }
{
(
<constante>
| <variable>
| "(" n = expresion() ")"
)
{ return n; }
}
A continuación se expone un nuevo ejemplo para ilustrar la utilidad que proporciona el valor devuelto por
un método de análisis sintáctico para la propagación (ascendente) de atributos (propiedades de las entidades analizadas).
∆ Ejemplo 3
Se pretende ampliar el analizador del ejemplo de presentación para que se emita un mensaje indicativo
cuando la expresión analizada sea una expresión en la que sólo aparecen constantes; en la solución que
se muestra sólo se incluyen las reglas sintácticas (en el resto de la especificación no hay modificaciones).
void unaExpresion() :
{ boolean esCte; }
{ /*
expresion() <EOF>
*/
esCte = expresion() <EOF>
{ if (esCte)
System.out.println("\nExpresion constante\n");
}
}
boolean expresion() :
{ boolean esCte, esTambienCte; }
{ /*
termino() ( "+" termino() )*
*/
esCte = termino()
( "+" esTambienCte = termino()
{ esCte = esCte && esTambienCte; }
)*
{ return esCte; }
}
boolean termino() :
{ boolean esCte, esTambienCte; }
{ /*
factor() ( "*" factor() )*
*/
esCte = factor()
( "*" esTambienCte = factor()
{ esCte = esCte && esTambienCte; }
{ return esCte; }
}
)*
21
boolean factor() :
{ boolean esCte; }
{
<constante>
| <variable>
| "(" esCte = expresion() ")"
{ return true; }
{ return false; }
{ return esCte; }
}
•
Reconsideración sobre las producciones vacías
Anteriormente se ha comentado que para representar una producción vacía en una especificación JavaCC
hay que hacer uso de los metasímbolos de opcionalidad; para ilustrar esta idea se ha incluido como ejemplo la especificación de una lista de parámetros compuesta por cero o más expresiones, separadas entre sí
por el carácter coma:
void parametros() :
{ }
{
[ expresion()
}
( ","
expresion() )*
]
Pero si se aprovecha la posibilidad de incorporar acciones sintácticas, se tiene una manera de especificar
directamente producciones vacías; para conseguirlo, basta considerar una producción vacía (no tiene nada
en su parte derecha) acompañada de una acción sintáctica cuyo bloque de código esté vacío. Según esta
posibilidad, la especificación anterior podría también escribirse así:
void parametros() :
{ }
{
expresion()
|
{
( ","
expresion() )*
}
}
En general, la acción sintáctica asociada a una producción vacía no tiene por qué ser un bloque vacío de
código; por ejemplo, si se quiere que el método que analiza los parámetros de una llamada devuelva la
cantidad de parámetros encontrados, se podría escribir la siguiente especificación
int parametros() :
{ int cantidad; }
{
(
expresion()
( ","
|
{ cantidad = 1; }
expresion()
{ cantidad++; } )*
{ cantidad = 0; }
)
{ return cantidad; }
}
y si no quisiera ponerse explícitamente la alternativa de la producción vacía, podría escribirse
int parametros() :
{ int cantidad = 0; }
22
{
[
expresion() { cantidad++; }
( "," expresion() { cantidad++; } )*
]
{ return cantidad; }
}
□
Comunicación entre los analizadores léxico y sintáctico
Cada vez que el analizador sintáctico realiza una llamada al analizador lexicográfico, recibe como resultado
de la llamada una representación de la pieza sintáctica encontrada en el texto analizado. En los analizadores
generados por JavaCC, la comunicación de la pieza se efectúa mediante un valor de clase Token. Esta clase está definida en el fichero Token.java que se obtiene en todo caso, siempre con el mismo contenido,
cualquiera que sea la especificación proporcionada como entrada al generador.
Entre los campos de la clase Token se encuentran los siguientes:
public int kind
número entero que sirve de representación interna de la pieza sintáctica; estos números asociados a
las piezas son asignados automáticamente por JavaCC; los valores considerados pueden consultarse en el fichero generado de nombre · · · Constants.java
public String image
cadena que contiene de la secuencia de caracteres (lexema) que constituyen la pieza sintáctica comunicada
public int beginLine, beginColumn, endLine, endColumn
posiciones ocupadas (números de línea y columna en el fichero de entrada) por el comienzo y el final del lexema de la pieza sintáctica comunicada
La clase Token tiene otros campos y métodos; en el fichero Token.java se tienen comentarios descriptivos sobre todos los campos y métodos que forman parte de la clase; todos los campos de la clase se
declaran como public.
Ya se ha comentado anteriormente que, cuando un método de análisis sintáctico (asociado a un símbolo no
terminal) tiene tipo, el valor devuelto tras una llamada se puede almacenar en una variable, por si se desea
utilizar en alguna acción sintáctica; para ello, se escribe
valor = nombreSimbolo()
(siendo valor el nombre de la variable y nombreSimbolo el nombre del símbolo no terminal). De
manera análoga, el objeto de la clase Token representativo de la pieza sintáctica encontrada en la entrada
por el analizador lexicográfico puede asignarse a una variable del mismo tipo para tener accesibles las características de la pieza, por si se precisaran en alguna acción sintáctica; para ello, en la parte de la especificación sintáctica donde aparezca la pieza sintáctica se escribe
dato = < nombrePieza >
donde nombrePieza es el nombre de la pieza sintáctica y dato es el nombre de una variable de tipo
Token en la que se anota el valor indicativo de las propiedades de la pieza sintáctica comunicada.
También existe otra manera de acceder a las características de la pieza sintáctica que el analizador lexicográfico comunica al analizador sintáctico; en la clase del analizador sintáctico se tiene declarado el campo
[
static
]
public Token token
23
cada vez que el analizador sintáctico recibe una pieza sintáctica comunicada por el analizador lexicográfico,
se deja anotada en el campo token, y ese contenido no se altera hasta que sea proporcionada la siguiente
pieza de la entrada.
∆ Ejemplo 4
Se considera una ampliación del ejemplo de presentación; además del análisis de la entrada, se pretende
obtener una relación numerada de las variables y las constantes, indicando el número de línea y el número de columna donde están situadas; por ejemplo, si en la entrada se tiene la expresión (grabada en dos
líneas).
x * (y + 11
) + 22
en la salida se tendrá
1.-
x
linea: 1
columna: 1
2.-
y
linea: 1
columna: 6
3.-
11
linea: 1
columna: 10
4.-
22
linea: 2
columna: 5
En la especificación que se propone como solución puede apreciarse:
- en la clase del analizador sintáctico se han incluido métodos auxiliares en los que se usan los nombres image, beginLine, beginColumn, que son campos de un objeto de clase Token,
- en la especificación sintáctica de factor se emplea la variable pieza, de tipo Token, para recoger las características de las piezas sintácticas detectadas,
- la variable numero hace de contador para numerar las constantes y variables.
options {
Ignore_Case = true;
}
PARSER_BEGIN (ExprMin)
public class ExprMin {
static int numero = 0;
private static void grabarLexema (int n, String lexema) {
System.out.print(n + ".- " + lexema + "
");
}
private static void grabarPosicion(int nL, int nC) {
System.out.print("linea: " + nL + "
");
System.out.println("columna: " + nC + "\n");
}
private static void grabarDatosPieza(int n, Token pieza) {
grabarLexema(n, pieza.image);
grabarPosicion(pieza.beginLine, pieza.beginColumn);
}
public static void main (String[] argum)
throws ParseException {
ExprMin anLexSint = new ExprMin (System.in);
anLexSint.unaExpresion();
System.out.println("Analisis terminado:");
System.out.println
("no se han hallado errores léxico-sintácticos");
24
}
}
PARSER_END (ExprMin)
void unaExpresion() :
{ }
{
expresion() <EOF>
}
void expresion() :
{ }
{
termino() ( "+"
}
void termino() :
{ }
{
factor() ( "*"
}
termino() )*
factor() )*
void factor() :
{ Token pieza; }
{
pieza = <constante>
{ numero++; grabarDatosPieza(numero, pieza); }
| pieza = <variable>
{ numero++; grabarDatosPieza(numero, pieza); }
| "(" expresion() ")"
}
TOKEN: {
< variable : ["a"-"z"]
TOKEN: {
< constante :
SKIP:
>
}
( ["0"-"9"] ) +
>
}
{ " " | "\n" | "\t" | "\r" }
Si se agrupan las dos acciones sintácticas comunes, la especificación de factor también puede escribirse de esta forma:
void factor() :
{ Token pieza; }
{
(
pieza = <constante>
| pieza = <variable>
)
|
}
{ numero++; grabarDatosPieza(numero, pieza); }
"("
expresion() ")"
25
Si se accede al campo token de la clase del analizador sintáctico, se puede resolver este ejemplo prescindiendo de la variable pieza:
void factor() :
{ }
{
(
|
<constante>
<variable>
)
| "("
{ numero++; grabarDatosPieza(numero, token); }
expresion() ")"
}
□
Tareas asociadas a las piezas sintácticas
Ya se ha visto que es posible asociar bloques de código Java a puntos de la estructura sintáctica para que se
realicen ciertas operaciones cuando el analizador sintáctico pase por esa zona de la estructura. También es
posible asociar código Java a las piezas sintácticas para que el analizador lexicográfico efectúe determinadas tareas cuando detecte en la entrada analizada piezas que tengan asociado código.
Acción ligada a una pieza sintáctica
Si se asocia un bloque de código a una pieza sintáctica; se consigue que, cada vez que se detecta en la entrada analizada la presencia de esa pieza, se ejecute en ese preciso momento el código del bloque asociado
a la pieza. Estas acciones se denominan “acciones lexicográficas” (lexical actions); el código del bloque se
traslada literalmente al código del analizador lexicográfico generado para que se ejecute oportunamente
cuando se detecte la pieza ligada al bloque de código. La acción lexicográfica se ejecuta inmediatamente
antes que la pieza sea comunicada al analizador sintáctico.
◊
Se puede asociar una acción lexicográfica a una pieza sintáctica declarada con TOKEN y también a una secuencia de caracteres declarada con SKIP; además de éstas, hay otras dos formas de declaraciones lexicográficas (no mencionadas en esta introducción), que también pueden llevar una acción asociada.
En una especificación JavaCC, la forma de una pieza sintáctica (o bien de una secuencia de caracteres) se
define poniendo su descripción delimitada entre llaves detrás de la indicación TOKEN: (o bien SKIP:); si
se quiere asociar una acción lexicográfica a la pieza (o a la secuencia), hay que colocar el bloque de código
de la acción a continuación de la descripción de la forma de la pieza (o de la secuencia); el bloque (delimitado por llaves) queda situado entre la descripción y la llave que cierra la especificación.
La pieza sintáctica especial que representa el final del fichero (EOF) también puede llevar asociada una acción lexicográfica; para ello, hay que poner
<*> TOKEN:
{
< EOF >
{ /* código de la acción asociada*/ }
}
para justificar la presencia de los caracteres <*> delante de la palabra TOKEN hay que basarse en el concepto de “estado lexicográfico” (cuya explicación queda fuera del alcance de esta introducción).
∆ Ejemplo 5
Se considera nuevamente el ejemplo de presentación; como resultado del análisis de una expresión se
pretende obtener una relación de los paréntesis contenidos en la expresión, en el mismo orden en que se
encuentran; además, por cada salto de línea del texto de entrada, se grabará en la salida el carácter # y
26
se pasará a una nueva línea; también se anotará la frase Final del trabajo cuando se alcance el
final del fichero de entrada. Así, por ejemplo, si la entrada analizada es
(a + b) *
(( x )) * (y + 7)
los resultados obtenidos serán
() #
(())() #
** Final del trabajo **
En la especificación JavaCC que se propone como solución, sólo se muestran las reglas sintácticas y lexicográficas (en el resto no hay cambios); en esta solución puede apreciarse:
- las piezas sintácticas correspondientes a los paréntesis se han definido de manera nominal, para poder asociarles una acción lexicográfica,
- los caracteres incluidos en la pieza de tipo SKIP se han descompuesto en dos grupos ya que la acción lexicográfica incorporada sólo afecta a uno de ellos.
void unaExpresion() :
{ }
{
expresion() <EOF>
}
void expresion() :
{ }
{
termino() ( "+"
}
void termino() :
{ }
{
factor() ( "*"
}
termino() )*
factor() )*
void factor() :
{ }
{
<constante>
| <variable>
| <pAbrir> expresion()
}
<pCerrar>
TOKEN: {
<
variable : ["a"-"z"]
<
constante :
>
}
TOKEN: {
( ["0"-"9"] ) +
>
}
TOKEN: {
< pAbrir : "(" >
{ System.out.print("("); }
27
}
TOKEN: {
< pCerrar : ")" >
{ System.out.print(")"); }
}
<*>
TOKEN: {
< EOF >
{
System.out.println
("** Final del trabajo **");
}
}
SKIP:
{
"\n"
{ System.out.print(" #\n"); }
}
SKIP:
{ " " | "\t" | "\r" }
Bloque de declaraciones lexicográficas
Cada definición de una pieza sintáctica puede llevar asociada una acción lexicográfica, que se ejecuta inmediatamente después de detectar en la entrada un lexema que se ajusta a la definición de la pieza; cada
acción lexicográfica es un bloque de código Java que puede contener sus propias declaraciones. Pero a veces, interesa tener declaraciones que sean compartidas por todas las acciones lexicográficas, con independencia de la pieza a la que esté asociada cada una de ellas.
◊
Para incluir declaraciones que puedan usarse desde el código asociado a cualquiera de las piezas sintácticas, se dispone de una construcción con el siguiente formato
TOKEN_MGR_DECLS : {
·
·
·
·
}
que consta de una palabra reservada, seguida del símbolo dos puntos, seguido de un bloque de código Java
(delimitado por llaves); esta declaración se puede colocar en cualquier parte de una especificación JavaCC;
si se incluye, sólo puede ponerse una vez.
Todas las declaraciones de variables o de métodos puestas en el bloque de declaraciones lexicográficas
compartidas son accesibles desde el código de cualquier acción lexicográfica; la justificación es sencilla: el
bloque de declaraciones se traslada al principio del código del analizador lexicográfico generado y las acciones lexicográficas también se trasladan a ese analizador.
Declaraciones lexicográficas predefinidas
De lo expuesto hasta ahora, se aprecia que en una acción lexicográfica asociada a una pieza sintáctica resultan accesibles:
- las declaraciones de su propio bloque (si tiene declaraciones),
- los componentes del bloque de declaraciones compartidas (si está incluido en la especificación).
Además de esto, se tienen otras declaraciones pertenecientes a la clase " · · · TokenManager", que
constituye el analizador lexicográfico, que también son utilizables desde cualquiera de las acciones lexicográficas; estas declaraciones pueden considerarse como “predefinidas” ya que son incorporadas automáticamente por el generador. Entre estas declaraciones accesibles se encuentran las siguientes:
◊
[ static ] StringBuffer image
contiene la cadena de caracteres representativa de la pieza sintáctica actual que se acaba de reconocer en le entrada; se trata de un campo de la clase y se declara como static cuando el analizador
28
se ha generado con la opción (por defecto) Static = true; si se generase con la opción Static = false, no quedaría declarada como static
Token matchedToken
variable de clase Token que representa la pieza sintáctica actual, la que se acaba de encontrar en
la entrada; se trata de una variable local del método al que se ha trasladado el código de la acción
(y, por ello, accesible desde ese código)
El campo image puede usarse tanto si la acción lexicográfica está asociada a una pieza declarada con
TOKEN como si lo está a una secuencia declarada con SKIP. Sin embargo, la variable matchedToken
no puede usarse en una acción asociada a un patrón especificado como SKIP; el motivo es que lo declarado como SKIP, no constituye propiamente una pieza sintáctica ya que no se va a comunicar al analizador
sintáctico.
∆ Ejemplo 6
Se propone aquí una nueva solución para el ejemplo nº 4 expuesto anteriormente.
Esta nueva solución tal y como se expone aquí admite algunas mejoras, ya que su pretensión sólo es servir de ilustración para algunos aspectos que se acaban de citar (más adelante se propone otra solución alternativa).
En la especificación de la solución es posible apreciar esto:
- las acciones lexicográficas pueden llevar sus propias declaraciones; las variables nLineaVar,
nColumnaVar, nLineaCte y nColumnaCte se declaran dentro de las acciones,
- la variable matchedToken a la que se hace referencia en el código de la acción lexicográfica asociada a una variable contiene la información sobre la pieza sintáctica que se acaba de detectar: una variable de la expresión analizada; análogamente con la referencia relativa a una constante,
- el nombre image que aparece en las acciones lexicográficas se refiere al campo de la clase ExprMinTokenManager (analizador lexicográfico); nótese que, en efecto, se comunica como un parámetro de clase StringBuffer,
- la variable numero se emplea para numerar las constantes y variables encontradas,
- los nombres beginLine y beginColumn son campos (públicos) de la clase Token, por ello,
se pueden aplicar a un objeto de esa clase.
options {
Ignore_Case = true;
}
PARSER_BEGIN (ExprMin)
public class ExprMin {
public static void main (String[] argum)
throws ParseException {
ExprMin anLexSint = new ExprMin (System.in);
anLexSint.unaExpresion();
System.out.println("Analisis terminado:");
System.out.println
("no se han hallado errores léxico-sintácticos");
}
}
PARSER_END (ExprMin)
void unaExpresion() :
29
{ }
{
expresion() <EOF>
}
void expresion() :
{ }
{
termino() ( "+"
}
void termino() :
{ }
{
factor() ( "*"
}
termino() )*
factor() )*
void factor() :
{ }
{
<constante>
| <variable>
| "(" expresion()
}
")"
TOKEN_MGR_DECLS :
{
static int numero = 0;
static void grabarLexema (int n, StringBuffer lexema) {
System.out.print(n + ".- " + lexema + "
");
}
static void grabarPosicion(int nL, int nC) {
System.out.print("linea: " + nL + "
");
System.out.println("columna: " + nC + "\n");
}
}
TOKEN: {
< variable : ["a"-"z"] >
{
int nLineaVar, nColumnaVar;
numero++;
nLineaVar = matchedToken.beginLine;
nColumnaVar = matchedToken.beginColumn;
grabarLexema(numero, image);
grabarPosicion(nLineaVar, nColumnaVar);
}
}
TOKEN: {
< constante : ( ["0"-"9"] ) +
{
int nLineaCte, nColumnaCte;
>
30
numero++;
nLineaCte = matchedToken.beginLine;
nColumnaCte = matchedToken.beginColumn;
grabarLexema(numero, image);
grabarPosicion(nLineaCte, nColumnaCte);
}
}
SKIP:
{ " " | "\n" | "\t" | "\r" }
La especificación precedente se puede escribir de otra manera más escueta, como se indica a continuación (sólo se muestran las modificaciones); sobre esta nueva especificación puede mencionarse:
- se han eliminado, por innecesarias, las variables declaradas dentro de las acciones lexicográficas,
- el nombre image que aparece en el bloque de las declaraciones compartidas se refiere al campo de
la clase Token; nótese que, en efecto, se aplica a un objeto de esa clase y se comunica como un parámetro de tipo String.
TOKEN_MGR_DECLS :
{
static int numero = 0;
static void grabarLexema (int n, String lexema) {
System.out.print(n + ".- " + lexema + "
");
}
static void grabarPosicion(int nL, int nC) {
System.out.print("linea: " + nL + "
");
System.out.println("columna: " + nC + "\n");
}
static void grabarDatosPieza(int n, Token pieza) {
grabarLexema(n, pieza.image);
grabarPosicion(pieza.beginLine, pieza.beginColumn);
}
}
TOKEN: {
< variable : ["a"-"z"]
{
>
numero++; grabarDatosPieza(numero, matchedToken);
}
}
TOKEN: {
< constante :
{
( ["0"-"9"] ) +
>
numero++; grabarDatosPieza(numero, matchedToken);
}
}
Acción lexicográfica común
Si hubiera que realizar la misma tarea tras la detección de todas y cada una de las piezas sintácticas, una
posibilidad incómoda sería repetir el mismo código en todas las acciones lexicográficas; para evitar tanta
reiteración, se tiene la opción Common_Token_Action; su valor por defecto es false, pero si se le
asigna el valor true, es posible definir una “acción lexicográfica común” que se escribe una única vez y
se aplica a todas las piezas sintácticas.
◊
31
Cuando la opción Common_Token_Action se establece con el valor true, es obligado incluir el bloque de declaraciones lexicográficas compartidas (TOKEN_MGR_DECLS). Además, en ese bloque se precisa la definición de un método cuyo encabezamiento sea
[
static
]
void CommonTokenAction(Token t)
el código de este método constituye la acción lexicográfica común que se ejecutará después de la detección
de cualquier pieza sintáctica; el método tiene un parámetro de tipo Token mediante el que se puede comunicar la información correspondiente a la pieza que se acaba de encontrar en la entrada analizada.
Si una pieza sintáctica tiene asociada una acción lexicográfica propia, la acción lexicográfica común (si está definida) se ejecutará inmediatamente después de realizar la acción propia.
La acción lexicográfica común se aplica a todas las piezas sintácticas definidas como TOKEN, bien sea nominalmente (en las reglas lexicográficas), o bien anónimamente (incorporadas dentro de las reglas sintácticas); sin embargo, no se aplica cuando la secuencia acoplada se corresponde con una expresión regular especificada como SKIP (nótese que, por el contrario, una especificación SKIP sí puede tener asociada una
acción lexicográfica propia).
El código del método CommonTokenAction se traslada, como un componente más del bloque de declaraciones compartidas (TOKEN_MGR_DECLS), al principio de la clase del analizador lexicográfico.
∆ Ejemplo 7
Se pretende ahora ampliar el ejemplo de presentación con la obtención de una relación numerada de los
lexemas de las piezas sintácticas encontradas en la entrada analizada; en el caso de las variables y de las
constantes, antes del lexema se mostrará un indicativo (Var / Const); por ejemplo, si la expresión de
la entrada es
( a + 987 ) * b
se grabará la salida
(
#1
Var a
#2
+
#3
Const 987
)
#5
*
#6
Var b
#7
#8
#4
En la solución que se propone a continuación, se muestran la definición de las opciones y las reglas sintácticas y lexicográficas (en el resto no hay modificaciones); puede resaltarse lo siguiente:
- las llamadas al método CommonTokenAction se incorporan automáticamente, no hay que incluirlas en la especificación; como parámetro de llamada a este método siempre se emplea el objeto que representa la pieza que se acaba de detectar,
- la acción lexicográfica común realiza la actualización del contador de piezas y la grabación del lexema de la pieza detectada, con su número de orden,
- la acción lexicográfica común también se aplica a las piezas sintácticas anónimas (paréntesis y operadores) y a la pieza especial que representa el final del fichero (EOF),
- las piezas sintácticas <constante> y <variable> tienen asociada una acción lexicográfica
propia, que se ejecuta antes de la acción lexicográfica común,
- los caracteres que se saltan (SKIP) no están afectados por la acción lexicográfica común.
options
{
Ignore_Case = true; Common_Token_Action = true;
32
}
void unaExpresion() :
{ }
{
expresion() <EOF>
}
void expresion() :
{ }
{
termino() ( "+"
}
void termino() :
{ }
{
factor() ( "*"
}
termino() )*
factor() )*
void factor() :
{ }
{
<constante>
| <variable>
| "(" expresion()
}
")"
TOKEN_MGR_DECLS :
{
static int numero = 0;
static void grabarIndicativo(String indicativo) {
System.out.print(indicativo + " ");
}
static void grabarLexema (int num, String lexema) {
System.out.print(lexema + "
#" + num + "\n");
}
static void CommonTokenAction(Token pieza) {
numero++;
grabarLexema(numero, pieza.image);
}
}
TOKEN: {
< variable : ["a"-"z"]
{
}
>
grabarIndicativo("Var");
}
33
TOKEN: {
< constante :
{
( ["0"-"9"] ) +
grabarIndicativo("Const");
>
}
}
SKIP:
{ " " | "\n" | "\t" | "\r" }
A modo de ejemplo de aplicación de la acción lexicográfica común, en lo que sigue se ofrece una solución
para la grabación (en el fichero predefinido de salida) del texto analizado, con las líneas numeradas.
[
∆ Ejemplo 8
Listado del texto analizado
]
Se desea completar el ejemplo de presentación con la grabación de un listado, con las líneas numeradas,
del texto analizado (nótese que una expresión puede escribirse ocupando varias líneas). No se muestra la
especificación sintáctica debido a que no hay ninguna modificación en ella. Los aspectos reseñables de
la solución que se propone son:
- se usa la variable numLin como contador de líneas,
- en la variable linea se van yuxtaponiendo los lexemas de las piezas sintácticas de una línea, esta
yuxtaposición se realiza mediante la acción lexicográfica común,
- las secuencias reconocidas con los patrones especificados como SKIP no implican la ejecución de la
acción lexicográfica común; por ello, su lexema (almacenado en la variable image) ha de yuxtaponerse explícitamente,
- hay que separar el patrón correspondiente al fin de línea, para detectar el momento en que ha de grabarse la línea completa cuyo final se ha alcanzado.
options {
Ignore_Case = true; Common_Token_Action = true;
}
PARSER_BEGIN (ExprMin)
public class ExprMin {
public static void main (String[] argum)
throws ParseException {
ExprMin anLexSint = new ExprMin (System.in);
anLexSint.unaExpresion();
System.out.println("\n\nAnalisis terminado:");
System.out.println
("no se han hallado errores léxico-sintácticos");
}
}
PARSER_END (ExprMin)
·
·
·
·
·
·
·
·
·
·
·
·
·
TOKEN_MGR_DECLS:
{
static int numLin = 0;
static String linea = "";
34
static void CommonTokenAction(Token pieza) {
linea = linea + pieza.image;
}
}
TOKEN:
{
<
variable : ["a"-"z"]
{
<
constante :
>
}
TOKEN:
( ["0"-"9"] ) +
>
}
SKIP:
{
< " " | "\t" | "\r" >
{ linea = linea + image; }
}
SKIP:
{
"\n"
{
numLin++;
System.out.println(numLin + ":
linea = "";
" + linea);
}
}
□
A modo de recapitulación
ↄ
Acción lexicográfica. Acción sintáctica
acción lexicográfica: bloque de código Java asociado a una pieza sintáctica nominal (TOKEN) o a una
secuencia de caracteres (SKIP); se ejecuta cuando se detecta en la entrada la pieza o la secuencia correspondiente; no se puede aplicar a piezas sintácticas anónimas (autodefinidas con comillas),
• acción sintáctica: bloque de código Java asociado a un punto de la estructura sintáctica (el punto indicado por el sitio de la producción donde se inserta el bloque); se ejecuta cuando el análisis de la entrada pasa por ese punto de la estructura.
•
ↄ
Valor de una pieza sintáctica comunicada
El analizador lexicográfico comunica al analizador sintáctico una pieza a través de un valor de la clase
Token (fichero Token.java); un objeto de la clase Token representa las características de la pieza
comunicada. En la clase Token, entre otros, se tienen los campos
String image
lexema de la pieza
int beginLine, beginColumn, endLine, endColumn
posición del lexema en el fichero analizado (indicada por los números de fila y de columna
de su comienzo y de su terminación)
ↄ
Valor asociado a un símbolo
35
En las producciones que especifican la sintaxis se pueden incluir asignaciones que sirven para dejar anotado el valor asociado a un símbolo:
no terminal
valor = nombreSimbolo()
el método de análisis asociado a nombreSimbolo se ha declarado de un cierto tipo (distinto
del tipo void); valor ha de ser una variable del mismo tipo que el método
• símbolo
terminal
dato = < nombrePieza >
el valor asignado es el valor de la clase Token representativo de la pieza sintáctica (nominal);
dato ha de ser una variable de tipo Token
• símbolo
ↄ
Miradas sobre la pieza sintáctica comunicada
La pieza sintáctica comunicada puede mirarse desde cada uno de los dos analizadores:
• analizador sintáctico
la pieza comunicada (desde el analizador lexicográfico) se tiene anotada en un campo de la clase
del analizador sintáctico declarado como
(static) public Token token
este valor es accesible desde el código de todos los métodos de análisis sintáctico; el contenido del
campo varía cuando se recibe una nueva pieza sintáctica
• analizador
lexicográfico
la pieza dispuesta para ser comunicada (al analizador sintáctico) se tiene anotada en una variable
local declarada como
Token matchedToken
este valor es accesible para el código de todas las acciones lexicográficas
ↄ
El lexema de la pieza comunicada
El lexema de la pieza sintáctica comunicada se encuentra disponible en dos sitios distintos que comparten el mismo nombre pero que son de distinto tipo y se usan de distinta manera:
• String
image
es un campo de la clase TOKEN; puede consultarse en relación con un objeto de esa clase que esté
disponible
• StringBuffer
image
es un campo de la clase del analizador lexicográfico ( ∙ ∙ ∙ ∙ TokenManager.java); es accesible
desde el código de la clase y, por lo tanto, desde todas las acciones lexicográficas (bloques de código que están incorporados al analizador lexicográfico);
se trata de un valor disponible tanto para las piezas sintácticas nominales (TOKEN) como para las
secuencias que se saltan (SKIP)
ↄ
Resumen relativo a la lexicografía
TOKEN
"∙ ∙ ∙"
SKIP
pieza sintáctica nominal
pieza sintáctica anónima
secuencia de caracteres que no forman pieza
36
1
2
3
4
acción
lexicográfica
común
acción
lexicográfica
propia
bloque de
declaraciones
lexicográficas
declaraciones
lexicográficas
predefinidas
Commom TokenAction
(acción ligada
a una pieza)
TOKEN_
MGR_DECLS
(disponibilidad
de uso)
TOKEN
si
si
si
si
"∙ ∙ ∙"
si
no
-
-
SKIP
no
si
si
si
▫ Las indicaciones "si" de las columnas 3 y 4 son consecuencia de las indicaciones de la columna 2.
▫ La acción lexicográfica común se ejecuta después de la acción lexicográfica propia.
□
Precisiones sobre el analizador sintáctico generado
Puede decirse que, en su funcionamiento más sencillo, cuando no se emplean las posibilidades de examen
por adelantado (“lookahead”), JavaCC comprueba que las reglas sintácticas cumplen la condición LL(1) y
generan un analizador descendente-predictivo-recursivo. Pero esta apreciación hay que matizarla, ya que el
analizador sintáctico generado no se ajusta en sentido estricto al modelo de analizador LL(1).
En el modelo de análisis LL(1), el orden del examen de las distintas alternativas de expansión de un símbolo no terminal es indiferente ya que se conocen los símbolos directores de cada una de ellas (que resultan
ser conjuntos disjuntos); en la implementación del subprograma de análisis asociado al símbolo no terminal
<NombreSimb> cuyas producciones son
<NombreSimb> ::=
α1
|
∙
|
α2
∙
∙
αn
se podría preguntar en cualquier orden si la pieza actual pertenece al conjunto de símbolos directores de las
distintas reglas. Sin embargo, en el analizador sintáctico generado por JavaCC, la implementación del método asociado al símbolo no terminal <NombreSimb> no sigue esa pauta.
Si las anteriores reglas sintácticas se transcriben en JavaCC de la forma
void nombreSimb () :
{ }
{
α1-jcc
| α2-jcc
∙
∙
∙
| αn-jcc
37
}
el método de análisis generado procede de la siguiente manera:
▫ se van considerando las alternativas, sucesivamente, en el mismo orden en el que están especificadas: primero la de α1 (α1-jcc), después la de α2 (α2-jcc), etc,
▫ para cada alternativa seleccionada, se comprueba si la pieza por adelantado coincide con alguno de
los símbolos iniciales de la parte derecha, y si hay coincidencia se prosigue el análisis con ella; si no
hay coincidencia se pasa a la alternativa siguiente,
▫ siguiendo con este orden, si se llega al final de las alternativas sin haberse encontrado coincidencia
alguna, se produce un error sintáctico.
En el analizador generado por JavaCC hay una peculiaridad que no se tiene en los analizadores LL(1) estrictamente considerados: la palabra vacía sí se considera como posible símbolo inicial de la parte derecha
una regla (esto ocurre siempre que la parte derecha es anulable). De esta peculiaridad se deriva una consecuencia que ha de tenerse en cuenta al escribir la especificación: dado que siempre es posible considerar la
palabra vacía como la siguiente pieza por adelantado, si se llega a examinar la alternativa correspondiente a
una producción anulable, el analizador generado siempre seleccionará esa alternativa con independencia de
la siguiente pieza que esté presente en la entrada que queda por analizar.
Así pues, si alguna de las producciones que definen un símbolo no terminal es anulable, es importante el orden en que se escriben las alternativas en una especificación sintáctica JavaCC: el funcionamiento del analizador generado sí depende de la colocación elegida para las reglas.
Para ilustrar estas peculiaridades del analizador sintáctico generado por JavaCC, a continuación se expone
un ejemplo.
∆ Ejemplo 9
Se quiere obtener un analizador léxico-sintáctico de textos que se ajustan al siguiente formato: una secuencia de uno o más nombres, seguida del símbolo igual, seguido de un número entero; si la secuencia
tiene más de un nombre, han de estar separados bien por una coma en todos los casos o bien por el símbolo dos puntos en todos los casos; esto es, son correctas las secuencias
uno, dos, tres = 123
exclusivo = 0
Abajo : Arriba = 57
y son incorrectas las secuencias
uno, dos : tres = 123
exclusivo
inclusive = 0
fuera : dentro : = 56
Una gramática que define las secuencias con este formato es:
<Lista> ::= nombre
<OtrosNombres> = numero
<OtrosNombres> ::= , nombre
|
: nombre
|
ε
{
{
, nombre
: nombre
}
}
se trata de una gramática que cumple la condición LL(1):
- el símbolo director para la alternativa vacía es el símbolo igual; es evidente, pues, que las tres reglas
de <OtrosNombres> tienen símbolos directores distintos,
38
-
la decisión de abandonar la repetición del análisis de un nuevo nombre (en las dos primeras alternativas de <OtrosNombres>) se puede tomar ante la presencia del símbolo igual.
En la implementación de un analizador sintáctico LL(1), en sentido estricto, la selección de la alternativa
vacía para expandir el símbolo <OtrosNombres> se haría tras comprobar explícitamente que la pieza sintáctica por adelantado es el símbolo igual.
El funcionamiento del analizador generado por JavaCC depende del orden de colocación de las reglas. A
continuación se exponen varias posibilidades de especificación y se comenta el analizador generado en
cada caso.
•
Especificación primera
Se transcriben literalmente las reglas tal y como se han escrito antes en la notación BNF; nótese que
para poder codificar la parte derecha de la regla vacía ha de incluirse una acción sintáctica vacía.
PARSER_BEGIN (Igualdad)
public class Igualdad {
public static void main (String[] argum)
throws ParseException {
Igualdad analisis = new Igualdad(System.in);
analisis.secuencia();
System.out.println("Analisis terminado:");
System.out.println
("no se han hallado errores léxico-sintácticos");
}
}
PARSER_END (Igualdad)
void secuencia() :
{ }
{
lista()
}
void lista() :
{ }
{
< nombre >
}
<EOF>
otrosNombres() "=" < numero >
void otrosNombres() :
{ }
{
"," < nombre >
| ":" < nombre >
|
{ }
}
( ","
( ":"
< nombre > )*
< nombre > )*
TOKEN : {
< nombre : ( [ "a" - "z", "A" - "Z", "ñ", "Ñ" ] )+ >
TOKEN : {
< numero : ( [ "0" - "9" ] )+ >
SKIP : { " " | "\r" | "\n" }
}
}
39
El analizador generado funciona correctamente puesto que en el método otrosNombres se selecciona la alternativa vacía después de haber comprobado que la pieza por adelantado no es símbolo inicial de ninguna de las dos alternativas precedentes (no es una coma ni un símbolo dos puntos).
•
Especificación segunda
Se considera la misma especificación anterior, pero cambiando el orden de colocación de las reglas:
ahora la producción vacía se coloca como primera alternativa (sólo se muestran las partes modificadas).
void otrosNombres() :
{ }
{
{
}
| "," < nombre >
| ":" < nombre >
}
( ","
( ":"
< nombre > )*
< nombre > )*
Con la entrada de esta especificación, el generador JavaCC produce un mensaje en el que se avisa de
que las dos alternativas que están colocadas detrás de la alternativa vacía nunca podrán seleccionarse;
aun así, se genera un analizador. Pero es un analizador que funciona incorrectamente; así, por ejemplo,
la entrada
mxa , nyz = 57
se considera incorrecta: después de visto el primer nombre, se intenta aplicar la producción vacía (se
espera encontrar el símbolo igual), pero habría que aplicar la regla cuyo símbolo director es la coma.
•
Especificación tercera
Se utilizan las posibilidades de la notación BNF-Ampliada para prescindir de la especificación explícita de la palabra vacía; se aprovecha que JavaCC admite el operador + para indicar repeticiones de
una o más veces; la alternativa vacía está implícitamente contemplada en la posibilidad de cero repeticiones que contempla el operador * (sólo se muestran las partes modificadas en la especificación).
void otrosNombres() :
{ }
{
( "," < nombre > )+
| ( ":" < nombre > )*
}
El método otrosNombres() generado hace las comprobaciones en el siguiente orden:
- se comprueba si la pieza por adelantado es una coma (símbolo inicial de la primera alternativa), y
en este caso, se selecciona la primera regla,
- si no es una coma, se comprueba si es un símbolo dos puntos (símbolo inicial de la segunda alternativa), y en este caso, se selecciona la segunda regla,
- si no es un símbolo dos puntos, se considera la palabra vacía como símbolo inicial (siempre posible implícitamente) para la segunda alternativa; en este caso, para que no haya error, la pieza por
adelantado ha de ser un símbolo igual.
Se trata, pues, de un analizador que funciona correctamente.
•
Especificación cuarta
40
Se pone la misma especificación anterior, pero cambiando el orden de consideración de la regla vacía:
ahora se incluye implícitamente en la primera alternativa (sólo se muestran las partes modificadas).
void otrosNombres() :
{ }
{
( "," < nombre > )*
| ( ":" < nombre > )+
}
Para esta especificación, el generador JavaCC emite un aviso en el que se indica que la segunda alternativa nunca se seleccionará; no obstante, se genera un analizador que no funciona como se supone
que debería hacerlo; por ejemplo, el análisis de la entrada
nyz : mxa = 1
produce un error: después de tratado el primer nombre, se intenta aplicar la producción vacía (correspondiente a la repetición de cero veces contemplada en la primera regla) por lo que no se encuentra el
esperado símbolo igual.
Descargar