3.3. Traducción de un programa, compilación, enlace de un programa, errores en tiempo de compilación

Ver comentarios

Contexto de un compilador:

En el proceso de construcción de un programa escrito en código maquina a partir del programa fuente, suelen intervenir, aparte del compilador, otros programas:

Preprocesador: Es un traductor cuyo lenguaje fuente es una forma extendida de algún lenguaje de alto nivel, y cuyo lenguaje objeto es la forma estándar del mismo lenguaje. Realiza la tarea de reunir el programa fuente, que a menudo se divide en módulos almacenados en archivos diferentes. También puede expandir abreviaturas, llamadas macros, a proposiciones del lenguaje fuente. El programa objeto producido por un preprocesador puede, entonces, ser traducido y ejecutado por el procesador usual del lenguaje estándar.

Ensamblador: Traduce el programa en lenguaje ensamblador, creado por el compilador, a código máquina.

Cargador y linkador (Enlace de un programa): Un cargador es un traductor cuyo lenguaje objeto es el código de la maquina real y cuyo lenguaje fuente es casi idéntico. Este consiste usualmente en programas de lenguaje máquina en forma reubicable, junto con tablas de datos que especifican los puntos en donde el código reubicable debe modificarse para convertirse en verdaderamente ejecutable. Por otro lado, un linkador es un traductor con los mismos lenguajes fuente y objeto que el cargador. Toma como entrada programas en forma reubicable que se han compilado separadamente, incluyendo subprogramas almacenados en librerías. Los une en una sola unidad de código máquina lista para ejecutarse. En general, un editor descarga y enlace une el código máquina a rutinas de librería para producir el código que realmente se ejecuta en la máquina.

Análisis Léxico (scanner):

Es la primera fase de la que consta un compilador. La parte del compilador que realiza el análisis léxico se llama analizador léxico (AL), scanner o explorador. La tarea básica que realiza el AL es transformar un flujo de caracteres de entrada en una serie de componentes léxicos o tokens. Se encargaría, por tanto, de reconocer identificadores, palabras clave, constantes, operadores, etc. La secuencia de caracteres que forma el token se denomina lexema. No hay que confundir el concepto de token con el de lexema. A un mismo token le pueden corresponder varios lexemas. Por ejemplo, se pueden reconocer como tokens de tipo ID a todos los identificadores. Aunque para analizar sintácticamente una expresión, solo nos hará falta el código de token, el lexema debe ser recordado, para usarlo en fases posteriores dentro del proceso de compilación. El AL es el único componente del compilador que tendrá acceso al código fuente. Por tanto, debe de encargarse de almacenar los lexemas para que puedan ser usados posteriormente.

Esto se hace en la tabla de símbolos. Por otro lado, debe enviar al analizador sintáctico, aparte del código de token reconocido, la información del lugar donde se encuentra almacenado ese lexema (por ejemplo, mediante un apuntador a la posición que ocupa dentro de la tabla de símbolos). Posteriormente, en otras fases del compilador, se irá completando la información sobre cada item de la tabla de símbolos. Finalmente, puesto que el AL es el único componente del compilador que tiene contacto con el código fuente, debe encargarse de eliminar los símbolos no significativos del programa, como espacios en blanco, tabuladores, comentarios, etc. Es conveniente siempre separar esta fase de la siguiente (análisis sintáctico), por razones de eficiencia. Además, esto permite el uso de representaciones diferentes del programa fuente, sin tener que modificar el compilador complete.

Análisis sintáctico.

