lunes, 22 de junio de 2009

COMPILADORES E INTERPRETES 2

1. Señale dos ventajas de la generación de código intermedio

· El código objeto es abstraído para una maquina virtual. esta abstracción ayuda a separar operaciones de alto nivel y realizar dependientes de la maquina.
· La generación de código y el asignamiento de registros temporales son separados de las rutinas semánticas, los cuales solo trabajan con la abstracción presentada por la representación intermedia. las dependencias del código objeto son aisladas de las rutinas de generación de código.
· La abstracción puede ser hecha en el nivel de representación intermedia. esta organización ayuda a hacer una optimización completamente independiente del código objeto, con lo que hace que las rutinas de optimización complejas sean más transportables. debido a que las representaciones intermedias son por diseño más abstracta y uniforme, las rutinas de optimización puede ser más simples.

2. Explique con un ejemplo la optimización de código.

Optimación de Código
La fase de optimación de código trata de mejorar el código intermedio de modo que resulte un código de máquina más rápido de ejecutar. Algunas optimaciones son triviales. Por ejemplo, un algoritmo natural genera el código intermedio (2) utilizando una instrucción para cada operador de la representación del árbol después del análisis semántico, aunque hay una forma mejor de realizar los mismos cálculos usando las dos instrucciones

Posición := inicial + velocidad * 60 (1) Código Inicial

temp1 := entarea1(60)
temp2 := id3 * temp1 (2) Generación de código intermedio
temp3 := id2 + temp2
id1 := temp3

Temp1 := id3 * 60.0 (3) Optimización de Código
Id1 := id2 + temp1

Este sencillo algoritmo no tiene nada de malo, puesto que el problema se puede solucionar en la fase de optimación de código. Esto es, el compilador puede deducir que la conversión de 60 de entero a real se puede hacer de una vez por todas en el momento de la compilación, de modo que la operación entero a real se puede eliminar. Además, temp3 se usa sólo una vez, para transmitir su valor a id1. Entonces resulta seguro sustituir a id1 por temp3, a partir de lo cual la última proposición de (2) no se necesita y se obtiene el código de (3).
Hay muchas variaciones en la cantidad de optimación de código que ejecutan los distintos compiladores. En lo que hacen mucha optimación llamados “compiladores optimadores”, una parte significativa del tiempo del compilador se ocupa en esta fase. Sin embargo hay optimaciones sencillas que mejoran significativamente del tiempo del compilador se ocupa en esta fase. Sin embargo, hay optimaciones sencillas que mejoran sensiblemente el tiempo de ejecución del programa objeto sin retardar demasiado la compilación.


3. Señale el proceso de compilación de un programa en C#.

Cuando estemos escribiendo código, lo estaremos haciendo en un lenguaje (X), en nuestro caso C#, todos nuestros archivos deberán tener la extensión .CS, estos archivos describen y muestran todo lo que hace nuestra aplicación en un lenguaje entendible para los humanos, cuanto más entendible sea el código más nivel tendrá el lenguaje y cuanto menos entendible menos nivel, de aquí el termino lenguajes de alto y bajo nivel, y el piso es el lenguaje maquina, ahora bien estos archivos que crearemos son el tesoro más preciado de nuestra comunidad, y se lo conoce como código fuente, ahora éste código no es ejecutable ya que la PC no lo entiende por ese motivo hay que traducirlo a un lenguaje que sea entendible por la PC, a este proceso de traducción se lo conoce como compilación, y consta de llevar el código fuente a un bajo nivel, que por lo general tras el proceso de compilación el resultado es un archivo de tipo .EXE,
ARCHIVO FUENTE (.CS) - COMPILACION - (MSIL) ARCHIVO EJECUTABLE (.EXE)
Pasos

