Sie sind auf Seite 1von 42

Sistema de tipos y Semntica

Ishmael: seguro que todo esto tiene un significado. Hermn Melville, Moby Dick

CONTENIDO DEL CAPTULO

3.1. Sistemas de tipos. 3.2. Dominios de semntica y transformacin de estado. 3.3. Semntica operacional. 3.4. Semntica axiomtica. 3.5. Semntica denotativa. 3.6. Ejemplo: semntica de asignaciones y expresiones de J-PL0.

Los sistemas de tipos se han convertido en algo muy importante en el diseo de lenguajes porque podemos utilizarlos para formalizar la definicin de los tipos de datos de un lenguaje y su utilizacin correcta en los programas. Con frecuencia asociamos los sistemas de tipos con la sintaxis, especialmente en lenguajes en cuyos programas se revisan los tipos en tiempo de compilacin. Para estos lenguajes, un sistema de tipos es una extensin de definiciones que impone restricciones sintcticas especficas (como por ejemplo, el requisito de tener que declarar todas las variables a las que se haga referencia en un programa) que no podemos expresar en BNF o EBNF. Para los lenguajes en cuyos programas la revisin de tipos se realiza en tiempo de ejecucin, podemos ver un sistema de tipos como parte de la semntica del lenguaje. Por tanto, el sistema de tipos de un lenguaje se encuentra entre la sintaxis y la semntica y podemos visualizarlo apropiadamente en cualquiera de los dos campos. La definicin de un lenguaje de programacin est completa slo cuando estn completamente definidos su semntica, su sintaxis y su sistema de tipos. La semntica de un lenguaje de programacin es una definicin del significado de cualquier programa que sea sintcticamente vlido desde los puntos de vista de la sintaxis concreta y de la revisin de tipos esttica1. Podemos definir el significado de un programa de varias maneras diferentes. Una idea intuitiva sencilla del significado de un programa es lo que sucede en una computadora (real o de ejemplo) cuando ejecutamos el programa. Una descripcin precisa de esta idea es la llamada semntica
No hace demasiado tiempo, era posible escribir un programa sintcticamente correcto en un lenguaje en particular que se comportaba de manera diferente cuando lo ejecutbamos en plataformas diferentes (con la misma entrada). Esta situacin surgi porque la definicin de la semntica del lenguaje no era lo suficientemente precisa como para exigir que todos sus compiladores tradujeran un programa a versiones de lenguaje lgicamente equivalentes. En estos ltimos aos, los diseadores de lenguajes se han dado cuenta de que el trato formal de la semntica es tan importante como el trato formal de la sintaxis para asegurarse de que un programa en particular significa lo mismo sin importar la plataforma en la que lo ejecutemos. Los lenguajes modernos son mucho mejores a este respecto que los ms antiguos.
1

operacional2. Otro modo de ver el significado de un programa es empezar con una especificacin
formal de lo que se supone que tiene que hacer el programa y despus demostrar rigurosamente que lo hace, utilizando una serie sistemtica de pasos lgicos. Este mtodo evoca la idea de semntica

axiomtica. Un tercer modo de ver la semntica de un lenguaje de programacin es definir el


significado de cada tipo de instruccin que se produce en la sintaxis (abstracta) como una funcin matemtica de transformacin de estado. As, podemos explicar el significado de un programa como una coleccin de funciones que operan en el estado del programa. Este mtodo se llama semntica

denotativa.
Los tres mtodos de definicin semntica tienen ventajas e inconvenientes. La semntica operacional tiene la ventaja de representar el significado del programa directamente en el cdigo de una mquina real (o simulada). Pero esto es tambin una debilidad potencial, debido a que la definicin de la semntica de un lenguaje de programacin con base a una arquitectura en particular, ya sea real o abstracta, restringe la utilidad de esa definicin a los escritores-compiladores y programadores que trabajen con arquitecturas diferentes. Adems, el equipo virtual en el que ejecutamos las instrucciones tambin necesita una descripcin semntica, lo que aade complejidad y puede llevarnos a definiciones viciadas. La semntica axiomtica es particularmente til en la exploracin de las propiedades formales de los programas. A los programadores que deben escribir programas correctos a partir de un conjunto de especificaciones preciso, les resulta particularmente til este estilo semntico. La semntica denotativa es valiosa porque su estilo funcional lleva la definicin semntica de un lenguaje a un nivel alto de precisin matemtica. A travs de ella, los diseadores de lenguajes obtienen una definicin funcional del significado de la elaboracin de cada lenguaje que es independiente de cualquier arquitectura de equipo en particular. En este captulo, introducimos un enfoque formal de la definicin del sistema de tipos de un lenguaje. Tambin introducimos los tres modelos de definicin semntica, prestando especial atencin al modelo denotativo. Utilizamos la semntica denotativa en captulos posteriores para estudiar distintos conceptos en el diseo de un lenguaje y para aclarar diversos problemas semnticos. Este modelo es particularmente valioso, porque tambin nos permite explorar activamente estos conceptos del diseo de un lenguaje en un entorno de laboratorio.

Tcnicamente, existen dos tipos de semntica operacional, llamadas semntica operacional (a veces semntica natural) tradicional y estructurada. En este captulo, hablaremos de esta ltima.

3.1.

SISTEMAS DE TIPOS

Un tipo es un conjunto bien definido de valores y de operaciones en esos valores. Por ejemplo, el tipo familiar int tiene valores {..., -2, -1, 0, 1, 2, ...} y operaciones {+, -, *,/,...} en esos valores. El tipo boolean tiene valores {verdadero, falso) y operaciones {&&, || y !} en esos valores. Un sistema de tipos es un sistema bien definido para asociar tipos con variables y otros objetos definidos en un programa. Algunos lenguajes, como C y Java, asocian un nico tipo con una variable a lo largo de la vida de esa variable en tiempo de ejecucin, y podemos determinar los tipos de todas las variables en tiempo de compilacin. Otros lenguajes, como Lizp y Scheme, permiten que el tipo de una variable, as como su valor, cambien mientras el programa se ejecuta. Los lenguajes de la primera clase se llaman lenguajes de tipos estticos, mientras que los otros son de tipos dinmicos. Un lenguaje de tipos estticos permite que las reglas de su tipo se definan completamente con base a su sintaxis abstracta. A menudo llamamos a esta definicin semntica esttica y nos ofrece un medio de aadir informacin sensible al contexto a un analizador que la gramtica BNF no puede proporcionarnos. Identificamos este proceso como anlisis semntico en el Captulo 2 y lo estudiaremos con ms atencin ms adelante. Un error de tipo es un error en tiempo de ejecucin que se produce cuando intentamos una operacin en un valor para el que no est bien definida. Por ejemplo, tomemos la expresin de C
x+u.p, donde u est definida como la unin {int a; double p;} e iniciada por la asignacin: u.a = 1;

Esta expresin puede provocar un error de tipo, debido a que los valores alternativos int y double de u comparten la misma direccin de memoria y u se inicia con un valor int. Si x es un double, la expresin x+u. p provoca un error que no podemos comprobar ni en tiempo de compilacin ni en tiempo de ejecucin. Un lenguaje de programacin es de tipos estrictos si su sistema de tipos permite la deteccin de todos los errores de tipo de los programas, ya sea en tiempo de compilacin como en tiempo de ejecucin, antes de que la instruccin en la que se pueden producir se ejecute. (El hecho de que un lenguaje tenga tipos estticos o dinmicos no evita que tenga tipos estrictos.) Por ejemplo, Java es un lenguaje con tipos estrictos, mientras que C no lo es. Es decir, en C podemos escribir y ejecutar la expresin x+1 sin importar si el valor de x es un nmero o no. Generalmente, los tipos estrictos fomentan programas ms fiables y se consideran como una virtud en el diseo de lenguajes de programacin. Un programa es de tipo seguro si sabemos que no tiene errores de tipo. Por definicin, todos los programas de un lenguaje con tipos estrictos son de tipo seguro. Adems, un lenguaje es de tipo

seguro si todos sus programas lo son. Los lenguajes con tipos dinmicos como Lisp deben ser, necesariamente, de tipo seguro. Como todas sus comprobaciones de tipos se realizan en tiempo de ejecucin, los tipos de las variables pueden cambiar dinmicamente. Esto supone cierto coste adicional en el rendimiento en tiempo de ejecucin de un programa, debido a que el cdigo de comprobacin de tipos ejecutable debe estar entremezclado con el cdigo escrito por el programador. Cmo podemos definir un sistema de tipos para un lenguaje de manera que podamos detectar los errores de tipo? Respondemos a esta pregunta en la seccin siguiente.

3.1.1.

Cmo formalizar el sistema de tipos

Una manera de definir el sistema de tipos de un lenguaje es escribir un conjunto de especificaciones de funciones que defina lo que significa para un programa ser de tipo seguro3. Podemos escribir estas reglas como funciones de valor boolean y podemos expresar ideas como todas las variables declaradas tienen nombres nicos o todas las variables utilizadas en el programa deben declararse. Como podemos expresar funcionalmente las reglas para escribir programas de tipo seguro, podemos implementarlas en Java como un conjunto de mtodos de valor boolean. La base de esta definicin funcional es un mapa de tipos, que es un conjunto de parejas que representa las variables declaradas y sus tipos. Podemos escribir el mapa de tipos, tm, de un programa as:

