Você está na página 1de 8

Herramientas para construir compiladores

1. Herramientas Tradicionales
Se incluyen en este apartado las herramientas clásicas: Lex/Yacc, Pclex, Pcyacc, Bison, Flex ,...

Ventajas:
• Los analizadores generados son muy eficientes, incluso más que los que pudiéramos hacer
de manera manual.

• Los analizadores ascendentes pueden reconocer la mayor parte de los lenguajes libres de
contexto.

Inconvenientes:
• Yacc necesita usar herramientas externas para que le provean los tokens necesarios.

• Las acciones semánticas asociadas a las producciones de los no terminales de las gramáticas
son difíciles de depurar. Puesto que este código pasa directamente al código del analizador
generado por Yacc, es necesario comprender el funcionamiento de un analizador
ascendentes, para saber donde falla algo.

• Yacc no genera ASTs (Abstract Syntax Trees) con lo cual no es fácil conseguir compiladores
que realicen sus tareas en varias pasadas. Los analizadores generados por Yacc, en general,
realizan sus tareas leyendo el fichero fuente una sola vez.

• Se mezclan las especificaciones sintácticas con las especificaciones semánticas.

• Se alarga el ciclo de desarrollo que pasa de ser edición-compilación-prueba a edición-yacc-


compilación-prueba.

• Se incrementa la dificultad del trabajo de depuración puesto que los errores que se cometen
en el fichero de especificación son sólo visibles en el analizador generado.

2. Herramientas de nueva generación (ANTLR y JAVACC)

 ANTLR

Se considera el Lex/Yacc del nuevo milenio. Genera analizadores sintácticos recursivos


descendentes basados en gramáticas LL(k). Está escrito íntegramente en Java y genera código
en Java o C++.

Ventajas:

• Buena integración de los analizadores léxico y sintáctico.

• Debido a la naturaleza de los analizadores descendentes, el código generado por Antlr es más
fácil de depurar y comprender que el de Yacc.

• Las especificaciones gramaticales de Antlr admiten la notación EBNF y genera ASTs.


Desventajas:

• Los analizadores generados por Antlr son menos eficientes que los generados por Yacc.

• Los ficheros de especificaciones de Antlr (como los de YACC) pueden volverse realmente
complejos ya que contienen muchas más cosas que la especificación gramatical propiamente
dicha.

• El proceso de depurado consta de los siguientes pasos: escribir el código de las acciones,
compilar el fichero de especificaciones, compilar y ejecutar el programa generado, localizar
los errores en el programa y corregir los errores en el fichero de especificaciones.

Ejemplo:

// Fichero calc.g

class CalcParser extends Parser;


options {
buildAST = true; // usa CommonAST por defecto
}

expr
: mexpr (PLUS^ mexpr)* SEMI! //! Operador para indicar que el
; // token será ignorado para el AST

mexpr
: atom (STAR^ atom)*
;

atom: INT
;

class CalcLexer extends Lexer;

WS : (' '
| '\t'
| '\n'
| '\r')
{ _ttype = Token.SKIP; }
;

LPAREN:'('
;

RPAREN:')'
;

STAR: '*'
;

PLUS: '+'
;

SEMI: ';'
;

protected
DIGIT :'0'..'9'
;

INT : (DIGIT)+
;
class CalcTreeWalker extends TreeParser;

expr returns [float r]


{
float a,b;
r=0;
}
: #(PLUS a=expr b=expr) {r = a+b;}
| #(STAR a=expr b=expr) {r = a*b;}
| i:INT {r = (float)Integer.parseInt(i.getText());}
;

// Fichero calc.java

#include <iostream>
#include <antlr/AST.hpp>
#include "CalcLexer.hpp"
#include "CalcParser.hpp"
#include "CalcTreeWalker.hpp"

int main()
{
using namespace std;
try {
CalcLexer lexer(cin);
CalcParser parser(lexer);
// Analiza la expresión de la entrada
parser.expr();
RefAST t = parser.getAST();
// Imprime el AST usando la notación LISP
cout << t->toStringTree() << endl;
CalcTreeWalker walker;
// Recorre el árbol creado por el analizador
float r = walker.expr(t);
cout << "value is " << r << endl;
} catch(exception& e) {
cerr << "exception: " << e.what() << endl;
}
}
Las especificaciones de Antlr incluyen la descripción de la gramática (al lado las producciones
aparecen las acciones en lenguaje Java) y la especificación del analizador léxico.

Las especificaciones del analizador léxico y del sintáctico serán creados en forma de dos clases, la
clase CalcLexer y la clase CalcParser.

No se necesita especificar la prioridad de los operadores puesto que los analizadores generados son
descendentes.

Se debe incluir al menos otro fichero con el método main para que se encargue de inicializar una
instancia del analizador léxico y del analizador sintáctico.

ANTLR genera:

• Una clase que contiene al lexer. Su función es obtener los tokens que va proporcionando al
analizador sintáctico.

