Você está na página 1de 10

INTRODUCIÓN

Los lenguajes de programación son anotaciones para describir cálculos a personas y máquinas. El
mundo tal como lo conocemos depende de los lenguajes de programación, porque todo el software
que se ejecuta en todas las computadoras fue escrito en algún lenguaje de programación. Pero,
antes de que un programa pueda ejecutarse, primero debe traducirse a una forma en la que pueda
ser ejecutado por una computadora.

Los sistemas de software que hacen esta traducción se llaman compiladores.

Este libro trata sobre cómo diseñar e implementar compiladores. Descubriremos que se pueden
usar algunas ideas básicas para construir traductores para una amplia variedad de lenguajes y
máquinas. Además de los compiladores, los principios y las técnicas para el diseño del compilador
son aplicables a tantos otros dominios que es probable que se reutilicen muchas veces en la carrera
de un informático. El estudio de la escritura del compilador toca lenguajes de programación,
arquitectura de máquinas, teoría del lenguaje, algoritmos e ingeniería de software.

En este capítulo preliminar, presentamos las diferentes formas de traductores de idiomas,


brindamos una visión general de alto nivel de la estructura de un compilador típico y discutimos las
tendencias en lenguajes de programación y arquitectura de máquinas que están dando forma a los
compiladores. Incluimos algunas observaciones sobre la relación entre el diseño del compilador y la
teoría de la informática y un resumen de las aplicaciones de la tecnología del compilador que van
más allá de la compilación. Terminamos con un breve resumen de los conceptos clave del lenguaje
de programación que serán necesarios para nuestro estudio de los compiladores.

Procesadores de lenguaje

En pocas palabras, un compilador es un programa que puede leer un programa en un solo idioma |
el idioma fuente | y traducirlo a un programa equivalente en otro idioma | el idioma de destino; ver
Fig. 1.1. Una función importante del compilador es informar cualquier error en el programa fuente
que detecte durante el proceso de traducción.
Si el programa de destino es un programa ejecutable en lenguaje máquina, el usuario puede llamarlo
para procesar entradas y producir salidas; ver Fig. 1.2.

Un intérprete es otro tipo común de procesador de lenguaje. En lugar de producir un programa de


destino como traducción, un intérprete parece ejecutar directamente las operaciones especificadas
en el programa fuente en las entradas proporcionadas por el usuario, como se muestra en la Fig.
1.3.

El programa de destino en lenguaje máquina producido por un compilador suele ser mucho más
rápido que un intérprete en la asignación de entradas a salidas. Sin embargo, un intérprete
generalmente puede ofrecer mejores diagnósticos de error que un compilador, ya que ejecuta la
declaración del programa fuente por declaración.

Ejemplo 1.1: Los procesadores de lenguaje Java combinan compilación e interpretación, como se
muestra en la Fig. 1.4. Un programa fuente de Java puede primero compilarse en una forma
intermedia llamada bytecodes. Los bytecodes son interpretados por una máquina virtual. Una
ventaja de esta disposición es que los códigos de bytes compilados en una máquina pueden
interpretarse en otra máquina, tal vez a través de una red.

Para lograr un procesamiento más rápido de entradas a salidas, algunos compiladores de Java,
llamados compiladores justo a tiempo, traducen los códigos de bytes al lenguaje de máquina
inmediatamente antes de ejecutar el programa intermedio para procesar la entrada. 2
Además de un compilador, se pueden requerir varios otros programas para crear un programa
objetivo ejecutable, como se muestra en la Fig. 1.5. Un programa fuente puede dividirse en módulos
almacenados en archivos separados. La tarea de recopilar el programa fuente a veces se confía a un
programa separado, llamado preprocesador. El preprocesador también puede expandir las
abreviaturas, llamadas macros, en declaraciones de lenguaje fuente.

El programa fuente modificado se alimenta a un compilador. El compilador puede producir un


programa en lenguaje ensamblador como salida, porque el lenguaje de ensamblaje es más fácil de
producir como salida y es más fácil de depurar. El lenguaje ensamblador luego es procesado por un
programa llamado ensamblador que produce código de máquina reubicable como su salida.

Los programas grandes a menudo se compilan en partes, por lo que el código de la máquina
reubicable puede tener que vincularse junto con otros objetos de objetos reubicables y archivos de
biblioteca en el código que realmente se ejecuta en la máquina. El enlazador resuelve direcciones
de memoria externa, donde el código en un archivo puede referirse a una ubicación en otro archivo.
El cargador reúne todos los objetos ejecutables en la memoria para su ejecución.

1.1.1 Ejercicios para la Sección 1.1

