Universidad Nacional del Santa Curso: Teoría de Compiladores TRATAMIENTO DE ERRORES INTRODUCCION Los errores encontrados en las distintas fases de análisis se envían a un módulo denominado manejo de errores. En el caso más sencillo puede ser un subprograma al que se le invoca enviándole el código de error, y que se encarga de escribir un mensaje con el error correspondiente, y el número de línea donde se ha producido, así como de cortar el proceso de traducción. Si se desea construir un tratamiento de errores más completo, por ejemplo detectando todos los errores del programa fuente, el módulo se complica dado que los analizadores deben proseguir su trabajo con falta de datos. A continuación se muestra un fragmento de código de un método de la clase Errores escrito en C++, para el tratamiento de errores sintácticos. En el caso que se presenta cada vez que se encuentra un error el compilador se detiene, y finaliza el proceso. Una de las funciones más importantes de un compilador es su respuesta a los errores en un programa fuente. Los errores pueden ser detectados durante casi cualquier fase de la compilación. Estos errores estáticos (o de tiempo de compilación) deben ser notificados por un compilador, y es importante que el compilador sea capaz de generar mensajes de error significativos y reanudar la compilación después de cada error. Cada fase de un compilador necesitará una clase ligeramente diferente de manejo de errores. Y, por lo tanto, un manejador de errores debe contener operaciones diferentes, cada una apropiada para una fase y situación específica. Es recomendable estudiar las técnicas de manejo de errores por cada fase. Docente: Ing. Mirko Manrique Ronceros ~1~ Universidad Nacional del Santa Curso: Teoría de Compiladores Una definición de lenguaje por lo general requerirá no solamente que los errores estáticos sean detectados por un compilador, sino también ciertos errores de ejecución. Esto requiere que un compilador genere código extra, el cual realizará pruebas de ejecución apropiadas para garantizar que todos esos errores provocarán un evento apropiado durante la ejecución. El más simple de tales eventos será detener la ejecución del programa. Sin embargo, a menudo esto no es adecuado, y una definición de lenguaje puede requerir la presencia de mecanismos para el manejo de excepciones. Éstos pueden complicar sustancialmente la administración de un sistema de ejecución, especialmente si un programa puede continuar ejecutándose desde el punto donde ocurrió el error. Ejemplo VAR ch : CHAR; (* Un identificador no se puede utilizar si *) ent: INTEGER; (* previamente no se ha definido. *) ... ch := ent + 1; (* En Pascal no es válido, en C sí. *) ü Análisis Léxico: Devuelve la secuencia de tokens: id asig id suma numero ptocoma ü Análisis Sintáctico: Orden de los tokens válido ü Análisis Semántico: Tipo de variables asignadas incorrecta TIPOS DE ERRORES Tipos de errores que suelen ocurrir (dependiendo de la fase): Léxicos: No concuerda con ninguna ER. ü Ejemplo: escribir mal una palabra clave Sintácticos: La estructura que se ha seguido no es correcta. ü Ejemplo: expresión con paréntesis no emparejados Semánticos: La estructura está bien pero hay errores de significado ü Ejemplo: operador y operandos incompatibles. Lógicos: Los comete el programador ü Ejemplo: una llamada infinitamente recursiva Algunos errores se pueden detectar en compilación otros solo en ejecución. El tratamiento de errores es una parte importante que se suele descuidar Docente: Ing. Mirko Manrique Ronceros ~2~ Universidad Nacional del Santa Curso: Teoría de Compiladores TRATAMIENTO DE LOS ERRORES LEXICOS Un traductor debe adoptar alguna estrategia para detectar, informar y recuperarse para seguir analizando hasta el final. Las respuestas ante el error pueden ser: ü Inaceptables: Provocadas por fallos del traductor, entrada en lazos infinitos, producir resultados erróneos, y detectar sólo el primer error y detenerse. ü Aceptables: Evitarla avalancha de errores (mala recuperación) y, aunque más complejo, informar y reparar el error de forma automática. La conducta de un Analizador de Léxico es el de un Autómata finito o “scanner”. ü Detección del error: El analizador de Léxico detecta un error cuando no existe transición desde el estado que se encuentra con el símbolo de la entrada. El símbolo en la entrada no es el esperado. Los errores léxicos se detectan cuando el analizador léxico intenta reconocer componentes léxicos y la cadena de caracteres de la entrada no encaja con ningún patrón. Son situaciones en las que usa un carácter invalido (@,$,",>,...), que no pertenece al vocabulario del lenguaje de programación, al escribir mal un identificador, palabra reservada u operador. Errores léxicos típicos son: 1. Nombre ilegales de identificadores: un nombre contiene caracteres inválidos. 2. Números incorrectos: Un numero contiene caracteres inválidos o no está formado correctamente, por ejemplo 3,14 en vez de 3.14 o 0.3.14. 3. Errores de ortografía en palabras reservadas: caracteres omitidos, adicionales o cambiados de sitio, por ejemplo la palabra while en vez de hwile. 4. Fin de archivo: se detecta un fin de archivo a la mitad de un componente léxico. Los errores léxicos se deben a descuidos del programador. En general, la recuperación de errores léxicos es sencilla y siempre se traduce en la generación de un error de sintaxis que será detectado más tarde por el analizador sintáctico cuando el analizador léxico devuelve un componente léxico que el analizador sintáctico no espera en esa posición. Los métodos de recuperación de errores léxicos se basan bien en saltarse caracteres en la entrada hasta que un patrón se ha podido reconocer; o bien usar otros métodos más sofisticados que incluyen la inserción, borrado, sustitución de un carácter en la entrada o intercambio de dos caracteres consecutivos. Una buena estrategia para la recuperación de errores léxicos: Docente: Ing. Mirko Manrique Ronceros ~3~ Universidad Nacional del Santa Curso: Teoría de Compiladores ü Si en el momento de detectar el error ya hemos pasado por algún estado final ejecutamos la acción correspondiente al último estado final visitado con el lexema formado hasta que salimos de él; el resto de caracteres leídos se devuelven al flujo de entrada y se vuelve al estado inicial; ü Si no hemos pasado por ningún estado final, advertimos que el carácter encontrado no se esperaba, lo eliminamos y proseguimos con el análisis. Construcción de un analizador léxico Los analizadores léxicos pueden construirse: § Usando generadores de analizadores léxicos: Es la forma más sencilla pero el código generado por el analizador léxico es más difícil de mantener y puede resultar menos eficiente. § Escribiendo el analizador léxico en un lenguaje de alto nivel: Permite obtener analizadores léxicos con más esfuerzo que con el método anterior pero más eficientes y sencillos de mantener. TRATAMIENTO DE LOS ERRORES SINTACTICOS Gran parte de la detección y recuperación se centra en el AS. Muchos errores de naturaleza sintáctica Recuperación: Al producirse un error el compilador debe ser capaz de informar del error y seguir compilando. (Ideal) El manejo de errores de sintaxis es el más complicado desde el punto de vista de la creación de compiladores. Nos interesa que cuando el compilador encuentre un error, se recupere y siga buscando errores. Por lo tanto el manejador de errores de un analizador sintáctico debe tener como objetivos: ü Indicar los errores de forma clara y precisa. Aclarar el tipo de error y su localización. ü Recuperarse del error, para poder seguir examinando la entrada. ü No ralentizar significativamente la compilación. Un buen compilador debe hacerse siempre teniendo también en mente los errores que se pueden producir; con ello se consigue: ü Simplificar la estructura del compilador. ü Mejorar la respuesta ante los errores. Tenemos varias estrategias para corregir errores, una vez detectados: Docente: Ing. Mirko Manrique Ronceros ~4~ Universidad Nacional del Santa Curso: Teoría de Compiladores Ignorar el problema (Panicmode) Consiste en ignorar el resto de la entrada hasta llegar a una condición de seguridad. Una condición tal se produce cuando nos encontramos un token especial (por ejemplo un ‘;’ o un ‘END’). A partir de este punto se sigue analizando normalmente. Recuperación a nivel de frase Intenta recuperar el error una vez descubierto. En el caso anterior, por ejemplo, podría haber sido lo suficientemente inteligente como para insertar el token ‘;’. Hay que tener cuidado con este método, pues puede dar lugar a recuperaciones infinitas. Reglas de producción adicionales para el control de errores La gramática se puede aumentar con las reglas que reconocen los errores más comunes. En el caso anterior, se podría haber puesto algo como: sent_erróne a Ú sent_sin_acabar sentencia_acabada ’;’ sentencia_acabada Ú sentencia ‘;’ sent_sin_acabar Ú sentencia Lo cual nos da mayor control en ciertas circunstancias Corrección Global Dada una secuencia completa de tokens a ser reconocida, si hay algún error por el que no se puede reconocer, consiste en encontrar la secuencia completa más parecida que sí se pueda reconocer. Es decir, el analizador sintáctico le pide toda la secuencia de tokens al léxico, y lo que hace es devolver lo más parecido a la cadena de entrada pero sin errores, así como el árbol que lo reconoce. Docente: Ing. Mirko Manrique Ronceros ~5~ Universidad Nacional del Santa Curso: Teoría de Compiladores TRATAMIENTO ERRORES SEMANTICOS COMPROBACIÓN DE TIPOS 1. Aspectos generales Un lenguaje con comprobación fuerte de tipos es capaz de garantizar que los programas se pueden ejecutar sin errores de tipo, por lo que los errores de tipo se detectarán siempre en tiempo de compilación. Como mínimo, ante un error, un comprobador de tipos debe informar de la naturaleza y posición del error y recuperarse para continuar con la comprobación del resto del programa a analizar. Veamos algunas de las operaciones a tener en cuenta en una comprobación de tipos: § Conversión de tipos: A veces es necesario transformar el tipo de una expresión para utilizar correctamente un operador o para pasar de forma adecuada un parámetro a una función. § Coerción: Es una conversión de tipos que realiza de forma implícita el propio compilador. Si es el programador el que realiza la conversión se tratará entonces de una conversión explícita. § Sobrecarga de operadores: La sobrecarga se resuelve determinando el tipo de cada una de las expresiones intervinientes en la sobrecarga. § Funciones polimórficas: Son aquellas que trabajan con argumentos cuyo tipo puede cambiaren distintas llamadas a la función. 2. Especificación de un comprobador de tipos básico Básicamente se deberán realizar dos tareas: a) Asignación de tipos: en las declaraciones. b) Evaluación y comprobación de tipos: En las expresiones y en las funciones, así como en las sentencias. Docente: Ing. Mirko Manrique Ronceros ~6~ Universidad Nacional del Santa Curso: Teoría de Compiladores Sea la gramática: Primer paso: Asignación de tipo Docente: Ing. Mirko Manrique Ronceros ~7~ Universidad Nacional del Santa Curso: Teoría de Compiladores Segundo paso: Comprobación de tipo en expresiones Tercer paso: Comprobación de tipo en sentencias Docente: Ing. Mirko Manrique Ronceros ~8~ Universidad Nacional del Santa 3. Otras comprobaciones semánticos Curso: Teoría de Compiladores semánticas y recuperación de errores Dentro de las comprobaciones estáticas (en el momento de la compilación), tenemos la detección e información de errores como: Comprobaciones de tipos: operadores aplicados a operandos incompatibles, asignación de tipos incompatibles, llamadas a funciones con tipos no adecuados, etc. Comprobaciones de flujo de control: las sentencias que hacen que el flujo de control abandone una construcción debe tener alg ´un lugar a donde transmitir el control. Por ejemplo: Unbreak debe estar dentro de una proposición while, for o switch en C. Comprobaciones de unicidad: situaciones en las que solo se puede definir un objeto una vez exactamente. Por ejemplo: Un identificador, las etiquetas case dentro de un switch. Solo nos hemos centrado en las comprobaciones de tipo. Las otras son en cierto modo rutinarias y se pueden realizar fácilmente insertando acciones intercaladas en el código para realizarlas, por eje. Cuando se introduce un identificador en la Tabla de Símbolos. Docente: Ing. Mirko Manrique Ronceros ~9~