• Una clase que contiene al parser. Como es descendente, comienza su análisis desde un
símbolo inicial y va derivando hasta encontrar los tokens que le proporciona el analizador
léxico. La clase generada contiene un método para cada una de las reglas de la gramática y es
subclase de LlkParser, que es la clase padre de todos los analizadores sintácticos que genera
Antlr.
• Una clase que contiene el AST. Los ASTs permiten construir un árbol de derivación genérico
que podrá ser aplicado a un programa concreto expresado en un lenguaje de programación
determinado. Sobre este AST se podrá aplicar uno o varios tree-walkers o clases que se
encarguen de recorrer el árbol.

Tratamiento de errores:
El mecanismo de tratamiento y recuperación de errores de Antlr se basa en el sistema de
tratamiento de excepciones presente en Java (y en otros lenguajes de programación como C++).
Incluye una jerarquía de excepciones con las siguientes clases:

• ANTLRException. La raíz de la jerarquía de excepciones.

• CharStreamException. Problemas leyendo del fichero que contiene la especificación de


entrada de Antlr.

• CharStreamIOException. Problemas de E/S.

• RecognitionException. Un error genérico que se produce cada vez que hay un problema
asimilando la entrada con las estructuras gramaticales.

• MismatchedCharException. El carácter leído no es el esperado.

• MismatchedTokenException. Lo mismo que antes pero en vez de un carácter es un


elemento terminal completo de la gramática.

• NoViableAltException. A la hora de elegir entre las producciones de un no terminal, el


token leído no se incluye en el comienzo de ninguna de ellas.

• NoViableAltForCharException. Lo mismo que el caso anterior pero un carácter en vez de


un token.

• SemanticException. Excepción para cuando hay problemas con los predicados semánticos.

El sistema se basa en el funcionamiento de los analizadores sintácticos descendentes. Se comienza


desde un símbolo inicial y se va derivando por las distintas producciones de los no terminales hasta
que se encuentran los terminales que se corresponden con la entrada del usuario. Si en ese proceso
de derivación se produce un error, se lanza una excepción que es tratada por alguna de las acciones
semánticas asociadas con la sección gramatical que ha invocado al método donde se produjo dicho
error.

 JAVACC

Inicialmente apareció con el nombre Jack y es una herramienta para construir compiladores
similar a ANTLR, es un analizador descendente. No existen muchas diferencias entre los dos
productos pero Javacc es un producto comercial de acceso libre (sin ficheros fuentes) mientras
que ANTLR es de dominio público (incluido los fuentes).

Ventajas:

• Buena integración de los analizadores léxico y sintáctico. Se utiliza la notación EBNF


• Genera analizadores sintácticos recursivos descendentes basados en gramáticas LL(k) e
incluye la herramienta JJTree para generar AST’s.

Desventajas: Las mismas que las citadas para ANTLR

Ejemplo:

// Una calculadora sencilla

PARSER_BEGIN(Calc0) // must define parser class


public class Calc0 {
public static void main (String args []) {
Calc0 parser = new Calc0(System.in);
for (;;)
try {
if (parser.expr() == -1)
System.exit(0);
} catch (Exception e) {
e.printStackTrace(); System.exit(1);
}
}
}
PARSER_END(Calc0)

SKIP: // defines input to be ignored


{ " " | "\r" | "\t"
}

TOKEN: // defines token names


{ < EOL: "\n" >
| < CONSTANT: ( <DIGIT> )+ > // re: 1 or more
| < #DIGIT: ["0" - "9"] > // private re
}

int expr(): // expr: sum \n


{} // -1 at eof, 0 at eol
{ sum() <EOL> { return 1; }
| <EOL> { return 0; }
| <EOF> { return -1; }
}

void sum(): // sum: product { +- product }


{}
{ product() ( ( "+" | "-" ) product() )*
}

void product(): // product: term { *%/ term }


{}
{ term() ( ( "*" | "%" | "/" ) term() )*
}
void term(): // term: +term | -term | (sum) | number
{}
{ "+" term()
| "-" term()
| "(" sum() ")"
| <CONSTANT>
}

En primer lugar, se especifica el código que ha de proveer el usuario para inicializar y terminar el
análisis.

La sección SKIP, contiene las expresiones regulares que se corresponden con las partes de la
entrada que se pueden ignorar en el proceso de análisis, como por ejemplo, comentarios.

La sección TOKEN, recoge las expresiones regulares asociadas a los tokens de la gramática. A
partir de esta información se generará el analizador léxico.

El resto de la especificación representa la gramática. Al lado de cada no terminal de la gramática se


colocan las distintas producciones y al lado de estas el código de acción. Se puede observar que los
no terminales se definen como métodos de la clase que se va a generar.

Aunque en el ejemplo se ve como todos los tokens han sido definidos en la sección correspondiente,
lo cierto es que se puede realizar definiciones de este tipo dentro del ámbito de una regla, aunque en
ese caso dicha definición solo tendrá validez dentro de esa regla.