· Lo primero que le ocurre a un fichero .c de código C es el preprocesado. En este paso se sustituyen todas las macros y se eliminan los comentarios. El resultado, si lo vemos independientemente, es un fichero de código C preprocesado, o .i.
· El segundo paso es la compilación propiamente dicha, en el que el código C preprocesado se convierte en código ensamblador, que si lo vemos independientemente es un fichero .s.
· El tercer paso es el ensamblado del código ensamblador, lo que lo convierte en código máquina. Un fichero de código máquina también es llamado fichero objeto, y su extensión típica es .o.
· Dado que el camino anterior normalmente convierte un fichero en un fichero, se suele hacer en un sólo paso, de código C a fichero objeto.
· El último paso es el enlazado de los ficheros objeto en el ejecutable final.

4. Generar CI para el código siguiente:

If x>10 and Not(y<0)>
Then
A+=2
B+=1
End if

for (i=0; i<10;>
{
x += i;
}





COMPILADORES E INTERPRETES

1. En el cuadro siguiente escriba dos tareas para cada analizador:
LEXICO
· Recibe como entrada el código fuente de otro programa (secuencia de caracteres) y produce una salida compuesta de tokens (componentes léxicos) o símbolos
· Revisa y controla que las "tokens" estén bien escritos, para una posterior etapa del proceso de traducción.
SINTACTICO
· Comprueba si los tokens van llegando en el orden correcto (orden permitido por el lenguaje). La salida "teórica" de la fase de análisis sintáctico sería un árbol sintáctico.
· Guía el proceso de traducción (traducción dirigida por la sintaxis).
SEMANTICO
· Se encarga de revisar que cada agrupación o conjunto de tokens tenga sentido, y no sea un absurdo
· Detecta si las variables, constantes y funciones han sido declaradas antes de ser utilizadas.

2. Transformar las siguientes expresiones a notación postfija y prefijo.

a) a * b % c – d * a – d % e

NOTACION POSFIJA: a b * c % d – a * d – e %

NOTACION PREFIJA: % - * - % * a b c d a d e

b) (a + c) – (d * e) / x % y

NOTACION POSFIJA: a b + d e * - x / y %

NOTACION PREFIJA: % / - + a b * d e x y

c) (h – i – j * k) % m / x / y

NOTACION POSFIJA: h i – j – k * m % x / y /

NOTACION PREFIJA: / / % * - - h i j k m x y
3. Dado el árbol de derivación, obtener la expresión en notación infija, postfija y prefija.


NOTACION INFIJA: (((a + b) % c)-d) * ((h - f) % e)

NOTACION POSTFIJA: a b * c % d – h f – e % *

NOTACION PREFIJA: * - % * a b c d % - h f e


ANALISIS SEMANTICO

El análisis semántico es posterior al sintáctico y mucho más difícil de formalizar que éste. Se trata de determinar el tipo de los resultados intermedios, comprobar que los argumentos que tiene un operador pertenecen al conjunto de los operadores posibles, y si son compatibles entre sí, etc. En definitiva, comprobará que el significado de lo que se va leyendo es válido.

También se encarga de revisar que cada agrupación o conjunto de token tenga sentido, y no sea un absurdo. En esta etapa se reúne la información sobre los tipos para la fase posterior, en esta etapa se utiliza la estructura jerárquica de la etapa anterior y así poder determinar los operadores, y operandos de expresiones y preposiciones.
Se compone de un conjunto de rutinas independientes, llamadas por los analizadores léxico y sintáctico.
El análisis semántico utiliza como entrada el árbol sintáctico detectado por el análisis sintáctico para comprobar restricciones de tipo y otras limitaciones semánticas y preparar la generación de código.

