Apuntes t8
-
Upload
amado-lopez-aguilar -
Category
Internet
-
view
226 -
download
0
Transcript of Apuntes t8
Enviad sugerencias y/o errores a la dirección de correo [email protected]
Procesadores de lenguajes
U nidad t emática 8:
G eneración de c ódigo i ntermedio
V . 11.1
2
!"#$"%&'%()*+'*,&)%
-.%/0&,1)2%,*+'34'&,)2%......................................................................................................%5!
"#"#!$%&'(!)*!+*&+*(*,-./%',*(!%,-*+0*)%.(!###########################################################################################################!1!
!"#$%&'()*"+#,&-$)................................................................................................................................................................)/!
012"3)45)+&(#$6&+)$2+#1$%#$)............................................................................................................................................)/!
7'4&8")45)#15+)4&15%%&"(5+)9%:$1#5#"+;).....................................................................................................................)<!
7'4&8")45)4"+)4&15%%&"(5+)9#51%5#"+;).........................................................................................................................)=!
"#2#!3,(-+4//%',*(!)*5!/6)%7'!)*!-+*(!)%+*//%',*(!#################################################################################################!8!
"#9#!:*,*+./%6,!)*!/6)%7'!%,-*+0*)%'!0*)%.,-*!4,.!7+.0;-%/.!)*!.-+%<4-'(!#########################################!=!
6.%7(('2)%"%4,'4#3)2%&'%8*"%'2+38(+83"%9%'$'4'*+)2%&'%8*%"33"9%...................................%-:!
2#"#!>//*('!.!0%*0<+'(!)*!4,.!*(-+4/-4+.!###########################################################################################################!"1!
2#2#!>//*('!.!*5*0*,-'(!)*!4,!.++.?!#######################################################################################################################!"@!
>.>.?.)@%%5+")$)3"+)535A5(#"+)45):()$11$B)A:3#&4&A5(+&"($3)5()C$+%$3)...................................................)?<!
>.>.>.)@%%5+")$)3"+)535A5(#"+)45):()$11$B)A:3#&4&A5(+&"($3)5()7)..............................................................)?D!
5.%;<=3'2,)*'2%$01,("2%'%,*2+38((,)*'2%&'%()*+3)$%&'%>$8?)%................................................%-@!
9#"#!A.+%.<5*(!?!*B&+*(%',*(!567%/.(!########################################################################################################################!"C!
9#2#!D*&+*(*,-./%6,!,40E+%/.!###################################################################################################################################!2F!
9#9#!D*&+*(*,-./%6,!&'+!/',-+'5!)*!G54H'!###############################################################################################################!21!
:.%A'>'3'*(,"2%*)%2"+,2>'(B"2.%A'$$'*)%=)3%3'+3)('2)%.......................................................%6C!
1#"#!I5.,-*.0%*,-'!)*5!&+'<5*0.!##############################################################################################################################!2@!
1#2#!D*55*,'!&'+!+*-+'/*('!############################################################################################################################################!28!
1#9!D*&+*(*,-./%6,!,40E+%/.!)*!*B&+*(%',*(!567%/.(!J/',!+*55*,'!&'+!+*-+'/*('K!###########################!2L!
1#1!D*&+*(*,-./%6,!)*!*B&+*(%',*(!567%/.(!&'+!/',-+'5!)*!G54H'!J/',!+*55*,'!&'+!+*-+'/*('K!#######!2=!
/./.?.)7'4&8")5()%"1#"%&1%:&#").....................................................................................................................................)EF!
/./.>.)G$1&$235+)3'8&%$+)B)15*15+5(#$%&'()*"1)%"(#1"3)45),3:-")......................................................................)EF!
/./.E.)H(+#1:%%&"(5+)45)%"(#1"3)45),3:-")...................................................................................................................)E?!
C.%D8#=3)13"4"2%.............................................................................................................%5:!
@#"#!D*7%(-+'(!)*!./-%M./%6,!*,!I.(/.5#!###################################################################################################################!91!
<.?.?.)I5%3$1$%&'().............................................................................................................................................................)E/!
<.?.>.)J3$A$4$)...................................................................................................................................................................)E<!
@#2#!N.+/'(!*,!O!################################################################################################################################################################!98!
<.>.?.)I5%3$1$%&'().............................................................................................................................................................)E=!
<.>.>.)J3$A$4$)...................................................................................................................................................................)E=!
E,#$,)13">F"%3'()4'*&"&"%...............................................................................................%5G!
;?'3(,(,)2%.........................................................................................................................%5H!
D)$8(,)*'2%"%$)2%'?'3(,(,)2%2'$'((,)*"&)2%.........................................................................%:H!
Generación de código intermedio
3
1. Códigos intermedios
Además de analizar el código fuente (fase de análisis) un compilador debe generar código
(fase de síntesis). Esta generación de código se realiza en dos fases. Una primer fase en la que el
“front-ent” del compilador produce como resultado una representación intermedia (“Intermediate
Representation” o IR) del programa fuente y una segunda fase en la que el “back-end” producirá
el código objeto.
La fase de síntesis puede generar directamente como salida un código máquina, pero en ese
caso el compilador será totalmente dependiente de la máquina destino. Además, debido a la gran
disparidad entre el tipo de instrucciones que se emplean en los lenguajes de alto nivel y en los
lenguajes máquina, la construcción del generador de código será larga y el código resultante
difícil de mantener. Otra alternativa consiste en que la fase de síntesis genere código en otro
lenguaje de alto nivel. En este caso se requiere del empleo de otro compilador para la máquina
destino que finalmente genere el código máquina. La construcción del traductor será mucho más
sencilla, pero a cambio el código ejecutable que obtendremos consumirá en general más recursos
(tiempo y memoria).
La utilización de una representación intermedia (IR) aparece como una solución a mitad de
camino entre las dos anteriores. El proceso de generación de código queda dividido en dos etapas
de traducción: una primera fase independiente de la máquina destino en la que se genera una
representación intermedia, y una segunda fase en la que, a partir de la representación intermedia,
se genera el código objeto ejecutable para la máquina destino. En la primera etapa conocida
como generación de código intermedio se genera una representación intermedia del código
fuente que se va a compilar. Esta representación intermedia, independiente de la máquina para la
que se generará código y del lenguaje fuente, será la entrada del módulo generador de código
encargado de realizar la segunda etapa de traducción. Esta tercera opción es la que vamos a
estudiar con más detalle a lo largo de este capítulo.
Un código intermedio es una estructura de código cuya complejidad está entre el código
fuente en el lenguaje de alto nivel y el código máquina. Este código intermedio puede ser
considerado como la interfaz entre el generador de código (“back-end”) y las fases anteriores del
compilador (“front-end”). Su utilización presenta algunas ventajas frente a la generación directa
de un código máquina:
• Se facilita la reutilización de toda la etapa inicial del compilador (“front-end”) para la construcción de compiladores de un mismo lenguaje fuente para distintas máquinas.
(Fig. 2.1), y de toda la etapa final para construcción de compiladores de otros
lenguajes fuente sobre la misma plataforma.
• Se puede aplicar a la representación intermedia un optimizador de código
independiente de la máquina.
Generación de código intermedio
4
• Facilita la división en fases del proyecto: Permite abstraerse de los detalles propios de
la máquina destino en el desarrollo de la etapa inicial del compilador y de los propios
del lenguaje fuente en el desarrollo de la etapa final.
Fortran
Pascal
C
C++
Sparc
PowerPC
x86
ARM
Fortran
Pascal
C
C++
Sparc
PowerPC
x86
ARM
Representación
Intermedia
(a) (b)
Figura 2.1. Compiladores para 4 lenguajes y 4 máquinas sin (a) y con (b) utilización de un código intermedio.
Una buena representación intermedia debe cumplir las siguientes propiedades:
• Debe ser sencilla de generar y manipular.
• Debe ser adecuada para permitir su traducción al código máquina de todas las máquinas-destino deseadas.
• Cada construcción debe tener un significado claro y simple que facilite la
implementación del módulo optimizador.
El código generado por un compilador de Java, conocido como bytecode, puede considerarse
como un ejemplo de código intermedio con la característica peculiar de que que posteriormente
es ejecutado en la máquina virtual.
1.1. Tipos de representaciones intermedias
Hay distintos tipos de representación intermedia. En el capítulo 10 de [Tremblay, 85] puede
encontrarse una explicación más extensa las formas de representación intermedias que se
presentan a continuación.
Notación postfija
Notación sencilla empleada en máquinas de pila donde un operador se coloca a continuación
de sus operandos. Por ejemplo, la instrucción de asignación x := a+b+(c*d) quedaría en notación
postfija como: x ab+cd*+ :=
Este tipo de representación intermedia es poco adecuada para la fase de optimización y es
difícil de emplear para representar estructuras de control de flujo e instrucciones de selección.
Árbol de sintaxis abstracta
Un árbol de sintaxis abstracta (“Abstract Sintax Tree” o AST) no es más que un árbol en el
que un nodo interior representa un operador, y donde sus operandos serán sus descendientes
Generación de código intermedio
5
directos. En la Fig. 2.2 se puede observar como queda un árbol de sintaxis abstracta para la
asignación x:= (a+b*c) / d.
Figura 2.2. Árbol de sintaxis abstracta para la instrucción x:= (a+b*c) / d.
Una variante de los árboles de sintaxis abstracta son los grafos dirigidos acíclicos (GDA ). La
principal diferencia con los árboles de sintaxis abstracta es que en los primeros se permite que un
nodo tenga más de un padre. Este tipo de representación facilita el análisis del código y su
optimización, por lo que es muy empleada en compiladores optimizadores.
Código de tres direcciones (cuartetos)
Representación intermedia en la que las instrucciones constan de un operador y tres
direcciones: dos para los operandos y una tercera para el resultado. Las instrucciones tendrán la
forma genérica
<operador> <op1> <op2> <resultado>
Por ejemplo, la instrucción de asignación x := (a+b*c)/d se podría representar mediante una
secuencia de instrucciones en un código de tres direcciones como
Operador op1 op2 res (1) MULT b c t1
(2) SUMA a t1 t2
(3) DIV t2 d t3
(4) ASIGNA t3 x
Un código de tres direcciones no es más que una representación lineal de un árbol de sintaxis
abstracta o de un GDA en el que se ha empleado una variable temporal para representar los
valores de los nodos interiores.
:=
x /
+ d
a *
b c
Generación de código intermedio
6
Código de dos direcciones (tercetos)
En un código de tres direcciones se suele emplear un gran número de variables auxiliares
(temporales) para almacenar los resultados parciales durante la evaluación de una expresión. Un
código de dos direcciones evita este problema permitiendo que los operandos que aparecen en
una instrucción sean apuntadores a las instrucciones cuyo resultado se desea emplear.
La forma general de una instrucción de dos direcciones es
<op> <operando1> <operando2>
El mismo ejemplo de instrucción de asignación que hemos vistos representada mediante un
código de tres direcciones, x := (a+b*c)/d se podría representar mediante un código de dos
direcciones como
Operador op1 op2 (1) MULT b c
(2) SUMA a (1)
(3) DIV (2) d
(4) ASIGNA (3) x
Donde (1), (2) y (3) representan punteros a las instrucciones de código dos direcciones 1, 2 3
respectivamente.
La principal ventaja del código de dos direcciones frente al de tres es el ahorro de variables
temporales.
1.2. Instrucciones del código de tres direcciones
Para este capítulo hemos escogido como representación intermedia un código de tres
direcciones. Se muestran a continuación los tipos de instrucciones de este código y la forma
general que tendrán. En la mayoría de los casos, los operadores podrán ser constantes,
direcciones de memoria donde se encuentra el valor a usar, o directamente direcciones de
memoria.
Asignación
x := y op z
Donde op es una operador aritmético.
x := op z
Siendo op un operador unario.
x := z
Salto incondicional
Generación de código intermedio
7
goto E
Donde E es la etiqueta (representada por un número) de la instrucción de código
intermedio a la que se saltará.
Salto condicional
if x op y goto E
Siendo op un operador lógico binario. Si se cumple la relación se saltará a la instrucción
etiquetada E.
Pila de activación
push x
Apila x en la pila de activación.
pop
Devuelve el valor que se desapila.
Llamada y retorno de subprograma
call E
Salta a la instrucción etiquetada E.
ret
Finaliza el subprograma y salta a la dirección que se encuentra en la cima de la pila de
activación.
Asignaciones relativas
a [i] := x
x quedará almacenada i posiciones más allá de la dirección base a.
x := a[i]
Asigna a x el valor de la posición que se encuentra i unidades más allá de la posición base
a.
Acceso a la pila de activación
Acceso en las instrucciones de asignación al “frame pointer” (FP) y al tope de la pila (TOP).
Generación de código intermedio
8
Generación de etiquetas
Los destinos de los saltos se indican mediante etiquetas. Al definir el código intermedio
podemos elegir entre dos formas de generar estas etiquetas:
Generación explícita de etiquetas: Cada vez que se desee poner una etiqueta a una
instrucción de código intermedio se debe crear y emitir una etiqueta que tendrá la forma:
<nombre_etiqueta> :
Esta forma es la usada habitualmente por los lenguajes ensambladores. En el ejercicio
8.20 se puede ver un ejemplo de utilización de este tipo de etiquetas.
Generación implícita de etiquetas: Podemos suponer que cada instrucción de código
intermedio generada tendrá implícitamente una etiqueta que se corresponderá con un
número entero. Este número se corresponde con el número de orden de la instrucción
dentro de programa. Esta forma es empleada en algunos lenguajes de alto nivel como
Basic, que comienza cada línea con un número que representa la etiqueta de la
instrucción.
El código intermedio que se emplea en este capítulo utiliza etiquetas implícitas.
1.3. Generación de código intermedio mediante una gramática de atributos
En temas anteriores se ha mostrado la utilización de las gramáticas atribuidas como
formalismo para la especificación semántica. En este tema se seguirán empleando las gramáticas
atribuidas, pero sus acciones generan código intermedio de tres direcciones. Para ello vamos a
emplear un procedimiento llamado emite que recibirá como parámetros una instrucción de
código intermedio y sus argumentos. Este procedimiento se encargará de almacenar
secuencialmente las instrucciones en una estructura intermedia (que podría ser un fichero o un
vector de instrucciones de código intermedio). De esta forma, como resultado colateral de la
evaluación de la gramática, en la estructura intermedia empleada por emite, tendremos toda la
secuencia de instrucciones de código intermedio generadas, las cuales forman el programa en
código intermedio equivalente al programa fuente. Este programa en código intermedio tendrá
todas sus instrucciones etiquetadas con un número entero: el número de instrucción que será
generado automáticamente por el procedimiento emite.
Las expresiones que aparezcan entrecomilladas en la llamada al procedimiento emite, se
tomarán literalmente, mientras que el resto de expresiones se evaluarán antes de generar la
instrucción de código intermedio.
Generación de código intermedio
9
Ejemplo 2.1.-
emite (x ‘:=’ 5 ‘+’ 9)
emite (x ‘:=’ 5 + 9)
La primera llamada al procedimiento emite generará la instrucción de código tres direcciones
correspondiente a la asignación en la posición x del resultado de sumar las constantes 5 y 9 (se
emite una instrucción de suma, es decir, la suma se realizará en tiempo de ejecución). En cambio
la segunda llamada al procedimiento emite generará una instrucción que almacenará en la
posición x el valor 14 (se emite una instrucción de asignación, la suma se ha realizado en tiempo
de compilación).
En las gramáticas atribuidas que construyamos para generar código intermedio utilizaremos
la siguiente notación:
id.nom: Atributo asociado el símbolo léxico identificador que representa su lexema (nombre).
Su valor es devuelto por el analizador léxico. Este nombre nos permitirá buscar la posición en la
tabla de símbolos (TDS) donde se encuentra la información del identificador.
id.pos: Atributo del símbolo léxico que representa la posición de memoria que contendrá
durante la ejecución el valor de la variable representada por el identificador. Se le asigna a la
variable cuando se declara, se almacena en la TDS y puede ser consultado mediante la función
BuscaPos(...). En un lenguaje con estructura de bloques esta posición suele estar representada
por el par (nivel, desplazamiento). Cuando otros símbolos de la gramática tengan un atributo con
el mismo (.pos) nombre tendrán el mismo significado.
id.tipo: Atributo que representará la expresión de tipo asignada al identificador. Está
almacenado en la TDS y se puede consultar mediante la función BuscaTipo(...). Cuando otros
símbolos de la gramática tengan un atributo con el mismo nombre tendrán el mismo significado.
num.val: Atributo asociado al símbolo léxico constante numérica (num) que representa su
valor. Es devuelto por el analizador léxico. Cuando otros símbolos de la gramática tengan un
atributo con el mismo nombre tendrán el mismo significado.
Para simplificar los esquemas de traducción dirigidos por la sintaxis, nos vamos a permitir
emplear en las acciones semánticas variables globales. Estas variables globales podrán ser
accedidas desde cualquier acción semántica, es decir, se corresponderán con variables globales
del código fuente del compilador desarrollado. Para distinguirlas, aparecerán siempre en
mayúsculas. Si en alguna acción semántica aparece una variable en minúsculas se tratará de una
variable local asociada al símbolo no-terminal del lado izquierdo de la producción en la que
aparece.
Generación de código intermedio
10
SIGINST: Variable global del compilador que estamos construyendo que representa el
número de la SIGuiente INSTrucción que se va a generar. Esta variable es inicializada a 0 y el
procedimiento emite se encarga de incrementarla automáticamente cada vez que se emite una
instrucción.
Dada la siguiente gramática que representa una instrucción de asignación de un lenguaje de
alto nivel similar a C
Asig ! id = E
E ! E + E
E ! id
podemos construir un esquema de traducción dirigido por la sintaxis (ETDS) que genere
código de tres direcciones para instrucciones de asignación usando el procedimiento emite:
Asig ! id = E { Asig.pos := BuscaPos ( id.nom ) ; emite ( Asig.pos ‘:=’ E.pos ); }
E ! E1 + E2 { E.pos := CrearVarTemp() ;
emite ( E.pos ‘:=’ E1.pos ‘+’ E2.pos ) }
E ! id { E.pos := BuscaPos (id.nom ) }
Veamos cada una de las acciones semánticas asociadas a estas producciones.
Asig ! id = E { Asig.pos := BuscaPos ( id.nom )
;
emite ( Asig.pos ‘:=’ E.pos ) }
En el ETDS (esquema de traducción dirigido por la sintaxis) anterior se puede observar que
no aparecen acciones semánticas necesarias para realizar las comprobaciones semánticas. Esto es
debido a que este capítulo se centra únicamente en la generación de código intermedio.
Se puede observar como en la primera acción semántica se realiza una llamada a la función
BuscaPos. La función BuscaPos recibe como argumento el lexema (nombre) del identificador,
busca la entrada del identificador en la TDS y devuelve la posición de memoria que se ha
asignado a dicho identificador. Esta posición de memoria en un lenguaje con estructura de
bloques será habitualmente el nivel de anidamiento (o contexto) del identificador y su
desplazamiento relativo.
La siguiente acción semántica es una llamada al procedimiento emite. Éste emitirá una
instrucción en código de tres direcciones que asignará en la posición del identificador id el
contenido de la posición de memoria representada por E.pos. También se puede observar que la
acción semántica devuelve en el atributo sintetizado Asig.pos la posición de memoria donde
Generación de código intermedio
11
estará en tiempo de ejecución el valor de la variable id.nom. Esto se hace porque en C una
instrucción de asignación debe devolver el valor asignado a la variable del lado izquierdo de la
asignación. La utilidad de este atributo sintetizado se puede observar si añadimos a la gramática
anterior la producción
E ! Asig { E.pos :=Asig.pos }
donde se puede observar que una expresión puede ser una asignación.
La siguiente producción del ETDS es:
E ! E1 + E2 { E.pos := CrearVarTemp() ;
emite ( E.pos ‘:=’ E1.pos ‘+’ E2.pos ) }
La primera acción de esta producción realiza una llamada a la función CrearVarTemp(). Está
función devolverá la dirección de la primera posición de memoria libre (no asignada) dentro del
área de datos del bloque que se está compilando. En esa posición de memoria se almacenará el
valor de una variable temporal (de ahí el nombre de la función).
La segunda acción emite una instrucción de código de tres direcciones. Esta instrucción se
almacenará en la estructura intermedia empleada por emite para guardar el código intermedio.
Cuando en tiempo de ejecución se ejecute la instrucción, se asignará a la posición de memoria
representada por E.pos (la posición que devolvió CrearVarTemp) el resultado de sumar el
contenido de las posiciones de memoria representadas por E1.pos y E2.pos. Podemos observar
que E.pos es un atributo sintetizado que toma valor en esta producción mientras que E1.pos y
E2.pos son dos atributos sintetizados cuyo valor se usa en esta producción.
E ! id { E.pos := BuscaPos (id.nom ) }
La única acción semántica asociada a esta producción se encarga de dar al atributo E.pos la
posición de memoria asignada al identificador id. Para ello se realiza un llamada a la función
BuscaPos que recibe como argumento el lexema del identificador (id.nom), lo busca en la tabla
de símbolos y devuelve la posición de memoria que se le adjudicó en la fase de asignación de
memoria.
Ejemplo:
Veamos el comportamiento de este ETDS mediante la construcción del árbol anotado
correspondiente a la cadena a = b + c (Fig. 2.3). Suponemos que en la TDS tenemos la
información asociada a cada identificador que aparece en la Fig. 2.3.
Generación de código intermedio
12
TDS
Nom tipo posición [nivel, desp]
a tentero [0,24]
b tentero [0,26]
c tentero [0,28]
Figura 2.3. Árbol anotado y código intermedio generado para la cadena
a=b+c.
Para que resulte más cómodo de leer el código intermedio, en lugar de poner las posiciones
de memoria, en el resto de este capítulo se utilizará el nombre de la variable a la que
corresponde. En el caso de las variables temporales, su nombre comenzará por la letra minúscula
t seguida de un número entero que indicará el orden en el que ha sido generada: t1, t2, … En la
Fig. 2.3 se puede observar como al llamar a la función CrearVarTemp(), ésta ha devuelto como
posición de memoria la primera posición libre dentro del área de datos del bloque para el que se
estaba generando código (nivel 0, desplazamiento 30). Como ésta es la primera variable temporal
que se crea se le ha dado el nombre t1. De esta forma el código generado anteriormente puede
representarse para mayor claridad como:
(1) t1 := b + c
(2) a := t1
Veamos a continuación como se puede extender el ETDS anterior para generar código
también para otras expresiones aritméticas sencillas.
Instruc
id.pos=[0,24] = E.pos = [0,30]
+ E.pos = [0,28]E.pos = [0,26]
id.pos=[0,26] id.pos=[0,28]
Código tres direcciones generado (1) [0,30] := [0,26] + [0,28]
(2) [0,24] := [0,30]
Generación de código intermedio
13
E ! E1 * E2 { E.pos := CrearVarTemp() ;
emite ( E.pos ‘:=’ E1.pos ‘*’ E2.pos ) }
E ! num { E.pos := CrearVarTemp() ;
emite ( E.pos ‘:=’ num.val ) }
E ! ( E1 ) { E.pos := E1.pos }
E ! - E1 { E.pos := CrearVarTemp() ;
emite ( E.pos ‘:= -‘ E1.pos) }
Las acciones semánticas de la primera producción son semejantes a las que aparecían en la
producción que representaba la suma de expresiones, pero en este caso se genera una instrucción
de código intermedio de multiplicación. En la segunda producción podemos observar que cuando
la expresión es una constante numérica se ha decidido crear una variable temporal y generar una
instrucción de código intermedio que asigne en la posición de la variable temporal el valor de la
constante numérica (num.val) que proporciona el analizador léxico. Una opción más adecuada
habría sido que el no-terminal E devolviese el valor de la constante numérica, lo que habría
evitado la creación de una variable temporal. Sin embargo, esta segunda opción habría obligado
a que el no-terminal E, además del atributo .pos tuviese al menos otro atributo para devolver
valores constantes, lo que obligaría a añadir más acciones semánticas a todas las producciones
del no-terminal E. Para simplificar los esquemas de traducción hemos elegido la primera opción.
Hay que tener en cuenta que esta elección no afectará a la calidad del código finalmente
generado porque este tipo de variables temporales son fácilmente eliminadas durante la fase de
optimización de código intermedio.
En la tercera producción, una expresión (E) se reescribe como una expresión entre paréntesis
(E1). Puesto que el valor de una expresión entre paréntesis es el mismo de la expresión sin
paréntesis, basta con hacer que el atributo E.pos tome el mismo valor del atributo E1.pos. De esta
manera los dos atributos representan la misma posición de memoria (y por lo tanto el mismo
valor de la expresión).
Finalmente, en la última expresión observamos que E se reescribe como una expresión
cambiada de signo. Este caso es diferente del anterior, puesto que la expresión representada por
E no tomará el mismo valor que E1, por lo tanto es necesario utilizar una nueva posición de
memoria para almacenar el nuevo valor.
Ejercicio
Construye una gramática no-ambigua equivalente a la anterior. A continuación asocia a
cada producción las acciones semánticas necesarias para generar el código intermedio
correspondiente.
Generación de código intermedio
14
2. Acceso a miembros de una estructura y elementos de un
array
2.1. Acceso a miembros de una estructura
El acceso a los miembros de un estructura en C depende de la información que se almacene
en la TDS sobre la posición de los miembros. La forma habitual consiste en almacenar, junto a
cada miembro, su desplazamiento con respecto al comienzo del área de memoria asignada a esa
estructura (Fig. 2.4).
Ejemplo 2.2.- Dada la definición de las variables a y b de tipo estructura en el lenguaje C
struct ej {
int c1 ;
float c2 ;
}a, b;
La TDS quedaría:
TDS nom tipo posición … ref
a testructura 0 , 124
b testructura 0 , 130
Figura 2.4. Asignación de memoria en la TDS para los miembros de una estructura.
De esta forma, en la entrada en la TDS de cada miembro de un tipo de estructura concreta se
almacenará el mismo desplazamiento independientemente de que hayan declaradas más de una
variable de ese tipo. Lo único que cambiará será la dirección base de cada variable de ese tipo de
estructura. Evidentemente antes de generar código intermedio para acceder a un miembro de una
estructura será necesario sumar a la dirección base de la variable estructura, el desplazamiento
asignado al miembro. En la Fig. 2.5 se muestra como quedaría un ETDS para acceder a los
miembros de una estructura de esta manera.
Se ha usado la función EsMiembro(id1.nom, id2.nom) que devolverá el valor TRUE si el
nombre id2.nom es un miembro de la estructura de nombre id1.nom. La función
BuscaPos(id1.nom) devuelve la posición de memoria base a partir de la cual se encontrarán en
tiempo de ejecución los miembros de la estructura id1.nom. La llamada a
BuscaPosMiembro(id1.nom, id2.nom) devolverá el desplazamiento relativo del miembro a partir
de la posición de memoria base de la estructura.
TALLA_REAL = 4
TALLA_ENTERO = 2
Tabla de miembros
nom tipo posición
c1 tinteger 0
c2 treal 2
Generación de código intermedio
15
Inst_simple ! id1 . id2 = E
{ si BuscaTipo (id1.nom) <> testructura ent MemError();
sino base := BuscaPos (id1.nom) ;
si no EsMiembro (id1.nom, id2.nom) ent MemError()
sino desp_miembro:=BuscaPosMiembro(id1.nom, id2.nom);
pos := base + desp_miembro ;
emite ( pos ‘:=’ E.pos ) }
Figura 2.5. ETDS Para el acceso a los miembros de una estructura.
2.2. Acceso a elementos de un array
En los lenguajes de alto nivel podemos ver que frecuentemente se permite definir arrays de
varias dimensiones (como en Pascal o C), con un límite inferior (Pascal) y superior (Pascal y C)
definido para cada una de las dimensiones. En cambio, en el código intermedio definido solo
disponemos de instrucciones de asignación con un modo de direccionamiento relativo: de
z:=a[x] (en la posición z se almacena el valor de la posición resultante de sumar a la
dirección base a el contenido de la posición x) y a[x]:=z. Esto obliga a realizar proceso de
traducción que requiere el cálculo de la posición de memoria que ocupará en tiempo de ejecución
cada elemento del array definido en el programa fuente.
2.2.1. Acceso a los elementos de un array multidimensional en Pascal
Una forma bastante frecuente y cómoda de almacenar las matrices consiste en almacenar todos sus elementos en posiciones consecutivas de memoria. Con esta representación, cada vez
que nos encontremos en el lenguaje de alto nivel con una expresión donde aparezca un elemento
de una matriz, será necesario calcular el desplazamiento que hay que sumar a la dirección base
de la matriz para acceder al elemento indicado. Veamos como se realiza este cálculo en el caso
en el que se pueden definir límites inferiores y superiores (Pascal).
Si suponemos la declaración de una variable A de tipo vector (matriz de una dimensión) en
Pascal:
A: array [inf..sup] of integer ;
y se desea acceder al elemento A[i], éste se encontrará en la posición
base + (i - inf) * Talla
donde
base es la dirección base donde se encuentra el primer elemento del vector,
inf es el límite inferior de vector, y
Generación de código intermedio
16
Talla es el número deposiciones de memoria que ocupa cada elemento del vector
(dependiente del tipo de elementos que contiene el vector).
De igual forma, si se define una matriz de dos dimensiones:
A: array [inf1..sup1, inf2..sup2] of integer ;
la fórmula para acceder a un elemento A[i1,i2] sería:
base + ((i1 – inf1) * n2 + (i2-inf2) ) *Talla
donde n2 es el número de elementos de la segunda dimensión de la matriz (n2 = sup2–
inf2+1)
que puede quedar como:
base + ( (i1*n2 +i2) – (inf1*n2 + inf2) )*Talla
(inf1* n2+inf2) puede ser calculado en tiempo de compilación, y recibe el nombre de
desplazamiento inicial de la matriz. Este valor, puesto que no depende del elemento de la matriz
al que se quiera acceder, puede ser almacenado en la TDS. En cambio, (i1*n2+i2) tendrá que ser
calculado en tiempo de ejecución y por lo tanto habrá que generar código para realizar este
cálculo.
En general, dada la declaración de una matriz n-dimensional:
A: array [inf1..sup1, inf2..sup2, ... infn..supn] of integer ;
la fórmula para acceder a un elemento A[i1,i2,...in ] quedará como:
base + ( (((i1*n2+i2)*n3+i3 …)*nn+in) – (((inf1*n2+inf2)*n3+inf3…)*nn+infn) ) * Talla
donde (((inf1*n2+inf2)*n3+inf3…)*nn+infn) es el desplazamiento inicial de la matriz.
Puesto que el acceso a un elemento de una matriz puede aparecer en el lado derecho o
izquierdo de una asignación, las producciones que definen la sintaxis de este acceso aparecerán
en dos lugares distintos de la gramática. Como el código generado para los dos casos es
prácticamente el mismo, se muestra únicamente uno de los casos (cuando el acceso al elemento
de la matriz aparece en el lado izquierdo de la asignación):
Inst_simple ! id [ Lista_indices ] := E
Lista_indices ! E Resto_LI
Resto_LI ! , E Resto_LI
| "
Generación de código intermedio
17
En la Fig. 2.6 se muestra un ETDS que genera código intermedio para el acceso a los
elementos de una matriz n-dimensional en Pascal.
Inst_simple -> id { Lista_indices.nom := id.nom }
[ Lista_indices ] := { temp := Lista_indices.pos ;
emite (temp := temp ‘–‘ Desp_inicial(id.nom)) ;
emite (temp := temp ‘*’ Talla (id.nom)) }
E { pos := BuscaPos (id.nom) ;
emite ( pos ‘[‘ temp ‘]’ := E.pos }
Lista_indices ! E { temp := CrearVarTemp()
emite (temp := E.pos); Resto_LI.ndim := 1;
Resto_LI.temp := temp ; Resto_LI.nom := Lista_indices.nom }
Resto_LI { Lista_indices.pos := Resto_LI.pos }
Resto_LI ! , E { Resto_LI.ndim := Resto_LI.ndim + 1;
emite (Resto_LI.temp :=
Resto_LI.temp‘*’Num_elementos(Resto_LI.nom, Resto_LI.ndim));
emite (Resto_LI.temp := Resto_LI.temp ‘+’ E.pos)
Resto_LI1.ndim := Resto_LI.ndim ;
Resto_LI1.temp := Resto_LI.temp ;
Resto_LI1.nom := Resto_LI.nom ; }
Resto_LI1 Resto_LI1 { Resto_LI.pos:= Resto_LI1 pos }
Resto_LI ! " { Resto_LI.pos:= Resto_LI.temp }
Figura 2.6. ETDS para el acceso a los elementos de un array en Pascal.
La función Desp_inicial(R_Inst_simple.nom) devuelve el desplazamiento inicial de la matriz
de nombre R_Inst_simple.nom que se guardó en la TDS cuando se declaró la matriz. De igual
forma, la función Talla(R_Inst_simple.nom), devuelve el tamaño de cada elemento de la matriz,
y Num_elemen(Lista_indices.nom,n_dim) devuelve el número de elementos de la dimensión
n_dim de la matriz Lista_indices.nom.
Ejemplo 2.3.- Veamos qué código generará este ETDS para el acceso a un elemento de una
matriz bidimensional que se define en el siguiente trozo de código fuente en Pascal:
var ...
A: array [2..11,1..20] of real ;
x,y: integer ; z: real;
begin
...
A[x,y] := z ...
Generación de código intermedio
18
TDS
nom tipo posición tipo_elem … desp_ini ref
A tarray 0, 140 treal 41
x tentero 0, 940 -
y tentero 0, 942 -
z treal 0, 944 -
Figura 2.7. Contenido de la TDS para el Ejemplo 2.3.
Suponiendo que el contenido en la TDS es el de la Fig. 2.7 se generará el siguiente código
intermedio:
(100) t1 := x
(101) t1 := t1 * 20
(102) t1 := t1 + y
(103) t1 := t1 – 41
(104) t1 := t1 * 4
(105) A [t1] := z
2.2.2. Acceso a los elementos de un array multidimensional en C
La generación de código intermedio para acceder a arrays multidimensionales en C es más sencilla que la del caso de Pascal, ya que en C todos los arrays comienzan en la posición 0. Se
trata por lo tanto de un caso particular del visto para Pascal.
En general, dada la declaración de un array n-dimensional en C
int A[n1][n2]... [nn];
la fórmula para acceder al elemento A[i1][i2]...[in] quedaría como:
base + (((i1*n2+i2)*n3+i3 …)*nn+in) * Talla
donde ni es el número de elementos de la i-ésima dimensión.
Se puede construir un ETDS que genere código para el acceso a los elementos de una matriz
n-dimensional en Pascal tal y como aparece en la Fig. 2.8.
Tabla de arrays
n_dim lim_inf lim-sup
1 2 11
2 1 20
TALLA_REAL = 4 TALLA_ENTERO = 2 desp_ini = (inf1 * n2 + inf2) = 2*20+1 =
41
Generación de código intermedio
19
Inst_simple -> id { LI.nom := id.nom }
LI = E { emite (LI.pos := LI.pos ‘*’ Talla (id.nom)) ;
pos := BuscaPos (id.nom) ;
emite ( pos ‘[‘ LI.pos ‘]’ := E.pos ; }
LI ! [ E ] { LI.pos := CrearVarTemp() ;
emite (LI.pos := E.pos); LI.ndim := 1; }
LI ! { LI1.nom := LI.nom ; }
LI1 [ E ] Resto_LI1 { LI.ndim := LI1.ndim + 1; LI.pos := LI1.pos ;
emite (LI.pos := LI.pos ‘*’ Num_elementos(LI.nom, LI.ndim)) ;
emite (LI.pos := LI.pos ‘+’ E.pos) ; }
Figura 2.6. ETDS para el acceso a los elementos de un array en C.
3. Expresiones lógicas e instrucciones de control de flujo
En un lenguaje de alto nivel las expresiones de tipo lógico se emplean principalmente como
expresiones condicionales en instrucciones de control de flujo. Debido a esto hay una
dependencia elevada entre la forma de representar los valores de las expresiones lógicas y el
código que se debe generar para traducir las instrucciones de control de flujo. Por esta razón
vamos a ver en la misma sección la representación de los valores lógicos y la traducción de
instrucciones de control de flujo.
3.1. Variables y expresiones lógicas
Cualquier variable necesita una posición de memoria en la que, durante la ejecución del
código generado, se almacene su valor. Cuando se trata de una variable de tipo lógico o boolena,
para representar su valor también se necesita una posición de memoria (¡bastaría un bit!) para
almacenar su valor. Como el código intermedio definido en este capítulo no dispone del tipo
lógico ni el tipo bit, en la posición de memoria de una variable lógica se almacenará un entero
que representará su valor. Para esta representación numérica se empleará el mismo criterio
seguido en el lenguaje C1 de que un cero representa el valor de verdad “falso”, mientras que
cualquier valor distinto de cero representa el valor de verdad “cierto”.
Pero además de variables lógicas, en los lenguajes de alto nivel aparecen otro tipo de
expresiones lógicas, conteniendo comparaciones y/o conectivas lógicas. Para representar el valor
de una expresión lógica se pueden emplear dos métodos: la representación numérica y la
representación por control de flujo.
1 El tipo bool (lógico) no aparece en el lenguaje C hasta el estándar C99 (a través de <stdbool.h>) y no todos los
compiladores lo soportan. Por esta razón, las instrucciones de control de flujo en C utilizan expresiones enteras (0 es
falso y distinto de cero es cierto) para decidir el flujo.
Generación de código intermedio
20
En la representación numérica de una expresión lógica toda expresión lógica tendrá asignada
una posición de memoria donde se almacenará su valor de verdad: un entero igual a 0 para
“falso” o distinto de cero para “cierto”. Por el contrario, en la representación por control de flujo
no se emplea una posición de memoria para representar el valor de verdad de la expresión. Su
valor vendrá determinado implícitamente por la instrucción de código intermedio que se ejecute.
Tal y como se afirma en [AHOA90], ninguno de los dos métodos es siempre mejor que el otro.
En este apartado se muestran las dos formas de representación.
3.2. Representación numérica
En la representación numérica a cada expresión lógica se le asigna una posición de memoria
donde se almacena un valor numérico que indica el valor de verdad de la expresión. Si el código
intermedio permite emplear valores y operadores binarios bastaría con usar un 0 para representar
el valor falso y un 1 para el valor cierto. Si, como es nuestro caso, el código intermedio no
permite el empleo de valores binarios, se puede usar un número entero con un valor de cero para
representar el valor falso y un número distinto de cero para representar el valor cierto. Con esta
forma de representación, la evaluación de una expresión lógica se convierte en la evaluación de
una expresión aritmética.
En los siguientes ETDS se va a presentar una generación de código intermedio para el caso
en el que el lenguaje fuente permite la utilización de expresiones booleanas (como en Pascal) en
lugar de utilizar valores enteros (como en C). Como se ha comentado anteriormente, esto
obligará a realizar una traducción entre la representación del lenguaje fuente (booleanos) y la del
lenguaje intermedio (enteros).
Podemos escribir un ETDS para una sencilla gramática que genere expresiones lógicas
similares a las de Pascal tal y como aparece en el Fig. 2.8.
E ! true { E.pos:= CrearVarTemp() ;
emite( E.pos ‘:=’ 1 ) }
E ! false { E.pos:= CrearVarTemp() ;
emite( E.pos ‘:=’ 0 ) }
E ! ( E1 ) { E.pos := E1.pos }
E ! id { E.pos := BuscaPos(id.nom) }
Figura 2.8. ETDS representación numérica de expresiones lógicas.
Tal y como se puede apreciar en el ETDS anterior, cuando una expresión lógica sea la
constante TRUE o FALSE, basta con crear una nueva variable temporal y asignar en la posición
de memoria correspondiente un 1 o un 0 respectivamente. En el caso de que la expresión lógica
sea una variable (id), al igual que ocurría en el caso de las expresiones aritméticas, basta con que
el atributo E.pos tome el valor de la posición de memoria donde se almacena el valor de verdad
del identificador (devuelto por la función BuscaPos).
Generación de código intermedio
21
Para evaluar una expresión relacional, como a<5 hay que tener en cuenta que su valor de
verdad, en general, no se puede conocer en tiempo de compilación. Por esta razón es necesario
generar código de tres direcciones cuya ejecución produzca como resultado la evaluación de la
expresión relacional, asignando en la posición de memoria reservada para la expresión un valor 0
o distinto de cero según sea falsa o cierta la expresión, respectivamente.
Ejemplo
Para evaluar la expresión a<5, se puede generar la siguiente secuencia de código de tres
direcciones:
(1) t1 := 1
(2) if a < 5 goto 4
(3) t1 := 0
(4) ...
Tal y como fácilmente se puede comprobar, la ejecución del fragmento anterior de código de
tres direcciones produce como resultado que si a < 5 se almacenará un 1 en la posición de
memoria reservada para almacenar el valor de verdad de la expresión (en el ejemplo t1). En caso
contrario se almacenará un 0. El ETDS para generar la secuencia de código intermedio anterior
es
E ! E1 oprel E2 { E.pos := CrearVarTemp() ;
emite( E.pos ‘:=’ 1 ) ;
emite( ‘if’ E1.pos oprel.op E2.pos ‘goto’ SIGINST+ 2 ) ;
emite( E.pos ‘:=’ 0 ) }
En este ETDS puede verse la utilización del atributo oprel.op. Este atributo represente al
valor del operador relacional que aparece en el código fuente que se está compilando: “=”, “>”,
”>=”, “<”,… También se puede observar la aparición de una nueva variable global: SIGINST.
En el código intermedio de este tipo de expresiones se debe generar una instrucción de salto
condicional que se salte la instrucción que le sigue. Para generar esta instrucción se debe conocer
cuál es el número de la instrucción que se está generando. Con este fin, en el ETDS (y por lo
tanto en el código fuente del compilador desarrollado) existe un contador del número de
instrucciones de código intermedio generadas: SIGINST. Esta variable global del compilador es
actualizada de forma automática por el procedimiento emite cada vez que se genera una
instrucción. De esta forma, si en el momento de llamar al procedimiento emite para generar la
instrucción de salto condicional ‘if’ E1.pos oprel.op E2.pos ‘goto’ SIGINST+ 2, la variable
SIGINST vale 15, se generará la instrucción (15) if pos1 oprel pos2 goto 17.
Cuando la expresión lógica incluye operadores lógicos “and” y “or”, si el código intermedio
no dispone de estos operadores, como es nuestro caso, será necesario buscar una equivalencia
con los operadores disponibles en el código intermedio. En nuestro caso, podemos observar que
el operador lógico “and” puede implementarse como una operación de multiplicación entre
Generación de código intermedio
22
valores de verdad, y el operador “or” como una suma. De esta manera, podemos obtener el
ETDS:
E ! E1 or E2 { E.pos:= CrearVarTemp() ;
emite( E.pos ‘:=’ E1.pos ‘+’ E2.pos ) }
! E1 and E2 { E.pos:= CrearVarTemp() ;
emite( E.pos ‘:=’ E1.pos ‘*’ E2.pos ) }
Al generar código intermedio de esta manera hay que tener en cuenta que, cuando se realice
una operación “or” entre dos expresiones con el valor de verdad “cierto”, el resultado será otra
expresión con el valor “cierto” pero representado con un valor entero distinto de “1”. Esto
introduce un nuevo problema: En algún momento podría darse el caso de que el valor entero
sobrepase el rango válido de los enteros, apareciendo enteros negativos. Esto a su vez puede
provocar que una operación “or” entre dos expresiones ciertas representadas por valores enteros
iguales pero con signos distintos den como resultado el valor falso. Así por ejemplo, si
consideramos dos expresiones booleanas con el valor “cierto” pero representadas
respectivamente por los valores enteros “-5” y “5”, la operación “or” entre ellas (su suma) dará
como resultado “0” (valor “falso”). Esto es un error provocado por la forma en la que generamos
el código y por lo tanto debe corregirse. Una posible forma de corregir este problema consiste en
asegurarse de que un valor “cierto” siempre se representa por el entero “1”. Para ello
generaremos una instrucción que compruebe el valor entero después de la operación “or”. De
esta manera, el ETDS anterior quedaría:
E ! E1 or E2 { E.pos:= CrearVarTemp() ;
emite( E.pos ‘:=’ E1.pos ‘+’ E2.pos )
emite( ‘if’ E.pos ‘<= 1’ goto’ SIGINST+2 ) ;
emite( E.pos ‘:= 1’)}
Para generar el código correspondiente al operador lógico “not”, si no está disponible en el
código intermedio, tal y como es nuestro caso, será necesario generar una secuencia de
instrucciones cuya ejecución produzcan el resultado equivalente. Así por ejemplo, para obtener
en la posición de memoria t1 el valor numérico de verdad de la expresión “not a” se puede
generar la secuencia de instrucciones de código intermedio:
Generación de código intermedio
23
(1) t1 := 0
(2) if a <> 0 goto 4
(3) t1 := 1
(4) ...
El ETDS correspondiente quedará como aparece en la Fig. 2.9.
E ! E1 or E2 { E.pos:= CrearVarTemp() ;
emite( E.pos ‘:=’ E1.pos ‘+’ E2.pos )
emite( ‘if’ E.pos ‘<= 1’ goto’ SIGINST+2 ) ;
emite( E.pos ‘:= 1’)}
! E1 and E2 { E.pos:= CrearVarTemp() ;
emite( E.pos ‘:=’ E1.pos ‘*’ E2.pos ) }
! not E1 { E.pos:= CrearVarTemp() ;
emite( E.pos ‘:=’ 0 ) ;
emite( ‘if’ E1.pos ‘<> 0 goto’ SIGINST+2 ) ;
emite( E.pos ‘:=’ 1) }
Figura 2.9. ETDS operadores lógicos con representación numérica.
Para ver un ejemplo de la utilización de las expresiones lógicas en las instrucciones de
control de flujo, se va a construir un ETDS que genere código de tres direcciones para la
instrucción de do-while de C. Su sintaxis es
I ! do I1 while ( E )
El esquema del código a generar se puede representar tal y como aparece en la Fig. 2.10. El
bloque I1 representa todas las instrucciones que se han generado en las producciones del no-
terminal I para el cuerpo del bucle. De igual forma, el bloque E representa todo el código
intermedio emitido para la evaluación de la expresión E.
I1
E
if E.pos = 0 goto
Figura 2.10: Esquema de código para un do-while con representación numérica.
La ejecución del código intermedio generado para la evaluación de la expresión lógica dejará,
tal y como se ha visto, en una posición de memoria (E.pos) un valor 0 o distinto de 0 (1) según
sea falsa o cierta la expresión. El código que se genera para la instrucción do-while, saltará al
inicio del bucle si en esa posición hay un número igual a 0. El ETDS quedará por lo tanto
Generación de código intermedio
24
I ! do
I1
while ( E )
{ I.inicio := SIGINST }
{ emite( ‘if’ E.pos ‘<> 0 goto’ I.inicio) }
Se puede observar que se ha utilizado el atributo I.inicio para guardar el valor de la variable
global SIGINST justo antes de I1. Este atributo almacenará el número de la primera instrucción
de código intermedio generada en I1. Esta instrucción será el destino del salto condicional
generado para ejecutar una nueva iteración del bucle. Este ETDS emite únicamente una
instrucción, el salto que aparece al final del bucle, ya que las instrucciones del cuerpo del bucle
serán emitidas por las acciones semánticas de las producciones I y las correspondientes a la
evaluación de la expresión lógica por las del no-terminal E.
De forma semejante se puede intentar construir una gramática atribuida que genere código de
tres direcciones para una instrucción “while” de C:
I! while
( E )
I1
{ I.inicio:= SIGINST }
{ emite( ‘if’ E.pos ‘=0 goto’ I.fin ) }
{ emite( ‘goto’ I.inicio ) ;
I.fin := SIGINST }
Sin embargo, se puede observar que esta gramática no es L-atribuida. Esto es debido a que en
la acción semántica de generación de la instrucción de salto condicional { emite( ‘if’ E.pos‘=0
goto’ I.fin ) } aparece el atributo I.fin que toma su valor en una acción que aparece a la derecha
de la acción semántica del emite. Esto significa que si se realiza una traducción dirigida por la
sintaxis, en el momento en que se realiza la llamada a emite para generar la instrucción de salto
condicional, no se conoce todavía el valor del atributo I.fin que debería contener el número de la
primera instrucción de código intermedio generada después del bucle (se trata de un argumento
de la llamada a emite cuyo valor no se conoce). Este problema de tener que generar una
instrucción antes de conocer todos sus argumentos se resolverá en la sección dedicada a las
referencias no satisfechas.
3.3. Representación por control de flujo
En la representación por control de flujo se considera la representación de expresiones
lógicas dentro del contexto en el que más frecuentemente aparecen: las instrucciones de control
del flujo. En una instrucción de control de flujo el valor de la expresión lógica se utiliza para
decidir a qué bloque de código se debe saltar. El código generado para una expresión lógica
consistirá en la generación de una instrucción de salto para el caso en el que la expresión sea
cierta y otra para el caso en el que la instrucción sea falsa. Veamos un ejemplo de utilización de
esta representación en el contexto de un lenguaje de programación de alto nivel similar a Pascal.
El código generado para las constantes lógicas TRUE y FALSE quedaría:
Generación de código intermedio
25
E ! true { emite( ‘goto’ E.verdadero) }
E ! false { emite( ‘goto’ E.falso) }
donde E.verdadero y E.falso son etiquetas que representan el número de la instrucción a la
que hay que saltar cuando la expresión es cierta o falsa respectivamente.
Considerando una representación de los valores lógicos por control de flujo, un ETDS para la
instrucción repeat-until de Pascal podría ser:
I ! repeat
I1
until E
{ E.falso:= SIGINST }
{ E.verdadero := SIGINST) }
Se puede observar que en este ETDS no se ha generado ninguna instrucción. Únicamente ha
sido necesario dar un valor a los atributos E.falso y E.verdadero. L instrucción de salto que
provocará que se vuelva al principio del bucle si la expresión E se avalúa a falso se ha generado
en las producciones para el no-terminal E.
Desgraciadamente en el momento de generar el código para la expresión lógica no siempre se
conoce el valor de estas etiquetas. Este problema de tener que generar una instrucción antes de
conocer todos sus argumentos apareció también en la representación numérica y se resolverá en
la sección dedicada a las referencias no satisfechas.
4. Referencias no satisfechas. Relleno por retroceso
4.1. Planteamiento del problema
En la sección anterior se ha visto que frecuentemente se debe generar una instrucción de
código de la que no se conoce el valor de todos sus argumentos. Una primera aproximación para
solucionar este problema es realizar dos pasadas: En una primera pasada se generan las
instrucciones aunque algunas de ellas contengan referencias cuyo valor no se conoce todavía.
Tras esta pasada, en la que se han generado todas las instrucciones, se realiza una segunda
pasada en la que se van sustituyendo las referencias por sus valores.
Una alternativa que permite solucionar el problema de las referencias no-satisfechas sin tener
que realizar una segunda pasada es la conocida como relleno por retroceso. Básicamente esta
técnica consiste en la generación de cada instrucción aunque alguno de sus argumentos quede
incompleto. Cada vez que se genera una instrucción incompleta se guarda el número de la
instrucción incompleta en un atributo relacionado con al argumento cuyo valor no se conoce.
Cuando posteriormente, al seguir generando el resto de las instrucciones, se conozca el valor se
incorpora directamente a la instrucción que quedó incompleta, quedando de esta manera
completada.
Generación de código intermedio
26
4.2. Relleno por retroceso
Para realizar la implementación de esta técnica se pueden definir tres funciones con el
siguiente perfil:
ptro CreaLans(E): Crea una lista que solo contiene un número de instrucción E a rellenar
posteriormente. Devuelve un puntero a dicha lista.
ptro CompletaLans( ptro, E ): Rellena todas las instrucciones incompletas, cuyo número está
contenido en la lista apuntada por ptro, con el valor de argumento E.
ptro FusionaLans (ptro, ptro): Concatena las listas apuntadas por sus dos argumentos y
devuelve un puntero a la nueva lista.
Veamos mediante un ejemplo cómo pueden utilizarse estas funciones para implementar la
técnica de relleno por retroceso. Supongamos que en el momento de generar la instrucción de
salto incondicional de la línea 100 no se conoce cuál será el número de la instrucción destino de
este salto.
100 goto –
Se puede anotar en una lista, relacionada con el destino del salto, el número de la instrucción
generada que contiene este argumento no-satisfecho. Esto se hace mediante una llamada a la
función CreaLans(100). La lista ahora contendrá el número de la instrucción 100: lista={100}
Se continúa generando código y, si se genera otra instrucción de salto incompleta (por
ejemplo la número 103) cuyo destino sea el mismo, se añade a la lista asociada a ese destino el
número de la nueva instrucción incompleta: lista={100, 103}.
101 ...
102 ...
103 if a>0 goto ---
Continúa la generación de código intermedio y, en el momento en el que se conozca el valor
del argumento (por ejemplo el destino de los dos saltos podría ser la instrucción número 105), se
llama a la función CompletaLans(lista ,105) que se encargará de completar las instrucciones
100 y 103 con el valor 105. El código del ejemplo quedará por tanto:
100 goto 105
101 ...
102 ...
103 if a>0 goto 105
104 ...
105 ...
En la sección 3.1 se construyó un ETDS para la instrucción while de C suponiendo que se
utilizaba una representación numérica para las expresiones lógicas:
Generación de código intermedio
27
I! while
( E )
I1
{ I.inicio:= SIGINST }
{ emite( ‘if’ E.pos ‘=0 goto’ I.fin )
}
{ emite( ‘goto’ I.inicio ) ;
I.fin := SIGINST }
En este ETDS ya aparece el problema de tener que generar una instrucción de salto
condicional en la que no se conoce el número de la instrucción destino. Ahora se puede resolver
este problema usando relleno por retroceso. Justo antes de generar la instrucción de salto se crea
una lista en la que se guarda el número de la instrucción que contiene la referencia no-satisfecha:
CreaLans(SIGINST). Cuando finalmente se conozca el destino del salto (la instrucción que se
genere justo después de la última instrucción del bucle), se llamará a la función
CompletaLans(final, SIGINST) para completar la instrucción de salto condicional. El ETDS
quedará por tanto como aparece en la Fig. 2.11.
I! while
( E )
I1
{ Iinicio:= SIGINST }
{ I.final:= CreaLans(SIGINST) ;
emite( ‘if’ E.pos ‘=0 goto’ --- ) }
{ emite( ‘goto’ I.inicio ) ;
CompletaLans(I.final, SIGINST) }
Figura 2.11. ETDS while con relleno por retroceso (representación numérica de expresiones lógicas).
4.3 Representación numérica de expresiones lógicas (con relleno por retroceso)
Usando relleno por retroceso se puede resolver también el problema de argumentos no
satisfechos que aparece al intentar generar código para otras instrucciones de control de flujo en
C. Suponiendo que se emplea una representación numérica de las expresiones lógicas, se puede
generar código intermedio para una instrucción if-else de C mediante el siguiente ETDS:
I ! if E
I1 else
I2
{ I.falso:= CreaLans(SIGINST);
emite( ‘if’ E.pos ‘=0 goto’ --- ); }
{ I.fin := CreaLans( SIGINST) ;
emite( ‘goto’ --- ) ;
CompletaLans(I.falso, SIGINST) }
{ CompletaLans (I.fin, SIGINST) }
En este ETDS, además del salto al código del “else” para el caso en que la expresión sea falsa,
se emite un salto “emite( ‘goto’ --- )” que garantiza que, tras la ejecución del código de I1 no se
Generación de código intermedio
28
ejecuta siempre el del I2. Las dos instrucciones de salto se generan de forma incompleta (falta el
destino de los saltos) y se completan posteriormente con las llamadas a “CompletaLans(I.falso,
SIGINST)” para añadir el destino del salto a I2 y con “CompletaLans (I.fin, SIGINST)” para
completar el salto emitido después del código de I1.
De forma similar, podemos construir un ETDS que genere código intermedio para un bucle
for parecido al del lenguaje C:
I ! for ( I1 ;
E ;
I2 )
I
{ I.cond := SIGINST; }
{ I.fin := CreaLans (SIGINST);
emite( ‘if’ E.pos ‘=0 goto’ --- );
I.cuerpo := CreaLans (SIGINST);
emite( ‘goto’ --- ) ;
I. incr := SIGINST ; }
{ emite(‘goto’ I.cond);
CompletaLans(I.cuerpo, SIGINST) ; }
{ emite(‘goto’ I.incr);
CompletaLans(I.fin, SIGINST) ;
}
4.4 Representación de expresiones lógicas por control de flujo (con relleno por retroceso)
Usando relleno por retroceso podemos resolver el problema que apareció en el ejemplo de la representación de expresiones lógicas por control de flujo en Pascal. El ETDS para la evaluación
de la expresión lógica (solo para el caso de las constantes TRUE y FALSE) quedaría:
E ! True { E.listaverdad := CreaLans( SIGINST) ;
E.listafalso := nil }
emite( ‘goto’ --- ) }
E ! False { E.listafalso := CreaLans( SIGINST) ;
E.listaverdad := nil ;
emite( ‘goto’ --- ) }
Como se puede apreciar, en cada producción se emite una instrucción de salto que queda
incompleta, pero se guarda una referencia a la instrucción en la lista E.listaverdad (para TRUE) o
E.listafalso (para FALSE). Estas instrucciones de salto incompletas se completarán cuando se
conozca el destino del salto. Por ejemplo, el ETDS para un repeat-until de Pascal quedaría tal y
como aparece en la Fig. 2.12.
Generación de código intermedio
29
I! repeat
I1
until E
{ I.inicio:= SIGINST }
{ CompletaLans(E.listafalso, I.inicio);
CompletaLans(E.listaverdad, SIGINST) }
Figura 2.12. ETDS repeat-until con relleno por retroceso (representación expresiones lógicas por control de flujo).
El ETDS para un do-while de C sería muy parecido, ya que bastaría con intercambiar los
valores con los que se completan las listas de referencias no satisfechas .listaverdad y .listafalso:
I! do
I1
while (E)
{ inicio:= SIGINST }
{ CompletaLans(E.listaverdad, inicio);
CompletaLans(E.listafalso, SIGINST) }
Figura 2.13. ETDS do-while con relleno por retroceso (representación expresiones lógicas por control de flujo).
Siguiendo esta técnica se puede construir un ETDS que genere código intermedio para la
evaluación de expresiones lógicas e instrucciones de control de flujo empleando una
representación de los valores lógicos mediante control de flujo, solucionando el problema de las
referencias no-satisfechas mediante relleno por retroceso.
Hemos visto que en la representación de los valores lógicos mediante control de flujo se
generan instrucciones de salto de las que no se conoce a donde se debe saltar. Para solucionar
este problema, se genera la instrucción de salto sin especificar la dirección destino. Se anota el
número de la instrucción incompleta en una lista que se corresponde con el valor de verdad de la
expresión: E.lv para el valor verdadero y E.lf para el valor falso. De esta forma, E.lv (E.lf) será
una lista que contendrá los números de las instrucciones de código intermedio donde se ha
generado un salto a la dirección destino para el caso de que la expresión sea cierta (falsa).
Cuando se conozca la dirección destino correspondiente, se completarán las instrucciones
incompletas con el valor de la dirección. El ETDS para las expresiones lógicas empleando esta
técnica quedará tal y como se muestra en la Fig. 2.14.
E ! E1 or
E2
{ CompletaLans( E1.lf,SIGINST) ;
E.lv := FusionaLans( E1.lv, E2.lv ) ;
E.lf := E2.lf }
E ! E1 and
E2
{ CompletaLans( E1.lv, SIGINST) ;
E.lv := E2.lv ;
E.lf := FusionaLans( E1.lf,E2.lf ) }
E ! not E1 { E.lv := E1.lf ;
E.lf := E1.lv }
E ! ( E1 ) { E.lv := E1.lv ;
Generación de código intermedio
30
E.lf := E1.lf }
E ! E1 oprel E2 { E.lv := CreaLans( SIGINST) ;
emite( ‘if’ E1.pos oprel.op E2.pos ‘goto’ --- ) ;
E.lf := CreaLans( SIGINST ) ;
emite( ‘goto’ --- ) }
E ! True { E.lf := nil ;
E.lv := CreaLans( SIGINST) ;
emite( ‘goto’ --- ) }
E ! False { E.lv := nil ;
E.lf := CreaLans( SIGINST) ;
emite( ‘goto’ --- ) }
E ! id { si BuscaTipo( id.nom ) = tlógico
ent E.lf := CreaLans( SIGINST)
emite( ‘if’ id.pos ‘= 0 goto’ --- ) ;
E.lv := CreaLans( SIGINST) ;
emite( ‘goto’ --- ) }
Figura 2.14. ETDS expresiones lógicas con representación por control de flujo y relleno por retroceso.
4.4.1. Código en cortocircuito
Del ETDS de la Fig. 2.14 se puede destacar el tratamiento que han recibido los operadores lógicos “and” y “or”. Para estos dos operadores se ha decidido generar código en la forma
conocida como “cortocircuito”. Esto supone que no siempre se avaluarán todos los componentes
de la expresión lógica: se finalizará la evaluación de la expresión tan pronto como se conozca su
valor de verdad. Así por ejemplo la evaluación de la expresión lógica
TRUE or (a<b) or (a=5)
no llegará hasta las dos últimas subexpresiones, puesto que al evaluar la primera
subexpresión (la constante TRUE) ya se conoce el valor lógico de toda la expresión.
El código intermedio que se genera realiza esta evaluación “en cortocircuito” porque las acciones
semánticas de la producción E ! E1 or E2 fusionan las listas de los atributos E1.lv y E2.lv. Lo
mismo se puede decir del operador “and”, solo que en este caso la evaluación de la expresión
lógica se interrumpirá en cuanto se evalúe una subexpresión cuyo valor sea FALSE.
4.4.2. Variables lógicas y representación por control de flujo
Teniendo en cuenta que una variable siempre debe tener almacenado su valor en una posición
de memoria, parece evidente que esto será también cierto para las variables lógicas. Por esta
razón es necesario utilizar una representación numérica para almacenar el valor de una variable
lógica. Si para representar los valores de expresiones lógicas también se utiliza una
representación numérica no habrá ningún problema, ya que en ambos casos se utiliza el mismo
tipo de representación. En cambio, si se desea realizar una asignación de una expresión lógica
Generación de código intermedio
31
cuya representación es por control de flujo a una variable lógica (cuya representación es
numérica), será necesario realizar una conversión. Para realizar esta conversión es necesario
generar código intermedio tal y como se muestra en el ETDS de la Fig. 2.15.
I ! id := E { id.pos := BuscaPos(id.nom) ;
si BuscaTipo( id.nom ) = tlógico
ent
CompletaLans( E.lf , SIGINST) ;
emite( id.pos ‘:= 0’ ) ;
emite( ‘goto’ SIGINST+ 2 ) ;
CompletaLans( E.lv, SIGINST) ;
emite ( id.pos ‘:= 1’ ) ;
sino
emite ( id.pos ‘:=’ E.pos ) }
Figura 2.15. ETDS asignación de valor a variable lógica.
4.4.3. Instrucciones de control de flujo
En el ETDS de la Fig. 2.15 puede apreciarse que el código emitido para evaluar las
expresiones lógicas contiene instrucciones de salto cuyo destino no se conoce en el momento de
emitirla (referencias no satisfechas). Por lo tanto, en las instrucciones de control de flujo será
necesario realizar el relleno de los argumentos no satisfechos. En el ETDS de la Fig. 2.16 se
emite código intermedio para tres instrucciones típicas de los lenguajes de programación de alto
nivel (en este caso Pascal) suponiendo que las expresiones lógicas se representan mediante
control de flujo. Obsérvese que en el ETDS se asume que el código intermedio generado para
evaluar las expresiones lógicas realiza la evaluación de éstas mediante “cortocircuito”.
I ! if E then
I1 else
I2
{ CompletaLans( E.lv, SIGINST) }
{ I. listaSig := CreaLans( SIGINST) ;
emite( ‘goto’ --- ) ;
CompletaLans( E.lf, SIGINST) }
{ CompletaLans (I. listaSig, SIGINST) }
I ! while
E
do I1
{ I. inicio := SIGINST }
{ CompletaLans( E.lv, SIGINST) }
{ emite( ‘goto’ I.inicio ) ;
CompletaLans( E.lf, SIGINST) }
Generación de código intermedio
32
I ! for id := E1
to E2
do I1
{ id.pos := BuscaPos (id.nom) ;
emite( id.pos ‘:=’ E1.pos ) }
{ I. inicio := SIGINST ;
condic := CreaLans( SIGINST) ;
emite( ‘if’ id.pos ‘>’ E2.pos ‘goto’ -- ) }
{ emite( id.pos ‘:=’ id.pos ‘+’ 1 ) ;
emite( ‘goto’ I. inicio ) ;
CompletaLans (condic, SIGINST) }
Figura 2.16a. ETDS instrucciones de control de flujo del lenguaje Pascal con relleno por retroceso (expresiones lógicas con representación por control de flujo).
Ejemplo 2.4.- Para entender un poco mejor como funciona el ETDS anterior, se puede
comparar el código que se generaría para la asignación x:= a<b or (c<d and e<f)
suponiendo que se utilice una representación por control de flujo o numérica para las expresiones
lógicas. En la Fig. 2.17. se muestra el árbol anotado para la instrucción cuando se emplea una
representación por control de flujo.
Instruc
id.pos
=X := E
orE
oprel.op
=<(
E
id.pos
=a id.pos
=b E)
E Eand
oprel.op
=<id.pos
=c id.pos
=doprel.
op=<id.
pos=e
id.pos
=f
.lv
= {100,104}
.lf = {103,105}
.lv
= {100}
.lf = {101}
.lv
= {104}
.lf =
{103,105}
.lv
= {102}
.lf = {103}
.lv
= {104}
.lf = {105}
.lv
= {104}
.lf = {103,105}
Figura 2.17: Árbol anotado para la cadena x:=a<b or (c<d and e<f).
Generación de código intermedio
33
Repr. Numérica Repr. por control de flujo 100 t1 := 1 100 if a<b goto 108
101 if a<b goto 103 101 goto 102
102 t1 := 0 102 if c<d goto 104
103 t2 := 1 103 goto 106
104 if c<d goto 106 104 if e<f goto 108
105 t2 := 0 105 goto 106
106 t3 := 1 106 x := 0
107 if e<f goto 109 107 goto 109
108 t3 := 0 108 x := 1
109 t4 := t2 * t3 109
110 t5 := t1 + t4
111 x := t5
Ejemplo 2.5.- Veamos cuál sería el código intermedio generado para un bucle while-do de
Pascal, utilizando una representación por control de flujo para las expresiones lógicas
while a<b or (c<d and e<f) do a:=a+1
100 if a<b goto 106 101 goto 102 102 if c<d goto 104 E w 103 goto 110 h 104 if e<f goto 106 i 105 goto 110 l 106 t1 := 1 e 107 t2 := a + t1 I 108 a := t2 109 goto 100 110
El siguiente ETDS genera código para varias instrucciones de control del flujo en C
suponiendo una representación del valor de las expresiones lógicas por control de flujo y usando
relleno por retroceso para resolver el problema de los argumentos no satisfechos:
Generación de código intermedio
34
I ! if E
I1 else
I2
{ CompletaLans( E.lv, SIGINST) }
{ I. listaSig := CreaLans( SIGINST) ;
emite( ‘goto’ --- ) ;
CompletaLans( E.lf, SIGINST) }
{ CompletaLans (I.listaSig, SIGINST) }
I ! while
( E )
I1
{ I.inicio := SIGINST }
{ CompletaLans( E.lv, SIGINST) }
{ emite( ‘goto’ I.inicio ) ;
CompletaLans( E.lf, SIGINST) }
I ! do
I1
while ( E )
{ I.inicio:= SIGINST }
{ CompletaLans(E.listaverdad, I.inicio);
CompletaLans(E.listafalso, SIGINST) }
Figura 2.16b. ETDS instrucciones de control de flujo del lenguaje C con relleno por retroceso (expresiones lógicas con representación por control de flujo).
En [Aho 90] pueden encontrarse ETDS similares a los presentados en esta sección, en los que
se utiliza un atributo .siguiente asociado a un auxiliar S, para representar el número (o etiqueta)
de la instrucción siguiente a las generadas por el auxiliar S. Además, puede encontrarse en ese
tema cómo se genera código cuando el código intermedio permite la utilización de etiquetas
explícitas (similar a uno de los ejercicios que aparecen al final de este tema).
5. Subprogramas
En el capítulo anterior ya se vio con bastante detalle cuál sería la secuencia de carga y
descarga de un registro de activación para la llamada y finalización de un subprograma. En esta sección se presentan los ETDS que especifican la generación del código intermedio que se
encarga de realizar la carga y descarga de un registro de activación (Pascal) y marco (C).
5.1. Registros de activación en Pascal.
5.1.1. Declaración
Cuando se genera código para la declaración de un subprograma es necesario generar el código que realiza la parte de la secuencia del carga y descarga del registro de activación que
lleva a cabo el subprograma llamado. Se muestra a continuación como quedaría esta parte del
ETDS correspondiente a la declaración a una función en Pascal. Obsérvese como es necesario
utilizar relleno por retroceso para completar la instrucción de salto incondicional encargada de
saltar el posible código de subprogramas locales al subprograma que se está compilando.
Generación de código intermedio
35
La instrucción PUSH permite apilar el valor del display del nivel de anidamiento
correspondiente al subprograma que estamos compilando (PUSH Display[NIVEL]). Así mismo,
suponemos que el tope de la pila se encuentra almacenado en la posición (o registro) TOP.
Decl_Subprg ! function id (Param_Form):
Tipo;
Bloque ;
Decl_Subprg
{emite(‘push Display[‘NIVEL’]’);
emite(‘display[‘NIVEL’] := top’);
area_datos := CreaLans(SIGINST);
emite(‘incTop’ ---) ; }
{ CompletaLans (area_datos, DESP)
emite(‘top := Display[‘NIVEL’]’);
emite(‘pop Display[‘NIVEL’]’);
emite(‘ret’) }
Bloque ! Decl_Var
Decl_Subprg begin
Instrucciones end
{ inst := CreaLans(SIGINST);
emite(‘goto’ ----); }
{ CompletaLans(inst,SIGINST); }
Figura 2.18. ETDS carga/descarga de un registro de activación por el procedimiento llamado.
5.1.2. Llamada
Cuando se genera código para la llamada a un subprograma es necesario generar el código que realiza la parte de la secuencia de carga y descarga de un registro de activación que lleva a
cabo el subprograma llamador. Se muestra a continuación como quedaría el ETDS
correspondiente al lenguaje Pascal. Obsérvese como se utiliza la instrucción INCTOP para
reservar espacio para el valor devuelto por la función, y la instrucción PUSH para apilar los
parámetros actuales. Posteriormente, durante la secuencia de descarga del registro de activación,
se utiliza una instrucción DECTOP para decrementar el valor del tope de la pila y de esta forma
desapilar los parámetros que se cargaron al realizar la llamada.
E ! id
( Param_Act )
{ E.pos := CreaVarTemp();
emite(‘incTop Talla_Tipo(id.nom)) }
{ emite(‘push’ <estado máquina>);
emite(‘call’ BuscaPos(id.nom)) ;
emite(‘pop’ <estado máquina>);
emite(‘decTop’ TallaParam(id.nom)) ;
emite(‘pop’ E.pos) }
Param_Act ! E { emite(‘push’ E.pos) }
Param_Act ! Param_Act , E { emite(‘push’ E.pos) }
Figura 2.19. ETDS carga/descarga de un registro de activación por el procedimiento llamador.
Generación de código intermedio
36
5.2. Marcos en C
5.2.1. Declaración
Se muestra a continuación como quedaría un ETDS correspondiente a la declaración a una
función en C. La instrucción “push FP” que se emite, permitirá en tiempo de ejecución apilar el
valor del “frame pointer” correspondiente al subprograma que se estará cargando.
Decl_Subprg ! Tipo id ( Param_Form )
Bloque ;
{ emite(‘push FP’);
emite(‘FP := TOP’);
area_datos := CreaLans(SIGINST);
emite(‘TOP := TOP + ---‘) ; }
{ CompletaLans (area_datos, DESP)
emite(‘TOP := FP’);
emite(‘FP := pop);
emite(‘ret’) }
Figura 2.18. ETDS carga/descarga de un frame en C realizado por la función llamada.
5.2.2. Llamada
Al igual que en Pascal, en C cuando se genera código para la llamada a una función es necesario generar el código que realiza la parte de la secuencia de carga y descarga del frame de
la función llamada. La instrucción “top := top + Talla_Tipo(id.nom)” se emite para que al
ejecutarse reserve espacio para el valor devuelto por la función. De igual forma, la instrucción
“push” se emite para que en tiempo de ejecución apile los parámetros actuales. Durante la
secuencia de descarga del frame se utiliza decrementa el valor del tope de la pila para, de esta
forma, desapilar los parámetros que se cargaron al realizar la llamada.
E ! id
( Param_Act )
{ E.pos := CreaVarTemp();
emite(‘TOP := TOP +’ Talla_Tipo(id.nom)) }
{ emite(‘push’ <estado máquina>);
emite(‘call’ BuscaPos(id.nom)) ;
emite(‘pop’ <estado máquina>);
emite(‘TOP := TOP -’ TallaParam(id.nom)) ;
emite(‘pop’ E.pos) }
Param_Act ! E { emite(‘push’ E.pos) }
Param_Act ! Param_Act , E { emite(‘push’ E.pos) }
Figura 2.19. ETDS carga/descarga de un frame realizado por la función llamadora.
Generación de código intermedio
37
Bibliografía recomendada
En [Tremblay 85] se puede encontrar un tema dedicado a formas de representación de código
intermedio. El tema de generación de código intermedio que más coincide con el contenido de
este tema es el que aparece en [Aho 90].
[Aho 08] Aho, Alfred V.; Lam, Monica S.; Sethi, Ravi; Ullman, Jeffrey D. Compiladores:
Principios, técnicas y herramientas. Segunda edición Pearson/Addison-Wesley
Iberoamericana, 2008
[Tremblay 85] Tremblay, Jean-Paul; Sorenson, Paul G. The Theory and Practice of
Compiler Writing. McGraw-Hill, 1985
Generación de código intermedio
38
Ejercicios
*8.1.- La siguiente gramática genera bucles “for” muy parecidos a los de Pascal estándar pero
con algunos matices:
I ! for id := E to E S do I
| begin LI end
S ! step E
| "
LI ! I ; LI
| I
...
donde E es una expresión de tipo entero.
La opción “step E” que puede aparecer en la cabecera del bucle indica que la variable
contador (id) en vez de incrementarse en una unidad (como ocurre cuando no aparece la opción
(“step”), se incrementará en “E” unidades al final de cada iteración.
Construir un ETDS que incluya las acciones semánticas necesarias para la comprobación de
tipos y la generación de código intermedio. Deben emplearse listas de referencias no satisfechas
(relleno por retroceso) para el control de los saltos.
*8.2.- Dado el siguiente fragmento de una gramática que genera instrucciones de asignación con
sumas y multiplicaciones, donde las llaves “{ }” representan la clausura reflexivo-transitiva
(0 o más ocurrencias):
IS ! id := E
E ! T { + T }
T ! F { * F }
F ! num
| id
| ( E )
...
a) Construir un ETDS para la generación de código intermedio.
b) Partiendo del ETDS anterior, incluir en el analizador descendente recursivo asociado a la
gramática, las acciones necesarias para la generación de código intermedio.
NOTAS:
• No incluir comprobaciones de tipo.
Generación de código intermedio
39
• Emplear el juego de instrucciones de tres direcciones. (Ejemplo: “id.pos:=id.pos*5”)
• El analizador léxico devuelve para cada identificador su entrada en la TDS en la variable
“ind”, y para una constante numérica su valor en la variable global “num”.
• Se suponen definidas las siguientes funciones:
• CrearVarTemp(): Crea una variable temporal y devuelve su posición.
• BuscaPos(id.nom): Devuelve la posición de memoria asignada al identificador cuya entrada en la TDS es id.nom.
• En la declaración de cada parámetro formal, deberá indicarse si el uso es por valor o
referencia.
*8.3.- Dada la siguiente gramática (inspirada en la proposición “switch” del C):
I ! switch ( E ) { L }
L ! case num : P ; L
L ! default : P ;
L ! " ...
P ! P ; P
P ! break
P ! I
P ! " ...
Donde:
a) Tanto “E” como la constante “num” deben ser enteras.
b) Si la constante de un “case” coincide con el valor de la expresión E, se deberá
comenzar a ejecutar todas las instrucciones asociadas a ese “case” y a los que le siguen.
La proposición “break” provoca una salida inmediata del “switch”. Las instrucciones
asociadas al “default” se ejecutarán siempre (excepto si se encontró antes una
instrucción “break”).
Construir un ETDS que realice las acciones semánticas necesarias para la generación de
código intermedio de tres direcciones.
*8.4.- Construir un Esquema de Traducción Dirigido por la Sintaxis que realice las
comprobaciones semánticas y genere código de tres direcciones para una gramática con las
siguientes reglas de producción.
Generación de código intermedio
40
S ! case E of L end
L ! num : S ; L
L ! "
...
Tanto las constantes de selección, como la expresión, deben ser de tipo entero.
*8.5.- Dada la siguiente gramática:
P ! D ; I .
D ! id : T # D ; D # "
T ! pila ( num ) de TS # TS
TS ! entero # real
I ! apilar ( id , E ) # id := E # I ; I # "
E ! desapilar ( id ) # cima ( id ) # id
Donde:
pila (num) de TS, representa la definición del tipo pila, con un máximo de num
elementos de tipo entero o real.
apilar (id, E), indica que se apila el valor de E en la pila id
desapilar (id), devuelve el elemento que desapila de la pila id
cima (id) devuelve el elemento que hay en la cima de la pila id (no lo desapila).
Construir un ETDS que realice las acciones semánticas necesarias para la comprobación de tipos, la asignación de memoria y la generación de código tres direcciones teniendo en cuenta que:
a) La talla de los enteros y reales es 1.
b) No se debe permitir la asignación de pilas.
c) No hay coerción de entero a real.
Se deben realizar las comprobaciones dinámicas para los controles de desborde de pila y pila
vacía.
Generación de código intermedio
41
*8.6.- Dada la siguiente gramática:
P ! D ; I .
D ! id : T | D ; D | "
T ! cadena ( num ) | TS
TS ! entero | real | caracter ...
I ! concatena ( id , id ) | id := E | I ; I | "
E ! longitud ( id ) | id
Donde:
cadena(num) representa la definición de un objeto de tipo “cadena de caracteres”
cuyo tamaño máximo viene dado por num.
concatena(id1, id2) concatena al final de la cadena id1 la cadena id2.
longitud(id) devuelve el tamaño de la cadena id.
Construir un ETDS que realice las acciones semánticas necesarias para la comprobación de
tipos, la asignación de memoria y la generación de código de tres direcciones teniendo en cuenta
que:
a) La talla de los enteros, reales y caracteres es 1, y no hay coerción entre tipos.
b) Se deben realizar las comprobaciones dinámicas para controlar que no se sobrepasa el
tamaño máximo permitido de una cadena.
c) No se deben permitir las asignaciones entre cadenas.
NOTA: Se recomienda implementar una cadena declarada de tamaño máximo num, como un
vector de num+1 elementos, donde el elemento 0 se puede usar para almacenar la longitud actual
de la cadena.
*8.7.- Incorporar a la gramática siguiente, las acciones semánticas necesarias para la
comprobación de tipos, la asignación de memoria y la generación de código intermedio (tres
direcciones):
S ! D S
D ! D ; D | id : T
T ! char | integer | string(num)
S ! S ; S | id := E
E ! F + F | id (num : num)
F ! id | /id/
NOTAS:
Generación de código intermedio
42
1) string(num) representa una cadena de caracteres de talla máxima num.
2) id(a:b) representa la subcadena de id comprendida entre a y b.
3) /id/ es la longitud de la cadena id.
4) Será necesario considerar explícitamente la talla actual de las cadenas.
*8.8.- Dado el siguiente fragmento de gramática.
I ! while E do I Rw
Rw ! except E do I
Construye un ETDS para la comprobación de tipos y emisión de código intermedio. La sentencia “while-do” tiene el comportamiento estándar de Pascal, pero con un pequeño matiz: La parte “except” permite indicar una instrucción alternativa a ejecutar en las iteraciones en las que se cumple la condición booleana introducida por la palabra “except”. La expresión del “except” se evalúa antes de entrar en el cuerpo del bucle.
Ejemplo: a := 9 ;
while a < 15 do
begin a:= a +1; write(a) end => 10 11 12 13 15
except a=13 do a:= a+1;
*8.9.- Escribe un ETDS que genere código intermedio para el siguiente fragmento de una gramática independiente del contexto, correspondiente a un lenguaje de programación. La semántica de la instrucción loop es la siguiente: Si la expresión “E” que sigue a loop es cierta se ejecutan las instrucciones que siguen al then y preceden a exitloop, y la instrucción loop finaliza, en caso contrario, se irán evaluando sucesivamente las expresiones que siguen al elseif hasta que una sea cierta, en cuyo caso, se ejecutan las instrucciones que siguen a ese then y el flujo del programa vuelve al principio de la instrucción loop. Si ninguna expresión es cierta se ejecutan las instrucciones que siguen al else y se vuelve al principio de la instrucción loop.
I ! loop E then I exitloop O
O ! elseif E then I O
O ! else I endloop
I ! I ; I
E ! E oprel E
El diagrama de flujo de la instrucción sería el de la figura.
Generación de código intermedio
43
*8.10.- Definir un ETDS que
realice la generación de código intermedio para la instrucción WHILE-DO, en la que se
permita la posibilidad de una instrucción EXITLOOP. La instrucción Exitloop supone la
salida inmediata del bucle. Para ello utilizar la sintaxis propia de Pascal.
*8.11.- Dada la siguiente gramática (inspirada en la proposición “for” del C), construir un ETDS
que realice las acciones semánticas necesarias para la comprobación de tipos y la generación
de código intermedio de tres direcciones usando una representación por control de flujo de
las expresiones boolenas.
I ! for ( I1 ; E ; I2 ) { P }
...
P ! I ; P
P ! I
...
Donde:
a) Tanto I1 como I2 son instrucciones, típicamente de asignación o llamadas a función. I1 se
ejecutará antes del bucle e I2 dentro del bucle después de las instrucciones asociadas a la
proposición P.
b) E debe ser una expresión booleana que determine la condición de finalización del bucle.
*8.12.- Construir un ETDS que realice la declaración, la comprobación estática de tipos y la generación de código intermedio para la siguiente gramática:
E
E
E
E
I
I
I
I
True
False
False
False
False
True
True
True
I
...
salida
Generación de código intermedio
44
P ! D ; I .
D ! id : T | D ; D | !
T ! integer | set of [ num ]
E ! member ( E , id ) | num | id
...
I ! for-each id in id do I
Siendo:
set of [ num ] un tipo de datos que representa un conjunto cuyos elementos pueden ser {0, 1,
2, 3, . . . num}. Por ejemplo: “a: set of [5]” declara la variable a cuyo valor puede ser cualquier
subconjunto de {0, 1, 2, 3, 4, 5}.
member(E, id) una función que devuelve TRUE (ó 1) si el elemento E está en el conjunto id
(FALSE ó 0, en caso contrario).
for-each id1 in id2 do I una instrucción donde la variable entera “id1” toma todos los valores
que corresponden a la variable conjunto id2 . Además, la instrucción I se ejecutará para cada uno
de los valores de id1 cuando este valor pertenezca al conjunto id2. Por ejemplo:
a: set of [5];
.....
a := {1, 3, 5}
for-each n in a do print (n);
producirá como resultado: 1, 3, 5.
Nota.- Se recomienda representar las variables de tipo conjunto “set of [num]” como un “num + 1” posiciones consecutivas de memoria. Donde cada posición de memoria representará la pertenencia (1 ó 0) de un elemento al conjunto. Por ejemplo, la variable “a”, de"nida anteriormente, se representaría por 6 posiciones de consecutivas memoria, que si contuviesen los valores (1, 0, 1, 0, 0, 1) indicarían que el conjunto a contiene los elementos {0, 2, 5}.
8.13.- Dada la siguiente gramática, construir un ETDS que realice: las comprobaciones semánticas y de tipo usuales en la instrucción FOR y la generación de código intermedio. Utilizar listas de referencias no satisfechas para la gestión de las expresiones booleanas.
S ! for id := E R E do S
R ! to | downto
8.14.- Construir un ETDS que genere código intermedio para comprobar en tiempo de ejecución que los índices de una matriz están dentro de los límites definidos en su declaración. Para la generación de este código, se puede suponer la existencia de otros atributos necesarios, cuya evaluación se realiza en otras producciones, siempre que se indique su carácter sintetizado o heredado.
Generación de código intermedio
45
8.15.- Considérese la siguiente instrucción cuya semántica viene dada por el diagrama de flujo que se adjunta. Diseñar el ETDS para la generación de código tres direcciones. Utilizar listas de referencias no satisfechas (relleno con retroceso) para el control de los saltos.
S ! pseudowhile id , E’ until E” do S’ then S”
8.16.- Dada la siguiente gramática que representa registros en Pascal, construir un ETDS que realice:
P ! D ; I
D ! D ; D | id : T
T ! integer | real | record LC end
LC ! id : T RLC
RLC ! ; LC | " I ! L := E
L ! L . id | id
E ! E + E | L
a) Las comprobaciones semánticas de: Compatibilidad de tipos, los tipos de los campos de un RECORD solo pueden ser de tipo simple y no se permite la asignación de registros.
b) El cálculo de las posiciones relativas de los objetos en memoria.
c) La generación de código intermedio para las expresiones e instrucciones.
d) Extender el ETDS para generar código intermedio para la asignación de registros.
e) Extender el ETDS para permitir registros con parte variante.
8.17.- La siguiente gramática representa expresiones en las que un operador (suma o producto) actúa sobre una lista de operandos que se encuentran entre paréntesis junto al operador. Además, alguno de estos operandos puede ser a su vez toda una expresión entre paréntesis.
S ! ( + P L ) | ( * P L )
P ! id | S
L ! P L | "
a) Escribir en PASCAL las rutinas del Análisis Sintáctico por Descenso Recursivo LL(1)
para esta gramática.
b) Introducir en el esquema anterior las acciones semánticas necesarias para la generación
de código tres direcciones.
id > E'
id > E''
S' S''
No
NoSi
Si
Generación de código intermedio
46
8.18.- Suponiendo que una matriz A[a1..b1,a2..b2,...an..bn] se representa en memoria por el método de división en subtablas (tal y como se ilustra en la figura). En las subtablas de los niveles 1..n-1 únicamente aparecen referencias (de talla 1) a los orígenes de las subtablas de los niveles inmediatamente siguientes. Solo en las subtablas de nivel n se almacenarán los elementos de la matriz.
a) Determinar la función de acceso para el elemento genérico A[i1,i2,...in].
b) Diseñar el ETDS para la generación del código intermedio correspondiente, para la
siguiente gramática.
S ! L := E
E ! L
L ! id
L ! I ]
I ! I , E
I ! id [ E
8.19.- Dada la siguiente gramática:
P! var D_V I
D_V ! D_V D_V | Lista_id : T ;
Lista_id ! Lista_id , id | id
T ! record ListaCampos Cvar end | T_simple
T_simple ! T_disc | integer | real
ListaCampos ! ListaCampos ; ListaCampos | id : T_simple
T_disc ! num..num | boolean
Cvar ! case id : T_disc of LCV | "
LCV ! LCV ; LCV | Val_sel : ( ListaCampos )
Val_sel ! num | id
I ! id := E | id . id := E | if E then I else I
E ! E oprel E | id . id | id
a) Dar un ETDS que realice el cálculo de las posiciones relativas de las variables en
memoria para esta gramática.
a1
b1
i1 i2
a2
b2
.
.
2
.
.
in-1in
.
.
.
.
n-1
.
.
n
.
.
an
bn
A[i1,i2,..in]
1
Generación de código intermedio
47
b) Ampliar el anterior ETDS para que realice la comprobación de tipos y genere el código
intermedio necesario para evaluar las expresiones y realizar las asignaciones adecuadas en
dicha gramática.
NOTA: Diseñar la estructura de la tabla de símbolos que permita la realización de los
apartados anteriores. Igualmente, describir brevemente los atributos, así como los perfiles y
acciones realizadas por las funciones y/o procedimientos que utilicéis.
*8.20.- El código intermedio definido en este capítulo se modifica para permitir generar etiquetas
y utilizarlas como destino de los saltos (similares a las utilizadas en los lenguajes
ensambladores). Por ejemplo, podríamos generar secuencias de código intermedio similares a:
if e<f goto L1
goto L2
L2: a:= a-1
L1: a := a + 1
a) Construye un ETDS que genere este código intermedio para la evaluación de expresiones
lógicas (mediante representación por control de flujo) para la siguiente gramática:
E ! E or E | E and E | not E
| ( E ) | E oprel E
| true | false | id
b) Construye otro ETDS que genere código intermedio para las instrucciones representadas
por la siguiente gramática, que sea coherente con el ETDS del apartado anterior.
Instruc ! if E then Instruc else Instruc
| while E do Instruc
| for id := E to E do Instruc
| id := E /* de tipo lógico */
c) ¿Qué código generará tu ETDS para las siguientes cadenas?
x := a<b or (c<d and e<f)
while a<b or (c<d and e<f) do a:=a+1
while (not a) or (x < b and true) do x := x + 1
Generación de código intermedio
48
Soluciones a los ejercicios seleccionados
8.1.-
E 1
id .p o s := E 1.p o s
E 2
if id > E 2 g o to
g o to
(s te p ) E
g o to
I
g o to
id := id + E
I ! for id := E1
to E2
S
do I
{ si E1.tipo <> tentero ent MemError()
id.pos:=BuscaPos(id.nom);
emite(id.pos ‘:=’ E1.pos) }
{ si E2.tipo <> tentero ent MemError()
fin:=CreaLans(SIGINST); comi:=SIGINST;
emite(‘if’ id.pos ‘>’ E2.pos ‘goto’ ---)
cont:=CreaLans(SIGINST);
emite(‘goto’ -- ); inc:=SIGINST; S.pos:=id.pos; }
{ emite(‘goto’ comi); CompletaLans(cont, SIGINST) }
{ emite(‘goto’ inc);
CompletaLans(fin, SIGINST) }
S ! step E { si E.tipo <> tentero ent MemError()
emite(S.pos ‘:=’ S.pos ‘+’ E.pos) }
S ! " { emite(S.pos ‘:=’ S.pos ‘+’ 1) }
8.2.-
a) E ! T1
{ +
T2
}
{ E.pos:=CreaVarTemp(); emite (E.pos ’:=’ T1.pos) }
{ emite(E.pos ‘:=’ E.pos ‘+’ T2.pos ) }
T ! F1
{ *
F2
{ T.pos:=CreaVarTemp(); emite (T.pos ‘:=’ F1.pos) }
{ emite(T.pos ‘:=’ T.pos ‘*’ F2.pos ) }
Generación de código intermedio
49
}
F ! num { F.pos:=CrearVarTemp();
emite(F.pos ‘:=’ num.val) }
F ! id { F.pos:=BuscaPos(id.nom) }
F ! ( E ) { F.pos:=E.pos }
IS ! id := E { id.pos:=BuscaPos(id.nom);
emite(id.pos ‘:=’ E.pos) }
b)
Procedure E ( var E.pos );
var
T1.pos, T2.pos
begin
T(T1.pos); E.pos:=CrearVarTemp(); emite (E.pos`:=´T1.pos);
while sim=mas do
begin
obtsym;
T(T2.pos); emite(E.pos ‘:=’ E.pos ‘+’ T2pos);
end;
end; { procedure E }
Procedure T ( var T.pos );
var
F1.pos, F2.pos
begin
F(F1.pos); T.pos:=CrearVarTemp(); emite(T.pos `:=´F1.pos);
while sim=multi do
begin
obtsym;
F(F2.pos); emite(T.pos ‘:=’ T.pos ‘*’ F2.pos);
end;
end; { procedure T }
Procedure F ( var F.pos );
var
E.pos
begin
case sim of
num:
F.pos:=CrearVarTemp(); emite(F.pos:=num.val);
obtsym;
identificador:
F.pos:=BuscaPos(id.nom);
obtsym;
abrepar:
Generación de código intermedio
50
obtsym;
E(E.pos); F.pos:=E.pos;
empareja(cierrapar);
end; { case }
end; { prcedure F }
Procedure IS;
var
E.pos, id.pos
begin
id.pos:=BuscaPos(id.nom);
empareja(identificador);
empareja(asignación);
E(E.pos); emite(id.pos:=E.pos);
end; { procedure IS }
8.3.-
E
if E<>cte1 goto
P1
goto
P2
goto (break)
if E<>cte2 goto
goto
P (default)
...
En la solución propuesta se puede observar que se genera una instrucción “goto” inmediatamente a continuación del cuerpo de un “case” cuya finalidad es saltar al cuerpo del siguiente “case”. Con esto se consigue que si se cumple la igualdad de la constante de un “case”, tras ejecutarse su cuerpo se ejecuten las instrucciones del cuerpo de todos los “case” que le siguen (hasta que se encuentre un salto generado para un break).
I ! switch ( E )
{ L }
{ L.pos:=E.pos; L.anterior := nil; L.fin:=nil}
L ! case num :
P ;
{ sigcond:=CreaLans(SIGINST) ;
emite(‘if’ num.val ‘<>’ L.pos ‘goto’ --);
CompletaLans(L.anterior, SIGINST) }
{ L1.anterior:=CreaLans(SIGINST) ; emite (‘goto’ --);
Generación de código intermedio
51
L1
L1.pos:=L.pos; CompletaLans(sigcond, SIGINST);
L1.fin:=FusionaLans(P.fin, L.fin) }
L ! default :
P ;
{ CompletaLans(L.anterior, SIGINST) }
{ CompletaLans(L.fin, SIGINST);
CompletaLans(P.fin, SIGINST) }
L ! " { CompletaLans(L.anterior, SIGINST)
CompletaLans(L.fin, SIGINST);}
P ! P1 ; P2 { P.fin:=FusionaLans(P1.fin,P2.fin) }
P ! break { P.fin:=CreaLans(SIGINST); emite(‘goto’ --) }
P ! I { P.fin:=I.fin }
P ! " { P.fin:=nil }
8.4.-
E
if E .p o s< > c te .va l g o to
S
g o to
S
if E .p o s < > ... g o to
g o to
...
L ! num :
S ;
L1
{ si num.tipo $ tentero ent MemError() ;
sig:=CreaLans(SIGINST);
emite(‘if’ num.val ‘<>’ L.pos ‘goto’ --) }
{ falsa:=CreaLans(SIGINST);
emite(‘goto’ --);
CompletaLans(sig, SIGINST);
L1.pos:=L.pos }
{ L.fin:=FusionaLans(falsa, L1.fin) }
L ! " { L.fin:=nil }
S ! case E
of L
{ si E.tipo $ tentero ent MemError();
L.pos:=E.pos }
{ CompletaLans(L.fin, SIGINST) }
8.5.-
Generación de código intermedio
52
Es importante observar que en tiempo de compilación no conocemos el tamaño de la pila, solo conocemos el tamaño máximo de la pila. Por lo tanto, será necesario reservar una posición de memoria para que en tiempo de ejecución se almacene allí el tope de la pila. Podemos implementar la pila de ‘num’ elementos como un vector (array) de ‘num+1’ elementos, donde el elemento 0 se puede usar para almacenar el tamaño actual (tope) de la pila. Cada vez que apilemos un nuevo elemento, lo haremos sobre la posición indicada en pila[0] (dirección base del vector). A continuación incrementaremos el valor de pila[0]. De esta forma, las acciones semánticas para generar código para apilar, desapilar, y obtener la cima podrían ser:
I ! apilar ( id ,
E )
{ pos := BuscaPos (id.nom) ;
emite ( pos ‘:=’ pos ‘+’ 1 );
emite (‘if’ pos ‘<=’ limite ‘goto’ SIGINST +2 )
emite (‘fin’) }
{ emite ( pos ’[‘ pos ’] :=’ E.pos) }
E ! desapilar (id) { pos := BuscaPos (id.nom) ;
emite (‘if’ pos ‘>’ 0 ‘goto’ SIGINST + 2 ) ;
emite (‘fin’) ;
E.pos := CrearVarTemp() ;
emite ( E.pos ‘:=’ pos ’[‘ pos ’]’ );
emite (pos‘:=‘ pos ‘-‘ 1) }
E ! cima (id) { pos := BuscaPos (id.nom) ;
emite (‘if’ pos ‘> 0 goto’ SIGINST +2 );
emite (‘fin’)
E.pos := CrearVarTemp();
emite (E.pos ‘:=’ pos ’[‘ pos ’]’ ) }
En el ETDS anterior hemos se puede ver que pos[0] y pos hacen referencia al mismo valor, ya que es lo mismo tomar el contenido de la dirección base del vector (pos) o el
contenido de la dirección resultante de sumar a la base del vector un desplazamiento cero (pos[0]) .
El ETDS, incluyendo algunas comprobaciones de tipo quedará finalmente: P !
D ; I .
{ DESP:=0 }
D ! id : T { InsertarTDS (id.nom, T.tipo, DESP);
DESP := DESP + T.talla ;
si T.tipo = Tpila(limite,tipo) ent emite (DESP ‘:=’ 0) }
D ! D ; D
D ! "
T ! pila ( num ) de { si num.val <=0 ent T.tipo := terror
Generación de código intermedio
53
TS sino T.tipo:= tpila (num.val, TS.tipo) ;
T.talla := TS.Talla * (num.val +1) }
T ! TS { T.talla := TS.talla ; T.tipo := TS.tipo }
TS ! entero { T.talla := 1 ; T.tipo := tentero }
TS ! real { T.talla := 1 ; T.tipo := treal }
I ! apilar ( id , E )
{ si BuscaTipo (id.nom) <> tpila (limite,tipo) ent I.tipo := terror
sino pos := BuscaPos (id.nom) ;
emite ( pos ‘:=’ pos ‘+’ 1 );
emite (‘if’ pos ‘<=’ limite ‘goto’ SIGINST + 2 )
emite (‘fin’)
si E.tipo <> tipo ent I.tipo := terror
sino emite ( pos’[‘ pos ‘] :=’ E.pos)
I.tipo := tvacío }
I ! id := E { id.tipo := BuscaTipo (id.nom) ;
si id.tipo = tpila (limite, tipo) ent I.tipo := terror
sino id.pos = BuscaPos (id.nom) ;
si id.tipo <> E.tipo ent I.tipo := terror
sino emite (id.pos ‘:=’ E.pos) ;
I.tipo := tvacio }
I ! I ; I
I ! "
E ! desapilar ( id ) { si BuscaTipo (id.nom) <> tpila (limite, tipo ) ent E.tipo := terror
sino pos := BuscaPos (id.nom) ;
emite (‘if’ pos ‘> 0 goto’ SIGINST + 2 ) ;
emite (‘fin’) ;
E.pos := CrearVarTemp() ;
emite ( E.pos ‘:=’ pos ’[‘ pos ’]’ );
emite ( pos ‘:=’ pos ‘-‘ 1 ) ;
E.tipo := tipo }
E ! cima ( id ) { si BuscaTipo (id.nom) <> tpila (limite, tipo) ent E.tipo := terror
sino pos := BuscaPos (id.nom) ;
E.pos := CrearVarTemp();
emite (E.pos ‘:=’ pos ’[‘ pos ’]’ );
E.tipo := tipo }
E ! id { E.tipo := BuscaTipo (id.nom) ; E.pos := BuscaPos (id.nom) }
Generación de código intermedio
54
8.6.-
P ! { DESP:=0 } D ; I .
D ! id : T
| D ; D
| "
{ InsertaTDS(id.nom, T.tipo, DESP) ; DESP:=DESP+T.talla }
T ! cadena ( num )
| TS
{ T.tipo:=tcadena(num.val) ; T.talla:=num.val }
{ T.tipo:=TS.tipo ; T.talla:=TS.talla }
TS ! entero
| real
| caracter
{ TS.tipo:=tentero ; TS.talla:=1 }
{ TS.tipo:=treal ; TS.talla:=1 }
{ TS.tipo:=char ; TS.talla:=1 }
I ! concatena ( id1 , id2)
| id := E
| I ; I
| "
{ id1.pos:=BuscaPos(id1.nom); id2.pos:=BuscaPos(id2.nom);
si BuscaTipo(id1.nom)=BuscaTipo(id2.nom)=tcadena
ent
I.tipo:=tvacío;
t1:=CrearVarTemp();
emite ( t1 ‘:=’ 1);
inicio:=SIGINST;
listaFin:=CreaLans(SIGINST);
emite ( ‘if’ t1 ‘>’ id2.pos ‘goto’ -- );
emite ( id1.pos ‘:=’ id1.pos ‘+’ 1 );
emite (‘if’ id1.pos’<=’Talla(id1.nom) ‘goto’ SIGINST+2 );
emite ( ‘fin’ );
emite ( id1.pos ’[‘ id1.pos ’]:=‘ id2.pos ’[‘ t1 ’]’ );
emite ( t1 ’:=’ t1 ’+’ 1 );
emite ( ‘goto’ inicio );
CompletaLans ( listaFin, SIGINST );
sino I.tipo:=terror; }
{ si (BuscaTipo(id.nom):=E.tipo) $ tcadena $terror
ent emite( BuscaPos(id.nom) ‘:=’ E.pos );
I.tipo:=tvacío;
sino I.tipo:=terror; }
E ! longitud ( id )
| id
{ si BuscaTipo(id.nom) $ tcadena ent E.tipo:=terror;
sino E.tipo:=tentero;
E.pos:=BuscaPos(id.nom); }
{ E.tipo:=BuscaTipo(id.nom); E.pos:=BuscaPos(id.nom); }
Se puede observar que en lugar de usar la técnica de relleno por retroceso con:
listaFin:=CreaLans(SIGINST);
Generación de código intermedio
55
emite ( ‘if’ t1 ‘>’ id2.pos ‘goto’ -- );
en este caso podíamos haber contado las instrucciones que separaban la instrucción de salto y su
destino, y haber emitido directamente la instrucción
emite ( ‘if’ t1 ‘>’ id2.pos ‘goto’ SIGINST +7 ) */
8.7.-
Las comprobaciones de tipo serán las típicas del bloque de declaraciones. Para asignar memoria basta con emplear una variable global que se incremente (según la talla del elemento definido) cada vez que se declare una variable. En el caso de una cadena de caracteres (tcadena), la talla nos viene determinada por el valor de la constante num (más uno por la razón que se explica más adelante).
Respecto a la generación de código intermedio, se trata de un ejercicio muy parecido a los dos anteriores. De forma parecida a como ocurría en ellos, en tiempo de compilación solo conocemos el tamaño máximo de la cadena, pero no su tamaño real en un instante dado de la ejecución. Una solución consistiría en reservar la primera posición de la cadena para que en tiempo de ejecución se almacene su tamaño. En este caso, cada vez que realicemos una asignación de cadenas, será necesario calcular el tamaño de la cadena resultado. Con este fin, se puede observar que la instrucción para obtener una subcadena: id(num1:num2) nos devuelve una cadena de tamaño num2 .val - num1.val + 1.
8.8.-
El esquema del código que queremos generar será:
E
I1
goto
goto
.v
.f
E (RW).v
.f
I (RW)
El ETDS pedido quedaría:
I ! while
E do
I1
RW
{ RW.inicio := SIGINST }
{ si E.tipo <> tlógico ent MemError() ;
RW.cuerpo := SIGINST }
{ emite (‘goto’ RW.inicio) ;
CompletaLans (E.lv , SIGINST) }
{ CompletaLans (E.lf, SIGINST) }
Generación de código intermedio
56
RW ! except E
do I
{ si E.tipo <> tlógico ent MemError() ;
CompletaLans (E.lv, SIGINST) ;
CompletaLans (E.lf, RW.cuerpo) }
{ emite (‘goto’ RW.inicio) }
8.9.-
Solución usando representación por control de flujo para las expresiones lógicas.
I ! loop
E then
I exitloop
O
{ O.inicio := SI; }
{ CompletaLans(E.lv, SI); }
{ fin:= CreaLans(SI); emite (‘goto’ -); CompletaLans(E.lf, SI);}
{ CompletaLans(fin, SI); }
O ! elseif E
then I
O1
{ CompletaLans(E.lv, SI); }
{ emite(‘goto’ O.inicio) ; CompletaLans(E.lf, SI);
O1. inicio := O.inicio ;}
O ! else I endloop { emite(‘goto’ O.inicio) ;}
I ! I ; I
E ! E oprel E { E.lv := CreaLans( SI) ;
emite( ‘if’ E1.pos oprel.op E2.pos ‘goto’ --- ) ;
E.lf := CreaLans( SI ) ;
emite( ‘goto’ --- ) }
8.10.-
Vamos a usar la gramática que se muestra a continuación, donde I representa una instrucción, y E una expresión. El no-terminal I tendrá más producciones, pero las que nos interesan son las relacionadas directamente con la generación de código intermedio para las instrucciones ‘while-do’ y ‘Exitloop’.
I ! while E do I
I ! Exitloop
I ! I ; I
I ! …
Usaremos listas de referencias no satisfechas (relleno por retroceso) para el control de los saltos. El no-terminal E, que representa la expresión lógica, tendrá asociados dos atributos: E.lv y E.lf que representan respectivamente las listas de referencias no satisfechas de saltos para cuando la expresión es cierta (E.lv) y para cuando es falsa (E.lf).
Generación de código intermedio
57
SIGINST, es una variable global que contiene la etiqueta de la siguiente instrucción de código intermedio que se va a generar.
I ! while
E
do I1
{ inicio := SIGINST }
{ CompletaLans (E.lv, SIGINST) }
{ emite (‘goto’ inicio)
CompletaLans (I1.salida, SIGINST) ;
CompletaLans (E.lf, SIGINST) ;
I.salida := nil }
I ! Exitloop { I.salida := CreaLans (SIGINST) ;
emite (‘goto’ ---) }
I ! I1 ; I2 { I.salida:= FusionaLans (I1.salida, I2.salida) ; }
8.11.-
El esquema del código intermedio que queremos generar es el siguiente:
I1
E
I2
goto
P
goto
.v
.f
El ETDS por tanto quedaría:
I ! for ( I1 ;
E ;
I2 )
{ P }
{ inicio:=SIGINST; }
{ si E.t $ tlógico ent MenError()
sino incr:=SIGINST; }
{ emite(‘goto’ inicio);
CompletaLans(E.lv, SIGINST) }
{ emite(‘goto’ incr);
CompletaLans(E.lf, SIGINST) }
Generación de código intermedio
58
8.12.-
P ! D ; I .
D ! id : T { insertaTDS( id.nom, T.tipo) }
| D ; D
| "
T ! set of [ num ] { T.tipo := conjunto(num.val) }
| integer { T.tipo := entero }
E ! member ( E1 , id ) { si (E1.tipo <> entero) or (BuscaTipo(id.nom) <> conjunto(n))
ent error;
E.tipo := lógico;
E.pos := CreaVarTemp;
emite( E.pos := id.pos[E1.pos]) }
| num { E.tipo := entero;
E.pos:= CreaVarTemp;
emite(E.pos:= num.val) }
| id { E.tipo := BuscaTipo(id.nom)
E.pos := BuscaPos(id.nom) }
I! for_each id1 in id2
do I for_each id1 in id2
{ si (BuscaTipo(id1.nom)<> entero)
or (BuscaTipo(id2.nom) <> conjunto(n))
ent error;
t1 := CreaVarTemp;
emite (id1.pos := 0);
fin := Crealans(si);
inicio := si;
emite( if id1.pos > buscalimite(id2.nom) goto __);
emite (t1 := id2.pos[id1.pos]);
salta := Crealans(si);
emite( if t1 = 0 goto __); }
{ completalans(salta, si);
emite( id1.pos := id1.pos + 1);
emite( goto (inicio));
completalans(fin, si); }
Generación de código intermedio
59
8.20.-
a) Usaremos la función CreaEtiqueta par crear una etiqueta. Para generar mediante el procedimiento emite una etiqueta bastará con realizar una llamada del tipo:
emite (<NombreEtiqueta> ‘:’) Se puede observar la similitud de este ETDS con el construido usando relleno por retroceso para la misma gramática:
- Las instrucciones generadas, excepto los destinos de los saltos que en este ETDS son etiquetas, son las mismas. - Donde se emite una etiqueta que anteriormente se usó como destino de un salto, en el ETDS mediante relleno por retroceso aparecía un completaLans.
El ETDS para las expresiones lógicas quedaría:
E !
E1 or
E2
{ E1.f := CreaEtiqueta(); E1.v := E.v }
{ emite( E1.f ‘:’);
E2.v := E.v; E2.f := E.f }
|
E1 and
E2
{ E1.v := CreaEtiqueta(); E1.f := E.f }
{ emite( E1.v ‘:’);
E2.v := E.v; E2.f := E.f }
| not E1 { E1.v := E.f ;
E1.f := E.v }
| ( E1 ) { E1.v := E.v ;
E1.f := E.f }
| E1 oprel E2 { emite( ‘if’ E1.pos oprel.op E2.pos ‘goto’ E.v );
emite( ‘goto’ E.f ) }
| True { emite( ‘goto’ E.v ) }
| False { emite( ‘goto’ E.f ) }
| id { emite( ‘if’ id.pos ‘= 0 goto’ E.f ) ;
emite( ‘goto’ E.v ) }
Instruc ! id :=
E
{ E.v := CreaEtiqueta(); E.f := CreaEtiqueta()
fin := CreaEtiqueta() }
{ emite( E.f ‘:’) ;
emite( id.pos ‘:= 0’ ) ,
emite( ‘goto’ SIGINST + 2 ) ,
emite( E.v ‘:’);
emite ( id.pos ‘:= 1’ ) ; emite( fin ‘:’ ) }
Generación de código intermedio
60
b)
Instruc !
if E then
Instruc1 else
Instruc2
{ E.v:= CreaEtiqueta(); E.f:= CreaEtiqueta(); fin:= CreaEtiqueta() }
{ emite( E.v ‘:’ ) }
{ emite( ‘goto’ fin ) ; emite( E.f ‘:’) }
{ emite( fin ‘:’) }
| while
E
do Instruc1
{ Inicio := CreaEtiqueta() ; E.t := CreaEtiqueta() ;
E.f := CreaEtiqueta() ; emite(inicio ‘:’) ’:’ }
{ emite( E.v ‘:’) }
{ emite( ‘goto’ inicio ) ;
emite( E.f ‘:’) }
| for id := E1 to E2 do Instruc1
{ id.pos := BuscaPos (id.pos) ;
emite( id.pos := E1.pos ) }
{ inicio := CreaEtiqueta() ;
fin := CreaEtiqueta() ;
emite( inicio ‘:’) ;
emite( ‘if’ id.pos ‘>’ E2.pos ‘goto’ fin ) }
{ emite( id.pos ‘:=’ id.pos ‘+’ 1 ) ;
emite( ‘goto’ inicio ) ;
emite( fin ‘:’) }
c) x := a<b or (c<d and e<f)
if a<b goto L1
goto L3
L3: if c<d goto L4
goto L2
L4: if e<f goto L1
goto L2
L2: x := 0
goto L5
L1: x := 1
L5:
while a<b or (c<d and e<f) do a:=a+1
L0:
if a<b goto L1
goto L3
L3: if c<d goto L4 E w
goto L2 h
L4: if e<f goto L1 i
goto L2 l
L1: t1 := 1 I e
t2 := a + t1
a := t3
goto L0
L2: