Recuperación de Errores

Las entradas de un traductor pueden contener errores. El lenguaje yapp proporciona un token especial, error, que puede ser utilizado en el programa fuente para extender el traductor con ``producciones de error'' que lo doten de cierta capacidad para recuperase de una entrada errónea y poder continuar analizando el resto de la entrada.

Consideremos lo que ocurre al ejecutar nuestra calculadora yapp con una entrada errónea. Recordemos la gramática:

 9  %right  '='
10  %left   '-' '+'
11  %left   '*' '/'
12  %left   NEG
13  %right  '^'
14
15  %%
16  input:  # empty
17          |   input line  { push(@{$_[1]},$_[2]); $_[1] }
18  ;
19
20  line:       '\n'       { $_[1] }
21          |   exp '\n'   { print "$_[1]\n" }
22          |   error '\n' { $_[0]->YYErrok }
23  ;

La regla line $ \rightarrow$ error '\n' es una producción de error. La idea general de uso es que, a traves de la misma, el programador le indica a yapp que, cuando se produce un error dentro de una expresión, descarte todos los tokens hasta llegar al retorno del carro y prosiga con el análisis. Además, mediante la llamada al método YYErrok el programador anuncia que, si se alcanza este punto, la recuperación puede considerarse ``completa'' y que yapp puede emitir a partir de ese momento mensajes de error con la seguridad de que no son consecuencia de un comportamiento inestable provocado por el primer error.

El resto de la gramática de la calculadora era como sigue:

24
25  exp:        NUM
26          |   VAR                 { $_[0]->YYData->{VARS}{$_[1]} }
27          |   VAR '=' exp         { $_[0]->YYData->{VARS}{$_[1]}=$_[3] }
28          |   exp '+' exp         { $_[1] + $_[3] }
29          |   exp '-' exp         { $_[1] - $_[3] }
30          |   exp '*' exp         { $_[1] * $_[3] }
31          |   exp '/' exp         {
32                                    $_[3]
33                                    and return($_[1] / $_[3]);
34                                    $_[0]->YYData->{ERRMSG}
35                                      =   "Illegal division by zero.\n";
36                                    $_[0]->YYError;
37                                    undef
38                                  }
39          |   '-' exp %prec NEG   { -$_[2] }
40          |   exp '^' exp         { $_[1] ** $_[3] }
41          |   '(' exp ')'         { $_[2] }
42  ;
en la ejecución activamos el flag yydebug a 0x10 para obtener información sobre el tratamiento de errores:
$self->YYParse( yylex => \&_Lexer, yyerror => \&_Error, yydebug => 0x10 );
Pasemos a darle una primera entrada errónea:

$ ./usecalc.pl
3-+2
Syntax error.
**Entering Error recovery.
**Pop state 12.
**Pop state 3.
**Shift $error token and go to state 9.
**Dicard invalid token >+<.
**Pop state 9.
**Shift $error token and go to state 9.
**Dicard invalid token >NUM<.
**Pop state 9.
**Shift $error token and go to state 9.
**End of Error recovery.

El esquema general del algoritmo de recuperación de errores usado por la versión actual de yapp es el siguiente:

  1. Cuando se encuentra ante una acción de error, el analizador genera un token error.

  2. A continuación pasa a retirar estados de la pila hasta que descubre un estado capaz de transitar ante el token error. En el ejemplo anterior el analizador estaba en el estado 12 y lo retira de la pila. Los contenidos del estado 12 son:
    	exp -> exp '-' . exp	(Rule 10)
    	'('	shift 7 '-'	shift 2 NUM	shift 6 VAR	shift 8
    	exp	go to state 21
    
    Obviamente no esperabamos ver un '+' aqui. El siguiente estado en la cima de la pila es el 3, el cual tampoco tiene ninguna transición ante el terminal error:

    	line -> exp . '\n'	(Rule 4)
    	exp -> exp . '+' exp	(Rule 9)
    	exp -> exp . '-' exp	(Rule 10)
    	exp -> exp . '*' exp	(Rule 11)
    	exp -> exp . '/' exp	(Rule 12)
    	exp -> exp . '^' exp	(Rule 14)
    
    	'*'	shift 17 '+'	shift 13 '-'	shift 12 '/'	shift 15
    	'\n'	shift 14 '^'	shift 16
    
    El pop sobre el estado 3 deja expuesto en la superficie al estado 1, el cuál ``sabe'' como manejar el error:
    	$start -> input . $end	(Rule 0)
    	input -> input . line	(Rule 2)
    
    	$end	shift 4 '('	shift 7 '-'	shift 2 '\n'	shift 5
    	NUM	shift 6 VAR	shift 8
    	error	shift 9
    
    	exp	go to state 3
    	line	go to state 10
    

  3. En este punto transita al estado correspondiente a desplazar el token error.

    En consecuencia, con lo dicho, en el ejemplo se va al estado 9:

    	line -> error . '\n'	(Rule 5)
    	'\n'	shift, and go to state 20
    

  4. Entonces el algoritmo de recuperación va leyendo tokens y descartandolos hasta encontrar uno que sea aceptable. En este caso hemos especificado que el terminal que nos da cierta confianza de recuperación es el retorno de carro:
    **Dicard invalid token >+<.
    **Pop state 9.
    **Shift $error token and go to state 9.
    **Dicard invalid token >NUM<.
    **Pop state 9.
    **Shift $error token and go to state 9.
    **End of Error recovery.
    

  5. Sólo se envían nuevos mensajes de error una vez asimilados (desplazados) algunos símbolos terminales. De este modos se intenta evitar la aparición masiva de mensajes de error.

Casiano Rodríguez León
2013-04-23