En compiladores de un solo paso, las llamadas a las rutinas semánticas se realizan directamente desde el analizador sintáctico y son dichas rutinas las que llaman al generador de código. El instrumento más utilizado para conseguirlo es la gramática de atributos. En compiladores de dos o más pasos, el análisis semántico se realiza independientemente de la generación de código, pasándose información a través de un archivo intermedio, que normalmente contiene información sobre el árbol sintáctico en forma lineal (para facilitar su manejo y hacer posible su almacenamiento en memoria auxiliar). En cualquier caso, las rutinas semánticas suelen hacer uso de una pila (la pila semántica) que contiene la información semántica asociada a los operandos (y a veces a los operadores) en forma de registros semánticos. En el caso de los operadores polimórficos (un único símbolo con varios significados), el análisis semántico determina cuál es el aplicable. Por ejemplo, consideremos la siguiente sentencia de asignación: A := B + C En Pascal, el signo “+” sirve para sumar enteros y reales, concatenar cadenas de caracteres y unir conjuntos. El análisis semántico debe comprobar que B y C sean de un tipo común o compatible y que se les pueda aplicar dicho operador. Si B y C son enteros o reales los sumará, si son cadenas las concatenará y si son conjuntos calculará su unión.

Funciones

Entre las funciones de un analizador semántico están las siguientes:
· Detectar si las variables, constantes y funciones han sido declaradas antes de ser utilizadas.
· Verificar que las variables, constantes y funciones sean accesibles (visibilidad) desde el ámbito en que son utilizadas. · Comprobar que los diferentes identificadores solo hayan sido declarados una vez.
· Comprobaciones de tipos al evaluar las expresiones. Por ejemplo que no se multiplique un número por una cadena que la expresión a evaluar en un IF sea del tipo booleano. Las reglas de inferencia de tipos gobiernan el tipo de una expresión en función del operador y el tipo de sus operandos.
· Verificar que no se intente modificar el valor de una constante.
· Generar la tabla de símbolos.

¿Cómo hacer un análisis semántico?

Para realizar el análisis semántico utilizaremos una gramática de atributos. Dichos atributos se asociaran tanto a los símbolos terminales como a los no terminales y se propagarán por el árbol sintáctico desde abajo hacia arriba, dando lugar al árbol semántico.
Lo primero que haremos será definir las reglas semánticas, para ello utilizaremos de nuevo la notición BNF a la que añadiremos en pseudocódigo las reglas semánticas.
Recorreremos recursivamente el árbol sintáctico en postorden (para cada nodo procesamos recursivamente todos los descendientes primero y luego el nodo). Iremos añadiendo atributos con información al árbol a medida que procesemos nodos. Como el recorrido se realiza en postorden cuando procesemos un nodo ya habremos procesado previamente todos sus descendientes por lo que dispondremos de la información de que nos provean sus atributos.

Añadiremos a la tabla de símbolos todas las constantes y variables que se vayan reconociendo, junto con la información del tipo y el valor (en el caso de las constantes). La tabla de símbolos será una lista de elementos del tipo símbolo.

Ejemplo

// Un identificador no se puede utilizar si previamente no se ha definido.
char a; // Variable tipo char
int
i; // Variable tipo entero
a = i + 2 ;


Análisis Léxico: Devuelve la secuencia de tokens: id asig id suma numero
Análisis Sintáctico: Orden de los tokens válido
Análisis Semántico: Tipo de variables asignadas incorrecta



miércoles, 13 de mayo de 2009

HERRAMIENTAS PARA LA CONSTRUCCION DE UN COMPILADOR

Herramientas

Utilidades y Generadores de Compiladores

A continuación se muestran algunas de las herramientas disponibles que pueden utilizarse para la realización del Proyecto de Compiladores. Todas estas herramientas funcionan bajo Windows.





Ensambladores Simbólicos ENS

Los ensambladores simbólicos ENS permiten ensamblar, ejecutar y depurar el código ensamblador generado por el compilador. Dentro de los ficheros comprimidos que se pueden obtener en la tabla, se encuentra información sobre su uso, su sintaxis y algún ejemplo de funcionamiento. El compilador construido en el Proyecto de Compiladores tiene que generar como código objeto uno de estos ensambladores.




GENERACION DE MEMORIA EN TIEMPO DE EJECUCION

ORGANIZACIÓN DE LA MEMORIA

Tiempo de ejecución

Se denomina Tiempo de ejecución (Runtime en inglés) al intervalo de tiempo en el que un programa de computadora se ejecuta en un sistema operativo. Este tiempo se inicia con la puesta en memoria principal del programa, por lo que el sistema operativo comienza a ejecutar sus instrucciones.