JavaCC genera las siguientes clases:

• TokenMgrError. Esta clase se encarga de que los mensajes de error presenten suficiente
información al usuario.

• ParseException. La excepción que se lanza en caso de error.

• Token. La clase que encapsula los elementos terminales que se encarga de obtener el
analizador léxico para el sintáctico.

• Parser. El analizador sintáctico principal.

• ParserTokenManager. Un gestor de elementos terminales para el analizador sisntáctico.

• ParserConstants. Constantes que son empleadas por el analizador sintáctico.

Tratamiento de errores:

• Está basado en el mecanismo de excepciones del lenguaje Java. No existe una jerarquía de
excepciones determinada sino que cuando se encuentra un error durante el proceso de
análisis se lanza una excepción de tipo ParserException.

• Una excepción podrá ser tratada en el lugar donde se produjo o en cualquier otra producción
que la pueda anteceder en el proceso de derivación.

• El usuario podrá utilizar otras excepciones además de ParserException habiéndolas definido


previamente. El código asociado al tratamiento de errores se incluye en la propia
especificación de JavaCC, junto al código de acción y al lado de la especificación sintáctica.
• Gracias a que JavaCC genera la clase TokenMgrError, es posible personalizar los mensajes
de error para que sean comprensibles para el usuario y le ayuden a resolver el error que ha
tenido lugar.

3. Otras Herramientas

 BYACC/JAVA

Es una extensión del YACC estándar para generar código Java en vez de código C/C++. El
fichero de especificaciones es el mismo que para YACC pero el código de las acciones, y la
secciones de código y declaraciones del lenguajes estarán escritas en Java.

Los pasos a seguir son:


yacc -j fichero.y genera el fichero parser.java

javac parser.java genera el fichero parser.clas que puede ejecutarse con:

java parser

 COCO/JAVA

Es un generador de compiladores que a partir de la descripción del lenguaje mediante una


gramática LL(1) genera un analizador sintáctico recursivo descendente y un analizador
léxico para dicho lenguaje. El programador solamente tiene que añadir una clase main para
que llame al parser y las clases semánticas necesarias.

 CUP

El interés por el desarrollo de compiladores en Java ha dado lugar a la versión Lex/Yacc


para Java con el nombre de Jlex/CUP. Su forma de trabajo es análoga.

 JACCIE (Java-based Compiler-Compiler in an Interactive Environment)

Una herramienta educativa que se puede utilizar para visualizar las técnicas de compilación.
Proporciona un entorno integrado con editores especiales para las definiciones del scaner y
parser, herramientas para ver información derivada como el conjunto de iniciales -first /
siguientes -follow y para el depurado del scaner y parser.

Los principales componentes de Jaccie son: un generador de analizadores léxicos y una


variedad de generadores sintácticos para LL(1), LALR(1), SLR(1).
 JELL

Es un generador de analizadores sintácticos que genera analizadores recursivos descendentes


a partir de gramáticas LL(1).

4. Kits para la construcción de compiladores

 COCKTAIL

Un conjunto de herramientas para construir compiladores. Se compone de las siguientes


herramientas:
- Rex. Un generador de analizadores léxicos basado en expresiones regulares y acciones
semánticas escritas en C o Módula-2.
- Lalr. Un generador de analizadores sintácticos basado en el tipo de análisis LALR(1) con
algunas diferencias respecto al YACC.
- Ell. Un generador de analizadores sintácticos basado en el tipo de análisis LL(1).
- Ast. Un generador de AST (árboles sintácticos abstractos).
- Ag. Permite procesar gramáticas atribuidas.

 ELI

Combina una variedad de herramientas estándar para implementar potentes estrategias en la


construcción de compiladores dentro de un dominio especifico.
Se pueden generar automáticamente implementaciones de lenguajes completos a partir de
las especificaciones de la aplicación.
Contiene librerías de especificaciones reusables.

 PCCTS (Purdue Compiler Construction Tool Set)

Originalmente PCCTS 1.33 fue escrito en C++ para generar compiladores escritos en C++.
Más tarde fue portado a Java y llamado ANTLR 2.xx. PCCTS consta de tres herramientas:

- ANTLR ( Generador de analizadores sintácticos recursivos LL(k))

- DLG ( Generador de analizadores léxicos basados en DFA, análogo al lex)

- SORCERER ( Herramienta para generar árboles sintácticos )

5. Direcciones
 ANTLR: http://www.antlr.org/
 JavaCC: http://www.metamata.com/javacc/
 Construcción de compiladores en JAVA: http://www.first.gmd.de/cogent/catalog/java.html
 Kits para la construcción de compiladores: http://www.first.gmd.de/cogent/catalog/kits.html
 Generadores de analizadores léxicos: http://www.first.gmd.de/cogent/catalog/lexparse.html

Você também pode gostar