JavaCC - Control de errores
En la versión 0.7.1 existen dos nuevos tipos de excepciones
Cuando el token manager detecta un problema, lanza una excepción TokenMgrError. Previamente, imprime un mensaje:
Lexical Error...
y seguidamente lanza la excepción.
Cuando el parser detecta un problema, lanza una exception ParseException.
Previamente, imprime un mensaje:
Encountered ... Was expecting one of ...
y seguidamente lanza la excepción.
En la versión 0.7.1 los mensajes de error nunca se imprimen explícitamente,
dicha información se almacena en objetos exception que son lanzados.
Todas las excepciones en Java son subclases de Throwable. Además, las excepciones están divididas en dos grandes categorías: errores y otras excepciones.
Los errores, son excepciones de las que no se espera recuperación, por ejemplo el error que sucede cuando se agota la memoria disponible (OutOfMemoryError). Los errores se indican como subclases "Error" de las excepciones, y no necesitan especificarse en la cláusula "throws" de las declaraciones del método.
Las excepciones normalmente se definen como subclases "Exception" de las excepciones y normalmente son manejadas por los programas de usuario y deben ser declaradas en la cláusula throws de la declaración del método (si es posible para el método throw esa excepción).
Las excepciones TokenMgrError son una subclase de Error, mientras que las excepciones ParseException son una subclase de Exception. El token manger nunca espera throw una excepción, por lo que debe tenerse cuidado cuando se definen las especificaciones de tokens de tal forma de cubrir todos los casos. Por eso se coloca el sufijo "Error" en TokenMgrError. No hay que preocuparse por esta excepción si los tokens están diseñados adecuadamente, nunca debería get thrown.
JavaCC ofrece dos clases de recuperación de errores:
- Recuperación Superficial
- Recuperación Profunda
La recuperación superficial, sucede cuando ninguna de las elecciones realizadas actualmente han sucedido , mientras que la recuperación profunda sucede cuando se se realiza una elección, pero durante el parsing acaece un error.
Recuperación Superficial
La recuperación superficial se explica a través de un ejemplo:
void Stm() : {}
{
IfStm()
|
WhileStm()
}
Aquí se asume que IFStm comienza con la palabra reservada "if"
y WhileStm comienza con la palabra reservada "while". Suponiendo
que se desea recuperarse saltando todas las secuencias para el siguiente
";" cuando el siguiente token no coincide ni con IfStm ni con
WhileStm (asumiendo un lookahead de 1). En otras palabras, el token siguiente
no es ni "if" ni "while".
Puede escribirse lo siguiente:
void Stm() : {}
{
IfStm()
|
WhileStm()
|
error_skipto(SEMICOLON)
}
Pero antes debe definirse "error_skipto". Hasta donde le concierne a JavaCC, "error_skipto" solamente funciona como otro no terminal. Una manera de definir "error_skipto" podría ser la siguiente (en este ejemplo se utiliza una producción JAVACODE estandard):
JAVACODE
void error_skipto(int kind) {
ParseException e = generateParseException(); // se genera el objeto excepción
System.out.println(e.toString()); // se imprime el mensaje de error
Token t;
do {
t = getNextToken();
} while (t.kind != kind);
El ciclo anterior consume los tokens hasta que encuentra uno de tipo "kind".
Se usa un ciclo do-while en lugar de while, ya que el token actual se halla
inmediátamente antes del token erróneo.
En futuras versiones de JavaCC se espera que pueda soportarse la composición modular de gramáticas. Cuando ello sucede, todas las rutinas de recuperación pueden agruparse en un módulo que puede ser importado al módulo principal de la gramática. Esta capacidad es incorporada para suplir una librería de rutinas muy utilizadas
Recuperación Profunda
Para comprender este tipo de recuperación, puede usarse el mismo ejemplo anterior:
void Stm() : {}
{
IfStm()
|
WhileStm()
}
En este caso, si bien se desea recuperarse de la misma manera, se desea
hacerlo cuando hay un error más profundo en el parser. por ejemplo, si
se supone que el token siguiente era "while", la elección realizada
también es "WhileStm". Pero si se supone que durante el parsing
de WhileStm se encuentra algún error -por ejemplo, si se tiene "while
(foo {stm;}" - es decir, que el paréntesis de cierre se ha omitido.
La recuperación superficial no actuará en esta situación. Para tratar esto,
es necesaria la recuperación profunda. Para ello, en JavaCC existe una
uneva entidad sintáctica, el bloque try-catch-finally.
Primero, se reescribe el ejemplo anterior para recuperación de errores
profunda y luego se explica en más detalle el bloque try-catch-finally:
Eso es todo lo que se necesita realizar. Si durante el parsing de IfStm o WhileStm existe algún error del que no se ha recuperado, entonces el bloque catch toma lugar. Puede existir cualquier número de bloques catch y también un bloque finally opcional (al igual que en Java). Dentro de los boques catch se incluye código Java, no expansiones de JavaCC. Por ejemplo, el ejemplo anterior podría haber sido reescrito como:
Ejemplo try-catch-finally modificado
La idea es evitar recargar al lector de la gramática con demasiado código
Java en los bloques catch y finally. Es mejor definir métodos que pueden
invocarse desde los bloques catch.
Debe notarse, que en la segunda manera de escribir el ejemplo, se copió el código fuera de la implementación de error_skipto. Pero se dejó fuera la primer sentencia, la llamada a generateParseException. Esto es porque en este caso, el bloque catdh provee la excepción. Pero si se desea llamar directamente este método, se obtendrá un objeto idéntico.