Site hosted by Angelfire.com: Build your free website today!


CAPITULO # 6.

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:

  1. El compilador debe ser capaz de detectar errores en la entrada;
  2. El compilador debe recuperarse de los errores sin perder demasiada información;
  3. Y sobre todo, el compilador debe producir un mensaje de error que permita al programador encontrar y corregir fácilmente los elementos (sintácticamente) incorrectos de su programa.

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:

  1. Errores visibles y Errores invisibles

 

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:

    1. Errores de ortografía y
    2. Errores que ocurren por omitir requisitos formales del lenguaje de programación.

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:

  1. Errores Léxicos;
  2. Errores Sintácticos;
  3. Errores Semánticos;

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.

  1. Un Error de Ejecución
  2. 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:

  3. Al menos el 40% de los programas compilados eran sintáctica o semánticamente incorrectos.
  4. Un 80% de las proposiciones incorrectas sólo tenían un error.

  5. El 13% de las proposiciones incorrectas tenían dos errores, menos del 3% tenían tres errores y el resto tenían cuatro o más errores por proposición.

  6. En aproximadamente la mitad de los errores de componentes léxicos olvidados, el elemento que faltaba era ":", mientras que omitir el "END" final ocupaba el segundo lugar, con un 10.5%.
  7. En un 13% de los errores de componentes léxico incorrecto se escribió "," en lugar de ";" y en más del 9% de los casos se escribió ":=" en lugar de "=".

Los errores que ocurren pueden clasificarse en cuatro categorías:

  1. Errores de puntuación,
  2. Errores de operadores y operandos,
  3. Errores de palabras clave y
  4. Otros tipos de errores.

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:

  1. El proceso de compilación de detiene al ocurrir el error y el compilador debe informar del error.
  2. El proceso de compilación continúa cuando ocurre el error y se informa de éste en un archivo de listado.
  3. El compilador no reconoce el error y por tanto no advierte al programador.

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:

  1. Nombres ilegales de identificadores: un nombre contiene caracteres inválidos;
  2. Números inválidos: un número contiene caracteres inválidos (por ejemplo; 2,13 en lugar de 2.13), no está formando correctamente (por ejemplo, 0.1.33), o es demasiado grande y por tanto produce un desbordamiento;
  3. Cadenas incorrectas de caracteres: una cadena de caracteres es demasiado larga (probablemente por la omisión de comillas que cierran);
  4. Errores de ortografía en palabras reservadas: caracteres omitidos, adicionales, incorrectos o mezclados;
  5. Etiquetas ilegales: una etiqueta es demasiado larga o contiene caracteres inválidos;
  6. Fin de archivo: se detecta un fin de archivo a la mitad de un componente léxico.

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:

  1. Paréntesis o corchetes omitidos, por ejemplo, x : = y * (1 + z;
  2. Operadores u operando omitidos, por ejemplo, x : = y (1 + z );
  3. Delimitadores omitidos, por ejemplo, x : = y + 1 IF a THEN y : = z.

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:

  1. Recuperación de emergencia (o en modo pánico): Al detectar un error, el analizador sintáctico salta todos los símbolos de entrada hasta encontrar un símbolo que pertenezca a un conjunto previamente definido de símbolos de sincronización. Estos símbolos de sincronización son el punto y como, el símbolo end o cualquier palabra clave que pueda ser el inicio de una proposición nueva, por ejemplo. Es fácil implantar la recuperación de emergencia, pero sólo reconoce un error por proporción. Esto no necesariamente es una desventaja, ya que no es muy probable que ocurran varios errores en la misma proposición (véase [IPL 78], por ejemplo). Esta suposición es un ejemplo típico del carácter heurístico de esta estrategia.
  2. Recuperación por inserción, borrado y reemplazo: éste también es un método fácil de implantar y funciona bien en ciertos casos de error. Usemos como ejemplo una declaración de variable en PASCAL . cuando una coma va seguida por dos puntos, en lugar de un nombre de variable, es posible eliminar esta coma. En forma similar, se puede insertar un punto y coma omitido o reemplazar un punto y coma por una coma en una lista de parámetros.
  3. Recuperación por expansión de gramática: De acuerdo con [RIPL 78], el 60% de los errores en los programas fuente son errores de puntuación, por ejemplo, la escritura de un punto y coma en lugar de una coma, o viceversa. Una forma de recuperarse de estos errores es legalizarlos en ciertos casos, introduciendo lo que llamaremos producciones de error en la gramática del lenguaje de programación. La expansión de la gramática con estas producciones no quiere decir que ciertos errores no serán detectados, ya que pueden incluirse acciones para informar de su detecció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:

  1. Identificadores no definidos;
  2. Operadores y operandos incompatibles.

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.