Ejercicio 1.1.1: ¿Cuál es la diferencia entre un compilador y un intérprete?

Ejercicio 1.1.2: ¿Cuáles son las ventajas de (a) un compilador sobre un intérprete

(b) un intérprete sobre un compilador?


Ejercicio 1.1.3: ¿Qué ventajas tiene un sistema de procesamiento de lenguaje en el que el
compilador produce lenguaje ensamblador en lugar de lenguaje de máquina?

Ejercicio 1.1.4: Un compilador que traduce un lenguaje de alto nivel a otro lenguaje de alto nivel se
llama traductor de fuente a fuente. ¿Qué ventajas tiene usar C como lenguaje de destino para un
compilador?

Ejercicio 1.1.5: Describa algunas de las tareas que un ensamblador necesita realizar.

1.2 La estructura de un compilador

Hasta este punto, hemos tratado a un compilador como un cuadro único que mapea un programa
fuente en un programa objetivo semánticamente equivalente. Si abrimos un poco este cuadro,
vemos que hay dos partes en este mapeo: análisis y síntesis.
La parte de análisis divide el programa fuente en partes constitutivas e impone una estructura
gramatical sobre ellas. Luego usa esta estructura para crear una representación intermedia del
programa fuente. Si la parte de análisis detecta que el programa fuente está sintácticamente mal
formado o semánticamente defectuoso, debe proporcionar mensajes informativos para que el
usuario pueda tomar medidas correctivas. La parte de análisis también recopila información sobre
el programa fuente y la almacena en una estructura de datos llamada tabla de símbolos, que se pasa
junto con la representación intermedia a la parte de síntesis.

La parte de síntesis construye el programa objetivo deseado a partir de la representación intermedia


y la información en la tabla de símbolos. La parte de análisis a menudo se llama el front end del
compilador; la parte de síntesis es el back end. Si examinamos el proceso de compilación con más
detalle, vemos que funciona como una secuencia de fases, cada una de las cuales transforma una
representación del programa fuente en otra. Una descomposición típica de un compilador en fases
se muestra en la figura 1.6. En la práctica, se pueden agrupar varias fases, y las representaciones
intermedias entre las fases agrupadas no necesitan construirse explícitamente. La tabla de símbolos,
que almacena información sobre el
programa fuente completo, es utilizado por todas las fases del compilador.

Algunos compiladores tienen una fase de optimización independiente de la máquina entre el


extremo frontal y el extremo posterior. El propósito de esta fase de optimización es realizar
transformaciones en la representación intermedia, de modo que el back-end pueda producir un
mejor programa objetivo que el que de otro modo habría producido a partir de una representación
intermedia no optimizada. Como la optimización es opcional, puede faltar una u otra de las dos fases
de optimización que se muestran en la Fig. 1.6.

1.2.1 Análisis léxico

La primera fase de un compilador se llama análisis o escaneo léxico. El analizador léxico lee el flujo
de caracteres que componen el programa fuente y agrupa los caracteres en secuencias significativas
llamadas lexemas Para cada lexema, el analizador léxico produce como salida un token de la forma

< token name attribute value >

que pasa a la fase posterior, análisis de sintaxis. En el token, el nombre del token del primer
componente es un símbolo abstracto que se utiliza durante el análisis de sintaxis, y el valor del
atributo del segundo componente apunta a una entrada en la tabla de símbolos para este token. La
información de la entrada de la tabla de símbolos es necesaria para el análisis semántico y la
generación de código.

Por ejemplo, suponga que un programa fuente contiene la instrucción de asignación

position initial rate 60 (1.1)

Los caracteres en esta asignación podrían agruparse en los siguientes lexemas y mapearse en los
siguientes tokens pasados al analizador de sintaxis:

1. position es un lexema que se mapearía en un token <id 1>, donde id es un símbolo abstracto
que representa el identificador y 1 puntos a la entrada de la tabla de símbolos para la
posición La entrada de la tabla de símbolos para un identificador contiene información
sobre el identificador, como su nombre y tipo.
2. El símbolo de asignación es un lexema que se asigna al token < > Dado que este token no
necesita ningún valor de atributo, hemos omitido el segundo componente. Podríamos
haber usado cualquier símbolo abstracto como asignar el nombre del token, pero por
conveniencia de notación, hemos elegido usar el propio lexema como el nombre del símbolo
abstracto.
3. initial es un lexema que se mapea en el token <id 2>, donde 2 puntos a la entrada de la tabla
de símbolos para inicial
4. es un lexema que se mapea en el token < >
5. rate es un lexema que se asigna al token <id 3>, donde 3 puntos a la entrada de la tabla de
símbolos para la tasa
6. es un lexema que está mapeado en el token hola
7. 60 es un lexema que se asigna en el token <60> 1*

