Diseño de compiladores Recordando la clase anterior Control de Tipos public class Clase1 implements Interfaz1 private string entero1; void metodo1() int[] x = new string; x[5] = entero1 * y; void metodo1() Tipo de datos equivocado public class Clase1 implements Interfaz1 private string entero1; void metodo1() int[] x = new string; x[5] = entero1 * y; void metodo1() Variable no declarada (y) No podemos multiplicar strings int fibonacci(int n) return metodo1() + fibonacci(n 1); int fibonacci(int n) return metodo1() + fibonacci(n 1); public class Clase1 implements Interfaz1 private string entero1; Que es un tipo? void metodo1() int[] x = new string; x[5] = entero1 * y; void metodo1() int fibonacci(int n) return metodo1() + fibonacci(n 1); No podemos sumar void La noción de tipo varia de lenguaje en lenguaje Hay cierto consenso en que: Es un conjunto de valores Es un conjunto de operaciones sobre esos valores Los errores de tipos ocurren cuando se realizan operaciones sobre valores que no soportan dichas operaciones 1
Chequeo de tipos Chequeo estático Analizamos el programa en tiempo de compilación para probar que no hay errores de tipo Idea: No dejar que pasen cosas malas en runtime Chequeo dinámico Chequeamos las operaciones en runtime, antes de hacerlas Mas preciso que el control estático, pero menos performante Sin control de tipos Mucha suerte! Sistemas de tipos Las reglas que determinan las operaciones permitidas en tipos, forman un sistema de tipos Sistemas fuertemente tipados NUNCA permiten un error de tipos Java, Phyton, Javascript, LISP, Haskell, etc. Sistemas débilmente tipados PERMITEN errores de tipos en runtime C, C++ Cual es mejor? Control de tipos (estático) Es un debate sin fin Los sistemas dinámicos facilitan la prototipación, mientras que los sistemas estáticos suelen tener menos fallas Los lenguajes fuertemente tipados son mas robustos, los lenguajes débilmente tipados, son mas rápidos En general implica 2 pasos Inferir los tipos de datos de las expresiones a partir de los tipos de datos de los componentes de las mismas Confirmar que los tipos de datos de las expresiones concuerdan con lo que se espera en ciertos contextos Conceptualmente son dos pasos diferentes, aunque suelen realizarse a la vez Ejemplo Ejemplo while (shiftbits(x + 5) <= 10) if (2.0 + 3.0) while (shiftbits(x + 5) <= 10) if (2.0 + 3.0) while (5 == null) while (5 == null) La expresión esta bien en termino de tipos Pero el tipo de la expresión no permite usarla en donde esta usada 2
Ejemplo while (shiftbits(x + 5) <= 10) if (2.0 + 3.0) while (5 == null) La expresión tiene un error de tipos + + INT 200 150 200 150 + + INT INT INT INT INT 200 150 200 150 3
= x Identificador = y Identificador true BoolConstant = = x Identificador = x Identificador = y Identificador true BoolConstant y Identificador true BoolConstant Reglas de inferencia Si x es un identificador que refiere a un objeto que tiene tipo T, entonces la expresión x tiene tipo T Si e es una constante entera, entonces e tiene tipo INT Si los operandos e1 y e2 de la expresión e1+e2 tienen tipos INT, entonces la expresión e1+e2 tiene tipo INT Control de tipos, como pruebas Podemos pensar el control de tipos, como la verificación de afirmaciones acerca de los tipos de las expresiones Comenzamos con un conjunto de axiomas a los que luego aplicamos reglas de inferencia para determinar el tipo de las expresiones Un sistema de tipos puede pensarse como un sistema de pruebas 4
Notación formal / Sistemas de tipos Esto se especifica con la siguiente sintaxis: Precondiciones PostCondiciones Esto se lee así: Si las precondiciones son verdaderas, podemos inferir las postcondiciones Notación formal / Sistemas de tipos Escribimos: e : T Si la expresión e tiene tipo T El símbolo significa Podemos inferir que Axiomas Algunas reglas de inferencia i es una constante INT s es una constante STRING i : INT s : STRING true : bool false : bool d es una constante DOUBLE d : DOUBLE Algunas reglas mas complejas Algunas reglas mas complejas Si podemos mostrar que e1 y e2 tienen tipo INT e1 : INT e2 : INT e1 : DOUBLE e2 : DOUBLE e1 : INT e2 : INT e1 : DOUBLE e2 : DOUBLE e1 + e2 : INT e1 + e2 : DOUBLE e1 + e2 : INT e1 + e2 : DOUBLE 5
Algunas reglas mas complejas Mas complicado todavía e1 : INT e2 : INT e1 : DOUBLE e2 : DOUBLE e1 : T e2 : T e1 : T e2 : T e1 + e2 : INT e1 + e2 : DOUBLE e1 == e2 : e1!= e2 : Entonces podemos afirmar que e1+e2 también tiene tipo INT Por que especificar así? Provee una definición rigurosa de los tipos de datos, independiente de cualquier implementación de compilador Flexibiliza la implementación Podemos implementar como queramos, siempre que respetemos las reglas Permite verificación formal de propiedades Permite realizar pruebas inductivas en la estructura del programa Un problema x:??? Un problema x:??? Como sabemos el tipo de x, si no sabemos a que referencia? if (x == 1.5) 6
if (x == 1.5) if (x == 1.5) x : int if (x == 1.5) if (x == 1.5) d es una constante double d: double x : int x : int 1.5 : double if (x == 1.5) if (x == 1.5) 7
e1 : T e2 : T e1 == e2 : x : int 1.5 : double x : int 1.5 : double if (x == 1.5) if (x == 1.5) e1 : T e2 : T e1 == e2 : e1 : T e2 : T e1 == e2 : x : int 1.5 : double x : int 1.5 : double if (x == 1.5) x == 1.5 : bool if (x == 1.5) x == 1.5 : bool Problema En el ejemplo anterior, no podemos llegar a inferir que x == 1.5 tiene tipo booleano, ya que tenemos hechos contradictorios para x Los hechos no tienen contexto Debemos reforzar los hechos, para indicar bajo que circunstancias son correctos Escribimos: Agregamos el scope S e : T Si en el scope S, la expresión e tiene tipo T Los tipos ahora son verificados según el scope en el que se encuentran 8
S true : S false : La regla correcta seria i es una constante INT s es una constante STRING S i : INT d es una constante DOUBLE S d : DOUBLE S s : STRING x es una variable en scope S con tipo T S x : T S e1 : DOUBLE S e2 : DOUBLE S e1 + e2 : DOUBLE S e1 : INT S e2 : INT S e1 + e2 : INT La regla correcta seria Reglas para funciones x es una variable en scope S con tipo T S x : T S f(e1,e2,,en) :? Reglas para funciones Reglas para funciones f es un identificador f es un identificador f es una función en el scope S S f(e1,e2,,en) :? S f(e1,e2,,en) :? 9
Reglas para funciones Reglas para funciones f es un identificador f es una función en el scope S f tiene tipo (T1,T2,,Tn) U S f(e1,e2,,en) :? f es un identificador f es una función en el scope S f tiene tipo (T1,T2,,Tn) U S ei : Ti para 1 i n S f(e1,e2,,en) :? Reglas para funciones Reglas para arrays f es un identificador f es una función en el scope S f tiene tipo (T1,T2,,Tn) U S ei : Ti para 1 i n S f(e1,e2,,en) : U S e1 : T [ ] S e2 : int S e1[e2] : T Reglas para asignaciones Reglas para asignaciones S e1 : T S e2 : T S e1 = e2 : T S e1 : T S e2 : T S e1 = e2 : T Que pasa con una expresión de este estilo? 10 = x; 10
Reglas para asignaciones Tipos en Clases S e1 : T S e2 : T S e1 = e2 : T Si estamos en un lenguaje orientado a objetos, si tenemos dos clases, Base y Derivada (que extiende a Base), funciona la regla para este código? Como incorporamos la herencia en nuestras reglas de inferencia? Debemos considerar la forma de las jerarquias de clases Base mybase; Derivada myderived; mybase = myderived; Herencia simple A Herencia múltiple A B C D B C D E Propiedades de herencia Cualquier clase es convertible consigo misma (Reflexibilidad) Si A es convertible en B, y B es convertible en C, entonces A es convertible en C (Transitividad) Si A es convertible en B y B es convertible en A, entonces A y B son el mismo tipo (Antisimetría) Esto define un orden parcial sobre los tipos Orden parcial en tipos Decimos que A <= B, si A es convertible en B Tenemos entonces que: A <= A A <= B, B <= C entonces A <= C A <= B, B <= A entonces A = B Entonces, la regla de asignación queda como: 11
Reglas para asignaciones Regla para comparaciones S e1 : T1 S e2 : T2 T2 <= T1 S e1 = e2 : T1 S e1 : T S e2 : T S e1 == e2 : bool S e1 : T1 S e2 : T2 T1 y T2 son clases T1 <= T2 o T2 <= T1 S e1 == e2 : bool Regla para comparaciones Regla para comparaciones S e1 : T S e2 : T S e1 : T1 S e2 : T2 T1 y T2 son clases T1 <= T2 o T2 <= T1 S e1 : T S e2 : T S e1 : T1 S e2 : T2 T1 y T2 son clases T1 <= T2 o T2 <= T1 S e1 == e2 : bool S e1 == e2 : bool S e1 == e2 : bool S e1 == e2 : bool Implica que están en la misma jerarquía Son comparables entonces Seria interesante unificar las reglas Estructura de los tipos Extendemos la convertibilidad A B C D E bool string double int arrays Si A es un tipo primitivo o un array, entonces A es convertible solo con A Si A y B son tipos, si A es un tipo primitivo o un array, entonces: A <= B implica que A = B B <= A implica que A = B 12
Regla para comparaciones Regla para funciones S e1 : T1 S e2 : T2 T1 <= T2 o T2 <= T1 S e1 == e2 : bool f es un identificador f es una función en el scope S f tiene tipo (T1,T2,,Tn) U S ei : Ri para 1 i n Ri <= Ti para 1 i n S f(e1,e2,,en) : U Que pasa con esto? Estructura de los tipos A S null :??? B C D E bool string double int arrays null type Manejando el null Definimos un nuevo tipo de datos, correspondiente al literal null, lo llamamos null type Hacemos que null type <= A, para cualquier clase A El null type no es accesible al programador, solo puede ser usado internamente Muchos lenguajes orientados a objetos tienen esta contruccion Null Type S null : Null Type 13
Que pasa con los tipos y las sentencias? Podemos probar la correctitud del tipo de las expresiones Pero Como podemos probar que una sentencia IF tiene condiciones booleanas bien formadas? Como podemos probar que una sentencia RETURN devuelve el tipo correcto? Que pasa con los tipos y las sentencias? Extendemos el sistema de pruebas a las sentencias, para verificar que están bien formadas Escribimos: S WF(stmt) Si la sentencia stmt se encuentra bien formada en el scope S Si podemos asignar un tipo T en el scope S a la expresión expr Un ejemplo Reglas para secuencias S expr : T S WF(expr;) S WF(stmt1) S WF(stmt2) S WF(stmt1 stmt2) Entonces decimos que la sentencia expr; esta bien formada en el scope S Reglas para loops Reglas para bloques S expr : bool Sea S el scope dentro del loop S WF(stmt) S WF(while (expr) stmt) Sea S el scope formado al agregar decls a S S WF(stmt) S WF( decls stmt ) 14
Reglas para return Chequeando WF S esta dentro de una función que retorna T S expr : T T <= T S WF(return expr;) S esta dentro de una función que retorna void S WF(return;) Podemos hacerlo recursivamente, recorriendo el AST Para cada sentencia Controlamos los tipos de cualquier sobrexpresión Si no podemos asignar tipos, reportamos error Si asignamos el tipo errado, reportamos error Controlamos los tipos de las subsentencias Controlamos la correctitud de la sentencia 15