Resto de Fases del Compilador Software de Sistemas Mónica Pinto Alarcón Curso 2005/2006
Fases del Compilador
Fase Fase de Anál Análisi isiss Análisis léxico Análisis sintáctico Análisis semántico
Fase Fase de Sínt Síntesi esiss
Genera Generació ción n de Código Código Interm Intermedi edio o Optimizaci Optimización ón de Código Código Intermedi Intermedio o Genera Generació ción n de Código Código Máquin Máquina a Optimi Optimizac zación ión del Código Código Máquin Máquina a 2/38
1
Análisis Semántico
Semántica El conjunto de reglas que especifican el significado de cualquier sentencia sintácticamente correcta y escrita en un determinado lenguaje.
Se pueden encontrar sentencias que son sintácticamente correctas, pero que no se pueden ejecutar porque carecen de sentido. En general, el análisis semántico se hace a la par que el análisis sintáctico introduciendo en éste unas rutinas semánticas . 3/38
Análisis Semántico
Ejemplo1:
Suponer que los identificadores de una sentencia son variables declaradas de tipo real. En la sentencia que se ha analizado existe una valor de tipo entero. Sin embargo, las operaciones se realizan entre identificadores reales, por lo que hay dos alternativas:
o emitir un mensaje de error “Discordancia de tipos”, o realizar una conversión automática de tipos, mediante una función auxiliar inttoreal().
Ejemplo2:
Suponer una sentencia en la que se suman dos identificadores, donde uno de ellos es el identificador de una variable de tipo entero y el otro el identificador de una variable de tipo array. El analizador semántico dará un error porque aunque la suma de dos identificadores sintácticamente es correcto en el lenguaje de programación, no está definida la operación de suma de un entero y un array.
4/38
2
Análisis Semántico
El compilador realiza una comprobación semántica estática, frente a la dinámica realizada en tiempo de ejecución Algunos ejemplos de Comprobaciones Semántica Estáticas son :
Comprobación de tipos: indicará error si un operador se aplica a un
operando incompatible. Ejemplo: sumar una variable de tipo vector con un identificador función. Comprobación del control de flujo: Las sentencias que producen la variación de flujo deben verificar hacia donde se transfiere el control. Ejemplo: la sentencia break de C hace que el control abandone el bucle while más interior, o una sentencia switch, aparecería un error si tal bucle no existe. Comprobación de unicidad: Existe situaciones en las que un elemento del
lenguaje sólo se define una vez.
Ejemplo: en la zona de declaraciones, un identificador debe ser declarado una sola vez, las etiquetas de la sentencia case deben ser distintas o los elementos de un tipo enumerado no pueden repetirse.
5/38
Análisis Semántico
Especificación Formal de la Semántica Una forma de proporcionar una especificación precisa de los aspectos semánticos de los lenguajes de programación es mediante las gramáticas atribuidas.
Gramática con Atributos
Una gramática con atributos es una gramática de contexto libre a cuyos símbolos X (terminales y no terminales) se les asocia un conjunto de atributos. Además a cada regla sintáctica se le asocian unas reglas semánticas que especifican como los atributos toman sus valores. Mediante una gramática atribuida podremos especificar qué debe hacerse con los atributos para realizar el análisis semántico y / o la traducción una vez que se ha realizado con éxito el análisis sintáctico. 6/38
3
Análisis Semántico
G = (N,T,S,P) Gramática tipo 2 o de contexto libre: A tira estando A en N y tira en (N U T)* Se puede cambiar A por tira independientemente del contexto en que aparezca A Ejemplo:
N: S, A, B T: a, b P : SaB | bA A a | aS | bAA B b | bS | aBB 7/38
Definición de las Reglas Semánticas
Reglas Semánticas:
Son una generalización o extensión de las reglas de la gramática.
Se basa en que el significado de una frase está directamente relacionado con su estructura sintáctica, según se representa en su árbol de análisis sintáctico. Se realizan en paralelo
Análisis Semántico Traducción
Esta notación asocia a cada símbolo de la gramática (nodo u hoja) un conjunto de atributos. 8/38
4
Definición de las Reglas Semánticas
Se utiliza la notación símbolo.atributo id.lexema //id, token identificando una palabra reservada var.valor //var, token identificando una variable var.dirección //var, token identificando una variable operación.código //operación, token identificando e.j. +,-,*,/ Se utilizan subíndices para diferenciar símbolos En lugar de describir la gramática como L::,id L
L:: , id L1 L.Trad :: “,” || id.lexema || L1.Trad Desde el punto de vista exclusivamente gramatical sería L::,id L
Los atributos pueden ser de dos tipos: Sintetizados: se calculan a partir de los valores de los atributos de los hijos. La información asciende por el árbol Heredados: se calculan a partir de los valores de los atributos de los
padres y / o hermanos.
Información descendente o de tránsito horizontal Se usan para transferir atributos entre las reglas 9/38
Definición de las Reglas Semánticas El formato es:
Reglas/Producciones de la gramática
A a1 a2 a3 ..... an
Reglas Semánticas
A.s = f (a1.s, a2.s, ……an.s) ak.h = f (A.h, a1.h,…..) A (A.s = f(a1.s, …, an.s))
a1 … an (a1.s) (an.s)
A
a1 … ak (a1.s) (ak.h=f(a1.s, A.s,an.s)
an (an.s)
A veces se utilizan atributos llamados ficticios del no terminal de la parte izquierda de la regla ( por ej: una función de imprimir ). 10/38
5
Tipos de Traducciones
Dos Notaciones:
DDS: Definiciones Dirigidas por la Sintaxis
Formalismo de alto nivel para describir traducciones Se ocultan los detalles de implementación No se impone orden de ejecución de las reglas Calcular los atributos en la producción en la que aparecen
ETDS: Esquema de Traducción Dirigido por la Sintaxis
Notación de bajo nivel para describir un traductor El traductor es de una sola pasada Se hace explícito el orden de ejecución de las acciones
11/38
Definiciones Dirigidas por Sintaxis
Gramáticas con atributos
Las reglas semánticas sólo manipulan atributos No existen otras acciones semánticas No existen efectos laterales
DDS
Es una generalización de las gramáticas con atributos Las reglas semánticas manipulan atributos Existen otras acciones semánticas
Escritura de valores Condicionales Modificación de Variables Globales
Existen efectos laterales 12/38
6
Ejemplo 1
Ejemplo de DDS
E:: E1 OP T if (OP.lexema == ‘+’){ //Cálculo atributo sintetizado E.valor := E1.valor + T.valor } else{ //Cálculo atributo sintetizado E.valor := E1.valor - T.valor } write(E.valor) //Efecto Lateral 13/38
Definiciones Dirigidas por Sintaxis
Se asume lo siguiente:
Los terminales sólo tienen atributos sintetizados, los proporcionados por el Analizador Léxico. El axioma no tiene atributo heredado, sino se indica lo contrario.
Ejemplo: Calculadora L E \n EE+T E E - T E T T T*F T T / F T F F(E) F d
El ANALEX entrega los tokens: d opsr opmd pa ,pc fin
L E fin E E opsr T
L.v= imprime(E.v) E.v=operación(E.v,T.v,opsr.o)
E T T T opmd F
E.v= T.v T.v=operación(T.v,F.v,opmd.o)
T F F pa E pc F d
T.v= F.v F.v= E.v F.v= d.v 14/38
7
Ejemplo 2
Cadena de entrada:
El Analizador Léxico entregaría los tokens:
4 + 5 * 6 + 3 \n d opsr d opmd d opsr d fin
Secuencia de pasos:
Token : d, atributo d.v = 4, en el análisis sintáctico
ascendente se va reduciendo :
dFTE El atributo no se puede perder, así pues se va trasladando su valor mientras se va reduciendo: F.v = d.v // T.v = F.v // E.v = T.v El proceso sigue hasta que en la pila está E opsr T que se reducirá por E
15/38
Ejemplo 2
L.v = imprime(37)
E.v = 37
E.v = 34
T.v = 30
E.v = 4
T.v = 4
T.v = 5
F.v = 4
F.v = 5
T.v = 3
F.v = 6
d.v = 4
opsr.o = mas
d.v = 5
opmd.o = por
d.v = 6
d
opsr
d
opmd
d
F.v = 3
opsr.o=mas
opsr
d.v = 3
d
fin
fin
16/38
8
Ejemplo 3
Ejemplo DDS
h – heredado, s – sintetizado t – tipo, c – representación
S:: var L:T;
L:: L1,id
L.th := T.ts //Propagación Horizontal S.cs := L.cs //Propagación Ascendente L.cs:=L1.cs||L.th||id.lexema||”;” L1.th:=L.th //Propagación Descendente L.cs:=L.th||id.lexema||”;” T.ts:=float T.ts:=int
L:: id T:: real T:: integer
17/38
Esquemas de Traducción Dirigido por Sintaxis
Notación ETDS
Traductores de una sola pasada Se insertan las acciones semánticas entre llaves y entre los símbolos de la parte derecha de las reglas de la gramática Indican explícitamente cuándo se realiza la acción semántica
Ejemplo:
Con atributos sintetizados:
Notación DDS:
TT*F
|
T.v = T.v * F.val
Notación EDT:
TT*F
{ T.v = T.v * F.val }
18/38
9
Esquemas de Traducción Dirigido por Sintaxis Con atributos heredados: EDT: E T { R.h = T.v } R { E.v = R.s } Se suele escribir y leer en el orden indicado: E T { R.h = T.v } R { E.v = R.s } Para especificar con EDT con atributos heredados se ha de ten er en cuenta los siguiente: Un atributo heredado de un símbolo se ha de calcular antes de la visita a dicho símbolo: después de visitar T y antes de visitar R se calcula su atributo heredado R.h Un atributo sintetizado de un símbolo se usará después de la visita a dicho símbolo. Rs se usa para calcular otro atributo después de visitar R Un atributo sintetizado de la parte izquierda de una regla (el padre) se calculará al final, después de calcular todos los atributos. E.v se calcula al final •
•
•
19/38
Ejemplo 4 1
2
E:: T {E’.th:=T.ts} E’ {E.ts := E’.ts} 3 4 E’::=op T {E’1.th:=E’.th||T.ts||op.lexema} E’1 {E’.ts:=E’1.ts} E’::= ξ {E’.ts := E’.th} 5 T ::= num {T.ts := num.lexema} 6
i,j Indica “orden evaluación,regla semántica” 9-5+2 2,1
10,2 9,4
1,6
4,3
3,6
8,4
6,3
5,6
7,5
20/38
10
Esquemas de Traducción Dirigido por Sintaxis
Restricciones de Diseño
Si existen atributos sintetizados, colocar la acción semántica después de todos los símbolos implicados o al final de la producción
A:: B C {A.a := f(B.a,C.a)} D A:: B C D {A.a := f(B.a,C.a)}
Si existen atributos heredados, deben calcularse antes de que aparezca el símbolo en la parte derecha
A:: B C {D.h:=f(B.a,C.a)} D 21/38
Ejemplo 5
Especificación de un Comprobador de Tipos Elemental Sea un lenguaje de programación en el que es obligatorio declarar todos los identificadores antes de ser usados.
En ese caso habría que realizar una comprobación de unicidad.
Supongamos que en la tabla de símbolos antes de las declaraciones el atributo tipo está inicializado a cero. Cuando se lea una declaración se debe añadir el valor correspondiente al tipo declarado. Si se encontrara una doble declaración de la misma variable el tipo ya no estaría a cero, detectándose un error en l a comprobación de unicidad. Así mismo cuando se use una variable se debe comprobar el atributo tipo, si su valor fuera cero se detectaría que no ha sido declarada. 22/38
11
Ejemplo 5
El código que habría que utilizar para estas comprobaciones sería:
Cuando se declara la variable:
if (buscatipo(id.e) == 0)
añadetipo(id.e,id.tipo);
else tratamientoerror();
/* ya declarada */
Cuando se usa la variable:
if (buscatipo(id.e) == 0)
tratamientoerror(); /* variable no declarada */
else ............../*tratamiento correcto......*/ 23/38
Ejemplo 5 (Continuación)
ETDS para la parte de Declaraciones
PD;E DD;D D id : T
{ if (buscatipo(id.e)==0) añadetipo(id.e,T.t); else tratamientoerror(); } T char {T.t =CHAR;} T integer {T.t = INTEGER;} T ^ T1 {T.t = pointer(T1.t);} T array[num] of T1 { T.t = array(1..num.v,T1.t);} 24/38
12
Ejemplo 5 (Continuación)
ETDS del comprobador de tipos de Expresiones
E literal E num E id
E E1 mod E2
E E1[E2]
E E1 ^
{ E.t= CHAR;} { E.t= INTEGER;} { if(buscatipo(id.e) !=0) E.t=buscatipo(id.e); else tratamientoerror();} { if (E1.t==INTEGER && E2.t==INTEGER) E.t=INTEGER; else E.t=tipo_error;} { if(E2.t==INTEGER && E1.t==array(x,y)) E.t=y; else E.t=tipo_error;} { if(E1.t==pointer(y)) E.t=y; else E.t=tipo_error;}
25/38
Ejemplo 5 (Continuación)
ETDS del comprobador de tipos de Sentencias
A diferencia de las expresiones las sentencias no son, en general, de ningún tipo. Se les pude asignar el tipo void si son correctas. Ahora usaremos P D ; S en vez de P D ; E
S id := E
{ if(buscatipo(id.e)!=0) id.t=buscatipo(id.e); else if (id.t==E.t) S.t=void; else S.t=tipo_error;} S if E then S1 { if(E.t==BOOLEAN) S.t = S1.t; else S.t = tipo_error;} S while E do S1 { if(E.t==BOOLEAN) S.t = S1.t; else S.t = tipo_error;} S S1 ; S2 { if (S1.t ==void &&S2.t==void) S.t=void; else S.t = tipo_error;} 26/38
13
Generación de Código Intermedio
Con la generación de Código intermedio se inicia la tarea de síntesis. No suele ser un lenguaje de programación de ninguna máquina real, sino el correspondiente a una máquina abstracta
se debe definir lo más general posible para que sea posible traducirlo a cualquier máquina real.
El objetivo del Código Intermedio es reducir el número de programas necesarios para construir traductores
permite mayor portabilidad de unas máquinas a otras.
27/38
Generación de Código Intermedio
Distinto formato de las instrucciones:
Código de tres direcciones
Se puede implantar mediante:
Cuartetos Tercetos Tercetos indirectos
Notación polaca inversa Árboles sintácticos Lenguaje de medio nivel como el C
28/38
14
Generación de Código Intermedio
Código de Tres direcciones – Cuartetos
Se implanta en una secuencia de registros con 4 campos:
Operación Argumento1 Argumento2 Resultado
Al generar temporales han de ser insertados en la TDS. Ejemplo para: a:=b* -c+b* -c El resultado, en código de tres direcciones y la implantación en cuartetos es: Código en 3 dir t1 := -c t2 :=b*t1 t3 :=-c t4 :=b*t3 t5 :=t2+t4 a:=t5
Operador -unario * -unario * + :=
Argumento1 Argumento2 Resultado c t1 b t1 t2 c b t3 t4 t2 t4 t5 t5 a 29/38
Ejemplo 6
Código de tres direcciones – Cuartetos: La expresión (a + b ) * ( c + d ) puede traducirse al siguiente conjunto de cuartetos:
(+, a, b, t1) (+, c, d, t2) (*,t1, t2, t3)
Se indica la operación que ha de realizarse con los dos operandos que van a continuación y el resultado se guarda en la variable que está en último lugar. Los nombres t1, t2 y t3 pueden ser registros de la máquina o posiciones de memoria temporales, que desaparecen cuando se termine la compilación. 30/38
15
Generación de Código Intermedio
Código de Tres direcciones – Tercetos
Para evitar introducir temporales en la Tabla de Símbolos, se hace referencia a un valor temporal mediante la posición de la proposición que lo calcula. En este caso los registros son de 3 campos. Para el ejemplo anterior:
Código en 3 dir t1 := -c t2 :=b*t1 t3 :=-c t4 :=b*t3 t5 :=t2+t4 a:=t5
Posición (0) (1) (2) (3) (4) (5)
Operador -unario * -unario * + :=
Argumento1 Argumento2 c b (0) c b (2) (1) (3) a (4)
31/38
Generación de Código Intermedio
Código de Tres direcciones – Tercetos Indirectos
Se hace una lista de los punteros a tercetos
Tercetos
Tercetos Indirectos
Proposición (0) (1) (2) (3) (4) (5)
Proposición (0) (1) (0) (1) (4) (5)
Posición (0) (1) (2) (3) (4) (5)
Posición (0) (1) (4) (5)
Operador -unario * -unario * + :=
Argumento1 Argumento2 c b (0) c b (2) (1) (3) a (4)
Operador -unario * + :=
Argumento1 Argumento2 c b (0) (1) (3) a (4)
32/38
16
Notación Polaca Inversa
En el que los operadores se colocan en el orden en que van a ser ejecutados y además no es necesario el uso de paréntesis. Una representación de la expresión anterior sería:
a b +c d+ *
// Notación post-fija
Habitualmente va ligada a máquinas que utilizan una pila y su representación interna sería:
PUSH a (Almacena “a” en la pila) PUSH b (Almacena “b” en la pila) ADD (Extrae dos valores de la pila, los suma y guarda el resultado en la pila) PUSH c (Almacena “c” en la pila) PUSH d (Almacena “d” en la pila) ADD (Extrae dos valores de la pila, los suma y guarda el resultado en la pila) MUL (Extrae dos valores de la pila, los multiplica y guarda el resultado en la pila) 33/38
Optimización de Código Intermedio
A partir del código intermedio anterior se crea un nuevo código más compacto y eficiente,
eliminando por ejemplo sentencias que no se ejecutan nunca, simplificando expresiones aritméticas, etc...
La profundidad con que se realiza esta optimización varía mucho de unos compiladores a otros.
En el peor de los casos esta fase se suprime. 34/38
17
Ejemplo 7
Ejemplo de Código intermedio de tres direcciones
temp1 := inttoreal(2) temp2 := IDENT3 * temp1 temp3 := IDENT2 + temp2 IDENT1 := temp3
Código optimizado
Es posible evitar la función inttoreal mediante el cambio de 2 por 2.0, obviando además una de las operaciones anteriores. El código optimizado quedaría como sigue:
temp1 := IDENT3 * 2.0 IDENT1:= IDENT2 + temp1 35/38
Generación de Código Máquina A partir de los análisis anteriores y de las tablas que estos análisis van creando durante su ejecución, esta fase produce un código o lenguaje objeto que es directamente ejecutable por la máquina. Es la fase final del compilador. Las instrucciones del código intermedio se traducen una a una en código máquina reubicable. Cada instrucción del código intermedio puede dar lugar a más de una de código máquina.
36/38
18
Ejemplo 8
El código anterior traducido a ensamblador quedaría MOVEt R1, id3 // R1 id3 MUL R1, 2.0 // R1 = R1 * 2.0 MOVEt R2, id2 // R2 id2 SUM R2, R1 // R2 = R2 + R1 MOVEf id1, R2 // id1 R2
Observar que este código ensamblador no se corresponde con el código de la máquina GOAT vista anteriormente.
Donde id1, id2 y id3 representan las posiciones de memoria en la que se hallan almacenadas estas variables; R1 y R2 son los registros de la máquina; y las instrucciones MOVEt, MOVEf, MUL, SUM representan las operaciones de colocar un valor de memoria en un registro, colocar un valor de un registro en memoria, multiplicar en punto flotante y sumar. 37/38
Optimización de Código Máquina A partir del código máquina anterior se crea un nuevo código máquina más compacto y eficiente‘ La profundidad con que se realiza esta optimización varía mucho de unos compiladores a otros.
En el peor de los casos esta fase se suprime.
38/38
19