Sei sulla pagina 1di 13

Proceso de traducción 7

1.3 PROCESO DE TRADUCCIÓN


Un compilador se compone internamente de vanas etapas, o fases, que realizan distintas
operaciones lógicas. Es útil pensar en estas fases como en piezas separadas dentro del com-
pilador, y pueden en realidad escribirse como operaciones codificadas separadamente aun-
que en la práctica a menudo se integren juntas. Las fases de un compilador se ilustran en la
figura 1.1, junto con los tres componentes auxiliares que interactúan con alguna de ellas o

Figura 1.1
fases de un compilador

Código objetivo
CAP. I 1 INTRODUCClON

con todas: la tabla de literales, la tabla de símbolos y el manejador de errores. Aquí descri-
biremos brevemente cada una de las fases, las cuales se estudiwiin con más detalle en los
capítulos siguientes. (Las tablas de literales y de símbolos se analizarán m6s"ampliamente
en la siguiente sección y el manejador de errores en la sección 1 S.)

ANALIZADOR LÉXICO O RASTREADOR (SCANNER)


Esta fase del compilador efectúa la lectura real del programa fuente, el cual general-
mente está en la forma de un flujo de caracteres. El rastreador realiza lo que se cono-
ce como análisis léxico: recolecta secuencias de caracteres en unidades significativas
denominadas tokens, las cuales son como las palabras de un lenguaje natural, como el
inglés. De este modo, se puede imaginar que un rastreador realiza una función similar
al deletreo.
Como ejemplo, considere la siguiente línea de código, que podría ser parte de un
programa en C:

Este código contiene 12 caracteres diferentes de un espacio en blanco pero sólo 8 tokens:

a identificador
I corchete izquierdo
index identificador
1 corchete derecho
- asignación
4 número
+ signo más
2 número