Los espacios en blanco que separan los lexemas serían descartados por el analizador léxico. La figura
1.7 muestra la representación de la declaración de asignación (1.1) después del análisis léxico como
la secuencia de tokens

<id 1> < > <id 2> < > <id 3> < > <60> (1.2)

En esta representación, los nombres de token =, + y son símbolos abstractos para los operadores de
asignación, suma y multiplicación, respectivamente.

1* Técnicamente hablando, para el lexema 60 deberíamos hacer una ficha como <number 4> donde
4 puntos a la tabla de símbolos para la representación interna del entero 60 pero diferiremos la
discusión de las fichas para los números hasta el Capítulo 2. El Capítulo 3 discute las técnicas para
construir analizadores léxicos.
1.2.2 Análisis de sintaxis

La segunda fase del compilador es el análisis de sintaxis o el análisis. El analizador utiliza los primeros
componentes de los tokens producidos por el analizador léxico para crear una representación
intermedia en forma de árbol que representa la estructura gramatical del flujo de tokens. Una
representación típica es un árbol de sintaxis en el que cada nodo interior representa una operación
y los elementos secundarios del nodo representan los argumentos de la operación. Se muestra un
árbol de sintaxis para el flujo de tokens (1.2)

como la salida del analizador sintáctico en la figura 1.7.

Este árbol muestra el orden en que las operaciones en la tarea

Position initial rate 60

deben ser realizados El árbol tiene un nodo interior etiquetado con <id 3> como su hijo izquierdo y
el entero 60 como su hijo derecho. El nodo oculto 3i representa la tasa de identificación. El nodo
etiquetado hace explícito que primero debemos multiplicar el valor de la tasa por 60. El nodo
etiquetado + indica que debemos agregar el resultado de esta multiplicación al valor de la raíz inicial.
El árbol, etiquetado =, indica que debemos almacenar el resultado de esta suma en la ubicación de
la posición del identificador. Este orden de operaciones es consistente con las convenciones usuales
de la aritmética que nos dicen que la multiplicación tiene mayor prioridad que la suma, y por lo
tanto la multiplicación se realizará antes de la suma.

Las fases posteriores del compilador utilizan la estructura gramatical para ayudar a analizar el
programa fuente y generar el programa objetivo. En el Capítulo 4 usaremos gramáticas libres de
contexto para especificar la estructura gramatical de los lenguajes de programación y discutir
algoritmos para construir analizadores de sintaxis ambiental automáticamente a partir de ciertas
clases de gramáticas. En los capítulos 2 y 5 veremos que las definiciones de sintaxis pueden ayudar
a especificar la traducción de construcciones de lenguaje de programación.

1.2.3 Análisis semántico

El analizador semántico utiliza el árbol de sintaxis y la información en la tabla de símbolos para


verificar la coherencia semántica del programa fuente con la definición del lenguaje. También
recopila información de tipo y la guarda en el árbol de sintaxis o en la tabla de símbolos, para su uso
posterior durante la generación de código intermedio.

Una parte importante del análisis semántico es la verificación de tipo, donde el compilador verifica
que cada operador tenga operandos coincidentes. Por ejemplo, muchas definiciones de lenguaje de
programación requieren que un índice de matriz sea un número entero; el compilador debe
informar un error si se usa un número de punto de referencia para indexar una matriz. La
especificación del lenguaje puede permitir algunas conversiones de tipo llamadas coerciones. Por
ejemplo, un operador aritmético binario puede aplicarse a un par de enteros o a un par de números
de punto de avena. Si el operador se aplica a un número de punto flotante y un número entero, el
compilador puede convertir o coaccionar el número entero en un número de punto flotante.
1.2. LA ESTRUCTURA DE UN COMPILADOR

Tal coerción aparece en la figura 1.7. Supongamos que la posición inicial, y

se ha declarado que la tasa son números de punto flotante, y que el lexema 60 por sí mismo forma
un número entero. El verificador de tipo en el analizador semántico de la figura 1.7 descubre que el
operador se aplica a una tasa de número de punto de referencia y un número entero 60. En este
caso, el número entero puede convertirse en un número de punto de referencia. En la Fig. 1.7,
observe que la salida del analizador semántico tiene un nodo adicional para el operador intto oat
que convierte explícitamente su argumento entero en un número de punto flotante. La verificación
de tipos y el análisis semántico se analizan en el Capítulo 6.

Você também pode gostar