tm = {v1, t1, v2, t2, ... , vn, tn}


donde cada vi indica una Variable y cada ti indica su Tipo declarado. Para el lenguaje J-PL0, las

Variables de un mapa de tipos son nicas entre s y cada tipo se toma de un conjunto de tipos
disponibles que se fijan en tiempo de compilacin. En J-PL0, ese conjunto es fijo permanentemente (es {int, boolean}). En la mayora de los lenguajes, como C y Java, el programador puede ampliar ese conjunto definiendo tipos nuevos (tipos definidos en C, y clases en Java)4. En cualquier caso, aqu tenemos un ejemplo de mapa de tipos para un programa que tiene tres variables declaradas; i y j con tipo int y p con tipo boolean:

tm = {i, int, j, int, ... , p, boolean}


Un trato formal de comprobacin de tipos estticos de un programa se basa en la existencia de
Es importante observar que la sintaxis concreta definida por BNF de un lenguaje no es adecuada para definir sus requisitos de comprobacin de tipos. Es decir, BNF no es un dispositivo lo suficientemente fuerte para expresar ideas como todas las variables declaradas deben tener nombres nicos. Esta limitacin importante de la gramtica BNF se expone normalmente en un estudio de lenguajes formales (tipo Chomsky) y se dara en un curso sobre la teora de la computacin o de compiladores. 4 Adems, las reglas para dar nombre a las variables en C y en Java son ms flexibles y complejas, teniendo en cuenta el entorno sintctico en el que se declara la variable. Volveremos a ver este tema en el Captulo 5.
3

un mapa de tipos que se haya extrado de las Declarations que aparecen en la parte superior del programa. Definimos la funcin typing para J-PL0 de este modo:

typing: Declarations

TypeMap
i{1,..., n}

typing ( Declarations d ) =

U d .v, d .t
i i

Implementamos fcilmente esta funcin en Java as:


