CAPITULO # 6.
|
||| COMO MANEJAR ERRORES
|||
|
Un compilador es un sistema que en la mayoría de los casos tiene que manejar una entrada incorrecta. Sobre todo en las primeras etapas de la creación de un programa, es probable que el compilador se utilizará para efectuar las características que debería proporcionar un buen sistema de edición dirigido por la sintaxis, es decir, para determinar si las variables han sido declaradas antes de usarla, o si faltan corchetes o algo así. Por lo tanto, el manejo de errores es parte importante de un compilador y el escritor del compilador siempre debe tener esto presente durante su diseño. Hay que señalar que los posibles errores ya deben estar considerados al diseñar un lenguaje de programación. Por ejemplo, considerar si cada proposición del lenguaje de programación comienza con una palabra clave diferente (excepto la proposición de asignación, por supuesto). Sin embargo, es indispensable lo siguiente:
Los mensajes de error de la forma *** Error 111 *** *** Ocurrió un error *** *** Falta declaración *** *** Falta delimitador *** no son útiles para el programador y no deben presentarse en un ambiente de compilación amigable y bien diseñado. Por ejemplo, el mensaje de error ‘Falta declaración’ podría reemplazarse por *** No se ha declarado la variable Nombre *** o en el caso del delimitador omitido se puede especificar cuál es el delimitador esperado. Además de estos mensajes de error informativos, es deseable que el compilador produzca una lista con el código fuente e indique en ese listado dónde han ocurrido los errores. No obstante, antes de considerar el manejo de errores en el análisis léxico y sintáctico, hay que caracterizar y clasificar los errores posibles (Sec. 6.1). Esta clasificación nos mostrará que un compilador no puede detectar todos los tipos de errores. Clasificación de Errores Durante un proceso de resolución de problemas existen varias formas en que pueden surgir errores, las cuales se reflejan en el código fuente del programa. Desde el punto de vista del compilador, los errores se pueden dividir en dos categorías:
Los errores invisibles en un programa son aquellos que no puede detectar el compilador, ya que no son el resultado de un uso incorrecto del lenguaje de programación, sino de decisiones erróneas durante el proceso de especificación o de la mala formulación de algoritmos. Por ejemplo, si se escribe a : = b + c ; en lugar de a : = b * c ; el error no podrá ser detectado por el compilador ni por el sistema de ejecución. Estos errores lógicos no afectan la validez del programa en cuanto a su corrección sintáctica. Son objeto de técnicas formales de verificación de programas que no se consideran aquí. Para conocer más sobre la verificación de programas, consulte, por ejemplo, [LOEC 87]. Los errores visibles, a diferencia de los errores lógico, pueden ser detectados por el compilador o al menos por el sistema de ejecución. Estos errores se pueden caracterizar de la siguiente manera:
Estos errores se presentará porque los programadores no tienen el cuidado suficiente al programador. Los errores del segundo tipo también pueden ocurrir porque el programador no comprende a la perfección el lenguaje que se utiliza o porque suele escribir sus programas en otro lenguaje y, por tanto, emplea las construcciones de dicho lenguaje (estos problemas pueden presentarse al usar a la vez lenguajes de programación como PASCAL y MODULA-2, por ejemplo). Clasificación de Ocurrencias Por lo regular, los errores visibles o detectables por el compilador se dividen en tres clases, dependiendo de la fase del compilador en el cual se detectan:
Por ejemplo, un error léxico puede ocasionarse por usar un carácter inválido (uno que no pertenezca al vocabulario del lenguaje de programación) o por tratar de reconocer una constante que produce un desbordamiento. Un error de sintaxis se detecta cuando el analizador sintáctico espera un símbolo que no corresponde al que se acaba de leer. Los analizadores sintácticos LL y LR tienen la ventaja de que pueden detectar errores sintácticos lo más pronto posible, es decir, se genera un mensaje de error en cuanto el símbolo analizado no sigue la secuencia de los símbolos analizados hasta ese momento. Los errores semánticos corresponden a la semántica del lenguaje de programación, la cual normalmente no está descrita por la gramática. Los errores semánticos más comunes son la omisión de declaraciones. Además de estas tres clases de errores, hay otros que serán detectados por el sistema de ejecución porque el compilador ha proporcionado el código generado con ciertas acciones para estos casos.
típico ocurre cuando el índice de una matriz no es un elemento del subintervalo especificado o por intentar una división entre cero. En tales situaciones, se informa del error y se detiene la ejecución del programa. Clasificación Estadística Ripley y Druseikis muestran resultados interesantes sobre el análisis estadístico de los errores de sintaxis en [RIPL 78]. Ellos investigaron los errores que cometen los programadores de PASCAL y analizaron los resultados en relación con las estrategias de recuperación. El resultado principal del estudio fue que los errores de sintaxis suelen ser muy simples y que, por lo general, sólo ocurre un error por frase. En el resumen siguiente se describen de manera general los resultados del estudio:
Los errores que ocurren pueden clasificarse en cuatro categorías:
La distribución estadística de estas cuatro categorías aparece en la figura 6.1. Efectos de los Errores La detección de un error en el código fuente ocasiona ciertas reacciones del compilador. El comportamiento de un compilador en el caso de que el código fuente contenga un error puede tener varias facetas:
La última situación nunca debe presentarse en un buen sistema de compilación; es decir, el compilador debe ser capaz de detectar todos los errores visibles. La detención del proceso de compilación al detectar el primer error es la forma más simple de satisfacer el requisito de que una compilación siempre debe terminar, sin importar cuál sea la entrada [BRIN 85]. Sin embargo, este comportamiento también es el peor en un ambiente amigable para el usuario, ya que una compilación puede tardar varios minutos. Por lo tanto, el programador espera que el sistema de compilación detecte todos los errores posibles en el mismo proceso de compilación. Entonces, en general, el compilador debe recuperarse de un error para poder revisar el código fuente en busca de otros errores. No obstante, hay que observar que cualquier "reparación" efectuada por el compilador tiene el propósito único de continuar la búsqueda de otros errores, no de corregir el código fuente. No hay reglas generales bien definidas acerca de cómo recuperarse de un error, por lo cual el proceso de recuperación debe hacerse en hipótesis acerca de los errores. La carencia de tales reglas se debe al hecho de que el proceso de recuperación siempre depende del lenguaje. Manejo de Errores en el Análisis Léxico Los errores léxicos se detectan cuando el analizador léxico intenta reconocer componentes léxicos en el código fuente. Los errores léxicos típicos son:
La mayoría de los errores léxicos se deben a descuidos del programador. En general, la recuperación de los errores léxicos es relativamente sencilla. Si un nombre, un número o una etiqueta contiene un carácter inválido, se elimina el carácter y continúa el análisis en el siguiente carácter; en otras palabras, el analizador léxico comienza a reconocer el siguiente componente léxico. El efecto es la generación de un error de sintaxis que será detectado por el analizador sintáctico. Este método también puede aplicarse a números mal formados. Las secuencias de caracteres como 12AB pueden ocurrir si falta un operador (el caso menos probable) o cuando se han tecleado mal ciertos caracteres. Es imposible que el analizador léxico pueda decidir si esta secuencia es un identificador ilegal o u número ilegal. En tales casos, el analizador léxico puede saltarse la cadena completa o intentar dividir las secuencias ilegales en secuencias legales más cortas. Independientemente de cuál sea la decisión , la consecuencia será un error de sintaxis. La detección de cadenas demasiado margas no es muy complicada, incluso si faltan las comillas que cierran, porque por lo general no está permitido que las cadenas pasen de una línea a la siguiente. Si faltan las comillas que cierran, puede usarse el carácter de fin de línea como el fin de cadena y reanudar el análisis léxico en la línea siguiente. Esta reparación quizás produzca errores adicionales. En cualquier caso, el programador debe ser informado por medio de un mensaje de error. Un caso similar a la falta de comillas que cierran en una cadena, es la falta de un símbolo de terminación de comentario. Como por lo regular está permitido que los comentario abarquen varias líneas, no podrá detectarse la falta del símbolo que cierra el comentario hasta que el analizador léxico llegue al final del archivo o al símbolo de fin de otro comentario (si no se permiten comentarios anidados). Si se sabe que el siguiente componente léxico debe ser una palabra reservada, es posible corregir una palabra reservada mal escrita. Esto se hace mediante funciones de corrección de errores, bien conocidas en los sistemas de lenguajes naturales, o simplemente aplicando una función de distancia métrica entre la secuencia de entrada y el conjunto de palabras reservadas. Por último, el proceso de compilación puede terminar si se detecta un fin de archivo dentro de un componente léxico. Manejo de Errores en el Análisis Sintáctico El analizador sintáctico detecta un error de sintaxis cuando el analizador léxico proporciona el siguiente símbolo y éste es incompatible con el estado actual del analizador sintáctico. Los errores sintácticos típicos son:
No hay estrategias de recuperación de errores cuya validez sea general, y la mayoría de las estrategias conocidas son heurísticas, ya que se basan en suposiciones acerca de cómo pueden ocurrir los errores y lo que probablemente quiso decir el programador con una determinada construcción. Sin embargo, hay algunas estrategias que gozan de amplia aceptación:
La recuperación de emergencia es la estrategia que se encontrará en la mayoría de los compiladores, pero la legalización de ciertos errores mediante la definición de una gramática aumentada es una técnica que se emplea con frecuencia. No obstante, hay que expandir la gramática con mucho cuidado para asegurarse de que no cambien el tipo y las características de la gramática. Los errores de sintaxis se detectan cuando el analizador sintáctico espera un símbolo que no concuerda con el símbolo que está analizando, a . En los analizadores sintácticos LL, los errores de sintaxis se detectan cuando a y el no terminal que están en la cima de la pila nos llevan a un índice de una posición vacía de la tabla de análisis sintáctico. En los analizadores sintácticos LR, los errores de sintaxis se detectan cuando hay un índice a una posición vacía de la tabla, o sea, cuando no se especifica ninguna transición al analizar á en el estado actual (véase Cap. 4). Sin embargo, si se emplea una gramática aumentada con producciones de error adicionales, no sólo se detectarán errores por medio de los índices a posiciones vacías de la tabla de análisis sintáctico. Errores Semánticos Los errores que puede detectar el analizador sintáctico son aquellos que violan las reglas de una gramática independiente del contexto. Ya hemos mencionado que algunas de las características de un lenguaje de programación no pueden enunciarse con reglas independientes del contexto, ya que dependen de él; por ejemplo, la restricción de que los identificadores deben declararse previamente. Por lo tanto, los principales errores semánticos son:
Es mucho más difícil introducir métodos formales para la recuperación de errores semánticos que para la recuperación de errores sintácticos, ya que a menudo la recuperación de errores semánticos es ad hoc. No obstante, puede requerirse que, por lo menos, el error semántico sea informado al programador, que se le ignore y que, por tanto, se suprimirá la generación de código. Sin embargo, la mayoría de los errores semánticos pueden ser detectados mediante la revisión de la tabla de símbolos, suponiendo un tipo que se base en el contexto donde ocurra o un tipo universal que permita al identificador ser un operando de cualquier operador del lenguaje. Al hacerlo, evitamos la producción de un mensaje de error cada vez que se use la variable no definida. Si el tipo de un operando no concuerda con los requisitos de tipo del operador, también es conveniente reemplazar el operando con una variable ficticia de tipo universal. Recuperación de Errores PL/0 A continuación ejemplificaremos algunos de los métodos antes mencionados para la recuperación de errores sintácticos. Para ellos expandiremos fragmentos del programa del analizador sintáctico descendente recursivo de PL/0 que vimos en el capítulo 4. Recuperación de Emergencia La idea del análisis sintáctico descendente recursivo es que un problema de análisis sintáctico se divida en subproblemas que se resuelven en forma recursiva. Ahora bien, la ocurrencia de un error en un subproblema significa que no sólo hay que informar del error al procedimiento que llama. Mas bien, hay que garantizar que el procedimiento del subproblema se recupere del error de modo que el procedimiento invocador pueda continuar con el proceso de análisis sintáctico, es decir, que termine de forma normal. Por ello, además de generar un mensaje de error, hay que ir saltándose la entrada hasta llegar a un símbolo de sincronización. Esto implica que cada procedimiento de un analizador sintáctico descendente recursivo debe conocer cuáles son los símbolos PROCEDURE Prueba(Siguiente, detención: conjsím; n: INTEGER); (*siguiente, detención: símbolos de sincronización*) (*n: número de error *) VAR símsinc : conjsím; BEJÍN IF NOT (símbolo IN siguiente) THEN Error (n); Símsinc := siguiente + detención; WHILE NOT (símbolo IN símsinc) DO Leer_Símbolo END END END Prueba; Figura 6.2 Procedimiento para revisar y saltar símbolos
PROCEDURE Expresión (siguiente: conjsím); VAR ADDoSUB: símbolos; PROCEDURE Término (siguiente: conjsím); VAR MULoDIV:símbolos; PROCEDURE Factor (siguiente: conjsím); VAR i: INTEGER; BEGÍN (*Factor*) Prueba (iniciofact, siguiente, ...); WHILE símbolo IN iniciofact DO ... Prueba (siguiente, [pareni], ...) END END Factor; BEGÍN (*Término*) Factor (siguiente + [por, diagonal]); WHILE símbolo IN [por, diagonal]) DO MULoDIV := símbolo; Leer_Símbolo; Factor (siguiente + [por, diagonal]); ... END END Término; BEGÍN (*Expresión*) ... END Expresión; Figura 6.3 Uso del procedimiento de prueba válidos que le pueden seguir. Para evitar el salto descontrolado de símbolos, se aumentan los conjuntos de símbolos de detención adicionales que indiquen las construcciones que no deben saltarse. Los símbolos siguientes y los símbolos de detención forman, en conjunto, los símbolos de sincronización. En la caso de la implantación, esto quiere decir que cada procedimiento de análisis sintáctico consta de un parámetro que especifica el conjunto de los símbolos válidos que siguen. La prueba para los símbolos de sincronización puede efectuarse fácilmente con el procedimiento presentado en la figura 6.2. este procedimiento prueba si un símbolo siguiente es legal. En caos de un símbolo ilegal, se genera un mensaje de error y se saltan los símbolos de entrada hasta detectar un símbolo de sincronización. Este procedimiento de prueba será invocado al final de cada procedimiento para verificar que le símbolo siguiente sea válido, pero también puede emplearse al iniciar un procedimiento de análisis sintáctico para verificar si el símbolo de entrada actual es un símbolo inicial permitido. El uso del procedimiento de prueba se ilustra en la figura 6.3 para el análisis sintáctico de expresiones aritméticas (donde ‘iniciofact’ indica los símbolos iniciales permitidos para ‘Factor’). Expansión de Gramática Como ya mencionamos, es un hecho bien conocido que los errores de puntuación son muy comunes. Por ejemplo, consideremos las constantes PL/0 que se separan por comas; un error frecuente en el cual podríamos pensar sería el uso de un punto y coma en lugar de la coma. Sabiendo esto, la estructura sintáctica de las declaraciones de constantes puede modificarse de manera que se permita usar coma y punto y coma, como se muestra en la figura 6.4. La declaración modificada de constantes de la figura 6.4 legaliza el error que acabamos de describir. El diagrama sintáctico de la figura 6.4 puede entonces traducirse al fragmento de programa de la figura 6.5, mediante las técnicas presentadas en el capítulo 4.
IF símbolo = símconst THEN Leer_Símbolo; REPEAT Declaración_const; WHILE símbolo = coma DO Leer_Símbolo; Declaración_const END; IF símbolo = puntocoma THEN Leer_Símbolo ELSE Error(...) END; UNTIL (símbolo <> ident); END; Figura 6.5 Código modificado para el análisis des de constantes
El fragmento del programa de la figura 6.5 permite la separación de constantes con comas o puntos y coma sin producir mensajes de error. Además de esta legalización, se aceptará la omisión de la coma y el punto y coma; sin embargo, en este caso sí se produce un mensaje de error. Es obvio que de esta misma forma podemos expandir la sintaxis de las declaraciones de variables para permitir la separación con puntos y coma o incluso con espacios (véase Fig. 6.6).
IF símbolo = símvar THEN Ler_Símbolo; REPEAT Declaración_var; WHILE símbolo = coma DO Leer_Símbolo; Declaración_var END; IF símbolo = puntocoma THEN Leer_Símbolo ELSE Error (...) END; UNTIL (símbolo <> ident); END; Figura 6.6 Código modificado para el análisis de declaraciones de variables
En forma análoga, puede permitirse la omisión del punto y coma entre dos proposiciones. Esto muestra en el fragmento de programa de la figura 6.7, donde ‘síminicioprop’ es el conjunto de símbolos iniciales de la proposiciones.
IF símbolo = símbegin THEN Leer_Símbolo; REPEAT Proposición (siguiente + [puntocoma, símend]); WHILE símbolo = puntocoma DO Leer_Símbolo; Proposición (siguiente + [puntocoma, símed]); END UNTIL NOT (símbolo IN síminicioprop); IF símbolo = símed THEN Leer_símbollo ELSE Error(...) END; END; Figura 6.7 Código modificado para el análisis de proposiciones. |