Esta es la segunda fase de la que consta un compilador. La parte del compilador que realiza el análisis sintáctico se llama analizador sintáctico o parser. Su función es revisar si los tokens del código fuente que le proporciona el analizador léxico aparecen en el orden correcto (impuesto por la gramática), y los combina para formar unidades gramaticales, dándonos como salida el árbol de derivación o árbol sintáctico correspondiente a ese código fuente. De la forma de construir este árbol sintáctico se desprenden los dos tipos de analizadores sintácticos existentes: Cuando se parte del axioma de la gramática y se va descendiendo, utilizando derivaciones más a la izquierda, hasta conseguir la cadena de entrada, se dice que el análisis es descendente. Por el contrario, cuando se parte de la cadena de entrada y se va generando el árbol hacia arriba mediante reducciones más a la izquierda (derivaciones más a la derecha), hasta conseguir la raíz o axioma, se dice que el análisis es ascendente. Si el programa no tiene una estructura sintáctica correcta, el analizador sintáctico no podrá encontrar el árbol de derivación correspondiente y deberá dar mensaje de error sintáctico. La división entre análisis léxico y sintáctico es algo arbitraria. Generalmente se elige una división que simplifique la tarea completa del análisis. Un factor para determinar cómo realizarla es comprobar si una construcción del lenguaje fuente es inherentemente recursiva o no. Las construcciones léxicas no requieren recursión, mientras que las sintácticas suelen requerirla. Las gramáticas libres de contexto (GLC) formalizan la mayoría de las reglas recursivas que pueden usarse para guiar el análisis sintáctico. Es importante destacar, sin embargo, que la mayor parte de los lenguajes de programación pertenecen realmente al grupo de lenguajes dependientes del contexto.

Análisis semántico:

Para que la definición de un lenguaje de programación sea completa, aparte de las especificaciones de su sintaxis (estructura o forma en que se escribe un programa), necesitamos también especificar su semántica (significado o dentición de lo que realmente hace un programa). La sintaxis de un lenguaje de programación se suele dividir en componentes libres de contexto y sensibles al contexto. La sintaxis libre de contexto define secuencias legales de símbolos, independientemente de cualquier noción sobre el contexto o circunstancia particular en que aparecen dichos símbolos. Por ejemplo, una sintaxis libre de contexto puede informarnos de que A := B + C es una sentencia legal, mientras que A:=B* no lo es. Sin embargo, no todos los aspectos de un lenguaje de programación pueden ser descritos mediante este tipo de sintaxis. Este es el caso, por ejemplo, de las reglas de alcance para variables, de la compatibilidad de tipos, etc. Estos son componentes sensibles al contexto de la sintaxis que define al lenguaje de programación.

Por ejemplo, A := B + C podrá no ser legal si las variables no están declaradas, o son de tipos incompatibles. Puesto que en la mayoría de los casos, como ya apuntamos en la sección anterior, se utilizan por simplicidad GLC para especificar la sintaxis de los lenguajes de programación, tenemos que hacer un tratamiento especial con las restricciones sensibles al contexto. Estas pasaran a formar parte de la semántica del lenguaje de programación. La fase de análisis semántico revisa el programa fuente para tratar de encontrar errores semánticos, y reúne la información sobre los tipos para la fase posterior de generación de código. Para esto se utiliza la estructura jerárquica que se construye en la fase de análisis sintáctico, para, por ejemplo, identificar operadores y operandos de expresiones y proposiciones. Además, accede, completa y actualiza con frecuencia la tabla de símbolos.

Una tarea importante a realizar en esta fase es la verificación de tipos. Aquí, el compilador comprueba si cada operador tiene operandos permitidos por la especificación del lenguaje fuente. Muy frecuentemente, esta especificación puede permitir ciertas conversiones de tipos en los operandos, por ejemplo, cuando un operador aritmético binario se aplica a un número entero ya otro real. En este caso, el compilador puede requerir la conversión del número entero a real, por ejemplo.

Resumiendo, algunas de las comprobaciones que puede realizar, son:

  • Chequeo y conversión de tipos.
  • Comprobación de que el tipo y número de parámetros en la declaración de funciones coincide con los de las llamadas a esa función.
  • Comprobación del rango para índices de arrays. Comprobación de la declaración de variables.
  • Comprobación de las reglas de alcance de variables.

Comentarios