El intervalo finaliza en el momento en que éste envía al sistema operativo la señal de terminación, sea ésta una terminación normal, en que el programa tuvo la posibilidad de concluir sus instrucciones satisfactoriamente, o una terminación anormal, en el que el programa produjo algún error y el sistema debió forzar su finalización.

La memoria durante el momento de ejecución debe estar subdividida de modo que pueda almacenar los siguientes elementos:

1. La tabla de símbolos.
2. Código objeto generado.
3. Espacio para la realización de operaciones o procedimiento.

El código objeto tiene un tamaño fijo determinado en el momento de la compilación, de este modo la asignación de memoria para éste elemento resulta estática y se deja un lugar en el cual no se efectúe el uso de la memoria. La razón de asignación estática es que las direcciones no van a estar en constante cambio.
De manera similar se puede conocer el tamaño de algunos datos en el momento de la compilación y por lo tanto estos pueden ir colocados en una zona estática.
Los datos cuyas direcciones están contenidas en la activación de un procedimiento se pueden asociar y colocar en una pila.
Un área distinta de la memoria para el monumento de la ejecución se llama montículo.

REGISTROS DE ACTIVACION

La información necesaria para usar una sola ejecución de un procedimiento se consigue utilizando un bloque contigo de memoria llamado registro de activación o marco, que consta del conjunto de campos listados a continuación:

TEMPORALES, los valores temporales como los que surgen en la evaluación de expresiones es almacenada en seta posición.
DATOS LOCALES, guardan los datos locales a una ejecución de un procedimiento.
ESTADO DE LA MAQUINA GUARDADO, contiene información sobre el estado de la maquina justo antes de que sea llamado el procedimiento. La información incluye los valores del contador del programa y los registro de la maquina que debe reponerse cuando el control regrese del procedimiento.

ENLACE DE ACCESO OPCIONAL,se utiliza para hacer referencia a los datos no locales guardados en otros registros de activación.
ENLACE DE CONTROL, apunta al registro de activación del autor de la llamada.
PARAMETROS ACTUALES, es utilizado por el procedimiento autor de la llamada para proporcionar parámetros al procedimiento que recibe la llamada. Existe espacio para los parámetros en el registro de activación, pero en la practica los parámetros se pasan en los registros de la maquina para una mayor eficiencia.
VALOR DEVUELTO, este campo es utilizado por el procedimiento que recibe la llamada para devolver un valor al procedimiento autor de la llamada. En la practica este valor se suele trasladar en un registro para mayor eficiencia.
Los tamaños de cada uno de estos campos se pueden determinar en el momento que es llamado un procedimiento; de hecho se pueden determinar en el momento de la compilación. Existe una excepción cuando un procedimiento tiene una matriz local cuyo tamaño venga determinando por el valor de un parámetro actual, disponible únicamente cuando el procedimiento sea llamado durante la ejecución.

DISPOSICION ESPACIAL DE LOS DATOS LOCALES

La cantidad de memoria necesaria para una variable viene determinada por su tipo. Un tipo de datos elemental, como un carácter, un entero o un real, se puede almacenar en un número entero de bytes. La memoria para un agregado como una matriz o un registro, debe ser lo suficientemente grande como para dar cabida a todos sus componentes.
Para acceder fácilmente a los componentes, la memoria para los agregados se coloca en un bloque contiguo de bytes.
El campo para los datos locales se perfila conforme se examinen las declaraciones en un procedimiento durante la compilación. Los datos de longitud variable se mantienen fuera de este campo. Se hace un recuento de las posiciones de memoria asignadas a declaraciones anteriores. Según el resultado se determina una dirección relativa de la memoria para un valor local con respecto a una posición, como el comienzo del registro de activación. La dirección relativa o desplazamiento es la diferencia entre las direcciones de esa posición y los objetos de datos.
La disposición de la memoria para los datos seta muy influida por las limitaciones de direccionamiento de la maquina objeto. El espacio que no se usa por consideraciones de alineación se llama relleno. Cuando este espacio es muy pequeño, un compilador puede empaquetar datos de modo que no queda ningún relleno; entonces puede ser necesario practicar instrucciones adicionales durante la ejecución para situar los datos empaquetados de modo que se puedan manipular como si estuvieran alineados en forma apropiada.

