Teoría: Análisis Semántico
Diseño y Construcción de Compiladores
Licenciatura en Ciencias de la
Computación
Profesora Responsable: Dra. Victoria Aragón Profesora Colaboradora: Lic. Susana Esquivel Jefe de Trabajos Prácticos: Dr. Edgardo Ferretti Auxiliar de Práctico: Lic. María José Garciarena Ucelay
https://sites.google.com/site/ddcompilersunsl/
Diseño y Construcción de Compiladores UNSL
ANÁLISIS SEMÁNTICO
Tarea Fundamental del Análisis
Semántico: garantizar que el programa “tiene
sentido”.
Objetivo: Asegurar que el tipo de una construcción
ANÁLISIS SEMÁNTICO
Estático: todos los controles pueden
efectuarse en tiempo de compilación.
Análisis Semántico
Dinámico: los controles se realizan en
ANÁLISIS SEMÁNTICO ESTÁTICO
El análisis semántico que se realiza en tiempo de compilación puede involucrar distintas actividades, dependiendo de las características del lenguaje de programación. Estas actividades pueden ser:
Chequeo de tipos: por ejemplo, un compilador deberá reportar un
error si un operador se aplica a un operando incompatible. Por ejemplo en C el operador % (módulo) requiere operandos de tipo entero.
Chequeo del flujo de control: cuando existen sentencias que
causan que el flujo de control deje una construcción, en este caso se debe controlar que exista algún lugar del programa al cual transferir el control, por ejemplo, en C una sentencia break causa que el control deje un while, un for, un switch.
ANÁLISIS SEMÁNTICO ESTÁTICO
Chequeos de Unicidad: Existen característica en las que un objeto
computacional debe definirse exactamente un sola vez por ej. En PACAL un identificador dentro de un bloque (procedimiento/función) debe ser definido una única vez, los rótulos de la sentencia CASE deben ser distintos, los elementos de un tipo escalar no deben estar repetidos.
Chequeos de Nombres: En algunos lenguajes un mismo nombre
puede aparecer dos o más veces. Por ejemplo, en ADA un ciclo o un bloque puede tener un nombre al principio y al final de la construcción, el compilador debe chequear que el mismo nombre es usado en ambos lugares.
CHEQUEO DE TIPOS
Objetivo: Asegurar que el tipo de una construcción del
programa coincida con el previsto en sus contexto.
Controles Habituales:
Declaración de identificadores antes de su uso.
Compatibilidad de tipos en las asignaciones.
Operadores aplicados a datos del tipo adecuado.
N° y tipo correcto de parámetros en la invocación de
CHEQUEO DE TIPOS
Además se debe considerar:
CONVERSIÓN DE TIPOS (cuando estos sean compatibles y
el lenguaje lo admita)
• Conversión IMPLÍCITA (coerciones) → realizada por el
compilador automáticamente.
• Conversión EXPLÍCITA → realizada por el programador
(cast en C).
CHEQUEO DE TIPOS
SOBRECARGA DE OPERADORES
• Símbolos del lenguaje que tienen asociados
distintos tipos y operaciones en función del
contexto.
Ejemplo
suma de enteros
Operador „+‟ suma de reales
CHEQUEO DE TIPOS
Para poder realizar el chequeo de tipos el compilador necesita:
Un sistema de tipos (contexto): que especifica el conjunto de
reglas que permite asociar tipos con las distintas construcciones de un lenguaje (que está definido por el diseñador del lenguaje) para poder verificar su corrección.
Un conjunto de construcciones sintácticas que define el
lenguaje y con las cuales se asociaran los tipos según el sistema de tipos.
Un conjunto de expresiones de tipo: los tipos tienen una
estructura que representaremos usando expresiones de tipo, donde
una expresión de tipo es un tipo asociado con una construcción del lenguaje.
EXPRESIONES DE TIPO
DEFINICIÓN
Una expresión de tipo es o bien un tipo básico, o es el resultado de aplicar un constructor de tipo a otra expresión de tipo de acuerdo a unas reglas de construcción (dadas por el lenguaje).
Tipos básicos: integer, char, bool, float, …. Tipos básicos especiales:
error_tipo: Indica que no se puede asociar una expresión
de tipo correcta con la construcción analizada.
void: Indica que una construcción del lenguaje es correcta,
pero no tiene ningún tipo asociado. Útil en la comprobación de sentencias.
Un nombre de tipo es una expresión de tipo.
EXPRESIONES DE TIPO
DEFINICIÓN
Un constructor de tipo permite construir tipos complejos a partir de tipos mas simples.
Un constructor de tipo aplicado a una expresión de tipo es una expresión de tipo. Los constructores incluyen, entre otros:
Constructor de arreglo: Si T es una expresión de tipo entonces
array(I, T) es una expresión de tipo.
(PASCAL) var a: array[0..9] of integer
array(0..9, integer) (C) int a [10]
EXPRESIONES DE TIPO
DEFINICIÓN
Constructor de producto cartesiano:
Si T
1y T
2son expresiones de tipo entonces su producto
cartesiano T
1x T
2es una expresión de tipo. Un objeto de este
tipo es una dupla ordenada, con el primer elemento de tipo T
1y el segundo de tipo T
2, se supone que „x‟ es asociativa a
izquierda.
EXPRESIONES DE TIPO
DEFINICIÓN
Constructor de registro: Similar a productos, pero con
nombre asociados a los campos.
Si u
1, u
2, . . . , un son los nombre de los campos de tipos T
1,
T
2, . . , T
n, la expresión de tipo asociada al registro es
record((u
1xT
1) x (u
2x T
2) × . . . × (u
nx T
n))
Ej.struct { record begin
char nombre[10]; nombre: array [0..9] of character; float altura; altura: real;
} end;
La correspondiente expresión de tipo es:
EXPRESIONES DE TIPO
DEFINICIÓN
Constructor de punteros: Sea T una expresión de tipo entonces pointer(T)
es una expresión de tipo.
var p: ↑ char;
pointer(char)
char * p
Constructor de funciones: Siendo D la expresión de tipo de los
argumentos de la función y T es la expresión de tipo del valor devuelto, la
expresión de tipo de la función es:
D → T
Para funciones con múltiples argumentos se usan productos
float ∗ f(char a, char b);
char x char → pointer(real)
EQUIVALENCIA DE TIPOS
Objetivo: Determinar si dos expresiones de tipo
se consideran equivalentes.
Equivalencia estructural:
Dos
expresiones
de
tipo
son
equivalentes
estructuralmente si son el mismo tipo básico, o si
están formadas por la aplicación del mismo
constructor de tipos a tipos estructuralmente
equivalentes
EQUIVALENCIA DE TIPOS
Ejemplo en PASCAL:
Type t_vector : array [0..9] of integer; array(0..9, integer)
t_matriz : array [0..9] of array [0..9] of integer; array(0..9, array(0..9,integer))
Var
uno: array [0..9] of integer;
dos: array [0..9, 0..9] of integer; Construir expresiones de tipo tres: t_matriz;
cuatro: array [0..9] of t_vector; cinco: array [0..9] of t_vector;
EQUIVALENCIA DE TIPOS
Ejemplo en PASCAL:
Type t_vector : array [0..9] of integer; array(0..9, integer)
t_matriz : array [0..9] of array [0..9] of integer; array(0..9, array(0..9,integer))
Var
uno: array [0..9] of integer; array(0..9, integer)
dos: array [0..9, 0..9] of integer; array(0..9, array(0..9, integer)) tres: t_matriz; array(0..9, array(0..9, integer)) cuatro: array [0..9] of t_vector; array(0..9, array(0..9, integer)) cinco: array [0..9] of t_vector; array(0..9, array(0..9, integer))
EQUIVALENCIA DE TIPOS
Equivalencia nominal o por nombre
Dos expresiones de tipo son nominalmente equivalentes si son el mismo tipo básico o si a ambas se les ha asociado el mismo nombre. (Semejante a la estructural pero sin reemplazar los nombres por la expresión de tipo asociada al nombre definido por el usuario)
Type t_vector : array [0..9] of integer; array(0..9, integer)
t_matriz : array [0..9] of array [0..9] of integer; array(0..9, array(0..9,integer))
var
uno: array [0..9] of integer; array(0..9, integer) dos: array [0..9, 0..9] of integer; array(0..9, array(0..9, integer)) tres: t_matriz; array(0..9, array(0..9, integer)
cuatro: array [0..9] of t_vector; array(0..9, t_vector) cinco: array [0..9] of t_vector; array(0..9, t_vector)
EQUIVALENCIA DE TIPOS
OTRO EJEMPLO:
typedef entero int; int
entero* a; pointer(int)
int *b; pointer(int)
¿ Son estructuralmente equivalentes? SI
¿ Son nominalmente equivalentes? NO
VERIFICACIÓN DE LA
EQUIVALENCIA ESTRUCTURAL
function esEquivalente(S, T): boolean begin
if S and T son el mismo tipo básico then return TRUE;
else if (S = array(S1, S2)) and (T = array(T1, T2))
then return [esEquivalente(S1,T1) and esEquivalente(S2,T2)]; else if (S = S1 × S2) and (T = T1 × T2)
then return [esEquivalente(S1,T1) and esEquivalente(S2,T2)]; else if (S = pointer(S1)) and (T = pointer(T1))
then return [esEquivalente(S1,T1)]; else if (S = S1 → S2) and (T = T1 → T2)
then return [esEquivalente(S1,T1) and esEquivalente(S2,T2)] else return FALSE;
End
NOTA: LA EQUIVALENCIA NOMINAL es más fácil de verificar, basta con comprobar que coinciden los nombres asociados con las expresiones de tipo.
CONVERSIÓN DE TIPOS
Tipos ≠ → Distintas instrucciones.
Distinta representación en memoria.
Conversión implícita (coerción):
• Se realizan automáticamente.
• No se puede perder información.
• Coerción de constantes.
Conversión explícita:
• Especificada por el programador.
• Como llamadas a funciones.
CONSTRUCCIÓN DE UN CHEQUEADOR
DE TIPOS USANDO ETDS
EJEMPLO: Sea la siguiente gramática
P → SD ; SS. SD → D ; SD1 | D D → T : LI
T → int | bool | stack(T) LI → id , LI1 | id
SS → S ; SS1
S → AS | REP | PUSH(id, EX) AS → id:= EX
REP → while EX do SS od EX → TE + EX1 | TE
EJEMPLO (CONTINUACIÓN)
Con el siguiente sistema de tipos:
Todo identificador debe ser declarado antes de su uso. El stack sólo puede contener elementos del mismo tipo. Una cte_num es de tipo entero.
Una constante con valor FALSE o TRUE es de tipo lógico. En la asignación no se admiten coerciones.
La expresión en la sentencia while debe ser booleana.
En la sentencia PUSH el identificador debe ser de tipo stack y la
expresión debe ser del tipo del elemento con el cual fue declarado el stack.
En la función POP el identificador debe ser de tipo stack y devuelve como resultado el elemento que se encuentra en el tope del stack.
El operador + está sobrecargado, si los dos operandos son enteros entonces representa la suma aritmética, si ambos operandos son de tipo lógico representa la operación AND, en otro caso error. Una sentencia tiene tipo void.
Esquema de Traducción
En el ETDS se usa:
Un atributo sintetizado para el tipo de las construcciones del lenguaje, de tipo string, su nombre es <noterminal>.tipos
Un atributo heredado para asociar el tipo con cada identificador en una declaración múltiple, es de tipo string y su nombre <noterminal>.tipoh
La función addTS ingresa información de los identificadores en la TS devolviendo un TRUE si lo pudo insertar y un FALSE si ya existía. Las variables deben ser declaradas antes de su uso. Considere, de acuerdo a su criterio, las acciones a llevar a cabo respecto a los identificadores duplicados.
La función searchTS recupera información de la tabla de símbolos, devolviendo el atributo tipo, si existe o un string “” caso contrario.
Considere, de acuerdo a su criterio, las acciones a llevar a cabo respecto a los identificadores no definidos.
ETDS para la declaraciones: Todo identificador debe ser
declarado antes de su uso.
SD → D ; SD1 {SD.tipos= SD1.tipos} |
D {SD.tipo = D.tipos}
D → T : {LI.tipoh = T.tipos} LI {D.tipos=void}
LI → id {if ! addTS(id.entry, LI.tipoh) then error_tipo},
{LI1.tipoh = LI.tipoh} LI1 |
id {if ! addTS(id.entry, LI.tipoh) then error_tipo}
T → int {T.tipos = integer} | bool {T.tipos= bool} | constructor de stack
ETDS para: el stack sólo puede contener elementos
del mismo tipo y las sentencias tienen tipo void.
S → PUSH(id, EX)
{aux= searchTS(id.entry, tipo);
if aux == “” then error_tipo (1) else {t= tipo_base(aux); if
aux != stack(t) || t!=EX.tipos then error_tipo (2); } S.tipos =
void}
En (1) se debe tratar el error de identificador no definido.
En (2) se debe tratar el error de que el elemento que se
quiere insertar en el stack no coincide con el tipo de los
elementos del stack.
tipo_base(x) retorna el tipo base de un constructor de stack.
Notar que independientemente de que haya o no existido
error la sentencia lleva tipo void
ETDS para: controlar los términos (TE)
Una cte_num es de tipo entero.
Una constante con valor FALSE o TRUE es de tipo lógico.
TE → cte_num {TE.tipos= integer}|
TRUE {TE.tipos = bool } |
FALSE {TE.tipos = bool}
Todo identificador debe ser declarado antes de su uso (uso del identificador).
TE → id {aux=searchTS(id.entry, tipo);
if aux ==“” then error_tipo else TE.tipos = aux}
En la función POP el identificador debe ser de tipo stack y devuelve como resultado el elemento que se encuentra en el tope del stack.
TE → POP(id) {aux =searchTS(id.entry, tipo) ;
if aux ==“” then error_tipo else {t= tipo_base(aux);
ETDS para: las sentencias
S
→ AS
{S.tipos=void}
| REP
{S.tipos =void}
AS
→ id:= EX
{aux = searchTS(id.entry, tipo); if
aux == “” then tipo_error else if aux !=Ex.tipos then
error_tipo; AS.tipos= void}
REP
→ while EX do SS od
{if EX.tipos != bool then
tipo_error; REP.tipos=void}
ETDS para Expresiones
El operador + está sobrecargado, si los dos operandos son enteros entonces representa la suma aritmética, si ambos operandos son de tipo lógico representa la operación AND, en otro caso error (esto se debe tener en cuenta en la generación de código)
EX → TE+EX1 {if TE.tipos == int ^ EX1.tipos== int then
EX.tipos = TE.tipos else if TE.tipos == bool ^ EX1.tipos ==
bool then EX.tipos = TE.tipos else error_tipo} | TE {EX.tipos = TE.tipos}
O
EX → TE+EX1 {if TE.tipos == EX1.tipos then EX.tipos =