jueves, 31 de marzo de 2016

Herramientas para la construcción de compiladores

Herramientas para la construcción de compiladores . El uso y perfeccionamiento de los compiladores ha traído consigo el desarrollo de herramientas que aportan a la realización de los mismos, estas se han ido especializando en las diferentes fases del proceso de compilación, brindándole a los desarrolladores una serie de facilidades a la hora de diseñar e implementar un compilador. En el mundo existen diversas herramientas de apoyo de este tipo, desarrolladas en diferentes lenguajes de programación, las cuales responden a los intereses de los múltiples sistemas operativos. Entre las herramientas más utilizadas se pueden encontrar el Flex, Yacc, Lex, Bison entre otras.
HerramientaDescripción
BisonGenerador de Analizadores Sintácticos Ascendentes tipo YACC
COCO/RGenerador de Analizadores Léxicos y Sintácticos Descendentes Recursivos
FlexGenerador de Analizadores Léxicos tipo Lex
LexGenerador de Analizadores Léxicos
SDGLL1Sistema Detector de Gramáticas LL(1)
TS 2006Tipo abstracto de datos Tabla de Símbolos de uso sencillo (beta0.4)
TSTipo abstracto de datos Tabla de Símbolos
TS-OOTipo abstracto de datos Tabla de Símbolos
YACCGenerador de Analizadores Sintácticos Ascendentes LR(1)

Bison

Es un generador de analizadores sintácticos de propósito general que convierte una descripción gramatical para una gramática independiente del contexto en un programa en C que analice esa gramática. Es utilizado en un amplio rango de analizadores de lenguajes, desde aquellos usados en simples calculadoras de escritorio hasta complejos lenguajes de programación.

Lex

Es un generador de analizador léxico, que sirve para generar los token para la siguiente fase . La principal característica de Lex es que va a permitir asociar acciones descritas en C, a la localización de las Expresiones Regulares que se hayan definido. Para e llo Lex se apoya en una plantilla que recibe como parámetro, y que se debe diseñar con cuidado. Internamente Lex va a actuar como un autómata que localizará las expresiones regulares que se le describan, y una vez reconocida la cadena representada por dicha expresión regular, ejecutará el código asociado a esa regla.

Yacc

Es un programa para generar analizadores sintácticos. Las siglas del nombre significan Yet Another Compiler Compiler, es decir, "Otro generador de compiladores más". Genera un analizador sintáctico (la parte de un compilador que comprueba que la estructura del código fuente se ajusta a la especificación sintáctica del lenguaje) basado en una gramática analítica.Yacc genera el código para el analizador sintáctico en el Lenguaje de programación C.

Flex

Es una herramienta para generar escáneres: programas que reconocen patrones léxicos en un texto. Flex lee los ficheros de entrada dados, o la entrada estándar si no se le ha indicado ningún nombre de fichero, con la descripción de un escáner a generar. Estas herramientas de apoyo han sido reescritas para otros lenguajes, incluyendo Ratfor, EFL, ML,Ada, Java, Python, y Limbo. De esta forma se ha logrado una mayor utilización de las mismas en diferentes compiladores desarrollados sobre tecnologías libres. Teniendo en cuenta las características de las aplicaciones antes mencionadas, se ha escogido para la realización del compilador las herramientas Yacc y Lex. En muchos de los compiladores desarrollados en el mundo suelen ser utilizados juntos. Yacc utiliza una gramática formal para analizar un flujo de entradas, algo que Lex no puede hacer con expresiones regulares simples (Lex se limita a los autómatas de estados finitos simples). Sin embargo, Yacc no puede leer en un flujo de entradas simple, requiere una serie de símbolos. Lex se utiliza a menudo para proporcionar a Yacc estos símbolos.

Ensambladores

El término ensamblador (del inglés assembler) se refiere a un tipo de programa informático que se encarga de traducir un fichero fuente escrito en un lenguaje ensamblador, a un fichero objeto que contiene código máquina, ejecutable directamente por el microprocesador

Funcionamiento

El programa lee el fichero escrito en lenguaje ensamblador y sustituye cada uno de los códigos nemotécnicos que aparecen por su código de operacióncorrespondiente en sistema binario para la plataforma que se eligió como destino en las opciones específicas del ensamblador.

Tipos de ensambladores

Podemos distinguir entre dos tipos de ensambladores:
  • Ensambladores básicos. Son de muy bajo nivel, y su tarea consiste básicamente en ofrecer nombres simbólicos a las distintas instrucciones, parámetros y cosas tales como los modos.
  • Ensambladores modulares 32-bits o de alto nivel. Son ensambladores que aparecieron como respuesta a una nueva arquitectura de procesadores de 32 bits, muchos de ellos teniendo compatibilidad hacia atrás pudiendo trabajar con programas con estructuras de 16 bits. Además de realizar la misma tarea que los anteriores, permitiendo también el uso de macros, permiten utilizar estructuras de programación más complejas propias de los lenguajes de alto nivel.

Preprocesadores

Un preprocesador es un programa separado que es invocado por el compilador antes de que comience la traducción real. Un preprocesador de este tipo puede eliminar los comentarios, incluir otros archivos y ejecutar sustituciones de macros.
Los preprocesadores pueden ser requeridos por el lenguaje (como en C) o pueden ser agregados posteriores que proporcionen facilidades adicionales (como el preprocesador Ratfor para FORTRAN).

Funciones