ESTRATEGIA PARA LA ASIGNACION DE MEMORIA.

1. ASIGNACION ESTATICA.- Dispone la memoria para todo los objetos de datos durante la compilación
2. ASIGNACION POR MEDIO DE UNA PILA.- Trata la memoria en ejecución como una pila.
3. ASIGNACION POR MEDIO DE UN MONTICULO.- Asigna y desasigna la memoria conforme se necesita durante la ejecución a partir de un área de datos llamada montículo.

ASIGNACION ESTATICA

Las variables se ligan a la memoria durante la compilación del programa. Como a los enlaces, no cambian durante la ejecución, cada vez que se activa un procedimiento, sus nombres se enlazan a las mismas posiciones de memoria. Está propiedad permite que los valores de los nombre locales sean retenidas durante la activación de un procedimiento. Es decir, cuando el control regresa a un procedimiento, los valores de los variables locales son los mismos que cuando el control salió por última vez.
Según el tipo de la variable, el compilador determina la cantidad de memoria que debe reservarse para dicha variable. La dirección de está memoria consta de un desplazamiento desde un extremo del registro de activación del procedimiento el compilador debe decir a donde van los registros de activación. Durante la compilación se pueden proporcionar las direcciones en las que el código objeto pueden encontrar los datos con los que opera.

LIMITACIONES

1. El tamaño de unos objetos de datos y las limitaciones en cuanto su posición en la memoria deben conocerse en el momento de la compilación.
2. Los procedimientos recursivos son escasos porque todas las actividades de un procedimiento utilizan los mismos enlaces para los nombres locales.
3. Las estructuras de datos no se pueden crea dinámicamente. Ya que no hay un mecanismo para la asignación de memoria durante la ejecución.

ASIGNACIÓN POR MEDIO DE UNA PILA

La asignación por medio de una pila se basa en la idea de una pila de control; la memoria se organiza como una pila, y los registros de activación se introducen y se sacan cuando las activaciones comienzan y terminan respectivamente. La memoria para las variables locales en cada llamada de un procedimiento está contenida en el registro de activación de dicha llamada. De este modo en cada activación, las variables locales se enlazan a una memoria nueva, puesto que se introducen a un nuevo registro de activación en la pila al realizar una llamada. Además, los valores de las variables locales se borran cuando finaliza la activación; es decir, los valores se pierden por que la memoria para las variables locales desaparecen cuando se extrae el registro de activación.

SECUENCIA DE LLAMADAS

Las llamadas a procedimientos se implantan mediante la generación de lo que se conoce como secuencia de llamadas en el código objeto. Una secuencia de llamada asigna un registro de activación e introduce información dentro de sus campos. Una secuencia de retorno restablece el estado de la máquina para que el procedimiento que efectúa la llamada pueda continuar con su ejecución.
La secuencia de llamadas y los registros de activación son diferentes, incluso para implementaciones del mismo lenguaje. El código en una secuencia de llamada esta dividido en el procedimiento que hace la llamada y el procedimiento que recibe la llamada. No hay una división exacta entre el llamador y el llamado de las tareas de ejecución (el lenguaje fuente, la máquina objeto y el S.O. impone requisitos que puedan hacer prevalecer una solución sobre la otra).
Un principio que ayuda al diseño de secuencia de llamadas y registros de activación es que los campos cuyos tamaños se fijan primero se colocan en el medio. En el registro de activación general, los campos de enlace de control, enlace de acceso y estado de la máquina aparecen en el medio. La decisión de utilizar o no los enlace de control de acceso es parte del diseño del compilador, de modo que estos campos se pueden fijar durante la construcción del compilador si se guarda exactamente la misma cantidad de información sobre el estado de la máquina, entonces el mismo código puede almacenar y restablecer para todas las activaciones.
Aunque el tamaño del campo para los valores temporales se fija en un determinado momento de la compilación este tamaño puede o no conocerse en la etapa inicial. La generación u optimización cuidadosa del código puede reducir el número temporal de variables que necesita el procedimiento. Por tanto en el registro de activación temporal, se muestra éste campo después de aquel para datos locales, donde los cambios en su tamaño no afectarán a los desplazamientos de los objetos de datos relativos a los cambios de en medio.

