22
Intérpretes y Compiladores Conceptos básicos Fundamentos de Computación II

Intérpretes y Compiladores

  • Upload
    others

  • View
    8

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Intérpretes y Compiladores

Intérpretes y Compiladores

Conceptos básicos

Fundamentos de Computación II

Page 2: Intérpretes y Compiladores

¿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.

Page 3: Intérpretes y Compiladores

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.

Page 4: Intérpretes y Compiladores

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.

Page 5: Intérpretes y Compiladores

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.

Page 6: Intérpretes y Compiladores

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

Page 7: Intérpretes y Compiladores

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:

Page 8: Intérpretes y Compiladores

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.

Page 9: Intérpretes y Compiladores

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.

Page 10: Intérpretes y Compiladores

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.

Page 11: Intérpretes y Compiladores

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

Page 12: Intérpretes y Compiladores

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

Page 13: Intérpretes y Compiladores

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

Page 14: Intérpretes y Compiladores

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

Page 15: Intérpretes y Compiladores

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.

Page 16: Intérpretes y Compiladores

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

Page 17: Intérpretes y Compiladores

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. . .

Page 18: Intérpretes y Compiladores
Page 19: Intérpretes y Compiladores

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

Page 20: Intérpretes y Compiladores

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.

Page 21: Intérpretes y Compiladores
Page 22: Intérpretes y Compiladores

Arquitectura real de

compiladores e intérpretes