public TypeMap typing(Declarations d) { // Coloca las variables y los tipos en un nuevo diccionario o // tabla de smbolos (map), el cual se retorna TypeMap map = new TypeMap(); for (int i = 0; i < d.size(); i++) { map.put(((Declaration)(d.elementAt(i))).v, ((Declaration)(d.elementAt(i))).t); } return map; }

De este modo, dado un Vector de Declarations di, el mtodo typing devuelve un TypeMap de Java cuyas claves son las variables declaradas di.v y cuyos valores son sus tipos respectivos di.t; en realidad, un TypeMap es una extensin de una HashTable de Java. Aqu, asumimos que la sintaxis abstracta de Declaration est definida en Java como sigue (vase el Apndice B):
class Declaration { // Declaration = Variable v; Type t Variable v; Type t; public void display() { // ... } }

Podemos expresar la comprobacin de tipos estticos de un lenguaje en notacin funcional, en la que cada regla que ayuda a definir el sistema de tipos es una funcin con valor boolean V(que significa vlido). V devuelve true o false dependiendo de si un miembro en particular de una clase sintctica abstracta es vlido o no, en relacin a estas reglas. Es decir,

V: Class

Por ejemplo, supongamos que queremos definir la idea de que una lista de declaraciones es vlida si todas sus variables tienen identificadores nicos entre s. Podemos expresar esta idea de manera precisa del siguiente modo:

V: Declarations

V ( Declaratio ns d ) = i , j {1,..., n} : (i j d i .v d j .v )
Es decir, cada par de variables de una lista de declaraciones tiene identificadores diferentes entre s. Recordemos que la variable i de una lista de declaraciones tiene dos partes, una Variable v y un

Tipo t. Esta particular regla de validez se dirige solamente al requisito de singularidad de las
variables. En un lenguaje real, es importante que la definicin de esta funcin especifique tambin que el tipo de cada variable debe tomarse del conjunto de tipos disponibles (como por ejemplo, {int, boolean} para J-PL0). Dejamos esta mejora como ejercicio. Dada esta funcin, la implementacin de la comprobacin de tipos en Java no es una tarea difcil. Como base, podemos utilizar la sintaxis abstracta que se define como un conjunto de clases. Asumiendo que implementamos Declarations como un Vector de Java (vase el Apndice B), podemos ver el mtodo V de Declarations como parte de una coleccin de mtodos V que definen todos los requisitos de comprobacin de tipos estticos de un lenguaje. Aqu tenemos el mtodo V de Java para las declaraciones:
public boolean V(Declarations d) { for (int i = 0; i< d.size() - 1; i++) for (int j=i+1; j<d.size(); j++) if ((((Declaration)(d.elementAt(i))).v).equals (((Declaration)(d.elementAt(j))).v)) return false; return true; }

En inters de la eficiencia, este mtodo no refleja exactamente la definicin dada en la funcin V para la singularidad entre los nombres de las variables declaradas. Es decir, el bucle interno controlado por j no cubre toda la serie {1,...,n} como sugiere la definicin formal de esta funcin. Sin embargo, los lectores deberan reconocer que esta implementacin es efectiva. Tambin debemos observar que, si estuviramos desarrollando un comprobador de tipos para un compilador, incluiramos un dispositivo para enviar mensajes de error tiles. Otra caracterstica comn de las reglas de escritura de un lenguaje es que toda variable a la que se haga referencia desde una instruccin de un programa debe declararse. Adems, toda variable utilizada en una expresin aritmtica debe tener un tipo aritmtico adecuado en su declaracin. Podemos definir estos requisitos utilizando las clases Statement y Expression, contextos en los que se producen las variables. Estos requisitos utilizan tambin la informacin del mapa de tipos del

programa como base para comprobar la existencia y conformidad de tipo de cada variable mencionada. Para un lenguaje completo, definiremos un conjunto de funciones V sobre todas sus clases sintcticas abstractas, incluida la clase ms general Program. Desde el punto de vista de la sintaxis abstracta, un programa completo tiene dos partes; una serie de Declarations y un cuerpo que es un

Block. Es decir,
class Program { // Program = Declarations decpart ; Block body Declarations decpart; Block body; public void display() { // ... } }

Por tanto, una comprobacin de tipos completa puede definirse funcionalmente a este nivel ms general con la funcin V, que se define as:

V: Program

V(Program p) = V (p.decpart) V (p.body, typing(p.decpart))

Es decir, si las Declarations tienen nombres de variables nicos y cada instruccin del cuerpo del programa es vlida con respecto al mapa de tipos generado a partir de las declaraciones, entonces todo el programa es vlido desde un punto de vista de comprobacin de tipos. Nuestra definicin de la sintaxis abstracta como una coleccin de clases de Java hace que esta especificacin en particular sea fcil de implementar.
public boolean V(Program p) { return V(p.decpart) && V(p.body, typing(p.decpart)); }

Es decir, un programa tiene dos partes, una decpart, que es Declarations, y un body, que es un

Block. Podemos comprobar la validez de los tipos del Block del programa slo en relacin al mapa de
tipos en particular que representa sus declaraciones. A continuacin, tenemos un resumen de todos los requisitos para la comprobacin de tipos de JPL0. Veremos los detalles de las funciones de validez de tipos de las clases individuales de

expresin e instruccin en un programa J-PL0 en el Captulo 4, donde hablaremos del diseo de lenguajes imperativos con mayor generalidad.

3.1.2.

Comprobacin de tipos en J-PL0

J-PL0 es un lenguaje de tipos estticos y estrictos. Por consiguiente, podemos comprobar todos los errores de tipo en tiempo de compilacin, antes de ejecutar el programa. Aqu tenemos un resumen informal de los requisitos para la comprobacin de tipos J-PL0. Cada Variable declarada debe tener un Identifier nico. Cada tipo de Variable debe ser int o boolean. Cada Variable mencionada dentro de una Expression del programa debe haber sido declarada. El tipo de resultado de cada Expression se determina as:

Si la Expression es una simple Variable o Value, entonces su tipo de resultado es el tipo de esa

Variable o Value.
Si el Operator de la Expression es aritmtico (+, -, * /), entonces sus trminos deben ser todos de tipo int y su tipo de resultado es, en consecuencia, int. Por ejemplo, la Expression x+1 exige que x sea int (ya que 1 es int) y su tipo de resultado es int. Si el Operator es relacional (<, <=, >, >=, = =, !=), entonces sus trminos deben ser de tipo int y su tipo de resultado es boolean. Si el Operator es boolean (&&, | | o !), su trmino o trminos deben ser de tipo boolean y su tipo de resultado es boolean. Para cada instruccin Assignment, el tipo (int o boolean) de su variable objetivo debe estar de acuerdo con el tipo de su expresin de origen. Para cada Conditional y Loop, el tipo de su expresin debe ser boolean. Para ilustrar estos requisitos, tomemos el programa J-PL0 sencillo que tenemos a continuacin:
// Resultado del clculo = el factorial del entero n void main () { int n, i, result; n = 8; i = 1; result = 1;

while (i < n) { i = i + 1; result = result * i; } }

En este programa, debemos declarar las variables n, i y result utilizadas. Por otra parte, las

Expressions result*i e i+1 tienen ambos operandos de tipo int. Adems, la Expression i<=n tiene
tipo boolean, ya que el operador <= toma operandos de tipo int y devuelve un resultado boolean. Por ltimo, cada una de las Assignments de este programa tiene una Variable de tipo int como objetivo y una Expression de tipo int como origen. En trminos generales, estas caractersticas hacen que este programa J-PL0 sencillo sea vlido con respecto a las reglas de tipos dadas anteriormente. En el Captulo 4 presentaremos estas ideas ms formalmente y las explicaremos con ms amplitud.

3.2.

DOMINIOS SEMNTICOS Y TRANSFORMACIN DE ESTADO

Los nmeros naturales, enteros, nmeros reales y los booleans y sus propiedades matemticas proporcionan un contexto fundamental para el diseo de lenguajes de programacin. Por ejemplo, el lenguaje J-PL0 se basa en nuestra intuicin matemtica sobre la existencia y comportamiento de N (los nmeros naturales), I (los enteros), B (el conjunto {true, false} de valores booleans) y S (el conjunto de cadenas de caracteres). Estos conjuntos son ejemplos de dominios semnticos de lenguajes de programacin. Un dominio semntico es cualquier conjunto cuyas propiedades y operaciones son bien entendidas independientemente y sobre el que se basan, en ltima instancia, las funciones que definen la semntica de un lenguaje. Tres dominios semnticos tiles de los lenguajes de programacin son el entorno, la memoria y las ubicaciones. El entorno y es un conjunto de pares que une variables especficas con ubicaciones de memoria. La memoria, u es un conjunto de pares que une ubicaciones especficas con valores. Las ubicaciones, en cada caso, son los nmeros naturales N. Por ejemplo, supongamos que tenemos variables i y j con valores 13 y -1 en algn momento durante la ejecucin de un programa. Supongamos que las ubicaciones de memoria estn numeradas en serie comenzando por el 0 y que, en ese momento, las variables i y j estn asociadas con las ubicaciones de memoria 154 y 155. Entonces, podemos expresar el entorno y la memoria que representa esta configuracin del modo siguiente5:
5

Utilizamos el valor especial undef para indicar el valor de una ubicacin de memoria (variable), que actualmente es indefinido (an no lo hemos asignado).

= {i, 154, j, 155} = {0, undef, ..., 154, 13, 155, -1, ...}
El estado de un programa es el producto de su entorno y su memoria. Sin embargo, para las explicaciones introductorias de este captulo, es conveniente representar el estado de un programa de una forma ms simplificada. Esta representacin pone fuera de juego las ubicaciones de memoria y define sencillamente el estado o de un programa como un conjunto de pares (v, val) que representa todas las variables activas y sus valores asignados actualmente en alguna etapa durante la ejecucin del programa6. Expresamos esto como sigue:

= {v1, val1, v2, val2, ..., vm, valm}


Aqu, cada vi indica una variable y cada vali indica su valor asignado actualmente. Antes de que el programa comience su ejecucin, = {v1, undef, v2, undef, ..., vm, undef}. Tambin utilizamos la expresin (v) para indicar la funcin que recupera el valor de la variable v del estado actual. Por ejemplo, la expresin que veremos a continuacin describe el estado de un programa que ha computado y asignado valores a tres variables, x, y y z, despus de ejecutar varias instrucciones:

= {x, 1, y, 2, z, 3}
Para este estado en particular, podemos recuperar el valor de una variable, digamos v, escribiendo la expresin as:

(y),

que da el valor 2. El siguiente paso en el programa podra ser una

instruccin de asignacin, como por ejemplo, y = 2 * z + 3; cuyo efecto sera cambiar el estado

= {x, 1, y, 9, z, 3}
El siguiente paso de la computacin podra asignar un valor a una cuarta variable, como en la asignacin w = 4; dando como resultado la siguiente transformacin de estado:
Ampliaremos esta definicin de estado en el Captulo 5, cuando ser importante para representar formalmente el entorno de un modo ms dinmico. All, un entorno dinmico nos permite describir de manera precisa las ideas de montculo y pila en tiempo de ejecucin y, de este modo, ocuparnos efectivamente de llamadas a procedimientos, creacin de objetos, paso de parmetros, recursividad, etc. Para el lenguaje elemental J-PL0 el entorno es esttico, por lo que podemos representar el estado sencillamente como un conjunto de pares nombre-valor de variables.
6

= {x, 1, y, 9, z, 3, w, 4}
Podemos representar matemticamente las transformaciones de estado que representan estos tipos de asignaciones mediante una funcin especial llamada unin de invalidacin, representada por el smbolo U. Esta funcin es similar a la unin de conjuntos ordinaria, pero se diferencia en que puede representar transformaciones como las que hemos visto anteriormente. Especficamente, viene definida por dos conjuntos de pares X e Y de este modo:

X Y = reemplaza en X todos los pares (x, v) cuyo primer miembro coincida con un par (x, w) de Y por (x, w) y despus aade a X cualquier par restante de K.
Por ejemplo, supongamos 1 = {x, 1, y, 2, z, 3} y 2 = {y, 9, w, 4}. Entonces 12={x, 1, y,9,z,3,w,4}. Otra manera de ver la unin de invalidacin es a travs de la reunin natural de estos dos conjuntos. La reunin natural 12 es el conjunto de todos los pares de 1 y 2 que tengan el mismo primer miembro. Por ejemplo,

{x, 1, y, 2, z, 3} {y, 9, w, 4} = {y, 2, y, 9}


Adems, la diferencia de conjuntos entre 1 y 12, expresada como 1 - (12), elimina de manera efectiva cada par de s1, cuyo primer miembro sea idntico al primer miembro de algn par de

2. Por tanto, la expresin (1 - (12)) 2 es equivalente a 12

Es posible que los lectores hayan observado que el operador es un modelo formal y generalizado de la operacin familiar de asignacin de la programacin imperativa. Juega as un papel fundamental en la semntica formal de lenguajes de programacin imperativa como C/C++, Ada, Java y muchos ms.

3.3.

SEMNTICA OPERACIONAL

La semntica operacional de un programa proporciona una definicin del significado del programa simulando el comportamiento del mismo en un modelo de equipo que tenga una organizacin de memoria y un conjunto de instrucciones muy sencillos (aunque no necesariamente realistas). Uno de

los primeros modelos de semntica operacional fue el equipo SECD [Landin 1966], que ofreca una base para definir formalmente la semntica de Lisp. Los modelos de semntica operacional estructurados utilizan un mtodo basado en reglas y unas cuantas suposiciones sencillas sobre la capacidad lgica y aritmtica del equipo subyacente. Ms adelante desarrollaremos este tipo de semntica operacional para J-PL0, utilizando su sintaxis abstracta como punto de partida. Nuestro modelo de semntica operacional utiliza la notacin (e) => v para representar la computacin de un valor v de la expresin e en estado . Si e es una constante, (e) es sencillamente el valor de esa constante en el dominio semntico subyacente. Si e es una variable,

(e) es el valor de esa variable en el estado actual . Los dos dominios semnticos de J-PL0 son I y B,
por lo que la funcin (e) dar un valor entero o un valor boolean para cualquier expresin J-PL0 e7. La segunda convencin de notacin de la semntica operacional es una regla de ejecucin. Una regla de ejecucin tiene la forma

premisa , que se lee: si la premisa es verdadera, entonces la conclusin

conclusin es verdadera. Consideremos la regla de ejecucin para la adicin en J-PL0, que tenemos a continuacin:

(e1 ) v1 (e2 ) v2 (e1 + e2 ) v1 + v2

Define la adicin con base a las sumas de los valores actuales de las expresiones e1 y e2, utilizando las propiedades del dominio de enteros en el cual residen los valores v, y v2. Definimos la semntica operacional de los otros operadores J-PL0, aritmtico, relacional y boolean, de manera parecida. Para las instrucciones, la semntica operacional viene definida por una regla de ejecucin aparte para cada tipo de instruccin de la sintaxis abstracta de J-PL0. Aqu tenemos la semntica operacional de una instruccin de asignacin s de la forma s.target = s.source;

( s.source) v ( s.t arg et = s.source; ) U{ s.t arg et , v}

Observemos la distincin entre las dos utilizaciones de a aqu: cuando lo utilizamos solo, indica un estado o un conjunto de pares nombre-valor; cuando lo escribimos (i(e), indica una funcin que hace corresponder expresiones con valores.

Por ejemplo, supongamos que tenemos la asignacin x = x + 1; y un estado actual en el que el valor de x es 5. Es decir, ={..., (x, 5), ...}. Por tanto, la semntica operacional de constantes, variables, expresiones y asignacin computa un estado nuevo en la siguiente serie de aplicaciones de reglas de ejecucin (leyendo de arriba hacia abajo):

( x) 5 (1) 1 ( x + 1) 6 ( x = x + 1; ) {..., x,5 ,...}U{ x,6}


Esto deja el estado transformado en = {..., x, 6, ...} La regla de ejecucin de las instrucciones s1s2 tambin se define intuitivamente:

( s1 ) 1 1 ( s2 ) 2 ( s1s2 ) 2
Es decir, si la ejecucin de s, en estado o da como resultado el estado o, y la ejecucin de s2 en estado a, da como resultado el estado o2, entonces el efecto compuesto de la ejecucin de la secuencia s\s2 en estado o es el estado o2. Para las condicionales J-PL0, que son de la forma s = if (s. test) s.thenpart else s.elsepart, la regla de ejecucin tiene dos versiones, una que aplicamos cuando la evaluacin de s.test es true y la otra que aplicamos cuando s.test es false.

(if ( s.test ) s.thenpart else s.elsepart ) 1 ( s.test ) false ( s.elsepart ) 1 (if ( s.test ) s.thenpart else s.elsepart ) 1
La interpretacin es sencilla. Si tenemos el estado = {..., x,6, ...} y la siguiente instruccin JPL0:
if (x<0) X = X - 1; else x = x + 1;

( s.test ) true ( s.thenpart ) 1

entonces la segunda regla es la nica cuya premisa (x < 0) => false satisface el valor actual de x. Esta regla concluye que el resultado de ejecutar la condicional es el mismo que el de ejecutar la asignacin x=x+1 en estado = {. . . , x,6, . . .}, dejando como resultado final el estado = {. . . ,

x,7, . . .}.
Los bucles son instrucciones con la forma general s = while (s.test) s.body. Sus reglas de ejecucin tienen dos formas diferentes, una la elegimos cuando la condicin s.test es true y la otra cuando s.test es false.

( s.test ) true ( s.body ) 1 1 ( while ( s.test ) s.body ) 2 ( while ( s.test ) s.body ) 2

La primera regla dice, en efecto, que cuando s.test es true ejecutamos s.body una vez y conseguimos el estado 1, y despus ejecutamos repetidamente el bucle comenzando en este estado nuevo. La segunda regla nos ofrece una salida del bucle; si s.test es false, completamos el bucle sin cambiar el estado . Por ejemplo, supongamos que tenemos el bucle siguiente en J-PL0, que duplica el valor de x hasta que x < 100 ya no es true:
while (x<100) x = 2*x;

Comenzando con el estado inicial = {. . . , x,7, . . .}, la primera regla se aplica repetidamente, dando la siguiente conclusin:

( x < 100) true ( x = 2 * x; ) 1 = {... x,14} 1 ( while( x < 100) x = 2 * x; ) 2 ( while ( x < 100) x = 2 * x; ) 2

Ahora, para computar el estado final 2, necesitamos volver a aplicar esta regla comenzando en el estado nuevo 1={...,x,14, ...}. Eventualmente, una serie de aplicaciones de esta regla deja el estado 1={...,x,112, ...}, en cuyo caso se satisface la premisa (x < 100) => false y el estado final para ejecutar el bucle pasa a ser 2={...,x,112, ...}.

3.3.

SEMNTICA AXIOMTICA

Aunque es importante que los programadores y los escritores de compiladores comprendan lo que hace un programa en todas las circunstancias, tambin es importante que los programadores puedan confirmar, o probar, que hace lo que se supone que tiene que hacer bajo todas las circunstancias. Es decir, si alguien presenta al programador una especificacin de lo que se supone que tiene que hacer un programa, el programador tiene que poder demostrar, ms all de cualquier duda razonable, que el programa y sta especificacin concuerdan de manera absoluta. Es decir, que el programa es correcto de una manera convincente. La semntica axiomtica nos ofrece un medio para desarrollar dichas pruebas. Por ejemplo, supongamos que queremos demostrar matemticamente que la funcin Max de C/C++ de la Figura 3.1 computa realmente como resultado el mximo de sus dos parmetros: a y b. Llamando a esta funcin una vez obtendremos una respuesta para unos parmetros a y b en particular, como 8 y 13. Pero los parmetros a y b definen una amplia gama de enteros, por lo que llamarla varias veces con todos los valores diferentes para demostrar su exactitud sera una tarea imposible. La semntica axiomtica nos ofrece un medio para razonar sobre los programas y sus clculos. Esto permite a los programadores predecir el comportamiento de un programa de un modo ms convincente y circunspecto que ejecutando el programa varias veces utilizando como casos de prueba diferentes opciones aleatorias de los valores de entrada.

int Max (int a, int b) int m; if (a >= b) m = a; else m = b; return m; } Figura 3.1.

Una funcin Max de C/C++.

[... Se omite toda una seccin propia del Anlisis y Diseo de Algoritmos que no viene al caso segn lo expuesto por Dijkstra ms adelante ...]

3.4.4.

Perspectiva

La semntica axiomtica y las tcnicas correspondientes para proporcionar la correccin en los

programas imperativos fueron desarrolladas a finales de los aos sesenta y principios de los setenta. En ese momento se esperaba que, de momento, la mayora de los programas se probaran como correctos automticamente. Es evidente que no fue as. En realidad, la importancia de las pruebas de correccin en el diseo del software ha sido objeto de un debate encendido, especialmente a principios de los aos noventa. Muchos ingenieros de software rechazan la nocin de prueba formal [DeMillo 1979], argumentando que el hecho de que los programadores la dominen es un proceso muy complejo y que consume mucho tiempo. En su lugar, utilizan mtodos de comprobacin muy elaborados para convencerse de que el software se ejecuta correctamente gran parte del tiempo. Dijkstra [1972] expres el argumento contrario, diciendo que la comprobacin slo probara la presencia de errores, nunca su ausencia. Consideremos un programa sencillo que introduce dos enteros de 32 bits, calcula alguna funcin y da como resultado un entero de 32 bits. Esto implica 264 entradas posibles (aproximadamente 1020), as que, aunque alguien fuera capaz de comprobar y verificar (!) 100 millones de casos por segundo, la comprobacin total le llevara aproximadamente 105 aos. Y este es uno de los programas ms sencillos que nos pudiramos imaginar. Hay un trmino medio entre las pruebas complejas y necesitadas de mucho tiempo y la comprobacin totalmente inadecuada? Creemos que s. Para empezar, podemos comprobar automticamente las propiedades de un programa distintas de la correccin. Entre ellas estn la seguridad de los programas, cuando es un aspecto crtico. Es frecuente que la ausencia de interbloqueo en programas concurrentes tambin se pueda probar formalmente. En segundo lugar, los mtodos de los programas orientados a objeto normalmente tienen un tamao bastante pequeo (como veremos en el Captulo 7). Las pruebas informales de dichos mtodos son posibles de forma automtica, aunque normalmente no se haga. Una razn es que muchos programadores, ignorantes de la correccin formal de programas, no son capaces de establecer de forma precisa los asertos de entrada y salida de los mtodos que escriben. En tercer lugar, los programadores con experiencia en correccin de programas pueden establecer los asertos de entrada y salida de los mtodos que escriben utilizando el ingls formal (u otro idioma natural); esto conduce a una documentacin muy mejorada. Como ejemplo de un caso en el que podra (y debera) utilizarse este formalismo, consideremos la documentacin sobre javadoc de Sun para los distintos mtodos String de JDK 1.1. El comentario del mtodo:

public String substring (int beginlndex, int endlndex);

quiere decir: Devuelve una nueva cadena que es una subcadena de esta cadena. Muy impreciso! Cmo podra un implementador llevar a cabo una prueba informal con una especificacin tan vaga? Cul es el intervalo de valores vlidos para beginlndex y endlndex? El valor vlido mnimo de beginlndex es 0 1? Un programador interesado en llevar a cabo una prueba informal de una implementacin de substring necesitara, al menos, una descripcin ms formal de este mtodo; dejamos esta descripcin como ejercicio.

3.5.

SEMNTICA DENOTATIVA

La semntica denotativa de un lenguaje define los significados de los elementos abstractos de dicho lenguaje como un grupo de funciones de transformacin del entorno y del estado. El entorno de un programa es el conjunto de objetos y tipos que estn activos en cada paso de su ejecucin. El estado de un programa es el conjunto de todos los objetos activos y sus valores actuales. Estas funciones de transformacin de estado dependen de la suposicin de algunos tipos y transformaciones primitivas. Mientras que la semntica axiomtica es valiosa para la claridad del significado de un programa como un texto abstracto, la semntica operacional (o denotativa) se centra en el significado de un programa como un objeto activo dentro de un entorno computacional (o funcional). La utilizacin de la semntica denotativa para la definicin del significado tiene ventajas y desventajas. Una ventaja es que podemos utilizar las denotaciones funcionales del significado de un programa como base para especificar un intrprete para el lenguaje. Sin embargo, esta ventaja hace emerger un problema adicional. Es decir, estrictamente hablando, la utilizacin de Java para implementar la definicin funcional de, por ejemplo, un bucle, necesita que definamos la semntica del propio Java antes de utilizarla para implementar la de J-PL0. Es decir, hay un cierto movimiento cclico al utilizar el modelo denotativo como base para la definicin del significado de un lenguaje. No obstante, la semntica denotativa es muy utilizada y nos permite definir el significado de un programa J-PL0 abstracto como una serie de transformaciones de estado resultante de la aplicacin de una serie de funciones M. Estas funciones definen individualmente el significado de cada clase de elementos que nos podemos encontrar en el rbol de sintaxis abstracta de un programa (Program,

Block, Conditional, Loop, Assignment, etc.).

Representemos el conjunto de todos los estados de un programa . Entonces, una funcin M es una correspondencia entre un miembro en particular de una clase abstracta determinada y el estado actual de con un nuevo estado de . Es decir8:

M: Class x

Por ejemplo, podemos expresar el significado de una instruccin abstracta como una funcin de transformacin de estado de forma.

M: Statement x

Estas funciones son, necesariamente, funciones parciales, lo que quiere decir que no estn bien definidas para todos los miembros de su dominio Class x E. Es decir, las representaciones abstractas de ciertas construcciones de programas en ciertos estados no tienen representaciones de significado finitas, aunque dichas construcciones sean sintcticamente vlidas. Por ejemplo, consideremos las siguientes instrucciones de C/C++ insidiosas, aunque sintcticamente vlidas (suponiendo que i sea una variable int).

for(i=l; i > -1; i++) i;

En este caso, no hay una definicin razonable para el estado final, ya que el valor de i cambia entre 1 y 0 segn el bucle se repite infinitamente. Como la sintaxis abstracta de un programa J-PL0 es una estructura de rbol cuya raz es el elemento abstracto Program, podemos definir su significado a partir de una serie de funciones, la primera de las cuales define el significado del Program.

M: Program

M(Program p) = M(p.body, {v,, undef, v2, undef, . . . , vm, undef})


La primera lnea de la definicin ofrece una funcin prototipo M para el elemento del programa que estamos definiendo (en este caso, Program), mientras que la segunda define el significado de un programa, utilizando las definiciones de sintaxis abstracta de los constituyentes directos del Program.

Algunos estudios de la semntica formal definen el significado de un programa como una serie de funciones en las que el nombre (por ejemplo, MSlatemen) las distingue de las dems. En estos estudios, MSlalemenl: 2 -* 2 sena equivalente a nuestra instruccin M: x 2 -* 2- Utilizamos esta variante en la implementacin del Esquema de la semntica de J-PL0 del Captulo 8.

Recordemos (vase el Apndice B) que las definiciones abstractas del Program tienen dos partes; una decpart (una serie de declaraciones abstractas) y un body, que es un block. As que esta definicin funcional nos dice que el significado de un programa es el significado del cuerpo (body) del programa con el estado inicial {v1 undef, v2, undef, ..., vm, undef}. Podemos predecir que, como estas funciones de significado se extienden y se aplican a los elementos de un programa especfico, este estado inicial cambiar al asignar y reasignar valores a las variables. Este estilo funcional de definicin de significados de un programa es particularmente sencillo de implementar en Java. El siguiente mtodo de Java implementa la definicin anterior de la funcin M, asumiendo la sintaxis abstracta del lenguaje J-PL0 que definimos en el Apndice B.
State M (Program p) { return M (p.body, initialState(p.decpart)); } State initialState (Declarations d) { State sigma = new State(); Value undef = new Value(); for (int i = 0; i < d.size(); i++) sigma.put(((Declaration)(d.elementAt(i))).v, undef); return sigma; }

El Captulo 4 introduce y explica la semntica de los elementos de programa abstractos restantes. En la Seccin 3.6 veremos un adelanto de dicha explicacin ilustrando la semntica formal de las expresiones e instrucciones de asignacin de J-PL0.

3.6.

EJEMPLO: SEMNTICA DE ASIGNACIONES Y EXPRESIONES DE J-PL0

En los programas imperativos abundan las expresiones y las asignaciones. Podemos expresar funcionalmente su semntica utilizando las nociones de las funciones de estado y de transformacin de estado que hemos introducido en la Seccin 3.5. Supongamos que tenemos la siguiente instruccin de asignacin concreta de J-PL0.
z = x + 2 * y;

Esta instruccin se traduce en una Assignment abstracta cuya representacin mostramos en la Figura 3.4, utilizando los mtodos descritos en el Captulo 2 y la sintaxis abstracta de J-PL0 del Apndice B. Por tanto, una Assignment abstracta tiene dos partes fundamentales: una expresin source y una variable target.

Figura 3.4. Extracto de la sintaxis abstracta de la asignacin de J-PL0.

3.6.1.

Significado de las asignaciones

Podemos definir el significado de una assignment a partir de una funcin de transformacin de estado del tipo siguiente:

M: Assignment x

M(Assignment a, State ) = {a.target, M(a.source, )}

Qu nos dice esto? Es, sencillamente, un mtodo formal para describir el estado resultante de transformar el estado actual

en uno nuevo que difiere de a slo en el par cuyo primer miembro es

la variable de la izquierda de la assignment. Creamos el nuevo par combinando dicha variable con el significado de la expresin de la parte derecha de la assignment. El significado de esta expresin est definida por otra funcin M, que explicaremos en detalle en la Subseccin 3.6.2. Todo lo que tenemos que saber es que el significado de una expresin est definido por una funcin M que devuelve un valor, ya que el segundo miembro de todos los pares del estado debe ser un valor. Volvamos a nuestro ejemplo de la Figura 3.4 y asumamos que el estado actual sigue:

es como

={*,2,y,-3,z,75}
De forma intuitiva, esperamos que el significado de la expresin fuente en este estado, o

M(a.source, ) = M(x + 2*y, {x, 2, y, -3, z, 75})

devolver el valor -4, ya que estamos asumiendo que M est totalmente definida para expresiones. Por tanto,

M(z = x + 2*y, {x, 2, y, -3), (z, 75}) = {x, 2, y, - 3, z, 75} {z, - 4} = {x, 2,y, -3,z, -4}

lo que completa la transformacin de estado de esta asignacin.

3.6.2.

El significado de las expresiones aritmticas

En los lenguajes de programacin reales, debemos definir cuidadosamente el significado de una

expresin aritmtica, ya que las expresiones tienen varias caractersticas que no son muy claras la
primera vez que se leen. Por ejemplo, la expresin inocua x + y/2 puede describir, en un contexto en el que x e y estn declarados como enteros, una simple divisin entre enteros seguida de una suma. Sin embargo, si x o y estn declarados con el tipo float, esta expresin podra tener distintas posibilidades de significados, ya que la divisin podra tener corno resultado un producto float, en lugar de un entero. Ms an, en algunos lenguajes, x e y podran indicar vectores o matrices, lo que le dara un significado absolutamente distinto a esta expresin. El Captulo 4 considera este tipo de problemas de forma ms cuidada e identifica los elementos que debemos aadir a la funcin de significado de las expresiones para que este tipo de situaciones estn bien definidas. En esta seccin, nicamente vamos a considerar el significado de una expresin abstracta de JPL0, as que podemos hacer algunas suposiciones bastante sencillas sobre los tipos de sus argumentos y resultado. Es decir, asumimos que la expresin slo tiene argumentos int y los cuatro operadores aritmticos (+, -, * y /) definidos en la sintaxis concreta de la expresin explicada en el Captulo 2. As, los resultados y las operaciones aritmticas individuales son del tipo int. Esta explicacin utiliza las convenciones matemticas siguientes:

v comprueba si hay un par cuyo identificador sea v en el estado actual . (v) es una funcin que extrae de el valor del par cuyo identificador sea v.

Para facilitar la definicin del significado de la expresin, definimos primero la funcin auxiliar

ApplyBinary. Esta funcin toma dos valores enteros y calcula su resultado entero. Aqu tenemos su
definicin para los operadores aritmticos; observemos que la definicin del operador / es una forma complicada y matemtica de especificar el cociente entero de v1 dividido por v2:

ApplyBinary: Operator x Value x Value op, Value v1, Value v2)


= vi + v2 = vi - v2 = vi x v2

Value ApplyBinary(Operator

si op = + si op = si op = *

v1 = floor v 2 xsigno(v1xv2)

si op = 1

Utilizando la sintaxis abstracta de la expresin ofrecida en el Apndice B, pero slo en lo que respecta a su aplicacin a expresiones que tienen operadores enteros, podemos definir el significado de una expresin as:

M : Expression x State M(Expression e, State )

Value if e es un Value if e es una Variable if e es un Binary

=e = (e) = ApplyBinary(e.op, M(e.term1, ), M(e.term2, ))

Esta funcin define el significado de una expresin en un estado en particular por casos. El primer caso extrae el valor si la propia funcin es un valor. El segundo caso extrae el valor de una variable del estado si la expresin es una variable. El tercer caso calcula un valor aplicando el operador binario adecuado a los valores de los dos trminos que acompaan al operador, caso de que la propia expresin sea binaria.

Para explicar todo esto, volvamos a nuestro ejemplo, la expresin x+2*y, y supongamos que est siendo evaluada en el estado = {x, 2, y, -3, z, 75}. Es decir, queremos utilizar estas definiciones funcionales para mostrar que M(x+2*y, {x, 2, y, -3, z, 75}) = -4. Fijmonos otra vez en la expresin x+2*y de la Figura 3.4. Como esta expresin es binaria, tenemos: M(x+2*y, {x, 2, y, - 3, z, 75}) = ApplyBinary(+, A, B) donde A = M(x, {x, 2, y, - 3, z, 75}) y B = M(2*y, x, 2, y, - 3, z, 75})

Ahora, el significado de A es el valor de x en estado = {x, 2, y, -3, z, 75}, o 2, ya que x es una variable. Sin embargo, el significado del binario B parte de otra aplicacin de la funcin M, que es: M(2*y, {x, 2, y, -3, z, 75}) = ApplyBinary(*, C, D) donde C = M(2, {x, 2, y, -3, z, 75}) El significado de C es 2, ya que 2 es un valor. El de D es -3, ya que y es una variable en estado = {x, 2, y, -3, z, 75}. Con esta informacin, la definicin de ApplyBinary nos ofrece el significado 2*y: M(2*y, {x, 2, y, -3, z, 75}) = ApplyBinary(*, 2, -3) = -6 As, el significado de nuestra expresin original queda as: M(x+2*y, {x, 2, y, -3, z, 75}) = ApplyBinary(+, 2, -6) = -4 Observemos que en este ejemplo slo hemos utilizado las definiciones de las funciones

ApplyBinary y M definidas anteriormente, junto con las propiedades matemticas de la aritmtica de


enteros, para obtener este resultado. Debera estar claro que para estas funciones hay significados completamente definidos de expresiones abstractas ms complejas con operaciones binarias y operadores enteros. En el Captulo 4 explicaremos e ilustraremos varios ejemplos ms de definicin de significados para otros elementos de programa abstractos de J-PL0.

3.6.3.

Cmo implementar las funciones semnticas

La implementacin de la semntica denotativa de un lenguaje de programacin proporciona un espacio de comprobacin para la experimentacin con modelos semnticos diferentes. La implementacin de la semntica de J-PL0 en Java necesita la definicin de la clase Semantics de Java, que contiene un mtodo para cada una de las distintas funciones M y funciones auxiliares (como ApplyBinary) que, juntos, definen el significado de un programa. Los mtodos de esta clase hacen referencia a objetos definidos por la sintaxis abstracta. Por tanto, la sintaxis abstracta sirve como un puente entre la sintaxis concreta de un programa y su significado. Un grupo completo de mtodos semnticos (es decir, un grupo que defina la semntica de todas las construcciones de la sintaxis abstracta) define, en efecto, un intrprete para un lenguaje. Podemos utilizar un intrprete de este tipo para comprobar la validez de las definiciones semnticas, al igual que los intercambios que se producen entre las definiciones semnticas alternativas. Al definir un intrprete de Java, volvemos a hacer hincapi en las limitaciones del mtodo de denotacin mencionado al principio de este captulo. Es decir, para que un intrprete de Java para J-PL0 est completo, tiene que dar por supuesto que el propio Java ha sido definido formalmente. De hecho, esto es casi cierto (en documentos de investigacin recientes se ha considerado muy cuidadosamente una definicin formal de Java). Los lectores que estn interesados pueden dirigirse a [Alves-Foss 1999] para obtener ms informacin. Teniendo en cuenta esta suposicin, vamos a revisar la definicin de la clase Semantics considerando las implementaciones de las funciones semnticas M para Assignment y Expression que introdujimos en las Subsecciones 3.6.1-3.6.2. El Captulo 4 continuar este desarrollo, de forma que podamos crear un intrprete completo para el pequeo lenguaje J-PL0. Como ste es un conjunto de pares clave-valor nicos, el estado de la computacin se implementa de forma natural como una HashTable de Java, como mostramos en la Figura 3.5. Esto significa que podemos definir e inicializar la variable sigma, que representa , como sigue:

State sigma = new State();

class State extends Hashtable { public State() { } public State(Variable key, Value val) { put(key, val); } // Sobreescribe la funcin unin "onion" con dos estados s y t;

public State onion (State t) { for (Enumeration e = t.keys(); e.hasMoreElements(); ) { Variable key = (Variable)e.nextElement(); put(key, t.get(key)); } return this; }

public void display () { ... } }

Podemos implementar en Java la expresin funcional (v), que extrae de el valor de la variable v, as:
sigma.get (v)

Para terminar, la operacin de unin de invalidacin, a U {(v, val)}, que sustituye o aade una nueva variable v y su valor val en el estado, puede implementarse como el mtodo onion, que contiene cdigo para insertar cada par en el nuevo estado t en la HashTable, reemplazando un par con base a una clave de concordancia o aadindolo con una nueva clave. Esta funcin ofrece un mtodo sencillo para implementar el significado M de una instruccin

Assignment a partir de una de sus definiciones:

M(Assignment a, State ) = {a.target, M(a.source, )}


Es decir, implementamos el mtodo de Java de la clase Semantics como sigue:
State M (Assignment a, State sigma) { return sigma.onion(new State(a.target, M (a.source, sigma))); }

El significado M de una expresin aritmtica con los operadores +, -, * y / tambin parte directamente de su definicin. Como no se producen transformaciones de estado, todo lo que pedimos es que el mtodo de Java devuelva un valor (en lugar de un estado). Recordando que el significado de una expresin aritmtica lo definimos as:

M(Expression e, State d) = e. val = (e) = ApplyBinary(e.op, M(e.term1, ), M(e.term2, )) si e es un Value si e es un Variable si e es un Binary

sugerimos la siguiente implementacin de Java:


Value M (Expression e, State sigma) { if (e instanceof Value) return (Value)e; if (e instanceof Variable) if (!sigma.containsKey((Variable)e)) return new Value(); else return (Value)(sigma.get((Variable)e)); if (e instanceof Binary) return applyBinary (((Binary)e).op, M(((Binary)e).term1, sigma), M(((Binary)e).term2, sigma)); if (e instanceof Unary) return applyUnary(((Unary)e).op, M(((Unary)e).term, sigma)); return null; }

Considerando la funcin ApplyBinary, que definimos para la expresin as:

ApplyBinary(Operator op, Value v1, Value v2) = v1 + v2 si op = + = v1 - v2 si op = = v1 x v2 si op = *

tenemos la siguiente codificacin directa de Java (observemos que el cdigo de Java para la divisin por enteros es ms sencillo que la definicin matemtica).
Value applyBinary (Operator op, Value v1, Value v2) { if (v1.type.isUndefined() || v2.type.isUndefined()) { return new Value(); } else if (op.ArithmeticOp( )) { if (op.val.equals(Operator.PLUS)) return new Value(v1.intValue + v2.intValue); if (op.val.equals(Operator.MINUS)) return new Value(v1.intValue - v2.intValue); if (op.val.equals(Operator.TIMES)) return new Value(v1.intValue * v2.intValue); if (op.val.equals(Operator.DIV)) return new Value(v1.intValue / v2.intValue); } ... return null; }

El mensaje de estas explicaciones es claro. La implementacin de estas funciones de significado

M en Java es relativamente sencilla, una vez que hemos establecido un esquema de representacin

para la nocin del estado. La clase HashTable de Java proporciona esta representacin directamente.

EJERCICIOS
3.1. Ample la funcin de comprobacin de tipo esttico V de Declarations de forma que defina el requisito de que el tipo de cada variable sea tomado de un pequeo grupo de tipos disponibles, digamos {int, boolean}. Utilice el mismo estilo funcional y la sintaxis abstracta de las Declarations explicadas en este captulo. 3.2. Ample el mtodo Java que implementa la funcin V de Declarations de forma que implemente el requisito adicional mencionado en la Pregunta 3.1. 3.4. Complete la semntica operacional de los operadores aritmtico, boolean y lgico de J-PL0 escribiendo una regla de ejecucin para cada uno de ellos.

Apndice B.4.

FUNCIONES DE COMPROBACIN DE TIPOS DE J-PL0

A continuacin, tenemos las reglas de comprobacin de tipos estticos de J-PL0 definidas formalmente. Tambin mostramos las implementaciones de estas funciones como una coleccin de mtodos de Java. Juntas, estas funciones proporcionan una especificacin completa de un comprobador de tipos estticos para programas abstractos de J-PL0. El mapa de tipos de un Programa es un conjunto de pares, cada uno de los cuales es una Variable v y su Type t declarado.

tm = {v1, t1, v2, t2, ... , vn, tn}


class TypeMap extends Hashtable { // TypeMap es implementado en un Hashtable de Java // Se adicionado el mtodo display() para facilitar la experimentacin public void display() { ... } }

La funcin typing crea un TypeMap a partir de una serie de Declarations. La implementacin de Java de TypeMap es una tabla de dispersin; como los identificadores son nicos, el mtodo put se asegura de que todas las variables declaradas y sus tipos estn representados en la tabla de dispersin.

typing: Declarations

TypeMap
i{1,...,n}

typing ( Declarations d ) =

U d .v, d .t
i i

public TypeMap typing(Declarations d) { // Coloca las variables y los tipos en un nuevo diccionario o // tabla de smbolos (map), el cual se retorna TypeMap map = new TypeMap(); for (int i = 0; i < d.size(); i++) { map.put(((Declaration)(d.elementAt(i))).v, ((Declaration)(d.elementAt(i))).t); } return map; }

Una serie de Declarations es vlida si sus nombres de variables son nicos. Observemos que la implementacin de Java tiene un bucle anidado ms efectivo que el que proporciona una codificacin estricta de la definicin funcional.

V: Declarations

V ( Declarations d ) = i , j {1,..., n} : (i j d i .v d j .v)

public boolean V(Declarations d) { for (int i = 0; i< d.size() - 1; i++) for (int j=i+1; j<d.size(); j++) if ((((Declaration)(d.elementAt(i))).v).equals (((Declaration)(d.elementAt(j))).v)) return false; return true; }

Un programa es vlido si sus Declarations son vlidas y su Block es vlido para el TypeMap definido por esas Declarations.

V: Program

V(Program p) = V (p.decpart) V (p.body, typing(p.decpart))

public boolean V(Program p) { return V(p.decpart) && V(p.body, typing(p.decpart)); }

La funcin auxiliar typeOf define el tipo de una Expression. Aqu, la expresin tm(e) significa que hay que recuperar el tipo de variable e del mapa de tipos.

typeOf: Expression x TypeMap = e. type - e tm tm(e) - e. op {ArithmeticOp} int

Type si e es un Value si e es una Variable si e es un Binary

typeOf(Expression e, TypeMap tm)

= e. op {BooleanOp, RelationalOp} boolean - e. op { UnaryOp) boolean si e es un Unary

public Type typeOf(Expression e, TypeMap tm) { if (e instanceof Value) return ((Value)e).type; if (e instanceof Variable) { Variable v = (Variable)e; if (!tm.containsKey(v)) return new Type(Type.UNDEFINED); else return (Type) tm.get(v); } if (e instanceof Binary) { Binary b = (Binary)e; if (b.op.ArithmeticOp( )) return new Type(Type.INTEGER); if (b.op.RelationalOp( ) || b.op.BooleanOp( )) return new Type(Type.BOOLEAN); } if (e instanceof Unary) { Unary u = (Unary)e; if (u.op.UnaryOp( )) return new Type(Type.BOOLEAN); } return null; }

Una Expression es vlida si es un Value, una Variable del mapa de tipos del programa o un Binary o

Unary cuyos operandos satisfacen las limitaciones adicionales. Un Binary cuyos operandos son ambos
enteros debe tener un ArithmeticOp o RelationalOp como operador, mientras que un Binary cuyos operandos son ambos booleanos debe tener un BooleanOp como operador. Un Unary debe tener un operando booleano y un UnaryOp como operador. Otras combinaciones de tipos de operandos y operadores son Expressions invlidas.

V: Expression x TypeMap

B si e es un Value si e es una Variable

V(Expression e, TypeMap tm) = true = e tm = V(e. terml, tm) V(e.term2, tm)

typeOf (e.term1, tm) = int typeOf(e.term2, tm) = int


si e es un Binary e.op {ArithmeticOp, RelationalOp} = V(e.terml, tm) V(e.term2, tm)

typeOf (e.terml, tm) = boolean typeOf(e.term2, tm) = boolean


si e es un Binary e.op {BooleanOp} = V (e.term, tm) typeOf(e.tem, tm) = boolean e.op = ! si e es un Unary

public boolean V(Expression e, TypeMap tm) { if (e instanceof Value) return true; if (e instanceof Variable) return tm.containsKey((Variable)e); if (e instanceof Binary) { Type typ1 = typeOf(((Binary)e).term1, tm); Type typ2 = typeOf(((Binary)e).term2, tm); if (! V(((Binary)e).term1, tm)) return false; if (! V(((Binary)e).term2, tm)) return false; if (((Binary)e).op.ArithmeticOp( ) ) return typ1.isInteger() && typ2.isInteger(); if ( ((Binary)e).op.RelationalOp( ) ){ if ( ((Binary)e).op.val.equals(Operator.EQ) || ((Binary)e).op.val.equals(Operator.NE) ) return (typ1.isInteger() && typ2.isInteger()); else return typ1.isInteger() && typ2.isInteger(); } if (((Binary)e).op.BooleanOp( )) return typ1.isBoolean() && typ2.isBoolean(); } if (e instanceof Unary) { Type typ1 = typeOf(((Unary)e).term, tm); return typ1.isBoolean() && V(((Unary)e).term, tm) && (((Unary)e).op.val).equals("!") ; } return false; }

Una Statement es vlida si sus partes constitutivas son vlidas individualmente. Clases de instrucciones diferentes tienen partes diferentes. (En el mtodo de Java V de Statement, la instruccin null es tambin vlida, porque se produce siempre que la else-branch de una Conditional no est presente.)

V: Statement x TypeMap

V(Statement s, TypeMap tm) = true si s es un Skip = s.target tm V(\v.source, tm) x tm(s.target) = typeOf(s.source, tm) si s es una Assignment

= V(s.test, tm) typeOf(s.test, tm) = boolean V(s.thenbranch, tm)

V(s.elsebranch, tm) si s es una Conditional = V(s.test, tm) typeOf(s.test, tm) = boolean V(s.body, tm)
= V(b1, tm) V(b2, tm) . . . V(bn, tm) si s es un Loop si s es un Block = b1, b2 , . . . bn bn 0

public boolean V(Statement s, TypeMap tm) { if (s instanceof Skip || s==null) return true; if (s instanceof Assignment) { boolean b1 = tm.containsKey(((Assignment)s).target); boolean b2 = V(((Assignment)s).source, tm); if (b1 && b2) return ((Type)tm.get(((Assignment)s).target)).id.equals (typeOf(((Assignment)s).source, tm).id); else return false; } if (s instanceof Block) { for (int j=0; j < ((Block)s).members.size(); j++) if (! V((Statement)(((Block)s).members.elementAt(j)), tm)) return false; return true; } return false; }

Todo lo explicado en B.4 puede reunirse en un solo archivo basado en la siguiente plantilla:

/* * StaticTypeCheck.java * * Chequeo de tipos estticos para J-PL0, definido por las funciones V y las * funciones auxiliares typing() typeOf(). Estas funciones usan las clases * definidas en la sintaxis abstracta. */ package xxxxxxxxxxx; import java.util.*; class TypeMap extends Hashtable { // TypeMap es implementado en un Hashtable de Java // Se adicionado el mtodo display() para facilitar la experimentacin public void display() { ... } } class StaticTypeCheck { public TypeMap typing(Declarations d) { ... } public boolean V(Declarations d) { ... } public boolean V(Program p) { ... } public Type typeOf(Expression e, TypeMap tm) { ... } public boolean V(Expression e, TypeMap tm) { ... } public boolean V(Statement s, TypeMap tm) { ... } }

Apndice B.5.

SEMNTICA DE J-PL0

A continuacin, tenemos la semntica de los programas de J-PL0 definida formalmente. Conjuntamente, estas funciones representan el comportamiento de un intrprete en tiempo de ejecucin de los programas abstractos de J-PL0. El estado de un Program es un conjunto de pares, cada uno de los cuales es una Variable y el valor que tiene asignado actualmente. Esto se implementa naturalmente como una Hashtable de Java, debido a que las Variables son nicas en un programa de J-PL0. El operador de unin de invalidacin, explicado en el libro, se implementa como el mtodo onion de la clase State.

= {v1, val1, v2, val2, ..., vm, valm}


class State extends Hashtable { public State() { } public State(Variable key, Value val) { put(key, val); } public State onion (State t) { for (Enumeration e = t.keys(); e.hasMoreElements(); ) { Variable key = (Variable)e.nextElement(); put(key, t.get(key)); } return this; }

public void display () { ... } }

Todos los valores de J-PL0 vienen del conjunto I U B U (undef), donde I y B son los dominios semnticos de enteros y booleanos con sus operadores matemticos normales. El valor especial undef marca el valor de una variable no inicializada. El significado de un Program es el significado de su cuerpo provisto de un estado inicial que contiene cada una de las variables declaradas m del programa con valores indefinidos.

M: Program

M(Program p) = M(p.body, {v,, undef, v2, undef, . . . , vm, undef})

State M (Program p) { return M (p.body, initialState(p.decpart)); } State initialState (Declarations d) { State sigma = new State(); Value undef = new Value(); for (int i = 0; i < d.size(); i++) sigma.put(((Declaration)(d.elementAt(i))).v, undef); return sigma; }

La funcin ApplyBinary (o ApplyUnary) calcula un Value cuando tenemos un Operator binario (o unario) y dos (o uno) Vales que son sus operandos. Asume que los operadores + , - , . . . tienen sus significados normales de los dominios semnticos de enteros I y booleanos B. Observemos que la definicin del operador / es una forma complicada y matemtica de especificar la divisin de enteros de v1 dividido por v2, aunque la implementacin de Java es sencilla (en realidad, ms sencilla de expresar).

ApplyBinary: Operator x Value x Value = v 1 + v2 = v 1 - v2 = v 1 x v2 si op = + si op = si op = *

Value

ApplyBinary(Operator op, Value vi, Value v2)

v1 = floor v 2 xsigno(v1xv 2) si op = 1

= v 1 < v2 = v 1 v2 = v 1 = v2 = vl v2 = v1 > v2 = v1 > v2 = v1 v2 = v1 v2

si op = si op = si op = si op si op = si op = si op = si op =

< <= == != > > && ||

Value applyBinary (Operator op, Value v1, Value v2) { if (v1.type.isUndefined() || v2.type.isUndefined()) { return new Value(); } else if (op.ArithmeticOp( )) { if (op.val.equals(Operator.PLUS)) return new Value(v1.intValue + v2.intValue); if (op.val.equals(Operator.MINUS)) return new Value(v1.intValue - v2.intValue); if (op.val.equals(Operator.TIMES)) return new Value(v1.intValue * v2.intValue); if (op.val.equals(Operator.DIV)) return new Value(v1.intValue / v2.intValue); } else if (op.RelationalOp( )) { if (op.val.equals(Operator.LT)) return new Value(v1.intValue < v2.intValue); if (op.val.equals(Operator.LE)) return new Value(v1.intValue <= v2.intValue); if (op.val.equals(Operator.EQ)) return new Value(v1.intValue == v2.intValue); if (op.val.equals(Operator.NE)) return new Value(v1.intValue != v2.intValue); if (op.val.equals(Operator.GE)) return new Value(v1.intValue >= v2.intValue); if (op.val.equals(Operator.GT)) return new Value(v1.intValue > v2.intValue); } else if (op.BooleanOp( )) { if (op.val.equals(Operator.AND)) return new Value(v1.boolValue && v2.boolValue); if (op.val.equals(Operator.OR)) return new Value(v1.boolValue || v2.boolValue); } return null; }

ApplyUnary: Operator x Value

Value si op = !

ApplyUnary (Operator op, Value v) = ~v

Value applyUnary (Operator op, Value v) { if (v.type.isUndefined()) return new Value(); else if (op.val.equals("!")) return new Value(!v.boolValue); return null; }

El significado de una Expression es un Value, el valor de una Variable en el estado actual o el resultado de la aplicacin de un operador binario o unario a los significados de sus operandos en el estado actual.

M: Expression x =e (e)

Value si e es un Value si e es una Variable si e es un Binary si e es un Unary

M(Expression e, State )

ApplyBinary(e.op, M(e.term1, ), M(e.term2, )) = ApplyUnary(e.op, M(e(.ierm, )))

Value M (Expression e, State sigma) { if (e instanceof Value) return (Value)e; if (e instanceof Variable) if (!sigma.containsKey((Variable)e)) return new Value(); else return (Value)(sigma.get((Variable)e)); if (e instanceof Binary) return applyBinary (((Binary)e).op, M(((Binary)e).term1, sigma), M(((Binary)e).term2, sigma)); if (e instanceof Unary) return applyUnary(((Unary)e).op, M(((Unary)e).term, sigma)); return null; }

El significado de una instruccin es, a su vez, el significado del tipo determinado de instruccin que representa.

M: Statement x

si s es un Skip si s es una Assignment si s es una Conditional si s es un Loop si s es un Block = M((Assignment)s, ) = M((Conditional)s, ) = M((Loop)s, ) = M((Block)s, )

M(Statement s, State ) = M((Skip)s, )

State M (Statement if (s instanceof if (s instanceof if (s instanceof if (s instanceof if (s instanceof return null; }

s, State sigma) { Skip) return M((Skip)s, sigma); Assignment) return M((Assignment)s, sigma); Conditional) return M((Conditional)s, sigma); Loop) return M((Loop)s, sigma); Block) return M((Block)s, sigma);

El significado de una instruccin Skip es, realmente, la funcin de identidad, ya que no cambia el estado actual del clculo.

M(Skip s, State ) =
State M (Skip s, State sigma) { return sigma; }

El significado de una Assignment es la unin de invalidacin del estado actual y un par nuevo formado por la Variable que es el objetivo de la Assignment y el significado de la Expression que es el origen.

M: Assignment x

M(Assignment a, State ) = {a.target, M(a.source, )}

State M (Assignment a, State sigma) { return sigma.onion(new State(a.target, M (a.source, sigma))); }

El significado de un Block es, o la funcin de identidad (si no tiene instrucciones), o el significado del resto del Block aplicado al estado nuevo conseguido mediante la ejecucin de la primera instruccin del Block. Podemos implementar esto con un bucle f o r o con un mtodo recursivo.

M(Block b, State ) =
= M((Block)b2
n,

si b = 0

M((Statement)b1, , ))

si b = b1 , b2 . . . bn

State M (Block b, State sigma) { for (int i=0; i<b.members.size(); i++) { sigma = M ((Statement)(b.members.elementAt(i)), sigma); } return sigma; }

El significado de una Conditional es el significado de una de sus ramas o de la otra, dependiendo de si la prueba es true o no.

M(Conditional c, State ) = M(c.thenbranch, ) = M(c.elsebranch, )


State M (Conditional c, State sigma) { if (M(c.test, sigma).boolValue) return M (c.thenbranch, sigma); else return M (c.elsebranch, sigma); }

si M(t.test, ) es true si no

El significado de un Loop es, o la funcin de identidad (si la prueba no es true) o el significado del mismo bucle cuando lo aplicamos al estado resultante de la ejecucin de su cuerpo una vez. Es una definicin recursiva que podemos implementar recursiva-mente (mostrado a continuacin) o en forma de bucle (no mostrado).

M(Loop l, State ) = M(l, M(l,body, )) =


State M (Loop l, State sigma) { if (M (l.test, sigma).boolValue) return M(l, M (l.body, sigma)); else return sigma; }

si M(l.test, a) es true si no

Reuniendo las explicaciones anteriores se tiene el siguiente compendio en el que se han obviado comentarios similares:

/* * Semantics.java * */ package xxxxxxxxxxxxx;

import java.util.*; class State extends Hashtable { public State() { } public State(Variable key, Value val) { put(key, val); }

public State onion (State t) { for (Enumeration e = t.keys(); e.hasMoreElements(); ) { Variable key = (Variable)e.nextElement(); put(key, t.get(key)); } return this; }

public void display () { ... } } public class Semantics { State M (Program p) { return M (p.body, initialState(p.decpart)); } State initialState (Declarations d) { State sigma = new State(); Value undef = new Value(); for (int i = 0; i < d.size(); i++) sigma.put(((Declaration)(d.elementAt(i))).v, undef); return sigma; } Value applyBinary (Operator op, Value v1, Value v2) { if (v1.type.isUndefined() || v2.type.isUndefined()) { return new Value(); } else if (op.ArithmeticOp( )) { if (op.val.equals(Operator.PLUS)) return new Value(v1.intValue + v2.intValue); if (op.val.equals(Operator.MINUS)) return new Value(v1.intValue - v2.intValue); if (op.val.equals(Operator.TIMES)) return new Value(v1.intValue * v2.intValue); if (op.val.equals(Operator.DIV)) return new Value(v1.intValue / v2.intValue); } else if (op.RelationalOp( )) { if (op.val.equals(Operator.LT)) return new Value(v1.intValue < v2.intValue); if (op.val.equals(Operator.LE)) return new Value(v1.intValue <= v2.intValue); if (op.val.equals(Operator.EQ)) return new Value(v1.intValue == v2.intValue); if (op.val.equals(Operator.NE)) return new Value(v1.intValue != v2.intValue); if (op.val.equals(Operator.GE)) return new Value(v1.intValue >= v2.intValue); if (op.val.equals(Operator.GT)) return new Value(v1.intValue > v2.intValue); }

else if (op.BooleanOp( )) { if (op.val.equals(Operator.AND)) return new Value(v1.boolValue && v2.boolValue); if (op.val.equals(Operator.OR)) return new Value(v1.boolValue || v2.boolValue); } return null; } Value applyUnary (Operator op, Value v) { if (v.type.isUndefined()) return new Value(); else if (op.val.equals("!")) return new Value(!v.boolValue); return null; } Value M (Expression e, State sigma) { if (e instanceof Value) return (Value)e; if (e instanceof Variable) if (!sigma.containsKey((Variable)e)) return new Value(); else return (Value)(sigma.get((Variable)e)); if (e instanceof Binary) return applyBinary (((Binary)e).op, M(((Binary)e).term1, sigma), M(((Binary)e).term2, sigma)); if (e instanceof Unary) return applyUnary(((Unary)e).op, M(((Unary)e).term, sigma)); return null; } State M (Statement if (s instanceof if (s instanceof if (s instanceof if (s instanceof if (s instanceof return null; } s, State sigma) { Skip) return M((Skip)s, sigma); Assignment) return M((Assignment)s, sigma); Conditional) return M((Conditional)s, sigma); Loop) return M((Loop)s, sigma); Block) return M((Block)s, sigma);

State M (Skip s, State sigma) { return sigma; } State M (Assignment a, State sigma) { return sigma.onion(new State(a.target, M (a.source, sigma))); } State M (Block b, State sigma) { for (int i=0; i<b.members.size(); i++) { sigma = M ((Statement)(b.members.elementAt(i)), sigma); } return sigma; }

State M (Conditional c, State sigma) { if (M(c.test, sigma).boolValue) return M (c.thenbranch, sigma); else return M (c.elsebranch, sigma); } State M (Loop l, State sigma) { if (M (l.test, sigma).boolValue) return M(l, M (l.body, sigma)); else return sigma; } }

Das könnte Ihnen auch gefallen