GENERACION DE CODIGO INTERMEDIO

El generador de código toma como entrada una representación intermedia del programa fuente y produce como salida un programa objeto equivalente; Es posible producir o no una fase de optimización antes de la generación de código. Dicha fase intente transformar el código intermedio en una forma de la que se pueda producir código objeto más eficiente.
El código intermedio es un código abstracto independiente de la máquina para la que se generará el código objeto. El código intermedio ha de cumplir dos requisitos importantes: ser fácil de producir a partir del análisis sintáctico, y ser fácil de traducir al lenguaje objeto. Esta fase puede no existir si se genera directamente código máquina, pero suele ser conveniente emplearla.

Ejemplo:

Consideremos, por ejemplo, un código intermedio de tercetos, llamado así porque en cada una de sus instrucciones aparecen como máximo tres operandos. La sentencia traducida a este código intermedio quedaría2:
temp1 := inttoreal (2)temp2 := id3 * temp1temp3 := id2 + temp2id1 := temp3


El analizador sintáctico va generando acciones que valida el analizador semántico y que se convierten en tercetos. Esta conversión en tercetos constituye el generador de código intermedio.
Dado que el lenguaje puede presentar distintas funciones anidadas, los tercetos los generamos por orden del parser y son almacenados en un sitio u otro dependiendo del contexto en que nos encontremos. Es decir, se almacenan en una lista de tercetos dependiente de la Tabla de Símbolos. Hay tantas listas de tercetos como funciones haya en el código fuente más una lista de tercetos asociada a la Tabla de Símbolos Global
No obstante una vez finalizado el análisis, todos estos tercetos repartidos en distintas listas se vuelcan a una sola lista de tercetos global. Esta será la que finalmente se optimice y a partir de la que se generará el programa en ensamblador.
El problema de tener que manejar tercetos indirectos fue resuelto modificando el método de inserción sobre la lista de tercetos utilizada en cada momento, de manera que se realiza previamente una búsqueda de algún terceto que sea exactamente igual al que estamos insertando. En caso afirmativo, insertamos en la lista no un terceto nuevo, sino un puntero al ya existente, y marcamos dicho terceto como terceto indirecto. Son tercetos indirectos aquellos marcados con un asterisco después del índice en los volcados de la lista de tercetos.

Ventajas:

• Permite abstraer la máquina, separar operaciones de alto nivel de su implementación a bajo nivel.
• Permite la reutilización de los front-ends y back-ends.
• Permite optimizaciones generales
• El código objeto es abstraído para una maquina virtual. esta abstracción ayuda a separar operaciones de alto nivel y realizar dependientes de la maquina.
• La generación de código y el asignamiento de registros temporales son separados de las rutinas semánticas, los cuales solo trabajan con la abstracción presentada por la representación intermedia. las dependencias del código objeto son aisladas de las rutinas de generación de código.
• La abstracción puede ser hecha en el nivel de representación intermedia. esta organización ayuda a hacer una optimización completamente independiente del código objeto, con lo que hace que las rutinas de optimización complejas sean más transportables. debido a que las representaciones intermedias son por diseño mas abstracta y uniforme, las rutinas de optimización puede ser más simples.

Desventajas:

• Implica una pasada más para el compilador (no se puede utilizar el modelo de una pasada, conceptualmente simple).
• Dificulta llevar a cabo optimizaciones específicas de la arquitectura destino.
• Suele ser ortogonal a la máquina destino, la traducción a una arquitectura específica será más larga e ineficiente