Cada token se compone de uno o más caracteres que se reúnen en una unidad antes de
que ocurra un procesamiento adicional.
Un analizador léxico puede realizar otras funciones junto con la de reconocimiento
de tokens. Por ejemplo, puede introducir identificadores en la tabla de símbolos, y
puede introducir literales en la tabla de literales (las literales incluyen constantes nu-
méricas tales como 3.1415926535 y cadenas de texto entrecomilladas como "iHola,
mundo!").

ANALliADOR SINT~CTICO(PARSER)
El analizador sintáctico recibe el código fuente en la forma de tokens proveniente del
analizador léxico y realiza el análisis sintáctico, que determina la estructura del progra-
ma. Esto es semejante a realizar el análisis gramatical sobre una frase en un lenguaje na-
tural. El análisis sintáctico determina los elementos estructurales del programa y sus re-
laciones. Los resultados del análisis sintáctico por lo regular se representan como un
árbol de análisis gramatical o un árbol sintáctico.
Como ejemplo, consideremos otra vez la línea de código en C que ya habíamos dado.
Repreienta un elemento estructural denominado expresión, la cual es una expresión de
aqignación compuesta de una expresión con subíndice a la izquierda y una expre~ión
aritmética entera a la derecha. Esta estructura se puede representar como un jrbol de
anlílisis gramatical de la forma siguiente:
Proceso de traducción

expresión

expresión de subíndice e.xpresión uditiva

expresión [ expresión 1 upraión + e.rpresión

I
identificador identificador número
I número
a index 4 2

Advierta que los nodos Internos del árbol de análisis gramatical están etiquetados con
los nombres de las estructuras que representan y que las hojas del árbol representan la
secuencia de tokens de la entrada. (Los nombres de las estructuras están escritos en un
tipo de letra diferente para distinguirlos de los tokens.)
Un árbol de análisis gramatical es un auxiliar útil para visualizar la sintaxis de un
programa o de un elemento de programa, pero no es eficaz en su representación de esa
estructura. Los analizadores sintácticos tienden a generar un árbol sintáctico en su lugar,
el cual es una condensación de la información contenida en el árbol de análisis gramati-
cal. (En ocasiones los árboles sintácticos se denominan árboles sintácticos abstractos
porque representan una abstracción adicional de los árboles de análisis gramatical.) Un
árbol sintáctico abstracto para nuestro ejemplo de una expresión de asignación en C es
el siguiente:

erpresión de asignación

/-"expresión de subíndice
\ exprestón aditiva

/ \
identificador identificador
/ \
número número
a index 4 2

A árbol sintáctico muchos de los nodos han desaparecido (incluyendo


los nodos de tokens). Por ejemplo, si sabemos que una expresión es una operación de
subíndice, entonces ya no será necesario mantener los paréntesis cuadrados [ y ] que
representan esta operación en la entrada original.
ANALIiADOR SEM~NTICO
La semántica de un programa es su "significado", en oposición a su sintaxis, o estruc-
tura. La semántica de un programa determina su comportamiento durante el tiempo de
ejecución, pero la mayoría de los lenguajes de programación tienen características que
se pueden determinar antes de la ejecución e incluso no se pueden expresar de manera
adecuada como sintaxis y analizarse mediante el analizador sintáctico. Se hace referencia
a tales características como semántica estática, y el análisis de tal semántica es la tarea
del analizador semántico. (La semántica "dinámica" de un programa, es decir, aquellas
propiedades del programa que solamente se pueden determinar al ejecutarlo, no se
pueden determinar mediante un compilador porque éste no ejecuta el programa.) Las ca-
racterísticas típicas de la semántica estática en los lenguajes de programación comunes
iicluyen las declaraciones y la verificación de tipos. Las partes extra de la información
(como los tipos de datos) que se calculan mediante el analizador semántico se llaman
atributos y con frecuencia se agregan al árbol como anotaciones o "decoraciones". (Los
atributos también se pueden introducir en la tabla de símbolos.)
En nuestro ejemplo de ejecución de la expresión en C

la información de tipo específica que se tendría que obtener antes del análisis de es-
ta línea sería que a sea un arreglo de valores enteros con subíndices proveniente de un
subintervalo de los enteros y que index sea una vaiabie entera. Entonces el 'analizador
semántico registrda el árbol sintác6co con los tipos de todas las subexpresiones y pos-
teriormente verificaría que la asignación tuviera sentido para estos tipos, y declararía un
error de correspondencia de tipo si no fuera así. En nuestro ejemplo todos los tipos
tienen sentido, y el resultado del análisis semántico en el árbol sintáctico podría repre-
sentarse por el siguiente árhol con anotaciones:

sentencia de risignacidri

expreskjn de subíndice r-rl>rt.sidn <iditivu


entero entero

i d e n t if i c a d o r i d e n t i f icador número número


a index 4 2
arreglo de enteros entero entero entero

OPTlMlZADOR DE CÓDIGO FUENTE


Los compiladores a menudo incluyen varias etapas para el mejoramiento, u optimi-
zación, del código. El punto más anticipado en el que la mayoría de las etapas de op-
timización se pueden realizar es precisamente después del análisis semántico, y puede
haber posibilidades para el mejoramiento del código que dependerán sólo del código
fuente. Indicamos esta posibilidad al proporcionar esta operación como una fase por
separado en el de compilación. Los compiladores individuales muestran una
amplia variación no sólo en los tipos de optimizaciones realizadas sino también en la
colocación de las fases de optimización.
En nuestro ejemplo incluimos una oportunidad para la optimización a nivel de fuen-
te; a saber, la expresión 4 + 2 se puede calcular previamente por el compilador para
ciar como resultado 6. (Esta optimización particular es conocida como incorporación
de constantes.) Claro que existen posibilidades mucho niás con>plejas(algunas de las
cuales se mencionarán en el capítulo 8). En nuestro ejemplo esta optimizaci6n se puede
realizar de manera directa sobre el árbol sintáctico (con anotaciones) al colapsar el
subárbol secundario de la derecha del nodo raíz a su valor constante:
Proceso de traducción

entero 6
entem

identificador iden tif icador


a index
arreglo de enteros entero

Muchas optimizaciones se pueden efectuar directamente sobre el árhol. pero en varios


casos es irás I'ácil optiniizar una forma linealizada del irbol que esté más cercana al
código ensamblador. Existen muchas variedades diferentes de tal código, pero una
elecciGn estándar es el código en tres direcciones, denominado así porque contiéne
las direcciones de (y hasta) tres localidbdes en memoria. Otra selección popular es el
código P. el cual se ha utilizado en muchos compiladores de Pascal.
En nuestro ejemplo el código en tres direcciones para la expresión original en C
podría parecerse a esto:

t = 4 + 2
a [index] = t

(Advierta el uso de una variable temporal adicional t para almacenar el resulrado inter-
medio de la suma.) Ahora el optimizador mejoraría este código en dos etapas, en primer
lugar calculando el resultado de la suma

y después reemplazando a t por su valor para obtener la sentencia en tres direcciones

En la figura 1.1 indicamos la posibilidad de que el optimizador del código fuente


pueda emplear código en tres direcciones al referirnos a su salida como a un código
intermedio. Históricamente, el código intermedio ha hecho referencia a una forma de
representación de código intermedia entre el código fuente y el código objeto, tal como
el código de tres direcciones o una representación lineal semejante. Sin embargo, tam-
bién puede referirse de manera más general a crcalquier representación interna para el
código fuente utilizado por el compilador. En este sentido, también se puede hacer re-
ferencia al árbol sintáctico como un código intermedio. y efectivamente el optimizador
del código fiieiite podría continuar el uso de esta representación en su salida. En oca-
siones este sentido más general se indica al hacer referencia al código intermedio como
representación intermedia, o RI.
GENERADOR DE CODIGO
El generador de código toma el código intermedio o R I y genera el código par3 la niá-
quina objetivo. En este texto escribiremos el c6digo objetivo en la fornia de lenguaje
ensarnh1;idor para facilitar su coinprerisión, aunque la mayoría tle los conipiladores
generan el código objeto de manera directa. Es en esta fase de la compilación en la que
las propiedades de la máquina objetivo se convierten en el factor principal. No sólo es
necesario emplear instrucciones que existan en la máquina objetivo, sino que las deci-
siones respecto a la representación de los datos desempeñarán ahora también un papel
principal, tal como cuántos bytes o palabras de memoria ocuparán las variables de tipos
de datos enteros y de punto flotante.
En iruestro ejemplo debemos decidir ahora cuántos enteros se almacenarán para
generar el código para la indización del arreglo. Por ejemplo, una posible secuencia
de código muestra para la expresión dada podría ser (en un hipotético lenguaje en-
samblado~)

MOV RO, index ;; valor de index - > RO


MUL RO, 2 ;; doble valor en RO
MOV R1, &a ;; dirección de a - > R1
ADD R1, RO ;; sumar RO a R1
MOV *R1, 6 ;; constante 6 - > dirección en R1

En este código utilizamos una convención propia de C para direccionar modos, de


manera que &a es la dirección de a (es decir, la dirección base del arreglo) y que *R1
significa direccionamiento indirecto de registro (de modo que la última instrucción al-
macena el valor 6 en la dirección contenida en Rl). En este código también partimos del
supuesto de que la máquina realiza direccionamiento de byte y que los enteros ocupan
dos bytes de memoria (de aquí el uso del 2 como el factor de multiplicación en la se-
gunda instrucción).
OPTlMlZADOR DE C~DIGOOBJETIVO
En esta fase el compilador intenta mejorar el código objetivo generado por el generador
de código. Dichas mejoras incluyen la selección de modos de direccionamiento para
mejorar el rendimiento, reemplazando las instrucciones lentas por otras rápidas, y elimi-
nando las operaciones redundantes o innecesarias.
En el código objetivo de muestra dado es posible hacer varias mejoras. Una de ellas
es utilizar una instrucción de desplazamiento para reemplazar la multiplicación en la
segunda instrucción (la cual por lo regular es costosa en términos del tiempo de eje-
cución). Otra es emplear un modo de direccionamiento más poderoso, tal como el di-
reccionamiento indizado para realizar el almacenamiento en el arreglo. Con estas dos
optimizaciones nuestro código objetivo se convierte en

MOV RO, index ;; valor de index - > RO


SHL RO ;Í doble valor en RO
MOV &a[ROl, 6 ;; constante 6 - > dirección a + RO

Esto completa nuestra breve descripción de las fases de un compilador. Querernos


enfatizar que esta descripción sólo es esquemática y no necesariamente representa la or-
ganización real de un compilador trabajando. En su Lugar, los compiladores muestran
una amplia variación en sus detalles de organización. No obstante, las fases que descri-
bimos están presentes de alguna forma en casi todos los compiladores.
También analizamos sólo de manera superficial las estructuras de datos necesarias
para mantener la información necesaria en cada fase, tales como el árbol sintáctico, el
código intermedio (suponiendo que éstos no sean iguales), la tabla de literales y la tabla
de símbolos. Dedicamos la siguiente sección a una breve perspectiva general de las es-
tructuras de datos principales cn u n compilador.
CAP. l I INTRODUCCI~N

MANEJO DE ERRORES
Una de las funciones más importantes de un compitador es su respuesta a los errores
en un programa fuente. Los errores pueden ser detectados durante casi cualquier fase
de la compilación. Estos errores estáticos (o de tiempo de compilac'ión) deben ser
notificados por un compilador, y es importante que el compilador sea capaz de generar
mensajes de error significativos y reanudar la compilación después de cada error. C d a
fase de un compilador necesitará una clase ligeramente diferente de manejo de erro-
res. y, por lo tanto, un manejador de errores debe contener operaciones diferentes,
cada una apropiada para una fase y situación específica. Por consiguiente, las técnicas
de manejo de errores para cada fase se estudiarán de manera separada en el capítulo
apropiado.
Una definición de lenguaje por lo general requerirá no solamente que los errores
estrlticos sean detectados por un compilador, sino también ciertos errores de ejecución.
Esto requiere que un compilador genere código extra, el cual realizará pruebas de
ejecución apropiadas para garantizar que todos esos errores provocarán un evento apro-
piado durante la ejecución. El más simple de tales eventos será detener la ejecución del
programa. Sin embargo, a menudo esto no es adecuado, y una definición de lenguaje
puede requerir la presencia de mecanismos para el manejo de excepciones. Éstos pueden
complicar sustancialmente la administración de un sistema de ejecución, especialmente
si un programa puede continuar ejecutándose desde el punto donde ocumó el error. No
consideraremos la implementación de un mecanismo así, pero mostraremos c h o un
compilador puede generar código de prueba para asegurar qué errores de ejecución es-
pecificados ocasionarán que se detenga la ejecución.

1.6 ARRANQUE AUTOMATICO Y PORTABILIDAD


Hemos analizado el lenguaje fuente y el lenguaje objetivo como factores determinantes en
la estructura de un compilador y la utilidad de separar cuestiones de lenguaje fuente y obje-
tivo en etapas inicial y final. Pero no hemos mencionado el tercer lenguaje involucrado en
el proceso de construcción de compiladores: el lenguaje en el que el compilador mismo es-
tá escrito. Para que el compilador se ejecute inmediatamente, este lenguaje de impleinenta-
ción (o lenguaje anfitrión) tendría que ser lenguaje de máquina. Así fue en realidad como
se escribieron los primeros compiladores, puesto que esencialmente no existían compi-
Iadores todavía. Un enfoque más razonable en la actualidad es escribir el compilador en otro
lenguaje para el cual ya exista un compilador. Si el compilador existente ya se ejecuta en la
máquina objetivo, entonces solamente necesitamos compilar el nuevo coinpilador utilizan-
do el compilador existente para obtener un programa ejecutable:

Cornpilador para lenguaje A Cornpilador en ejecución


escrito en lenguaje B para lenguaje A

Si el compilador existente para el lenguaje B se ejecuta en una máquina diferente de la in5-


quina objetivo, entonces la situación es un poco inás complicada. La compilación produ-
ce entonces un compilador cruzado, es decir, un compilador que genera código objetivo
para tina iniquina diferente de aquella en la que puede ejccutarse. Ésta y otras situaciones
niis coutplejas se describen mejor al esquematizar un conipiliidor como un diagrama T
(Ilanriido así ticbido a su fortna). Un compilador escrito en el lengwje H (por 1~111,qiccqr
hosi
Arranque automático y portabilidad 19

o anfitrión) que traduce lenguaje S (de source o fuente) en lenguaje T (por I<iri,qlcigr Tcit.,y~t
u objetivo) se dibuja como el siguiente diagrama T:

Advierta que esto es equivalente a decir que el compilador se ejecuta en la "ináquina" H (si
H no es código de ináquina, entonces la consideraremos como ccídigo ejecutable para una
máquina hipotética). Típicamente, esperamos que H sea lo mismo que T (es decir, el com-
pibador produce código para la misma máquina que aquella en la que se ejecuta), pero no es
necesario que éste sea el caso.
Los diagrainas T se pueden combinar en dos maneras. Primero, si tenemos dos com-
piladores que se ejecutan en la misma mhquina H, uno de los curtles traduce lenguaje A al
lenguaje B mientras que el otro traduce el lenguaje B al lenguaje C, entonces podemos coin-
binarlos dejando que la salida del primero sea la entrada al segundo. El resultado es un
compilador de A a C en la máquina H. Expresamos lo anterior como sigue:

En segundo lugar podemos utilizar un compilador de la "máquina" ti a la "tnáquina" K para


traducir el lenguaje de irnplementación de otro compilador de H a K. Expresamos esto últiino
como sigue:

Ahora el primer escenario que describimos anteriormente, es decir, utilizando un coin-


pilador existente para el lenguaje B en la máquina H para traducir un compilador de lengua-
je A a H escrito en B, puede verse como el siguiente diagrama, el cual es precisamente un
caso especial del diagrama anterior:
CAP. I I INTRODUCCI~N

El segundo escenario que describimos (donde el compilador de lenguaje B se ejecuta en


una máquina diferente, lo cual resulta en un compilador cruzado para A) puede describirse
de manera similar como sigue:

Es común escribir un compilador en el mismo lenguaje que está por compilarse:

Mientras que parece ser un enredo de circularidad (puesto que, si todavía no existe compila-
dor para el lenguaje fuente, el compilador mismo no puede ser compilado) existen ventajas
importantes que se obtienen de este enfoque.
Considere, por ejemplo, cómo podemos enfocar el problema de la circularidad. Podemos
escribir un compilador "rápido e impreciso" en lenguaje ensamblador, traduciendo d a -
mente aquellas características de lenguaje que en realidad sean utilizadas en el compilador
(teniendo, naturalmente, limitado nuestro uso de esas características cuando escribamos el
compilador "bueno"). Este compilador "rápido e impreciso" también puede producir códi-
go muy ineficiente (¡solamente necesita ser correcto!). Una vez que tenemos el compilador
"rápido e impreciso" en ejecución, lo utilizamos para compilar el compilador "bueno".
Entonces volvemos a compilar el compilador "bueno" para producir la versión final. Este
proceso se denomina arranque automático por transferencia. Este proceso se ilustra en
las figuras 1 . 2 y~ 1.26.
Después del arranque automático tenemos un compilador tanto en código fuente como
en código ejecutable. La ventaja de esto es que cualquier mejoramiento al código fuente del

Figura 12a
Primera etapa en un proceso
de arranque automático

Compilador es&o en su
propio lenguaje A
, / ~ompilado;en ejecucibn
pero ineficiente
Compilador "rápido e impreciso"
escrito en lenguaje de máquina
Lenguaje y compilador de muestra TN
lY

Fiqura 1.2b
segunda etapa en un
proceso de arranque
automático

Compilador escrito en su Versión final


propio lenguaje A del compilador
~om~ilador-enejecución pero ineficiente
(de la primera etapa)

compilador puede ser transferido inmediatamente a un coinpilador que esté trabajando. apli-
cando el mismo proceso de dos pasos anterior.
Pero existe otra ventaja. Transportar ahora e1 compilador a una nueva computadora
anfitrión solamente requiere que la etapa final del código fuente vuelva a cscribirse para
generar código para la nueva máquina. Éste se compila entonces utilizando el compilador
antiguo para producir un compilador cniz.ado, y el compilador es nuevamente recompilado
mediante el compilador cruzado para producir una versión de trabajo para la nueva máquina.
Esto se ilustra en las figuras 1 . 3 ~y 1.3h.

Fioura 1.3a
Transportación de un
compilador escrito en su
propio lenguaje fuente
(paso 1)

Cornpilador original

figura 13b
A K
Transportación de un - --- -
campilador escrito en su *-
propio lenguaje fuente
(paso 4
Código fuente del cornptlador Compilador redirigido
redirtgido a K
Compilador cruzado
Ejercicios 27

cual llamaremos C-Minus. Contiene enteros, arreglos de enteros y funciones (incluyendo


procedimientos, o funciones sin tipo). Tiene declaraciones (estáticas) locales y globales y
funciones recursivas (simples). 'Tiene una sentencia "if' y una sentencia "while". Carece
de casi todo lo demás. Un programa se compone de una secuencia de declaraciones de va-
riables y funciones. Una función main se debe declarar al último. La ejecución comienza
con una llamada a rnain.'
Como un ejemplo de un programa en C-Minus, en la figura 1.Sescribimos el programa
factorial de la figura 1.4 usando una función recursiva. La entradalsalida en este progra-
ma es proporcionada por una función read y una función write que se pueden definir en
términos de las funciones estándar de C scanf y grintf.
C-Minus es un lenguaje más complejo que TINY, particularmente en sus requerimien-
tos de generación de código, pero la máquina TM todavía es un objetivo razonable para su
compilador. En el apéndice A proporcionamos una guía sobre cómo modificar y extender
el compilador TINY a C-Minus.

Figura 1.5 int fact( int x )


Un programa de C-Minur que / * función factorial recursiva * /
da a la salida el factarial de { if (X > 1)

su entrada return x * fact(x-1);


else
return 1;
1

void main( void )


{ int x;
x = read() ;
if ( x > O ) write( fact(x) );
1

EJERCICIOS 1. I Seleccione un compilahr conocido que venga empacado con un ambiente de desarrollo, y haga
una lista de todos los programas acompaiíantes que se encuentran disponibles con el compilador
junto con una breve descripción de sus funciones.
1.2 Dada la asignación en C

dibuje un irbol de análisis gramatical y un árbol sintáctico para la expresión utilizando como
guía el ejemplo semejante de la sección 1.3.
1.3 Los emres de compilación pueden dividirse aproximadamente en dos categorías: errores sin-
tácticos y errores semánticos. Los errores sintácticos incluyen tokens olvidados o colocados de

2. Para que sea consistente con otras funciones en C-Miiius, main es declarüda como una función
void ("sin tipo") con una lista de parámetros void. Mientras qne esto difiere del C ANSI, muchos
compiladores de C aceptaran csta anotación.
CAP. I I INTRODUCCI~N

manera incorrecta, tal como el paténtesis derecho olvidado en la expresión aritmética (2+3 .
Los errores semánticos incluyen tipos incorrectos en expresiones y variables no declaradas (en
la mayoría de los lenguajes), tal como la asignación x ;. 2, donde x es una variable de tipo
arreglo.
a. Proporcione dos ejemplos más de errores de cada clase en un lenguaje de su elección.
b. Seleccione un compilador con el que esté familiarizado y determine si se enumeran todos
los errores sintácticos antes de los ermres semibticos o si los errores de sintaxis y de
semántica están entremezclados. ¿Cómo influye esto en el número de pasadas'?
1.4 Esta pregunta supone que usted trabaja cowun compillidor que tiene una opción para producir
salida en lenguaje ensamblador.
a. Determine si su compilador realiza optiiiiizaciones de incorporación de constantes.
b. Una optimización relacionada pero ntás avanzada es la de propagación de constantes: una
variable que actualmente tiene un valor constante es reemplazada por ese valor en las ex-
presiones. Por ejemplo, el código (en sintaxis de C)

se reemplazaría, utilizando propagación de constantes (e incorporación de constantes), por


el código

Determine si su compilador efectuó proptación de constantes.


c. Proporcione tantas razones como pueda de por qué la propagación de constantes es más
difícil que la incorporación de constantes.
d. Una situación relacionada con la propagación de constantes y la incorporación de constantes
es el uso de las constantes nombradas en un programa. Utilizando una constante nombrada
x en vez de una variable podemos traducir el ejemplo anterior como el siguiente código
en C:

const int x = 4;
e..

y = x + 2 ;

Determine si su compilador realiza propagaciÓn/incorporación bajo estas circunstancias.


¿En qué difiere esto del inciso b?
1.5 Si su compilador aceptara la entrada directamente desde el teclado, determine si dicho compi-
lador lee el programa entero antes de generar mensajes de error o bien genera mensajes de
error a medida que los encuentra. ¿Qué implicación tiene hacer esto en el número de pasadas?
1.6 Describa las tareas que realizan los programas siguientes y explique en qué se parecen o se re-
lacionan con los compiladores:
a. Un preprocesador de lenguaje b. Un módulo de impresión c. Un formateador de texto
1.7 Suponga que tiene un traductor de Pascal a C escrito en C y un compilador trabajando para C.
Utilice diagramas T para describir los pasos que tornaría para crear un compilador trabajando
pira Pascal.
1.8 Utilizamos una flecha * para indicar la reducción de un patrón de dos diagramas T a un solo
diagrama T. Podemos considerar esta flecha como una "relación de reduccih" y formar su ce-
rradura transitiva **. en la cual permitimos que tenga lugar una secuencia de redncciones.
Notar y referencias 29

Dado el siguiente diagrama, en el que las letras establecen lenguajes arbitrarios, determine
cuáles lenguajes deben ser iguales para que La reducción sea válida, y muestre los pasos de la
reducción simple que la hacen válida:

Proporcione un ejemplo prktico de la reducción que describe este diagrama.


1.9 Una alternativa al método de transportar un compilador descrito en la sección 1.6 y en la figura
1.3 es utilizar un intérprete para el código intermedio producido por el compilador y suprimir
una etapa final del todo. Un método así es utilizado por el sistema P de Pascal, el cual inclu-
ye un compilador de Pascal que produce códiga P, una clase de código ensamblador para una
miquina de pila "genérica" y un intérprete de código P que,simula la ejecución del código P.
Tanto el compilador de Pascal como el intérprete de código P están escritos en código P.
a. Describa los pasos necesarios para obtener un compilador trabajando para Pascal en una
máquina arbitra& dado un sistema P de Pascal.
b. Describa los pasos necesarios para obtener un compilador trabajando en el cúdigo nativo
de su sistema, en el inciso a (es decir, un compilador que produce código ejecutable para
la máquina anfitrión, en vez de utilizar el intérprete de código P).
1.10 El proceso de transportar un compilador puede ser considerado como dos operaciones distintas:
reasignaeión del objetivo (la modificación del compilador para producir código objetivo para
una nueva máquina) y reasignación del anfitrión (la modificación del compilador para ejecu-
tarse en una nueva miquina). Analice las diferencias de estas dos operaciones en términos de
diagramas T.

Potrebbero piacerti anche