|
Grado en Ingeniería Informática
Procesadores de Lenguajes
Curso 2022/2023
|
Práctica 3 |
|
La herramienta JavaCC: Especificación léxica
|
|
OBJETIVOS |
- Presentar la herramienta JavaCC.
- Descargar e instalar la herramienta.
- Descargar el plugin de JavaCC para Eclipse.
- Explicar la sintaxis básica de la herramienta.
- Explicar en detalle la especificación léxica utilizada por la herramienta.
- Presentar la especificación léxica del lenguaje Tinto en la herramienta JavaCC.
|
|
CÓDIGO A UTILIZAR |
El código de esta práctica es equivalente al desarrollado
en la práctica anterior, pero en este caso el analizador léxico se ha generado de manera automática por medio de la herramienta JavaCC.
El paquete tinto.parserjj contiene el archivo TintoParser.jj con la descripción léxica del lenguaje Tinto, así como los
archivos generados automáticamente con la herramienta JavaCC. El paquete tinto contiene la clase
TintoCompiler que se limita a comprobar el funcionamiento del analizador léxico.
|
|
Características de JavaCC |
|
JavaCC (Java Compiler Compiler) es una herramienta de generación automática
de analizadores gramaticales basada en Java. La herramienta es propiedad de
Oracle, la compañía propietaria del lenguaje Java, por lo que se
ha convertido en el metacompilador más usado por los programadores en Java.
El funcionamiento de la herramienta consiste en analizar
un fichero de entrada, que contiene la descripción de una gramática, y
generar un conjunto de ficheros de salida, escritos en Java, que contienen
la especificación de un analizador léxico y de un analizador sintáctico para
la gramática especificada.
Las características más importantes de esta herramienta
son las siguientes:
-
Es la herramienta más utilizada en Java. Los
propietarios estiman en cientos de miles el número de descargas de la
herramienta y los foros de discusión congregan a miles de usuarios
interesados en JavaCC.
-
Se basa en una análisis sintáctico descendente
recursivo.
-
Por defecto JavaCC analiza gramáticas de tipo LL(1),
pero permite fijar un Lookahead mayor (para analizar gramáticas
LL(k)) e incluso utilizar un Lookahead adaptativo.
-
Las especificaciones léxica y sintáctica de la
gramática a analizar se incluyen en un mismo fichero.
-
La especificación léxica se basa en expresiones
regulares y la especificación sintáctica utiliza el formato EBNF.
-
Junto a la herramienta principal se incluyen dos
utilidades: JJTree, para crear automáticamente un generador de árboles
sintácticos, y JJDoc, para generar automáticamente la documentación de la
gramática en formato HTML.
-
La distribución incluye numerosos ejemplos de
gramáticas y existen repositorios en internet con la especificación de
muchísimas gramáticas en el formato de JavaCC.
-
La gestión de errores léxicos y sintácticos está basada
en excepciones y contiene información muy valiosa respecto al origen del
error y su posición.
-
Existe un plugin para Eclipse que facilita la edición y
ejecución de la herramienta dentro del desarrollo de cualquier aplicación
en Java.
|
|
Descarga e instalación de JavaCC |
|
La herramienta JavaCC se distribuye como software de código abierto. La
página oficial de JavaCC es la siguiente:
https://javacc.org/
Desde esta página es posible descargarse la herramienta en forma de fichero
comprimido. La página incluye la última versión de la herramienta en formato
comprimido (ZIP) y algunas distribuciones más antiguas.
Para instalar la herramienta basta con descomprimir el
archivo de la distribución. Esto genera los directorios "/bin", "/doc"
y "/examples", así como el archivo "LICENSE" que contiene los
términos de la licencia de distribución. El directorio "/bin"
contiene archivos script para la ejecución de las herramientas JavaCC,
JJTree y JJDoc. Este directorio contiene también el subdirectorio "/lib"
que contiene el archivo "javacc.jar". Este es el archivo contiene la
librería de clases que desarrollan estas tres herramientas. El directorio "/doc"
contiene la documentación de la herramienta en formato HTML. Por último, el
directorio "/examples" incluye un conjunto de ejemplos para la
herramienta.
Para ejecutar la herramienta podemos añadir el directorio
"/bin" a la variable de entorno PATH con el siguiente comando:
| >set PATH=%PATH%;C:\javacc\bin |
donde hemos supuesto que el directorio de instalación de la herramienta ha
sido “C:\javacc”. De esta forma, podremos ejecutar la herramienta sobre un
fichero de especificación “Gramatica.jj” con el siguiente comando:
Una segunda opción consiste en almacenar el archivo "javacc.jar" en
el directorio "/lib/ext" de la instalación de Java. En este caso,
para ejecutar la herramienta sobre el fichero "Gramatica.jj" debemos
utilizar el comando:
| >java javacc Gramatica.jj |
|
|
Instalación del plugin de JavaCC
para Eclipse |
|
La herramienta JavaCC puede integrarse en el entorno de desarrollo Eclipse
gracias al siguiente plugin:
http://eclipse-javacc.sourceforge.net/
Existen dos formas de instalar el plugin. La primera consiste en descargar
el archivo
EclipseJavaCC-1.5.10.zip y descomprimirlo en el directorio de eclipse
(por ejemplo, C:\eclipse). La segunda
consiste en descargarlo desde Eclipse como una actualización del entorno. En
este segundo caso, los pasos a seguir son los siguientes:
Menu Help / Install New Software...
Work with > Add
Name : SF Eclipse JavaCC
URL :
http://eclipse-javacc.sourceforge.net/
Características del plugin:
Editor for .jj, .jjt and .jtb files.
Outline. (Menu Window->ShowView->Outline)
JavaCC Options setting for project. (Right click on project->Properties->JavaCC
options). There you can set the javacc.jar you whish to use. You can enable
builder to compile .jj, .jjt, .jtb on file save. Once the javacc.jar is
defined, you can compile with a right click on a .jj or .jjt file and choose
"Compile with JavaCC"
Console for JavaCC outputs. (Menu Window->ShowView->Others...->JavaCC
Console). Provides more complete information on errors reported by JavaCC.
Generated files are identified with a small 'G' on top right. Right click on
a generated file->Properties->Info,
uncheck Derived to remove this decorator. Open the file, edit, save to see
removal of this Decorator.
JJDoc compilation is available when a .jj or .jjt file is opened.
JTB compilation is available on a .jtb file
Help navigate into rules definitions. Click on a rule, and right click to "goto
definition". Use Workbench "Back" to go back.
|
|
Sintaxis de JavaCC |
|
JavaCC utiliza la extensión ".jj" para identificar los archivos de entrada
que contienen la especificación de la gramática a analizar. La estructura de
estos archivos se divide en cuatro partes.
La primera permite seleccionar una serie de opciones, como el valor del
Lookahead, si se van a generar métodos estáticos o no, si se va a distinguir
entre mayúsculas y minúsculas, etcétera.
La segunda parte permite definir el nombre del analizador e incluir el
código Java que se va a añadir directamente a este analizador, como el
nombre del paquete en el que se incluye, las claúsulas import, la cabecera
de la definición de la clase, los constructores de la clase, las variables
de instancia o los métodos que pueden ser utilizados en la definición de la
semántica de la gramática.
La tercera sección contiene la especificación léxica de la gramática.
La última parte del archivo la ocupa la descripción de las reglas
sintácticas de la gramática.
A continuación se muestra un ejemplo del contenido de un fichero de
especificación gramatical:
/* opciones generales */
options {
IGNORE_CASE = true;
STATIC = false;
...
}
PARSER_BEGIN(MiGramatica) /* definición del nombre del analizador */
/* codigo Java utilizado en la descripción del analizador */
import java.io.*;
public class MiGramatica {
...
}
PARSER_END(MiGramatica)
/* especificación léxica de la gramática */
TOKEN: /* Identificadores */
{
< ID: ( <LETTER> )+ ( "_" | "$" | "#" | <DIGIT> | <LETTER> )*
>
| < #LETTER: ["A"-"Z", "a"-"z"] >
| < #DIGIT: ["0"-"9"] >
}
SKIP:
{
" "
| "\n"
| "\r"
| "\t"
}
/* especificación sintáctica de la gramática */
void CreateTable() :
{
Token token;
}
{
<CREATE> <TABLE>
token = <ID> { System.out.println("Table: " + token.image); }
"(" ColumnList() ")"
}... |
El nombre utilizado en las sentencias PARSER_BEGIN y PARSER_END (que en el
ejemplo anterior sería MiGramatica) se utiliza como prefijo para generar
tres archivos: “MiGramatica.java”, “MiGramaticaConstants.java” y “MiGramaticaTokenManager.java”.
El primero de ellos desarrolla el analizador sintáctico. El segundo archivo
almacena un conjunto de constantes relacionadas con las categorías léxicas.
El tercer archivo desarrolla el analizador léxico para la gramática. Además
de estos archivos, la herramienta genera otros cuatro archivos cuyo
contenido es siempre el mismo: “JavaCharStream.java” (que define un flujo de
datos de entrada basado en caracteres ASCII), “ParseException.java” (que
define los errores sintácticos), “Token.java” (que define una categoría
léxica) y “TokenMgrError.java” (que define los errores léxicos).
El siguiente enlace contiene un ejemplo completo de descripción de una
gramática (ejemplo)
|
|
Opciones de JavaCC |
|
Las opciones de la herramienta se incluyen en la sección inicial del fichero
de especificación de la gramática. Esta sección es de la forma:
options {
NOMBRE_OPCION = valor;
NOMBRE_OPCION = valor;
...
} |
La siguiente tabla muestra las diferentes opciones
soportadas por JavaCC, su tipo y su valor por defecto.
|
Opción |
Tipo |
Valor por defecto |
|
LOOKAHEAD |
int |
1 |
|
CHOICE_AMBIGUITY_CHECK |
int |
2 |
|
OTHER_AMBIGUITY_CHECK |
int |
1 |
|
STATIC |
boolean |
true |
|
DEBUG_PARSER |
boolean |
false |
|
DEBUG_LOOKAHEAD |
boolean |
false |
|
DEBUG_TOKEN_MANAGER |
boolean |
false |
|
OPTIMIZE_TOKEN_MANAGER |
boolean |
false |
|
ERROR_REPORTING |
boolean |
true |
|
JAVA_UNICODE_ESCAPE |
boolean |
false |
|
UNICODE_INPUT |
boolean |
false |
|
IGNORE_CASE |
boolean |
false |
|
USER_TOKEN_MANAGER |
boolean |
false |
|
USER_CHAR_STREAM |
boolean |
false |
|
BUILD_PARSER |
boolean |
true |
|
BUILD_TOKEN_MANAGER |
boolean |
true |
|
SANITY_CHECK |
boolean |
true |
|
FORCE_LA_CHECK |
boolean |
false |
|
COMMON_TOKEN_ACTION |
boolean |
false |
|
CACHE_TOKENS |
boolean |
false |
|
OUTPUT_DIRECTORY |
String |
directorio actual |
Vamos a comentar brevemente el
significado de algunas de las opciones de la herramienta. Para obtener una
descripción más detallada de todas ellas puede consultarse la documentación
distribuida con la herramienta, que se encuentra en el directorio "/doc" de
la instalación.
-
La opción LOOKAHEAD permite especificar
el número de tokens a considerar para predecir la regla de producción a
expandir. Esta opción permite analizar gramáticas de tipo LL(k).
-
La opción STATIC permite seleccionar el
tipo de métodos que van a desarrollar el analizador. Por defecto se
generan métodos estáticos, de manera que para inicializar todas las
variables internas se debe utilizar el método ReInit(). Es importante
tener en cuenta que si se utiliza una descripción estática, no es posible
ejecutar análisis en paralelo.
-
La opción UNICODE_INPUT permite generar
analizadores que tomen como entrada ficheros en formato Unicode. Por
defecto se asume que los ficheros de entrada a analizar se encuentran en
formato ASCII.
-
La opción IGNORE_CASE se utiliza para
aquellos lenguajes que sean independientes de mayúsculas o minúsculas
(como SQL, por ejemplo).
|
|
Descripción léxica en JavaCC |
|
La especificación léxica de la gramática se introduce detras de la claúsula
PARSER_END, formando la tercera sección del fichero de entrada de la
herramienta. Esta especificación se basa en cuatro tipos de declaraciones:
TOKEN, SKIP, SPECIAL_TOKEN y MORE. Las declaraciones de tipo TOKEN permiten
definir expresiones regulares que representan tokens del lenguaje que serán
enviados al analizador sintáctico por medio del método getNextToken(). Las
declaraciones de tipo SKIP describen las expresiones regulares de las
categorías que son filtradas por el analizador léxico, es decir, los blancos
y los comentarios del lenguaje. Las declaraciones de tipo SPECIAL_TOKEN
generan tokens que no son enviados directamente al analizador sintáctico,
sino que se añaden en el campo specialToken del siguiente token reconocido.
Las declaraciones de tipo MORE permiten reconocer una secuencia que se
considera un prefijo del siguiente token. Tras reconocer una expresión de
tipo MORE seguida de una expresión de tipo TOKEN o SPECIAL_TOKEN, el lexema
reconocido como MORE se añade al comienzo del lexema del tipo TOKEN o
SPECIAL_TOKEN. Las especificaciones de tipo MORE y SPECIAL_TOKEN no se
utilizan demasiado.
La sintaxis básica de una declaración léxica es la siguiente:
TIPO_DECLARACION : {
< Token1_id : Expresión_regular_1 >
| < Token2_id : Expresión_regular_2 >
| < Token3_id : Expresión_regular_3 >
...
} |
El siguiente ejemplo muestra la definición de una constante de tipo real:
TOKEN : {
< FLOAT_LITERAL :
(["0"-"9"])+ "." (["0"-"9"])* (<EXPONENT>)? (["f","F"])?
| "."
(["0"-"9"])+ (<EXPONENT>)? (["f","F"])?
| (["0"-"9"])+
(<EXPONENT>)? (["f","F"])?
>
| < #EXPONENT: ["e","E"] (["+","-"])? (["0"-"9"])+ >
} |
Como puede apreciarse en el ejemplo, la expresión regular asociada a la
definición de una categoría léxica (FLOAT_LITERAL en este caso) puede
incluir otras categorías léxicas (como EXPONENT). El símbolo # delante del
identificador del token indica que se trata de una definición auxiliar que
va a ser utilizada en otras definiciones, pero no corresponde a la
definición de un token del lenguaje.
|
|
Descripción sintáctica en JavaCC |
|
La especificación sintáctica de la gramática se describe a continuación de
la especificación léxica. (En realidad, las definiciones léxicas y
sintácticas pueden estar mezcladas, pero resulta mucho más claro si se
definen de forma ordenada.) Esta especificación corresponde a la definición
de las reglas de producción asociadas a cada símbolo no terminal de la
gramática. La sintaxis básica de una declaración sintáctica es la siguiente:
tipo identificador (
lista_de_parámetros ) :
{
código_java
}
{
descripción_EBNF
} |
Como ya hemos comentado, JavaCC se basa en un análisis
sintáctico descendente recursivo. En este tipo de análisis, cada símbolo no
terminal genera una función. La cabecera de la declaración sintáctica
corresponde a la cabecera de la función asociada al símbolo no terminal des-crito.
El tipo se refiere al tipo de dato que devuelve la función, que puede ser
tanto un tipo de datos simple como un objeto de una determinada clase. El
identificador corresponde al nombre del símbolo no terminal, que coincide
con el nombre de la función asociada. La lista de paráme-tros se refiere a
los parámetros a utilizar en la llamada a la función y se describen de la
misma forma que en un método Java. El código Java que sigue a la cabecera de
la declaración es inclui-do literalmemte en la función generada y se suele
utilizar para declarar las variables que se van a utilizar en la función.
La descripción EBNF de la regla de producción permite
utilizar todas las operaciones de este formato: la disyunción (por medio del
carácter ‘|’), la clausura (siguiendo el formato ‘(‘ expresión ‘)’* ), la
clausura positiva (con el formato ‘(‘ expresión ‘)’+ ) y la opcionalidad
(utilizando ‘[‘ expresión ‘]’ ). Para referirnos a símbolos terminales, es
decir, a tokens, se utiliza la expresión ‘<‘ nombre_del_token ‘>’. Para
referirnos a símbolos no terminales se utiliza la expresión
identificador(lista_de_parámetros), es decir, se introduce una llamada a la
función asociada al símbolo no terminal.
Como hemos comentado, las funciones asociadas a los
símbolos terminales pueden devolver un valor de cualquier tipo válido en
Java. Por su parte, los símbolos terminales devuelven un valor de tipo
Token. Estos valores pueden asignarse variables de la siguiente forma:
variable = <TOKEN_ID>
variable = identificador(lista_de_parámetros) |
Estas variables deben haber sido declaradas en el bloque
Java incluido al comienzo de la declaración sintáctica.
A continuación se muestra un ejemplo de definición de una
regla de producción sintáctica:
void asignacion() :
{
Token id;
}
{
id = <ID> <IGUAL> expresion() <PUNTO_Y_COMA>
} |
|
|
Declaraciones léxicas |
|
La sintaxis básica de una declaración
léxica en JavaCC es la siguiente:
TIPO_DECLARACIÓN : {
expresión_regular
| expresión_regular
| ...
} |
La herramienta JavaCC ofrece cuatro tipos de
declaraciones léxicas:
-
TOKEN: permite definir un conjunto de categorías léxicas
(tokens) que serán devueltas al analizador sintactico.
-
SKIP: define un conjunto de categorías léxicas que serán
filtradas por el analizador léxico, es decir, que no serán enviadas al
analizador sintáctico.
-
SPECIAL_TOKEN: define un conjunto de categorías léxicas
que no serán enviadas directamente al analizador sintáctico, sino que se
añaden en el campo specialToken de la siguiente categoria léxica a
enviar.
-
MORE: define un conjunto de expresiones regulares que no
generan una categoría léxica, sino que son añadidas como prefijo en el
lexema de la siguiente categoría léxica reconocida.
|
|
Expresiones regulares |
|
JavaCC ofrece cuatro formas de describir una expresión regular:
expresión_regular ::=
“cadena”
expresión_regular ::= < identificador : expresión_regular_compleja
>
expresión_regular ::= < identificador >
expresión_regular ::= < EOF > |
La primera forma permite definir patrones constantes, por ejemplo "/*" para
el comienzo de un comentario o "\n" para indicar un salto de línea. Este
tipo de expresiones no asigna un identificador a la expresión regular, por
lo que se suele utilizar en declaraciones de tipo SKIP o MORE.
La segunda forma permite describir expresiones regulares
complejas y asignarles un identificador. Este formato es el utilizado
habitualmente en las declaraciones de tipo TOKEN y SPECIAL_TOKEN. Si el
identificador comienza con el símbolo #, entonces la expresión regular no
define una categoría léxica (un token), sino que se considera una definición
auxiliar que puede ser utilizada en otras expresiones regulares.
La dos últimas formas de describir una expresión regular
representan una referencia a otras expresiones regulares. La tercera forma
representa una referencia a una expresión regular definida en otro punto,
mientras que la última forma se refiere al símbolo especial de fin de
entrada.
Las expresiones regulares complejas pueden contener
cadenas de caracteres (por ejemplo, "import"), listas de caracteres
entre corchetes (por ejemplo, ["a"-"z","A"-"Z","0"-"9"]), referencias
a otras expresiones regulares (por ejemplo, <DIGITO>), así como los
operadores de clausura (por ejemplo, ( <DIGITO> )* ), clausura
positiva (por ejemplo, (<DIGITO>)+ ), disyunciones (por ejemplo
("import" | "IMPORT") ) y opcionalidad (por ejemplo,
(<DIGITO>)? ). Para representar a cualquier carácter excepto
algunos se utiliza ~ (por ejemplo, ~["\\"], representa
cualquier carácter que no sea una barra).
A continuación se muestra un ejemplo de especificación
léxica:
TOKEN: {
<MAS: "+">
| <MENOS: "-">
| <POR: "*">
| <DIV: "/">
| <PARAB: "(">
| <PARCE: ")">
| <NUM: (["0"-"9"])+ ( "." (["0"-"9"])* )? (<EXPONENT>)? >
| < #EXPONENT: ["e","E"] (["+","-"])? (["0"-"9"])+ >
}
SKIP: {
" "
| "\n"
| "\r"
| "\t"
}
|
|
|
Acciones asociadas al
reconocimiento de expresiones regulares |
|
La sintaxis de la especificación léxica en JavaCC permite incluir código
Java que será ejecutado al reconocer una determinada expresión regular. Este
código se incluye entre llaves detrás de la definición de la expresión
regular. Por ejemplo, con la siguiente declaración léxica se escribe un
mensaje cada vez que se reconoce la palabra class en el flujo de
entrada.
TOKEN : {
<CLASS: “class”> { System.out.println(“Palabra
class reconocida”); }
| <INTERFACE: “interface”>
} |
Dentro del código Java que describe las acciones
asociadas a las expresiones regulares se puede acceder a las siguientes
variables y métodos del analizador léxico:
-
image (StringBuffer): contiene la cadena de
caracteres reconocida por medio de la expresión regular. Se puede
modificar, aunque generalmente no produce efectos sobre el lexema
entregado al analizador sintáctico ya que éste ya ha sido creado en el
momento de ejecutar la acción. Para modificar el lexema es necesario
acceder a matchedToken.image.
-
lengthOfMatch (int): longitud de la cadena
reconocida. No tiene en cuenta los caracteres que puedan ser añadidos por
una declaración MORE.
-
curLexState (int): código del contexto léxico
actual. Veremos los contextos léxicos más adelante.
-
inputStream (InputStream): flujo de datos de
entrada del analizador. En el momento de ejecutar la acción se encuentra
posicionado en el último carácter reconocido, de manera que el método
read() nos devuelve el siguiente carácter.
-
matchedToken (Token): categoría léxica que se va a
devolver al analizador sintáctico, es decir, valor que va a devolver el
método getNextToken() del analizador léxico.
-
void SwitchTo(int código): modifica el contexto
léxico actual.
|
|
Código auxiliar incluido en el
analizador léxico |
|
La herramienta JavaCC permite definir un código que se introduce
directamente en el analizador sintáctico. Este código es el que aparece
entre las cláusulas PARSER_BEGIN y PARSER_END. También es posible definir
código que se incorpora directamente en el analizador léxico, aunque en este
caso el código no incluye la definición de la clase. Para ello se utiliza la
siguiente expresión:
TOKEN_MGR_DECLS : {
código_JAVA
} |
Este código permite definir variables de instancia y métodos que pueden ser
utilizados en las acciones asociadas a las expresiones regulares.
|
|