Formas de Representación Intermedia

En la historia de los compiladores han sido utilizadas una amplia variedad de representaciones intermedias sin embargo la forma más simple es la notación posfija, la cual también es conocida en matemáticas como notación libre de paréntesis para expresiones aritméticas que han sido utilizadas mucho tiempo antes que los compiladores. Como su nombre lo implica la notación posfija es una representación en la cual los operadores aparecen después que los operandos para los cuales se aplican.
Una de las atracciones principales de la notación posfija es la simplicidad en la traducción de los procesos, y la consistencia de la representación. Estos factores hacen de esta notación la más usual como una representación intermedia para el manejo de intérpretes. de hecho la notación posfija no es muy efectiva como entrada para un optimizador a un generador de código, a menos que el código objeto a generar tenga una arquitectura en forma de pila.

La siguiente clase de representación de código intermedio se refiere a un árbol de 3 direcciones, 2 para los operandos y una para la ubicación del resultado. Esta clase incluye un amplio número de representaciones diferentes entre las cuales encontramos cuádruplos y triples. la principal diferencia entre estas notaciones y la notación posfija es que ellos incluyen referencias explicitas para los resultados de los cálculos intermedios. Mientras que la notación posfija los resultados son implícitos al representarlos en una pila.
La diferencia entre triples y cuádruplos es que con los triples es referenciado el valor intermedio hacia el número del triple que lo creo, pero en los cuádruplos requiere que ellos tengan nombre implícitos.
Los triples tienen una ventaja obvia de ser más consistente, pero ellos dependen de su posición, y hacen que la optimización presente cambios de código mucho más compleja.

Aspectos de Diseño de un Generador de Código

Entrada al generador de código

La entrada para la generación de código consta de una representación intermedia del programa fuente, junto con la información de la tabla de símbolos que se utiliza para determinar las direcciones durante la ejecución de los datos denotados por los nombres de la representación intermedia. El código se puede generar desde el punto de vista del código de 3 direcciones, notación posfija, cuádruplos, triples, etc.
Se asume que antes de la generación de código la etapa inicial a hecho los análisis léxico, semántico, sintáctico y se ha traducido el programa fuente a una representación intermedia razonablemente detallada, así que los valores de los nombres que aparecen en el lenguaje intermedio pueden ser representados por cantidades que la maquina objeto puede manipular directamente. Por lo tanto, la fase de generación de código puede proseguir con la hipótesis de que su entrada no contiene errores.
Programas objeto: La salida del generador de código es el programa objeto. Al igual que el código intermedio esta salida puede adoptar una variedad de formas: Lenguajes de maquina absoluto, lenguaje de maquina relocalizable o lenguaje ensamblador

Lenguaje Máquina Absoluto


Para producir como salida un programa en lenguaje absoluto tiene la ventaja de que se puede colocar en una posición fija de memoria y ejecutarse inmediatamente. Un programa pequeño se puede compilar y ejecuta.

Lenguaje Máquina Relocalizable


Producir como salida un lenguaje maquina relocalizable (modulo objeto) permiten que los programas se compilen por separado .Un conjunto de módulos objeto relocalizables se pueden enlazar. Aunque se tenga que pagar costo de enlazar y cargar si se producen módulos objetos relocalizables, se gana mucha flexibilidad al poder compilar subrutinas por separado y llamar desde el modulo objeto a otros programas previamente compilados.

Lenguaje Ensamblador


Producir Como la salida un programa en lenguaje ensamblador facilita la generación de código. Se pueden generar instrucciones simbólicas y utilizar las macros del ensamblador para ayudar a generar el código, el precio que se paga es el paso de ensamble después de la generación de código.
Como producir código ensamblador no duplica la tarea completa del compilador esta elección es otra alternativa razonable, especialmente para una maquina con memoria pequeña, donde un compilador debe utilizar varias pasadas.