Escuela Técnica Superior de Ingeniería

 

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:

>javacc Gramatica.jj

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:

  • Desde la ventana principal de Eclipse abrir los menús:

Menu Help / Install New Software...

  • En la ventana "Available software", abrir

Work with  > Add

  • En esta ventana rellenar los datos:

Name : SF Eclipse JavaCC
URL : http://eclipse-javacc.sourceforge.net/

  • Seleccionar "SF Eclipse JavaCC" y pulsar Finish

Características del plugin:
 

  1. Editor for .jj, .jjt and .jtb files.

  2. Outline. (Menu Window->ShowView->Outline)

  3.  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"

  4. Console for JavaCC outputs. (Menu Window->ShowView->Others...->JavaCC Console). Provides more complete information on errors reported by JavaCC.

  5. 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.

  6. JJDoc compilation is available when a .jj or .jjt file is opened.

  7. JTB compilation is available on a .jtb file

  8. 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.

  1. 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.

  2. 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.

  3. La tercera sección contiene la especificación léxica de la gramática.

  4. 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.