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