!"#$"%&'%()*+'*,&)%
Procesadores de lenguajes
-.%/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#"+;).....................................................................................................................)//*('!.!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?!
U nidad t emática 8: G eneración de c ódigo i ntermedio
C.%D8#=3)13"4"2%.............................................................................................................%5:! @#"#!D*7%(-+'(!)*!./-%M./%6,!*,!I.(/.5#!###################################################################################################################!91! <.?.?.)I5%3$1$%&'().............................................................................................................................................................)E/! <.?.>.)J3$A$4$)...................................................................................................................................................................)E.?.)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! V . 11.1
Enviad sugerencias y/o errores a la dirección de correo
[email protected]
2
Generación de código intermedio
Generación de código intermedio
• 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.
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.
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. 3
Sparc
Fortran
Pascal
PowerPC
Pascal
C
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.
Fortran
C++
x86
C
ARM
C++
(a)
Sparc PowerPC Representación Intermedia
x86 ARM
(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 4
Generación de código intermedio
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.
:=
x
Generación de código intermedio
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.
/
+
a
La forma general de una instrucción de dos direcciones es
d
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
*
b
c
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
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 (1) (2) (3) (4)
MULT b SUMA a DIV t2 ASIGNA t3
c t1 d
res t1 t2 t3 x
Operador
op1 op2
(1) (2) (3) (4)
b c a (1) (2) d (3) x
MULT SUMA DIV ASIGNA
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. A signación x := y op z Donde op es una operador aritmético. x := op z Siendo op un operador unario.
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 := z Salto incondicional
5
6
Generación de código intermedio
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
Generación de código intermedio
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:
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.
<nombre_etiqueta> :
Pila de activación
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.
push x Apila x en la pila de activación.
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.
pop Devuelve el valor que se desapila. Llamada y retorno de subprograma call E Salta a la instrucción etiquetada E.
El código intermedio que se emplea en este capítulo utiliza etiquetas implícitas.
ret Finaliza el subprograma y salta a la dirección que se encuentra en la cima de la pila de activación. A signaciones 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. A cceso a la pila de activación en las instrucciones de asignación al “frame pointer” (FP) y al tope de la pila (TOP).
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.
7
8
Generación de código intermedio
Generación de código intermedio
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.
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:
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:
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.
Asig ! id = E E ! E1 + E2
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.
9
E ! id
{ Asig.pos := BuscaPos ( id.nom ) ; emite ( Asig.pos ‘:=’ E.pos ); } { E.pos := CrearVarTemp() ; emite ( E.pos ‘:=’ E1.pos ‘+’ E2.pos ) } { 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 10
Generación de código intermedio
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.pos :=Asig.pos } E ! Asig
Generación de código intermedio
TDS Nom a b c
tipo
posición [nivel, desp]
tentero tentero tentero
[0,24] [0,26] [0,28]
donde se puede observar que una expresión puede ser una asignación. Instruc
La siguiente producción del ETDS es: E ! E1 + E2
{ E.pos := CrearVarTemp() ; emite ( E.pos ‘:=’ E1.pos ‘+’ E2.pos ) }
id.pos=[0,24]
La primera acción de esta producción realiza una llamada a la función CrearV arTemp(). 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.
=
E.pos = [0,30]
Código tres direcciones generado E.pos = [0,26]
+
E.pos = [0,28]
(1) [0,30] := [0,26] + [0,28] (2) [0,24] := [0,30]
id.pos=[0,26]
id.pos=[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
Ejemplo:
(2) a := t1
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.
11
Veamos a continuación como se puede extender el ETDS anterior para generar código también para otras expresiones aritméticas sencillas.
12
Generación de código intermedio
E ! E1 * E2 E ! num E ! ( E1 ) E ! - E1
Generación de código intermedio
2. a de una estructura y elementos de un array
{ E.pos := CrearVarTemp() ; emite ( E.pos ‘:=’ E1.pos ‘*’ E2.pos ) } { E.pos := CrearVarTemp() ; emite ( E.pos ‘:=’ num.val ) } { E.pos := E1.pos }
2.1. a de una estructura
{ 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.
13
El a los de un estructura en C depende de la información que se almacene en la TDS sobre la posición de los . 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;
TALLA_REAL = 4 TALLA_ENTERO = 2
La TDS quedaría: Tabla de
TDS nom
tipo
a b
testructura testructura
posición … ref 0 , 124 0 , 130
nom
tipo
posición
c1 c2
tinteger treal
0 2
Figura 2.4. Asignación de memoria en la TDS para los 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 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 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.
14
Generación de código intermedio
Inst_simple ! id1 . id2 = E
Generación de código intermedio
Talla es el número deposiciones de memoria que ocupa cada elemento del vector (dependiente del tipo de elementos que contiene el vector).
{ 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 ) }
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:
Figura 2.5. ETDS Para el a los de una estructura.
base + ((i1 – inf 1) * n2 + (i2-inf 2) ) *Talla
2.2. 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. a los elementos de un array multidimensional en Pascal
donde n2 es el número de elementos de la segunda dimensión de la matriz (n2 = sup2– inf 2+1) que puede quedar como: base + ( (i1*n2 +i2) – (inf 1*n2 + inf 2) )*Talla (inf 1* n2+inf 2) 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.
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
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) – (((inf 1*n2+inf 2)*n3+inf 3… )*nn+inf n) ) * Talla donde (((inf 1*n2+inf 2)*n3+inf 3… )*nn+inf n) es el desplazamiento inicial de la matriz. Puesto que el 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 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 al elemento de la matriz aparece en el lado izquierdo de la asignación): Inst_simple
donde
! id [ Lista_indices ] := E
Lista_indices ! E Resto_LI
base es la dirección base donde se encuentra el primer elemento del vector,
Resto_LI ! , E Resto_LI
inf es el límite inferior de vector, y
| "
15
16
Generación de código intermedio
Generación de código intermedio
En la Fig. 2.6 se muestra un ETDS que genera código intermedio para el a los elementos de una matriz n-dimensional en Pascal. Inst_simple -> id
{ Lista_indices.nom := id.nom }
[ Lista_indices ] :=
E Lista_indices ! E
Resto_LI Resto_LI ! , E
Resto_LI1 Resto_LI ! "
{ temp := Lista_indices.pos ; emite (temp := temp ‘–‘ Desp_inicial(id.nom)) ; emite (temp := temp ‘*’ Talla (id.nom)) } { pos := BuscaPos (id.nom) ; emite ( pos ‘[‘ temp ‘]’ := E.pos } { temp := CrearVarTemp() emite (temp := E.pos); Resto_LI.ndim := 1; Resto_LI.temp := temp ; Resto_LI.nom := Lista_indices.nom }
TDS nom
tipo
A x y z
tarray tentero tentero treal
posición tipo_elem … desp_ini 0, 140 0, 940 0, 942 0, 944
treal -
TALLA_REAL = 4 TALLA_ENTERO = 2
ref
41
Tabla de arrays n_dim lim_inf lim-sup 1 2
2 1
11 20
desp_ini = (inf1 * n2 + inf2) = 2*20+1 = 41 Figura 2.7. Contenido de la TDS para el Ejemplo 2.3.
{ Lista_indices.pos := Resto_LI.pos } { 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 ; }
Suponiendo que el contenido en la TDS es el de la Fig. 2.7 se generará el siguiente código intermedio:
(100) (101) (102) (103) (104) (105)
t1 := x t1 := t1 * 20 t1 := t1 + y t1 := t1 – 41 t1 := t1 * 4 A [t1] := z
2.2.2. a los elementos de un array multidimensional en C
{ Resto_LI.pos:= Resto_LIResto_LI 1 1 pos } { Resto_LI.pos:= Resto_LI.temp }
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.
Figura 2.6. ETDS para el 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.
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
Ejemplo 2.3.- Veamos qué código generará este ETDS para el a un elemento de una matriz bidimensional que se define en el siguiente trozo de código fuente en Pascal: var
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 a los elementos de una matriz n-dimensional en Pascal tal y como aparece en la Fig. 2.8.
...
A: array [2..11,1..20] of real ; x,y: integer ; z: real; begin ... A[x,y] := z ...
17
18
Generación de código intermedio
Inst_simple -> id LI = E
LI ! [ E ] LI !
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.
{ LI.nom := id.nom } { emite (LI.pos := LI.pos ‘*’ Talla (id.nom)) ; pos := BuscaPos (id.nom) ; emite ( pos ‘[‘ LI.pos ‘]’ := E.pos ; } { LI.pos := CrearVarTemp() ; emite (LI.pos := E.pos); LI.ndim := 1; } { LI1.nom := LI.nom ; }
LI1 [ E ]
Generación de código intermedio
{ LI.ndim := LI1.ndim Resto_LI + 1; 1LI.pos := LI1.pos ; emite (LI.pos := LI.pos ‘*’ Num_elementos(LI.nom, LI.ndim)) ; emite (LI.pos := LI.pos ‘+’ E.pos) ; }
3.2. Representación numérica
Figura 2.6. ETDS para el 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. 19
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 ! ( E1 )
{ E.pos:= CrearVarTemp() ; emite( E.pos ‘:=’ 1 ) } { E.pos:= CrearVarTemp() ; emite( E.pos ‘:=’ 0 ) } { E.pos := E1.pos }
E ! id
{ E.pos := BuscaPos(id.nom) }
E ! true E ! false
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). 20
Generación de código intermedio
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.
Generación de código intermedio
valores de verdad, y el operador “or” como una suma. De esta manera, podemos obtener el ETDS: E ! E1 or E2 ! E1 and E2
Ejemplo Para evaluar la expresión a<5, se puede generar la siguiente secuencia de código de tres direcciones: (1) (2) (3) (4)
t1 := 1 if a < 5 goto 4 t1 := 0 ...
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
{ 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.pos:= CrearVarTemp() ; E ! E1 or E2 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:
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 21
{ E.pos:= CrearVarTemp() ; emite( E.pos ‘:=’ E1.pos ‘+’ E2.pos ) }
22
Generación de código intermedio
(1) (2) (3) (4)
Generación de código intermedio
t1 := 0 if a <> 0 goto 4 t1 := 1 ...
I ! do I1 while ( E )
El ETDS correspondiente quedará como aparece en la Fig. 2.9. E ! E1 or E2
! E1 and E2 ! not E1
{ 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.
{ E.pos:= CrearVarTemp() ; emite( E.pos ‘:=’ E1.pos ‘+’ E2.pos ) emite( ‘if’ E.pos ‘<= 1’ goto’ SIGINST+2 ) ; emite( E.pos ‘:= 1’)} { E.pos:= CrearVarTemp() ; emite( E.pos ‘:=’ E1.pos ‘*’ E2.pos ) } { E.pos:= CrearVarTemp() ; emite( E.pos ‘:=’ 0 ) ; emite( ‘if’ E1.pos ‘<> 0 goto’ SIGINST+2 ) ;
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:
emite( E.pos ‘:=’ 1) }
I! while (E) I1
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 noterminal 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
{ 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
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 23
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: 24
Generación de código intermedio
E ! true
{ emite( ‘goto’ E.verdadero) }
E ! false
{ emite( ‘goto’ E.falso) }
4.2. Relleno por retroceso
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
Generación de código intermedio
{ E.falso:= SIGINST }
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.
{ 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
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}.
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. 25
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 101 102 103 104 105
goto 105 ... ... if a>0 goto 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:
26
Generación de código intermedio
I! while (E) I1
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.
{ 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
Generación de código intermedio
{ Iinicio:= SIGINST } { I.final:= CreaLans(SIGINST) ; emite( ‘if’ E.pos ‘=0 goto’ --- ) } { emite( ‘goto’ I.inicio ) ; CompletaLans(I.final, SIGINST) }
De forma similar, podemos construir un ETDS que genere código intermedio para un bucle for parecido al del lenguaje C: I ! for ( I1 ; { I.cond := SIGINST; } { I.fin := CreaLans (SIGINST); E; emite( ‘if’ E.pos ‘=0 goto’ --- ); I.cuerpo := CreaLans (SIGINST); emite( ‘goto’ --- ) ; I. incr := SIGINST ; } { emite(‘goto’ I.cond); I2 ) CompletaLans(I.cuerpo, SIGINST) ; } { emite(‘goto’ I.incr); I CompletaLans(I.fin, SIGINST) ; }
4.4 Representación de expresiones lógicas por control de flujo (con relleno por retroceso)
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:
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 ! False 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) }
{ E.listaverdad := CreaLans( SIGINST) ; E.listafalso := nil } emite( ‘goto’ --- ) } { 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.
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 27
28
Generación de código intermedio
I! repeat I1 until E
Generación de código intermedio
{ I.inicio:= SIGINST } E ! E1 oprel E2
{ 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).
E ! True
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:
E ! False I! do I1 while (E)
{ inicio:= SIGINST } E ! id
{ 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. { CompletaLans( E1.lf,SIGINST) ; E ! E1 or E2 E ! E1 and E2
E.lv := FusionaLans( E1.lv, E2.lv ) ; E.lf := E2.lf } { CompletaLans( E1.lv, SIGINST) ; E.lv := E2.lv ;
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
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.
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
{ E.lv := E1.lf ; E.lf := E1.lv }
E ! ( E1 )
E.lf := CreaLans( SIGINST ) ; emite( ‘goto’ --- ) } { E.lf := nil ; E.lv := CreaLans( SIGINST) ; emite( ‘goto’ --- ) } { E.lv := nil ; E.lf := CreaLans( SIGINST) ; emite( ‘goto’ --- ) } { si BuscaTipo( id.nom ) = tlógico ent E.lf := CreaLans( SIGINST) emite( ‘if’ id.pos ‘= 0 goto’ --- ) ; E.lv := CreaLans( SIGINST) ; emite( ‘goto’ --- ) }
4.4.2. Variables lógicas y representación por control de flujo
E.lf := FusionaLans( E1.lf,E2.lf ) } E ! not E1
E.lf := E1.lf } { E.lv := CreaLans( SIGINST) ; emite( ‘if’ E1.pos oprel.op E2.pos ‘goto’ --- ) ;
{ E.lv := E1.lv ; 29
30
Generación de código intermedio
Generación de código intermedio
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. { 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 ) }
I ! id := E
{ 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
4.4.3. Instrucciones de control de flujo
Instruc
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”.
id.pos=X
E
id.pos=a
I2 I ! while E do I1
{ I. inicio := SIGINST ; condic := CreaLans( SIGINST) ; emite( ‘if’ id.pos ‘>’ E2.pos ‘goto’ -- ) }
to E2
do I1
Figura 2.15. ETDS asignación de valor a variable lógica.
I ! if E then I1 else
{ id.pos := BuscaPos (id.nom) ; emite( id.pos ‘:=’ E1.pos ) }
I ! for id := E1
:=
E
.lv = {100,104} .lf = {103,105}
.lv = {100} .lf = {101}
or
E
oprel.op=<
{ CompletaLans( E.lv, SIGINST) } { I. listaSig := CreaLans( SIGINST) ; emite( ‘goto’ --- ) ; CompletaLans( E.lf, SIGINST) } { CompletaLans (I. listaSig, SIGINST) }
id.pos=b
(
E .lv = {102} .lf = {103}
{ I. inicio := SIGINST } { CompletaLans( E.lv, SIGINST) } { emite( ‘goto’ I.inicio ) ; CompletaLans( E.lf, SIGINST) }
id.pos=c
oprel.op=<
id.pos=d
.lv = {104} .lf = {103,105}
) E .lv = {104} .lf = {103,105}
and
id.pos=e
E
.lv = {104} .lf = {105}
oprel.op=<
id.pos=f
Figura 2.17: Árbol anotado para la cadena x:=a
31
32
Generación de código intermedio
Repr. Numérica 100 101 102 103 104 105 106 107 108 109 110 111
Generación de código intermedio
Repr. por control de flujo
t1 := 1 if a
100 101 102 103 104 105 106 107 108 109
I ! if E I1 else
if a
I2 I ! while (E) I1 I ! do I1 while ( E )
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
{ CompletaLans( E.lv, SIGINST) } { I. listaSig := CreaLans( SIGINST) ; emite( ‘goto’ --- ) ; CompletaLans( E.lf, SIGINST) } { CompletaLans (I.listaSig, SIGINST) } { I.inicio := SIGINST } { CompletaLans( E.lv, SIGINST) } { emite( ‘goto’ I.inicio ) ; CompletaLans( E.lf, SIGINST) } { 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).
while a
if a
101
goto 102
102
if c
103
goto 110
104
if e
105
goto 110
106
t1 := 1
107
t2 := a + t1
108
a := t2
109
goto 100
E
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).
w h i l
5. Subprogramas
e I
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).
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:
33
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. 34
Generación de código intermedio
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 ;
Generación de código intermedio
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.
{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’) }
Decl_Subprg ! Tipo id ( Param_Form )
Bloque ;
Decl_Subprg Bloque ! Decl_Var Decl_Subprg begin Instrucciones end
{ inst := CreaLans(SIGINST); emite(‘goto’ ----); } { CompletaLans(inst,SIGINST); }
Figura 2.18. ETDS carga/descarga de un frame en C realizado por la función llamada.
5.2.2. Llamada
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.
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.
Param_Act ! E
{ 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) } { emite(‘push’ E.pos) }
Param_Act ! Param_Act , E
{ emite(‘push’ E.pos) }
E ! id
Param_Act ! E
{ 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) } { emite(‘push’ E.pos) }
Param_Act ! Param_Act , E
{ emite(‘push’ E.pos) }
E ! id ( Param_Act )
{ 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’) }
( Param_Act )
Figura 2.19. ETDS carga/descarga de un frame realizado por la función llamadora.
Figura 2.19. ETDS carga/descarga de un registro de activación por el procedimiento llamador. 35
36
Generación de código intermedio
Generación de código intermedio
Bibliografía recomendada
Ejercicios
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].
*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
[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
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.
37
38
Generación de código intermedio
Generación de código intermedio
• Emplear el juego de instrucciones de tres direcciones. (Ejemplo: “id.pos:=id.pos*5”)
S ! case E of L end
• 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”.
L ! num : S ; L
• Se suponen definidas las siguientes funciones:
...
L !"
• CrearV arTemp(): Crea una variable temporal y devuelve su posición.
Tanto las constantes de selección, como la expresión, deben ser de tipo entero.
• BuscaPos(id.nom): Devuelve la posición de memoria asignada al identificador cuya entrada en la TDS es id.nom.
*8.5.- Dada la siguiente gramática:
• En la declaración de cada parámetro formal, deberá indicarse si el uso es por valor o referencia.
P!D;I. D ! id : T
D;D
#
T ! pila ( num ) de TS
*8.3.- Dada la siguiente gramática (inspirada en la proposición “switch” del C):
TS ! entero
I ! switch ( E ) { L }
#
I ! apilar ( id , E )
L ! case num : P ; L
E ! desapilar ( id )
L ! default : P ;
# " #
TS
real # #
id := E
#
cima ( id )
I;I #
#
"
id
Donde:
L !" ... P !P;P
pila (num) de TS, representa la definición del tipo pila, con un máximo de num elementos de tipo entero o real.
P ! break P !I
apilar (id, E), indica que se apila el valor de E en la pila id
P !" ...
desapilar (id), devuelve el elemento que desapila de la pila id
Donde:
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) 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.
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.
*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.
39
40
Generación de código intermedio
Generación de código intermedio
*8.6.- Dada la siguiente gramática:
1) string(num) representa una cadena de caracteres de talla máxima num.
P !D;I. D ! id : T | D ; D | " T ! cadena ( num ) | TS TS ! entero | real | caracter
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.
...
I ! concatena ( id , id ) | id := E | I ; I | " E ! longitud ( id ) | id
*8.8.- Dado el siguiente fragmento de gramática.
Donde:
I
cadena(num) representa la definición de un objeto de tipo “cadena de caracteres” cuyo tamaño máximo viene dado por num.
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.
concatena(id1, id2) concatena al final de la cadena id1 la cadena id2. longitud(id) devuelve el tamaño de la cadena id.
Ejemplo:
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 := 9 ; while a < 15 do begin a:= a +1; write(a) end except a=13 do a:= a+1;
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):
! while E do I Rw
*
=> 10 11 12 13 15
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
S!DS D!D;D
I
| id : T
T ! char |
integer |
S!S;S
id := E
|
string(num)
E!F+F
| id (num : num)
F ! id
/id/
|
! I;I
E ! E oprel E
El diagrama de flujo de la instrucción sería el de la figura.
NOTAS:
41
42
Generación de código intermedio
Generación de código intermedio
P!D ; I. D ! id : T
True
E
I
T ! integer
False
!
|
num | id
I
I ! for-each id in id do I True
Siendo:
I
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}.
False
... salida True
E
|
set of [ num ]
...
False
E
D;D |
E ! member ( E , id ) True
E
|
member(E, id) una función que devuelve TRUE (ó 1) si el elemento E está en el conjunto id (FALSE ó 0, en caso contrario).
I
False
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:
I
a: set of [5];
*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
..... 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.
P !I ...
S ! for id := E R E do S
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: 43
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.
44
Generación de código intermedio
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”
Generación de código intermedio
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. . .
No
id > E'
. .
a1 i1
Si
S'
an in-1
i2
in
A[i1,i2,..in]
S''
b1 No
. .
a2
b2 1
Si
bn
2 . .
id > E''
n-1 . .
n . .
a) Determinar la función de para el elemento genérico A[i1,i2,...in]. 8.16.- Dada la siguiente gramática que representa registros en Pascal, construir un ETDS que realice:
b) Diseñar el ETDS para la generación del código intermedio correspondiente, para la siguiente gramática.
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
S ! L := E E!L L ! id L!I ] I!I , E I ! id [ E
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.
8.19.- Dada la siguiente gramática:
b) El cálculo de las posiciones relativas de los objetos en memoria.
P! var D_V I
c) La generación de código intermedio para las expresiones e instrucciones.
D_V ! D_V D_V |
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.
T ! record ListaCampos Cvar end T_simple ! T_disc
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!(+PL)
Lista_id : T ;
Lista_id ! Lista_id , id | id |
T_simple
| integer | real
ListaCampos ! ListaCampos ; ListaCampos T_disc ! num..num
| (*PL)
| id : T_simple
| boolean
Cvar ! case id : T_disc of LCV | "
P ! id | S
LCV ! LCV ; LCV |
L!PL | "
Val_sel ! num
a) Escribir en PASCAL las rutinas del Análisis Sintáctico por Descenso Recursivo LL(1) para esta gramática.
| id
I ! id := E | id . id := E | if E then I else I E ! E oprel E | id . id | id
b) Introducir en el esquema anterior las acciones semánticas necesarias para la generación de código tres direcciones. 45
Val_sel : ( ListaCampos )
a) Dar un ETDS que realice el cálculo de las posiciones relativas de las variables en memoria para esta gramática. 46
Generación de código intermedio
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.
Generación de código intermedio
Soluciones a los ejercicios seleccionados 8.1.E1 id .p o s := E 1 .p o s E2 if id > E 2 g o to g o to (s te p ) E id := id + E g o to I g o to
*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
L2: L1:
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 )
|
true
| | |
E and E
|
I ! for id := E1 { si E1.tipo <> tentero ent MemError() id.pos:=BuscaPos(id.nom); emite(id.pos ‘:=’ E1.pos) } { si E2.tipo <> tentero ent MemError() to E2 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) } S { emite(‘goto’ inc); do I CompletaLans(fin, SIGINST) }
not E
E oprel E 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 */
S ! step E S !"
c) ¿Qué código generará tu ETDS para las siguientes cadenas? x := a
{ si E.tipo <> tentero ent MemError() emite(S.pos ‘:=’ S.pos ‘+’ E.pos) } { emite(S.pos ‘:=’ S.pos ‘+’ 1) }
8.2.a) E ! T1 {+ T2 } T ! F1 {* F2 47
48
{ E.pos:=CreaVarTemp(); emite (E.pos ’:=’ T1.pos) } { emite(E.pos ‘:=’ E.pos ‘+’ T2.pos ) } { T.pos:=CreaVarTemp(); emite (T.pos ‘:=’ F1.pos) } { emite(T.pos ‘:=’ T.pos ‘*’ F2.pos ) }
Generación de código intermedio
Generación de código intermedio
}
F ! id
{ F.pos:=CrearVarTemp(); emite(F.pos ‘:=’ num.val) } { F.pos:=BuscaPos(id.nom) }
F !(E)
{ F.pos:=E.pos }
IS ! id := E
{ id.pos:=BuscaPos(id.nom); emite(id.pos ‘:=’ E.pos) }
F ! num
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 }
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 }
8.3.E if E<>cte1 goto P1 goto if E<>cte2 goto P2 goto (break) 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).
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:
I ! switch ( E ) {L}
{ L.pos:=E.pos; L.anterior := nil; L.fin:=nil}
L ! case num :
{ sigcond:=CreaLans(SIGINST) ; emite(‘if’ num.val ‘<>’ L.pos ‘goto’ --); CompletaLans(L.anterior, SIGINST) } { L1.anterior:=CreaLans(SIGINST) ; emite (‘goto’ --);
P; 49
50
Generación de código intermedio
Generación de código intermedio
L1.pos:=L.pos; CompletaLans(sigcond, SIGINST); L1.fin:=FusionaLans(P.fin, L.fin) }
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:
L1
P ! P1 ; P2
{ CompletaLans(L.anterior, SIGINST) } { CompletaLans(L.fin, SIGINST); CompletaLans(P.fin, SIGINST) } { CompletaLans(L.anterior, SIGINST) CompletaLans(L.fin, SIGINST);} { 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 }
L ! default : P; L !"
I ! apilar ( id ,
E)
8.4.-
E ! desapilar (id) E if E .p o s < > c te .v a l g o to S g o to if E .p o s < > ... g o to
E ! cima (id)
S g o to
...
L ! num :
S;
L1 L ! " S ! case E of L
{ pos := BuscaPos (id.nom) ; emite ( pos ‘:=’ pos ‘+’ 1 ); emite (‘if’ pos ‘<=’ limite ‘goto’ SIGINST +2 ) emite (‘fin’) } { emite ( pos ’[‘ pos ’] :=’ E.pos) } { 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) } { 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]) .
{ 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.fin:=nil }
El ETDS, incluyendo algunas comprobaciones de tipo quedará finalmente: { DESP:=0 }
P! D;I. D ! id : T
{ si E.tipo $ tentero ent MemError(); L.pos:=E.pos } { CompletaLans(L.fin, SIGINST) }
{ 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
8.5.51
52
{ si num.val <=0 ent T.tipo := terror
Generación de código intermedio
TS T ! TS
sino T.tipo:= tpila (num.val, TS.tipo) ; T.talla := TS.Talla * (num.val +1) } { T.talla := TS.talla ; T.tipo := TS.tipo }
TS ! entero
{ T.talla := 1 ; T.tipo := tentero }
TS ! real I ! apilar ( id , E )
{ T.talla := 1 ; T.tipo := treal }
I ! id := E
Generación de código intermedio
8.6.P ! { DESP:=0 } D ; I .
{ 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 }
D ! id : T | D;D | " T ! cadena ( num ) | TS
{ InsertaTDS(id.nom, T.tipo, DESP) ; DESP:=DESP+T.talla }
TS ! entero | real | caracter
{ TS.tipo:=tentero ; { TS.tipo:=treal ; { TS.tipo:=char ;
I ! concatena ( id1 , id2)
{ 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; }
{ 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 )
E ! cima ( id )
E ! 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 } { 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.tipo := BuscaTipo (id.nom) ; E.pos := BuscaPos (id.nom) }
| id := E
{ T.tipo:=tcadena(num.val) ; T.talla:=num.val } { T.tipo:=TS.tipo ; T.talla:=TS.talla } TS.talla:=1 } TS.talla:=1 } TS.talla:=1 }
| I;I | " 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); 53
54
Generación de código intermedio
Generación de código intermedio
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 ) */
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.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).
8.9.Solución usando representación por control de flujo para las expresiones lógicas.
O
{ CompletaLans(fin, SI); }
E then
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(num 1:num 2) nos devuelve una cadena de tamaño num 2 .val - num 1.val + 1.
O ! elseif E then I
{ CompletaLans(E.lv, SI); } { emite(‘goto’ O.inicio) ; CompletaLans(E.lf, SI); O1. inicio := O.inicio ;}
O1 O ! else I endloop
{ emite(‘goto’ O.inicio) ;}
I!I;I
8.8.El esquema del código que queremos generar será:
E
I exitloop
{ O.inicio := SI; } { CompletaLans(E.lv, SI); } { fin:= CreaLans(SI); emite (‘goto’ -); CompletaLans(E.lf, SI);}
I ! loop
E ! E oprel E
{ E.lv := CreaLans( SI) ; emite( ‘if’ E1.pos oprel.op E2.pos ‘goto’ --- ) ; E.lf := CreaLans( SI ) ;
.v .f
emite( ‘goto’ --- ) }
I1 goto E (RW)
8.10.-
.v .f
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 (RW) goto
I ! while E do I El ETDS pedido quedaría: I
! while E do I1 RW
I ! Exitloop I!I;I
{ 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) }
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).
55
56
Generación de código intermedio
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
I ! Exitloop I ! I 1 ; I2
{ inicio := SIGINST } { CompletaLans (E.lv, SIGINST) } { emite (‘goto’ inicio) CompletaLans (I1.salida, SIGINST) ; CompletaLans (E.lf, SIGINST) ; I.salida := nil } { I.salida := CreaLans (SIGINST) ; emite (‘goto’ ---) } { I.salida:= FusionaLans (I1.salida, I2.salida) ; }
Generación de código intermedio
8.12.-
P!D;I. D ! id : T | D;D | " T ! set of [ num ] | integer E ! member ( E1 , id )
8.11.El esquema del código intermedio que queremos generar es el siguiente: | num
| id
I1 E
.v .f
I! for_each id1 in id2
I2 goto P goto
El ETDS por tanto quedaría: I ! for ( I1 ; { inicio:=SIGINST; } { si E.t $ tlógico ent MenError() E; sino incr:=SIGINST; } I2 ) { emite(‘goto’ inicio); CompletaLans(E.lv, SIGINST) } {P} { emite(‘goto’ incr); CompletaLans(E.lf, SIGINST) }
do I
57
58
{ insertaTDS( id.nom, T.tipo) }
{ T.tipo := conjunto(num.val) } { T.tipo := entero } { 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]) } { E.tipo := entero; E.pos:= CreaVarTemp; emite(E.pos:= num.val) } { E.tipo := BuscaTipo(id.nom) E.pos := BuscaPos(id.nom) } { 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, for_each id1si); in id2 emite( id1.pos := id1.pos + 1); emite( goto (inicio)); completalans(fin, si); }
Generación de código intermedio
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 (
‘:’) Se puede observar la similitud de este ETDS con el construido usando relleno por retroceso para la misma gramática:
Generación de código intermedio
b) Instruc ! if E then Instruc1 else Instruc2 | while
- 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.
do Instruc1 | for id := E1 to E2
El ETDS para las expresiones lógicas quedaría:
do Instruc1 E! E1 or
{ E1.f := CreaEtiqueta(); E1.v := E.v } { emite( E1.f ‘:’);
E2
E2.v := E.v; E2.f := E.f } { E1.v := CreaEtiqueta(); E1.f := E.f } { emite( E1.v ‘:’);
| E1 and E2 | not E1
{ { { {
E.v:= CreaEtiqueta(); E.f:= CreaEtiqueta(); fin:= CreaEtiqueta() } emite( E.v ‘:’ ) } emite( ‘goto’ fin ) ; emite( E.f ‘:’) } emite( fin ‘:’) }
{ Inicio := CreaEtiqueta() ; E.t := CreaEtiqueta() ; E.f := CreaEtiqueta() ; emite(inicio ‘:’) ’:’ } { emite(EE.v ‘:’) } { emite( ‘goto’ inicio ) ; emite( E.f ‘:’) } { 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 ‘:’) }
E2.v := E.v; E2.f := E.f } { E1.v := E.f ; E1.f := E.v }
c) x := a
| ( E1 )
{ E1.v := E.v ; E1.f := E.f }
L3:
| E1 oprel E2
{ emite( ‘if’ E1.pos oprel.op E2.pos ‘goto’ E.v );
L4:
| True | False | id
emite( ‘goto’ E.f ) } { emite( ‘goto’ E.v ) } { emite( ‘goto’ E.f ) } { emite( ‘if’ id.pos ‘= 0 goto’ E.f ) ; emite( ‘goto’ E.v ) }
L2: L1: L5:
if a
while a
L0:
{ 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 ‘:’ ) }
L3: L4: L1:
L2:
59
60
if a
E
w h i l
I
e
Generación de código intermedio
while (not a) or (x < b and true) do x := x + 1 L0: L3: L4: L1:
if a = 0 goto L1 goto L3 if c
E
w h i
I
l e
L2:
61