intérpretes y compiladores
Post on 09-Jul-2022
8 Views
Preview:
TRANSCRIPT
Intérpretes y Compiladores
Conceptos básicos
Fundamentos de Computación II
¿QUE ES UN COMPILADOR?
Un compilador es un programa que convierte o traduce el código fuente de un programa
hecho en lenguaje de alto nivel a un lenguaje de bajo nivel o lenguaje de maquina.
Es un software que se encarga de traducir el programa hecho en lenguaje de programación
a un lenguaje de maquina que pueda ser comprendido por el equipo y pueda ser procesado
o ejecutado por éste.
CARACTERÍSTICAS PRINCIPALES DE UN COMPILADOR
Para cada lenguaje de programación se requiere un compilador separado.
El compilador traduce todo el programa antes de ejecutarlo.
Los programas compilados se ejecutan mas rápido que los interpretados, debido a que ha
sido completamente traducidos a lenguaje de maquina.
Informa al usuario de la presencia de errores en el programa fuente
Poseen un editor integrado con un sistema de coloreado para los comandos, funciones,
variables y demás partes de un programa.
Un compilador no suele funcionar de manera aislada sino que se apoya en otros programas
para conseguir su objetivo.
Algunos de estos programas de apoyo son:
El preprocesador se ocupa de incluir ficheros, expandir macros, eliminar comentarios, etc.
El enlazador (linker) construye el fichero ejecutable añadiendo al fichero objeto las
cabeceras necesarias y las funciones de librerías utilizadas por el programa fuente.
El depurador permite seguir paso a paso la ejecución del programa.
Muchos compiladores generan un programa en lenguaje ensamblador que debe después
convertirse en un ejecutable mediante la utilización de un ensamblador.
La importancia de los compiladores radica en que sin estos programas no existiría ninguna
aplicación informática, que son la base de la programación en cualquier plataforma.
Componentes de un compilador
Un compilador es un programa complejo que no es fácil distinguir unas partes de otras.
Se ha conseguido establecer una división lógica del compilador en fases, lo que permite
formalizar y estudiar por separado cada una de ellas
En la practica estas fases no siempre se ejecutan secuencialmente sino que lo hace
simultáneamente, pudiendo ser unas fases tratadas como subrutinas de otras.
1. Análisis léxico
El analizador léxico , también conocido como scanner, lee los caracteres del programa
fuente, uno a uno, desde el fichero de entrada y va formado grupos de caracteres con
alguna relación entre sí ( tokens o identificadores).
Cada token es tratado como una única entidad, constituyendo la entrada de la siguiente
fase del compilador.
Existen diferente tipos de tokens y a cada uno se le puede asociar un tipo y en algunos
casos , un valor.
Los tokens se pueden agrupar en dos categorías:
1) Cadenas específicas, como las palabras reservadas ( if, while…) signos de puntuación ( ,
=, ,, . ,…) operadores aritméticos ( +,*,…) y lógicos (and,, or, …) etc.
Habitualmente las cadenas especificas no tienen asociado ningún valor, solo su tipo, lo que
representan.
2) Cadenas no específicas, como los identificadores o las constantes numéricas o de texto.
Las cadenas no especificas siempre tienen tipo y valor.
Ejemplo: si “Contador” es un identificador , el tipo del token será: identificador y su valor
será la cadena “Contador”.
Frecuentemente el analizador léxico funciona como una subrutina del analizador sintáctico.
Para el analizador léxico se utilizan los autómatas finitos.
valor= valor+inc; /*Actualizamos */
1. Se analiza la entrada carácter a carácter.
2. Se divide en unidades elementales: componentes léxicos.
3. Cada uno de los componentes se divide en categorías.
4. El criterio para la clasificación es la pertenencia a un lenguaje o no.
Esta fase se encarga de eliminar los espacios en blanco y los comentarios
Categorías: identificadores, la suma, la asignación y el punto y coma. Se puede suponer que
los identificadores son secuencias de letras y dígitos que comienzan con una letra. Los
blancos y los comentarios se omiten.
Teniendo en cuenta eso el analizador léxico ve:
Y lo que pasa al sintáctico es:
id es el identificar léxico que representa a los identificadores asig representa la asignación suma representa las sumas pyc representa el punto y coma
2. Analizador sintáctico
El analizador sintáctico también llamado parser, recibe como entrada los tokens que genera el
analizador léxico y comprueba si estos tokens van llegando en el orden correcto.
Siempre que no se hayan producido errores, la salida teórica de esta fase del compilador será un
árbol sintáctico.
Si el programa es incorrecto, se generaran los mensajes de error correspondientes.
Para el diseño de los analizadores sintácticos se utilizan los autómatas de pila.
En el ejemplo podemos pensar que las reglas que se siguen son:
Una asignación se compone de un identificador, seguido de un símbolo de asignación,
seguido de una expresión y de un punto y coma. Escrito en la gramática en forma de
regla seria: (Asig) id asig (Exp) pyc.
Se puede decir que una expresión es bien un identificador y la suma de dos expresiones.
En reglas: (Exp) id
(Exp) (Exp) suma (Exp) El árbol que se puede derivar de estas reglas sería:
NOTA: Tanto en las reglas como en la construcción del árbol, solo se tiene en cuenta su
categoría.
Ejercicio: posición = inicial + velocidad * 60.
3. Analizador semántico
El analizador semántico trata de determinar si el significado de las diferentes instrucciones del
programa es válido.
Para conseguirlo tendrá que calcular y analizar información asociada a las sentencias del
programa, por ejemplo, deberá determinar el tipo de los resultados intermedios de las
expresiones, comprobar que los argumentos de un operador pertenecen al conjunto de los
operandos posibles, comprobar que los operandos son compatibles entre sí, etc.
La salida “teórica” de esta fase es el árbol semántico.
Esta es una ampliación de un árbol sintáctico en el que cada rama del árbol ha adquirido,
además el significado que debe tener el fragmento de programa que representa.
Esta fase del análisis es mas difícil de formalizar que las dos anteriores y se utilizan las
gramáticas atribuidas.
Ejemplo: considerar 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.
VAR ch : CHAR;
ent: INTEGER;
...
ch := ent + 1;
En pascal no es válido, en C si.
Cuando una empresa se dedica a la generación de compiladores, normalmente trabaja
con muchos lenguajes fuentes (m) y muchos lenguajes objetos (n) diferentes.
Para evitar tener que construir m*n compiladores, se utiliza el lenguaje intermedio.
De esta forma solo hay que construir rm programas que traduzcan cada lenguaje
intermedio a cada lenguaje objeto (back ends) .
La utilización del lenguaje intermedio permite construir en menos tiempo compiladores
para nuevos lenguajes y para nuevas maquinas. Desgraciadamente no existe consenso
para utilizar un lenguaje intermedio.
En esta etapa se traduce la entrada a una representación independiente de la máquina
pero fácilmente traducible a lenguaje ensamblador.
En el ejemplo valor= valor+inc; se podría traducir algo parecido a:
Generación de código intermedio
Generación de código objeto
Una vez obtenido el código intermedio, es necesario generar el código objeto.
En esta fase el código intermedio optimizado es traducido a una secuencia de instrucciones en
ensamblador o en código de maquina.
Por ejemplo la sentencia A:= B+C se convertiría en una colección de instrucciones que
podrían representarse así:
LOAD B
ADD C
STORE A
Volviendo al ejemplo anterior valor= valor+inc;
Se tiene en cuenta el tamaño de las variables (por eso se emplea -4 y -8 en lugar de -2 y -1), se utilizan registros de la maquina concreta y se emplean instrucciones especiales como leal para aprovechar mejor el procesador
Optimización de código
La mayoría de los compiladores suelen tener una fase de optimización de código intermedio,
independiente de los lenguajes fuente y objeto, y una fase de optimización de código objeto,
no aplicable a otras máquinas.
Esas fases se añaden al compilador para conseguir que el programa objeto sea mas rápido y
necesite menos memoria para ejecutarse.
1. Eliminar expresiones comunes. Ejemplo:
A:=B+C+D Aux:= B+C E:=(B+C)*F se convierte en A:=Aux + D E:=Aux * F 2. Optimizar los bucles. Se trata de sacar de los bucles aquellas expresiones que sean invariantes REPEAT B:=1 A:=A-B UNTIL A=0 la asignación B:=1 se puede sacar del bucle
Tanto a la hora de generar código intermedio como código objeto es habitual encontrarse
con que el resultado de la traducción es muy ineficiente. Esto es debido a que la
traducción se realiza de manera local, lo cual provoca la aparición de código redundante.
Por ejemplo, la sentencia: a[i]=a[i]+1 genera el siguiente código intermedio:
NOTA: no hace falta calcular dos veces la dirección de a[i], con lo que se pueden ahorrar, al
menos, tres instrucciones. Ejercicio: Escribir el código de menor longitud que se te ocurra
para el ejemplo anterior. Utilizar solo las instrucciones mostradas
Tabla de símbolos
El compilador necesita gestionar la información de los elementos que se van encontrando
en el programa fuente: variables, tipos, funciones, clases, etc..
Esta información se almacena en una estructura de datos interna conocida como tabla de
símbolos.
Para que la compilación sea eficiente, la tabla debe se diseñada cuidadosamente de manera
que tenga toda la información que el compilador necesita.
Además hay que prestar atención especial a la velocidad de acceso a la información con el
objeto de no ralentizar el proceso.
Ejemplos de la información guardada:
Constantes: tipo, valor.
Variables: tipo, dirección en memoria, tamaño.
Funciones: número y tipo de los argumentos, tipo devuelto, dirección.
Es importante tener en cuenta que la información asociada con un identificador puede
variar a lo largo del programa. Por ejemplo:
int main()
{
int i=1;
{ float i=2.0;
printf("%f\n", i); /* i es un float */
}
printf("%d\n", i); /* i es un int */
}
Una cuestión de gran importancia será encontrar una estructura de datos eficiente para
acceder a los elementos de la tabla.
El identificador i se refiere a dos variables distintas
Control de errores
Informar en forma adecuada al programador de los errores que hay en su programa es
una de las misiones mas importantes y complejas de un compilador.
Es una tarea difícil porque a veces unos errores ocultan a otros o porque un error
desencadena una avalancha de errores asociados.
El control de errores de lleva a cabo, sobre todo, en las etapas de análisis sintáctico y
semántico.
Por ejemplo, ante la entrada a:= b 1 ; no es posible saber si falta un + entre la b y el 1 o
si sobra un espacio o si sobra el uno. . .
Mientras que el objetivo de los compiladores es obtener una traducción del programa
fuente a otro lenguaje, los intérpretes tienen como objeto la obtención de los resultados del
programa.
Para ello deben realizar dos tareas:
1) analizar su entrada
2) llevar a cabo las acciones especificadas por ella.
La parte de análisis puede realizarse de manera idéntica a como se lleva a cabo en los
compiladores.
Es la parte de síntesis es la que se diferencia sustancialmente. En el caso de la
interpretación, se parte del árbol de sintaxis abstracta y se recorre, junto con los datos de
entrada, para obtener los resultados.
Intérpretes
valor=valor+inc; asignación
Id valor id suma
idvalor idinc
El recorrido será:
1) Analizar el nodo asignación.
2) Visitar su hijo derecho, la suma, para obtener el valor que hay que asignar:
• Visitar el hijo izquierdo de la suma, recuperar el valor actual de valor.
• Visitar el hijo derecho de la suma, recuperar el valor actual de inc.
• Hacer la suma.
3) Guardar el resultado de la suma en valor.
Arquitectura real de
compiladores e intérpretes
top related