Los preprocesadores producen la entrada para un compilador, y pueden realizar las funciones siguientes:
  • Procesamiento de macros. Un preprocesador puede permitir a un usuario definir macros, que son abreviaturas de construcciones más grandes.
  • Inclusión de archivos. Un preprocesador puede insertar archivos de encabezamiento en el texto del programa. Por ejemplo, el preprocesador de C hace que el contenido del archivo <global.h> reemplace a la proposición #include <global.h> cuando procesa un archivo que contenga a esa proposición.
  • Preprocesadores "racionales". Estos preprocesadores enriquecen los lenguajes antiguos con recursos más modernos de flujo de control y de estructuras de datos. Por ejemplo, un preprocesador de este tipo podría proporcionar al usuario macros incorporadas para construcciones, como proposiciones while o if, en un lenguaje de programación que no las tenga.
  • Extensiones a lenguajes. Estos preprocesadores tratan de crear posibilidades al lenguaje que equivalen a macros incorporadas. Por ejemplo, el lenguaje Equeles un lenguaje de consulta de base de datos integrado en C. El preprocesador considera las proposiciones que empiezan con ## como proposiciones de acceso a la base de datos, sin relación con C, y se traducen a llamadas de procedimiento a rutinas que realizan el acceso a la base de datos.
Los procesadores de macros tratan dos clases de proposiciones: definición de macros y uso de macros. Las definiciones normalmente se indican con algún carácter exclusivo o palabra clave, como define o macro. Constan de un nombre para la macro que se está definiendo y de un cuerpo, que constituye su definición. A menudo, los procesadores de macros admiten parámetros formales en su definición, esto es, símbolos que se reemplazarán por valores (en este contexto, un "valor" es una cadena de caracteres). El uso de una macro consiste en dar nombre a la macro y proporcionar parámetros reales, es decir, valores para sus parámetros formales. El procesador de macros sustituye los parámetros reales por los parámetros formales del cuerpo de la macro; después, el cuerpo transformado reemplaza el uso de la propia macro.

Contexto de un compilador

Compilar es traducir. Podemos modelar el proceso de traducción entre dos lenguajes como el resultado de dos etapas. En la primera etapa se analiza la entrada para averiguar qué es lo que se intenta comunicar. Esto es lo que se conoce como análisis. El fruto de esta etapa es una representación de la entrada que permite que la siguiente etapa se desarrolle con facilidad. La segunda etapa, la síntesis, toma la representación obtenida en el análisis y la transforma en su equivalente en el lenguaje destino.
Análisis
El objetivo de esta etapa es obtener una representación de la entrada que nos permita realizar la síntesis o la interpretación con comodidad. La representación que nosotros utilizaremos es la que se llama árbol de sintaxis abstracta.
El paso de la entrada al árbol de sintaxis abstracta no es trivial. Para facilitarlo, se divide la tarea en varias partes. Supón que tuvieras que describir un lenguaje de programación. Una manera de hacerlo será comenzando por describir cuales son las unidades elementales tales como identificadores, palabras reservadas, operadores, etc. que se encuentran en la entrada. Después podrás describir cómo se pueden combinar esas unidades en estructuras mayores tales como expresiones, asignaciones, bucles y demás. Finalmente, especificaras una serie de normas que deben cumplirse para que el programa, además de estar bien escrito, tenga significado. Estas normas se refieren a aspectos tales como que las variables deben declararse o las reglas que se siguen para decidir los tipos de las expresiones.
Las tres fases que hemos mencionado tienen su reflejo en las tres fases en que se divide el análisis:
  • Análisis léxico: se encarga de la división de la entrada en componentes léxicos.
  • Análisis sintáctico: se encarga de encontrar las estructuras presentes en la entrada.
  • Análisis semántico: se encarga de comprobar que se cumplen las restricciones semánticas del lenguaje.
Síntesis
Una vez analizado el programa de entrada, es necesario generar código, a ser posible eficiente, para la maquina objetivo.
  • Generación de código intermedio: se traduce la entrada a una representación independiente de la maquina pero fácilmente traducible a lenguaje ensamblador
  • Generación de código objeto: traduce el código intermedio a código de maquina
  • Optimización de los códigos anteriores.

Detección o información de 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:
  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 queuna 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;

Fases de un compilador

Un compilador está formado por dos procesos análisis y síntesis.
1. Análisis: El cual se trata de la escritura correcta del código fuente. Esta a su vez comprende varias fases:
· Análisis léxico: esta fase es la encargada de leer el código fuente y separarlo en lotes para poder ser leído por el análisis sintáctico.
· Análisis sintáctico: esta fase evalúa los lotes de código con el fin de que este cumpla con los requerimientos definidos por el compilador.
· Análisis semántico: en esta fase se busca establecer que el código fuente cumpla con la semántica solicitada por el compilador, es decir que el código este correctamente escrito para poder ser interpretado.
2. Síntesis: Después del proceso de análisis se procede a generar grupos de los componentes que conforman el programa, para generar una salida.
· Generación de código intermedio: este código se genera con el fin de mejorar el uso de la memoria con el fin de optimizar código fuente.
· Optimización de código: el objeto de esta fase es mejorar el código para que sea más rápido ejecutarlo.
· Generación de código: Aquí se crea el código final de salida que va a ser interpretado por la máquina.

Análisis de un programa a fuente

La fase de análisis supone la división del programa fuente en componentes, y a cada uno de ellos le impone una estructura gramatical. Si durante el análisis se detecta que el programa fuente está mal formado en cuanto a la sintaxis, o que no tiene una semántica consistente, entonces debe proporcionar mensajes informativos para que el programadorpueda corregirlo.
En la fase del análisis también se recolecta información sobre el programa fuente y se almacena en una estructura de datos llamada tabla de símbolos, la cual se pasa junto con la representación intermedia a la fase de la síntesis.