Download UNIVERSIDAD SIM´ON BOLÍVAR Java + Especificaciones

Document related concepts
no text concepts found
Transcript
UNIVERSIDAD SIMÓN BOLÍVAR
Ingenierı́a de la Computación
Java + Especificaciones
por
Gabriela Valentina Montoya Castillo
Proyecto de Grado
Presentado ante la Ilustre Universidad Simón Bolı́var
como Requisito Parcial para Optar al Tı́tulo de
Ingeniero en Computación
Sartenejas, Diciembre de 2005
UNIVERSIDAD SIMÓN BOLÍVAR
DECANATO DE ESTUDIOS PROFESIONALES
COORDINACIÓN DE INGENIERÍA DE COMPUTACIÓN
ACTA FINAL DE PROYECTO DE GRADO
Java + Especificaciones
Presentado por
Gabriela Valentina Montoya Castillo
Este proyecto de grado ha sido aprobado en nombre de la Universidad Simón Bolı́var por el
siguiente jurado examinador:
Prof. Blai Bonet
Jurado
Prof. Pedro Borges
Jurado
Prof. Ascánder Suárez
Profesor Guı́a
Prof. Jesús Ravelo
Tutor Académico
Sartenejas, Diciembre de 2005
Java + Especificaciones
por
Gabriela Valentina Montoya Castillo
Resumen
En este proyecto se plantea el diseño e implementación de una extensión a Java, para incluir las
herramientas necesarias para especificar clases mediante el establecimiento de contratos (del tipo
precondición, postcondición e invariante); esto busca ayudar a mejorar la elaboración del código
en Java, y facilitar la determinación de qué sección de código es la que esta fallando ante un caso
de prueba cualquiera. Se realiza una verificación dinámica de los contratos establecidos, esto es,
a momento de ejecución se verifica que el programa (con los parámetros actuales) cumpla con las
condiciones establecidas, lo cual, aunque no es demostración de que el programa siempre cumple el
contrato, contribuye a detectar instancias en las que el contrato es roto (esto es, contraejemplos).
Se siguieron los lineamientos de Java Modeling Language (JML), extensión de Java que permite
especificaciones y se encuentra en pleno desarrollo.
iii
Índice general
Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1. Introducción . . . . . . . . . . . . . . . . . . .
1.1. Planteamiento y Formulación del Problema
1.2. Objetivos . . . . . . . . . . . . . . . . . . .
1.3. Justificación . . . . . . . . . . . . . . . . . .
1.4. Delimitación y Alcance . . . . . . . . . . . .
III
.
.
.
.
.
1
1
3
4
5
2. Marco Teórico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1. Antecedentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2. Bases Teóricas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
7
10
3. Marco Metodológico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
4. Propuesta . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1. Formalización de la Extensión . . . . . . . . . . . . . . .
4.2. Mecanismo para comprobar la validez de los programas
4.3. Análisis de Complicaciones . . . . . . . . . . . . . . . .
4.3.1. Herencia . . . . . . . . . . . . . . . . . . . . . . .
4.3.2. Apuntadores . . . . . . . . . . . . . . . . . . . .
4.3.3. Excepciones . . . . . . . . . . . . . . . . . . . . .
4.4. Ventajas sobre Extensiones ya Realizadas . . . . . . . .
4.5. Testing . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.5.1. Plan de Testing . . . . . . . . . . . . . . . . . . .
4.5.2. Usando JUnit para realizar las pruebas . . . . .
4.5.3. Usando TestJCR . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
14
14
17
17
17
18
18
19
19
20
21
21
5. Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22
6. Conclusiones y Recomendaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
Bibliografı́a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
iv
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
A. Código de los casos de prueba para JUnit . . . . . . . . . . . . . . . . . . . . . .
33
B. Formato de los casos de prueba de TestJCR . . . . . . . . . . . . . . . . . . . . . .
36
C. Programas en JML . . . . .
C.1. Programa tipo Algoritmos
C.2. Programa tipo Algoritmos
C.3. Programa tipo Algoritmos
.
.
.
.
37
37
37
39
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
42
1. Ejemplo de programa en JContractor . . . . . . . . . . . . . . . . . . . . . . . . .
45
2. Ejemplo de programa en JML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
49
D. Detalles de Implementación
. . . . . . . . . . . . . . .
I: Factorial . . . . . . . .
I: Ordenamiento Burbuja
II: TAD Diccionario . . .
v
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Capı́tulo 1
Introducción
1.1.
Planteamiento y Formulación del Problema
¿Qué son aserciones? En el ámbito de la programación imperativa son afirmaciones, predicados
que se deben cumplir en cierto punto de un programa. Sirven para verificar que se cumplan ciertas
condiciones que se dan por ciertas, que el programa deberı́a satisfacer en ese punto de la ejecución,
para comprobar que el programa está realizando lo que se cree que está procesando.
Las aserciones se pueden usar para verificar formalmente la correctitud de un programa, esto
mediante el uso de las mismas como precondiciones, postcondiciones, invariantes y aserciones
puntuales en el desarrollo del código. Se utilizan como base las teorı́as de Hoare [12] y Dijkstra
[8, 9] que definen la noción de correctitud de un programa imperativo, esto es: si se le da un estado
inicial que satisfaga la precondición indicada, cumplirá con obtener un estado final que satisfaga
la postcondición; ası́, realiza lo que se espera de él.
Java[15] es un lenguaje de programación muy popular en los últimos años, y cada vez son más
los grandes sistemas que son desarrollados usando Java como base. Se usa tanto en aplicaciones
de interés comercial, como cientı́fico o educativo; se usa en campos en los que puede llegar a ser
crı́tico su correcto funcionamiento. Esto es, una falla en el programa, un bug sin resolver, puede
llegar a ocasionar conflictos e incluso grandes pérdidas, tanto en materia económica como en otros
aspectos incluso más importantes (por ejemplo: salud, ambiente, etc.). Por tanto, se hace altamente
1
2
importante el desarrollo de programas en Java que se sepa que son correctos, que hacen lo que se
diseñó que deben realizar, y a este objetivo podemos acercarnos más utilizando aserciones en los
programas desarrollados en Java.
Aserciones en Java
Java[15] provee una instrucción:
assert <exprBooleana> o
assert <exprBooleana> : <valor>,
siendo <exprBooleana> la aserción a verificar en el punto donde se coloca esta instrucción y
<valor> un parámetro a pasar para dar un mensaje de error adecuado al manejador de excepciones, ya que en caso de que la aserción falle, esto es, sea falsa, se levanta una excepción.
Esta instrucción se utiliza para verificar que el programa que se está desarrollando se está
comportando en efecto como se ha estimado. Las limitaciones que presenta están estrechamente
relacionadas con el limitado conjunto de operadores que pueden tomar parte en las expresiones
booleanas a incluir (como <exprBooleana>). En ella sólo se pueden incluir las expresiones booleanas de Java, como lo son constantes (true y false), operadores unarios (!), operadores binarios
(&, ^, |, &&, ||), operadores ternarios (?:), relacionales (>, >=, <, <=, ==, !=) y funciones (cuyo
valor de retorno sea booleano). Estas expresiones, tal cual las ofrece Java, son inadecuadas para lograr el propósito de que mediante aserciones se analice la correctitud del programa, ya que
en muchos casos son insuficientes para especificar lo que deseamos expresar sobre el estado del
programa en un punto dado y en otros tienen un nivel de abstracción demasiado bajo, se pierde
la semántica de la especificación y se aumenta el grado de dificiltad para el programador. Hace
falta otros operadores lógicos adicionales, como lo es la implicación, para dar mayor claridad a las
aserciones (a pesar de que se podrı́a traducir a los operadores existentes), y cuantificadores (∀, ∃,
3
∑, ∏, ↑, ↓,
S
, ... ), además del manejo de las aserciones sobre clases (o tipos abstractos de datos)
y su uso adecuado en los métodos.
Todo esto se encuentra limitado por las facilidades que ofrece Java. Es por ello que proponemos
el desarrollo de una extensión la cual ofrezca expresiones adicionales a las de Java para lograr
especificar adecuadamente el código, y ası́ ayudar a garantizar la correctitud del programa, y
obtener códigos bien especificados, más confiables y fáciles de reutilizar adecuadamente.
1.2.
Objetivos
Objetivo General
Diseñar e implementar una extensión a Java que contemple las facilidades necesarias para la
inclusión de especificaciones.
Objetivos Especı́ficos
Definir el lenguaje extensión de Java.
Desarrollar el traductor de Java extendido a Java básico.
Analizar posibles complicaciones del Java extendido en relación con las teorı́as de correctitud
existentes.
Actividades Asociadas
Realizar revisión bibliográfica de diversas extensiones a Java
Estudiar las extensiones a Java previamente realizadas para inclusión de especificaciones;
evaluación de aspectos cubiertosy y pendientes en el desarrollo de las mismas.
4
Utilizar la herramienta Claire para la construcción de la gramática de Java; proponer mejoras
a la herramienta para vencer posibles limitaciones frente a otras existentes.
Profundizar en los conocimientos previos en especificación formal de programas.
Diseñar un plan de testing que, mediante la automatización, nos ayude a garantizar la
correctitud de los programas desarrollados a lo largo de este proyecto.
1.3.
Justificación
En un ambiente en el cual se pone tanto énfasis en la correctitud de los programas, como
lo es en el académico, es pertinente la creación de un lenguaje que ayude a los estudiantes, en
primer pero no único lugar, a realizar sus programas de manera más confiable, contando con una
verificación a tiempo de ejecución de los contratos que plantean como válidos para su código. Y
en general, un lenguaje con tales caracterı́sticas incrementa la confianza que se tiene en el código
producido haciendo uso de especificaciones, y permite comprobar que el programa cumple con las
condiciones establecidas en instancias especı́ficas. Como se mencionó con antelación, se sabe el
tremendo auge que está teniendo Java; por tanto también se considera importante que desde el
inicio los estudiantes se adentren en el uso y producción de código Java. Luego, resulta importante
el planteamiento de extender Java para incluirle especificaciones, y que estas especificaciones usen
en la medida de lo posible el mismo estilo de sintaxis de Java para, en primer lugar, facilitar el
aprendizaje simultáneo de Java y la extensión planteada, ası́ como, en el caso más general, facilitar
al usuario el aprendizaje de este lenguaje de especificación.
Por otra parte, en el ámbito industrial, también se están realizando verificaciones formales de
código Java con especificaciones. Un caso particular de esto es VerifiCard[14], que es un proyecto
cuyo objetivo es garantizar que las nuevas Smart Cards que sean más seguras; para esto hacen
5
uso de la especificación formal, y de la herramienta LOOP[26] (la cual utiliza un demostrador de
teoremas para verificar las obligaciones de prueba derivadas de la especificación).
1.4.
Delimitación y Alcance
La extensión planteada se limita a la versión 1.5 de Java, que es la más reciente. Para este
proyecto de grado se plantea al menos realizar la estructuración y análisis de los aspectos a
contemplar en la extensión completa a Java, mas por razones de tiempo, se realizará sólo una
primera versión del traductor, la cual en manera general reconozca código Java extendido, elabore
los árboles abstractos del mismo y genere código Java básico. Los aspectos que queden pendientes
se dejarán documentados en este trabajo para su estudio posterior.
Siendo este proyecto realizado en Venezuela y siendo Java el nombre de una semilla de café,
se decidió que el nombre de este proyecto debı́a indicar algo propio de este paı́s y siendo una de
las bebidas tı́picas el carajillo, que consiste en café con ron u otro licor, se escogió Carajillo o
JCR, abreviación de Java Con Ron, como nombre para el proyecto completo planteado. En las
secciones subsiguientes se referirá al proyecto completo como JCR, y a este proyecto de grado
como este proyecto. En el capı́tulo 2, se presenta una revisión de los antecedentes de este tipo de
extensión a Java, ası́ como también una revisión de las bases teóricas que sustentan el desarrollo
de JCR y dan sentido al mismo. Luego, en el siguiente capı́tulo (3) se explica la metodologı́a
seguida en el desarrollo del proyecto. Luego, en el capı́tulo 4 presenta formalmente la extensión
planteada, se realiza un análisis de las complicaciones del lenguaje a tener presentes en el diseño
e implementación de la extensión, y se estudia la misma comparativamente frente a otras ya
existentes. También se plantea el plan de testing a seguir para evaluar el funcionamiento de la
extensión y su traductor. Luego, en el capı́tulo 6 se presentan las conclusiones a las que se llegó
gracias a la realización de las actividades concernientes a este proyecto y también se presentan
6
una serie de recomendaciones al momento de realizar trabajos similares y los posibles puntos de
continuidad de este proyecto en el ámbito de JCR. Se incluye un glosario para aclarar algunos
términos utilizados a lo largo de este informe y algunos apéndices entre ellos destaca el apéndice D,
donde se incluyen una serie de acotaciones acerca del desarrollo de este proyecto, donde se pondrán
de manifiesto las soluciones que se le dieron a algunos conflictos presentados en el desarrollo debido
al alcance del analizador sintáctico utilizado.
Capı́tulo 2
Marco Teórico
2.1.
Antecedentes
Una primera referencia, a pesar de no ser una extensión a Java, es sin duda a alguna Eiffel[20],
lenguaje de programación que desde su creación incluye especificaciones y, por tanto, permite
establecer contratos entre las clases. Se establecen precondiciones, postcondiciones e invariantes
de clases (con las palabras reservadas: require, ensure e invariant respectivamente).
Con respecto a extensiones a Java para mejorar su carencia de facilidades para establecer
especificaciones, existe jContractor[16], propuesto en 1999 por Murat Karaorman, Urs Hölzle y
John Bruno en el artı́culo “jContractor: a Reflective Java Library to Support Design by Contract”
y desarrollado por Parker Abercrombie, el cual es una herramienta que permite la inserción de
precondiciones, postcondiciones e invariantes de clases, directamente en el código escrito en Java,
mediante el uso de métodos que siguen un patrón preestablecido en el nombre, contenido, parámetros y valor de retorno. Estos métodos se pueden incluir tanto en la clase misma en la que se
desarrolla el programa o ser incluidos en una clase adicional, lo cual es muy útil si no se tiene
el código fuente de la clase a la que se le desea colocar las aserciones (esto es, si sólo se tienen
los “.class” ) o si se desea estructurar estos métodos en una clase adicional para darle mayor
claridad al código. Esta herramienta usa una librerı́a para ampliar el conjunto de operadores booleanos que se pueden utilizar en los contratos, Java Quantification Library (JaQuaL)[1], la cual
7
8
incluye una clase Logical y como métodos los operadores adicionales, como lo son la implicación
(Logical.implies(<ExprBooleana>, <ExprBooleana>)), y abstracciones de los cuantificadores
sobre colecciones (ForAll.in(<Collection>).ensure(<ExprBooleana>)). Esta extensión requiere, al momento de ejecutar el programa, que la lı́nea de comando sea de la siguiente manera:
java jContractor <nombrePrograma>, si los métodos que definen el contrato están en la
misma clase, o,
java jInstrument ./ <nombrePrograma>.class
java <nombrePrograma>, en caso de que estos métodos se definan en una clase independiente.
JContractor actualmente se encuentra en fase alfa, por lo cual no es fácil conseguir documentación.
Se puede observar un ejemplo de un programa especificado con JContractor en el anexo 1.
Otra extensión de Java con aserciones es Java Modeling Language (JML)[17]. Explican su
utilización como lenguaje para diseño por contratos Gary T. Leavens y Yoonsik Cheon en el artı́culo
“Design by Contract with JML”. En esta extensión las aserciones se especifican en la documentación
(en la cual todas las lı́neas que son aserciones inician con @): con requires para la precondición;
ensures para la postcondición; signals para postcondiciones excepcionales, o cuando se lanza una
excepción durante la ejecución; y public invariant para especificar un invariante de clase cuando
los objetos son visibles al usuario, especı́ficamente al final del constructor e inicio y fin de cada
uno de los métodos de la clase. Además, en las aserciones se pueden incluir operadores booleanos
adicionales a los provistos por Java como lo son implicaciones (==>), consecuencia (<==), doble
implicación (<==>), inequivalencia (<=!=>), variables de especificación (\old(<Variable>) para
indicar el valor que tenı́a al inicio de un bloque/método una variable) y cuantificadores (\forall,
\exists, \sum, \product, \min, \max, \num_of). Esta extensión requiere un compilador adicional
(“JML compiler”, que se usa con el comando jmlc) o se puede usar con una herramienta de testing,
9
como lo es “unit” (en este caso jmlunit). Su documentación aún se encuentra en desarrollo. Se
puede apreciar un ejemplo de un programa especificado con JML en el anexo 2.
Esta última, por su trayectoria (ha estado en desarrollo desde 1998 hasta el presente), porque
varias de las otras extensiones se han ido apegando a su notación, y por la cantidad de herramientas
que se han ido ajustando para prestar soporte a este lenguaje, se ha considerado un modelo a seguir
para el desarrollo de este proyecto, ya que establece lineamientos de cómo adaptar los aspectos de
diseño por contratos de Eiffel a Java, siguiendo las caracterı́sticas y notación propias de Java, para
ası́ facilitarle a los usuarios el aprendizaje del lenguaje extendido que permite especificaciones.
Entre las extensiones que usan las especificaciones de JML están las siguientes: Korat[5] (que
permite el testing automático incluyendo la generación de casos de prueba), LOOP[26] (que genera
las obligaciones de prueba y provee acoplamiento con un probador de teoremas), ESC/Java[7] (que
detecta errores a tiempo de compilación).
Referente al tema del manejo de la herencia entre clases, se tiene la alternativa planteada en
el artı́culo “Contract Soundness for Object-Oriented Languages”[10] de Robert Bruce Findler y
Matthias Felleisen, en donde se presenta una propuesta, al menos teórica, frente a la debilidad
que tienen las extensiones existentes en este ámbito. Y es que aunque en el desarrollo teórico de
las extensiones se evidencia que los diseñadores tienen presente que se debe preservar la herencia
de las especificaciones agregadas, tal y como lo expresan Leavens y Cheon en [17], es importante
garantizar que los subtipos sean behavioral subtypes, y puedan manejar los casos que manejaba la
superclase, lo cual se resume en que sean teoremas las siguientes implicaciones:
presuperClase ⇒ presubClase y postsubClase ⇒ postsuperClase
pero hasta ahora lo que verifican es:
presuperClase || presubClase y postsubClase && postsuperClase
(♦)
10
Lo cual no garantiza ninguna relación entre la subclase y la superclase. Entonces se permiten
subtipos que en realidad pueden no cumplir los contratos de la superclase, y sólo lanzan excepción
si se violan ambas precondiciones o alguna de las postcondiciones. También se obliga a las subclases
a ejecutarse en momentos en los que el estado del programa no garantiza su propia precondición,
ya que en estos casos cumplir la de la superclase no conlleva a cumplir la propia.
2.2.
Bases Teóricas
El planteamiento de este proyecto se sostiene en una serie de teorı́as que justifican el uso de la
especificación de programas y dan confianza en la subsecuente correctitud garantizada. La primera
de estas teorı́as es la propuesta por Hoare[12], que establece la noción inicial de razonamiento
deductivo sobre programas, estableciendo mediante axiomas (similarmente como se hace en la
matemática tradicional) las propiedades y condiciones que debe cumplir el programa al realizar
las acciones básicas del lenguaje (como por ejemplo asignación de valores a variables) y su posible
uso para probar la correctitud de un programa completo mediante el uso de la especificación,
que no es más que el establecimiento formal y claro de las propiedades que el programa cumple
en ciertos puntos (o estados), como el estado inicial y final (precondición y postcondición). Esta
teorı́a propone que si un programa cumple con una aserción en el estado inicial (precondición) y
el programa termina, entonces termina en un estado que cumple cierta propiedad (postcondición),
luego es una teorı́a de correctitud parcial, también llamada correctitud condicional. Aunque quien
realiza esta formalización ciertamente es Hoare, quien primero abordó el tema y realizó las primeras
observaciones fue Floyd[11]. Luego Edsger W. Dijkstra[8, 9] propone teorı́as que amplian esta
noción de correctitud, introduciendo la noción del transformador de predicados wp, que no es más
que la más débil precondición que se debe cumplir para que una instrucción genere estados que
satisfacen una postcondición en particular, y con esta noción obliga a las instrucciones a terminar
11
y a terminar de manera adecuada (acorde con la postcondición).
Otra teorı́a que se siguió es la de Liskov y Wing, expuesta en el artı́culo “Behavioral Subtyping Using Invariants and Constraints”[19], donde proponen las condiciones que debe cumplir un
subtipo, para estar bien definido, y estas condiciones son las de sustitución. A tiempo de ejecución
un supertipo debe poder ser sustituido por cualquiera de sus subtipos sin generar conflictos con
las especificaciones, esto ya que ambas deben estar relativamente bien definidas. Respecto a la
relación evidente entre ambas clases, debe cumplirse que la precondición de la subclase puede ser
más débil que la de la superclase y que la postcondición de la subclase puede ser más fuerte que
la de la superclase. Y esto corresponde con otra teorı́a (importante en materia de especificación
formal de programas): teorı́a de refinamiento de Morgan[21] sobre instrucciones de especificación,
ignorando algunos detalles, esto es:
[ pre, post] v [ pre0 , post0 ], si [ pre ⇒ pre0 ] y [ post0 ⇒ post]
que expresa que una instrucción de especificación es refinada por otra, si para cualquier estado
del programa la precondición de la refinada implica la precondición del refinamiento y la postcondición del refinamiento implica la postcondición de la refinada; siendo estos refinamientos los
establecidos por cada método que la subclase sobreescribe de su superclase y estas implicaciones
las mencionadas por Liskov y Wing[19].
Capı́tulo 3
Marco Metodológico
En este capı́tulo se describe la metodologı́a seguida en el desarrollo de este proyecto.
Se siguió la metodologı́a Crystal Clear[6], por el tamaño del proyecto y del equipo de trabajo.
Esta metodologı́a de la familia de metodologı́as Crystal es la indicada para equipos de trabajo
pequeños, en el caso de este proyecto: una persona.
Debido a las condiciones de trabajo se prescindió entonces de la serie de entregables tı́picos del
desarrollo de software con un nivel mayor de complejidad de las condiciones (equipo de trabajo
de mayor tamaño y proyecto de mayor duración y extensión), y se sustituyeron por entrevistas
regulares con el cliente (tutor del proyecto de grado), en las cuales se informaba del estatus del
proyecto y se fijaban conjuntamente las próximas prioridades en las cuales hacer énfasis. Además,
tal y como lo recomienda esta metodologı́a, se le dió importancia desde el inicio del desarrollo a la
fase de testing del proyecto, a plantear las maneras en que se probarı́a cada uno de los programas
generados en el proceso de desarrollo. Y, además del testing realizado continuamente, se trabajó
en un plan sistemático y automatizado que facilitara ese proceso, a efectos de garantizar que se
realizarı́an todas las pruebas que se consideraran necesarias. Aunque una caracterı́stica de esta
metodologı́a es su flexibilidad respecto a la forma de trabajo del programador, se puede resumir
en: entrevistas con el cliente, desarrollo y pruebas; esto de manera iterativa.
Este proyecto fue desarrollado de manera incremental, se comenzó con la elaboración de un
12
13
pequeño reconocedor de expresiones booleanas básicas y paulatinamente se fueron considerando
los demás elementos de Java.
Capı́tulo 4
Propuesta
Se plantea la elaboración de una extensión de Java que intente cubrir el concepto de diseño por
contrato completamente, intentando que no queden ambigüedades referentes a ningún aspecto de
cómo se deben manejar las precondiciones y postcondiciones, tanto en la relación clase-superclase
como en las llamadas a métodos. Para ello también se plantea la definición y elaboración de
las expresiones extendidas, al agregar implicación y cuantificadores a las expresiones de Java. Se
plantea el usar como referencias para el diseño las citadas entre los antecedentes (especialmente
JML) y, para la implementación, estudiar la posibilidad de adaptar a las necesidades de esta
extensión el código de GaCeLa[24] (implementación de GCL[8] desarrollada en la Universidad
Simón Bolı́var por Ascánder Suárez). Se sigue la sintaxis y semántica del lenguaje JML ya que,
como se expuso en los antecedentes, esta propuesta es muy aceptada en la actualidad y cumplen
con los requerimientos planteados al inicio de este proyecto. Ejemplos de especificación con JML,
de programas tipo Algoritmos I y Algoritmos II, son presentados en el apéndice C.
4.1.
Formalización de la Extensión
La extensión será un superconjunto de Java. Para diferenciar lo que pertenece a Java originalmente y lo que se ha incluido, y no causar problemas al compilar el código en Java, todo lo
14
15
perteneciente a la extensión debe usarse dentro de las anotaciones-JML, que no son más que comentarios válidos en Java que inician con un sı́mbolo ’@’. Se debe incluir un conjunto más amplio
de expresiones, que permita enriquecer la expresividad de este lenguaje, para ası́ en un más alto
nivel lograr especificaciones más concisas y precisas. Los operadores a incluir son las siguientes:
implicación, denotado por ==>.
consecuencia, denotado por <==.
equivalencia, denotado por <==>.
inequivalencia, denotado por <=! =>.
cuantificaciones universal, existencial, sumatoria, productoria, minimoria, maximoria y cantidad de elementos, denotados respectivamente por \forall, \exists, \sum, \product,
\min, \max, \num_of.
subtipo, denotado por <:.
Además, se incluyen cláusulas y modificadores diversos propios de JML que ayudan a especificar
mejor el comportamiento de las clases. Entre ellos caben destacar:
model, es un modificador que indica que la clase o campo al que se le atribuye es solamente
visible para las especificaciones; esto es, su uso fuera de las anotaciones-JML no tiene sentido.
spec_public y spec_protected, son modificadores que cambian el nivel de privacidad de
la especificación, es decir la hace visible a más niveles a pesar del modificador que pueda
contener la clase o campo.
pure, indica que un método no tiene efectos de borde y esto se hace para permitir el uso
de estos métodos en las especificaciones. En JML no se verifica que un método declarado
16
con este modificador esté, realmente, libre de efectos de borde; tal verificación requiere de
un análisis no trivial de todas las instrucciones y expresiones usadas en incluso los métodos
que se invoquen en el mismo
requires, ensures y signals, para indicar la precondición, postcondición y postcondición
excepcional (en caso de que ocurra alguna excepción), respectivamente.
invariant, palabra clave que indica que el predicado a continuación de ella es el invariante
de clase.
loop_invariant, palabra clave que indica que el predicado a continuación es el invariante
de ciclo.
weakly, modificador que indica que una clase no tendrá behavioral subtypes, si no weak
behavioral subtypes, ya que las subclases no heredan los history constraints, que indican la
relación que debe existir entre un estado del programa y los posibles siguientes estados del
mismo.
refine, palabra clave que se usa en la clausula de refinamiento, bien sea para indicar que
la clase es refinada por otra o que es un refinamiento de otra (distinguiendo esto según la
terminación del archivo)
Entre otras particularidades que permite la detallada especificación del código en JML (que son
descritas en más detalle en el manual de referencia de JML[18]), cabe mencionar que se permite
declarar e interactuar con conjuntos por comprensión.
17
4.2.
Mecanismo para comprobar la validez de los programas
Existen diversas maneras de hacer uso de las especificaciones para comprobar que un programa
está cumpliendo con el contrato establecido. Una de ellas es la verificación formal de correctitud
de teoremas, esto es, se verifica que los predicados correspondientes a las especificaciones son
válidos en toda instancia; realizar esto de manera automatizada no es algo sencillo aunque en
la actualidad existen verificadores de teoremas, no son capaces aún de lograr realizar todas las
pruebas necesarias. Otra manera de utilizar estos contratos es mediante una verificación dinámica,
que es en el momento de ejecución del programa: con los parámetros actuales se chequea que se
cumplan las condiciones. Esta forma tiene ciertas limitaciones, y es que no se comprueba realmente
que el programa es válido para toda instancia, se chequea que es válido para una instancia de
ejecución. Sin embargo, sólo esto, ayudado por un buen plan de testing que cubra muchas de
las posibles situaciones a las que se puede enfrentar el programa, ayudan a tener confianza en el
código que se produce.
4.3.
4.3.1.
Análisis de Complicaciones
Herencia
El manejo adecuado de la herencia es una complicación, ya que la manera de realmente garantizar que se cumplen las condiciones adecuadas entre clase y superclase, requieren prueba de
teoremas para demostrar que las condiciones de refinamiento que surgen de las especificaciones
son válidas. Estas condiciones a demostrar serı́an las implicaciones correspondientes a behavioral
subtypes indicadas en (♦), en la sección 2.1 (en la página 7), y que son las exigidas por Liskov y
Wing en [19], como se indica en la sección 2.2 (en la página 10).
18
Se plantea realizar sólo la verificación dinámica de que se cumplen ambas condiciones: las correspondientes a la superclase y a la subclase (esto en precondiciones, postcondiciones e invariantes
de clase). Chequeando esto estarı́amos limitando las posibilidades de las subclases, pues aceptarı́an
contratos que su superclase hubiese aceptado, pero no se permite la posibilidad de que exista una
situación en la que una subclase se use en un momento en el que por ejemplo, su precondición no
se cumple pero sı́ la de su superclase.
4.3.2.
Apuntadores
En un lenguaje como Java, en el cual los arreglos y las clases se manejan mediante apuntadores,
por lo cual es posible tener aliasing, se hace necesario al momento de revisar la correctitud de un
código considerar las complicaciones determinadas por esto. Para que las especificaciones expresen
exactamente lo que proponemos y no quede lugar a posibles conflictos (generados por tener, al
menos, dos apuntadores a un mismo objeto) parece adecuado seguir el mismo razonamiento de
John Reynolds en su artı́culo “Separation Logic: A Logic for Shared Mutable Data Structures”[22],
y establecer que en lo relativo a objetos pasados por referencia hay manera, en las aserciones, de
indicar que los objetos se ubican en distintas partes de la memoria. De momento, no se cuenta
con un diseño ni una implementación formal de esto para Java/JML. En etapas futuras de JCR,
se plantea hacer énfasis en el desarrollo de ésto.
4.3.3.
Excepciones
Los programas en Java tienen la posibilidad de tener y especificar diversas condiciones de
terminación excepcional, debido a las condiciones de ejecución y a que hay factores que el programador no puede controlar (tales como que el usuario introduzca datos que generen problemas,
como lo puede ser 0 como denominador de una división), y aunque todas estas salidas producen
excepciones, y en Java todas son subclases de la clase Exception, es adecuado poder indicar, para
19
cada una, qué condición cumplen las variables del programa en caso de que se produzca alguna
de ellas, al momento de especificar el código y establecer el contrato de la clase. En [13] se puede
consultar una formalización de cómo se trata este aspecto en Java. Esto se especifica mediante
la cláusula signals en JML, y por tanto en JCR, donde se establece ante cierta excepción qué
condición cumplen las variables del programa.
4.4.
Ventajas sobre Extensiones ya Realizadas
Se quiere mejorar el tratamiento que se le está dando a la verificación de subtipos. Esto es, se
quiere chequear a tiempo de ejecución que los subtipos que se declaren sean behavioral subtypes
según la definición dada por Liskov y Wing[19]. Sólo en términos de verificación dinámica de JCR
sin un demostrador de teoremas, se ha propuesto verificar la conjunción de las condiciones (bien
sea precondiciones, postcondiciones o invariantes de clase). Se desea asegurar el cumplimiento de
las implicaciones adecuadas y con este mecanismo tenemos una buena aproximación, a nivel de
verificación dinámica, a lo que se desea. Esta es una significativa mejora en relación a JContractor
y JML (ver información al respecto en “Antecedentes”, sección 2.1 página 7).
Además, se plantea la posibilidad de realizar modificaciones a JCR, a mediano plazo, para
incluirle algunas ventajas adicionales, como puede ser incluir tipos algebraico-libres como los que
tiene Haskell[4] (que es un proyecto en GaCeLa[24]), lo cual, se sabe, está fuera del contexto de
Java como fue definido originalmente, pero podrı́a realizarse un análisis de la conveniencia de esta
posible modificación.
4.5.
Testing
El proceso de testing es una fase fundamental en el desarrollo de software. En esta fase se
verifica que el software cumpla con los requerimientos del proyecto, entre estos los niveles de
20
calidad adecuados relativos principalmente a que esté libre de errores. Esta parte del desarrollo
es muy importante y compleja, ya que al ser completada se puede lanzar y distribuir el producto.
Hay que tener una idea clara de cómo se va a realizar el testing, entre otras cosas qué aspectos se
desean probar, y qué tan exhaustivas serán las pruebas a realizar, hallando la relación adecuada
entre costo del testing y calidad del producto.
Se plantea llevar una fase de testing iterativa acorde al desarrollo iterativo incremental que
tenga el proyecto, el cual inicia por un reconocedor y evaluador que reconozca las expresiones booleanas básicas incluidas en Java, irle incorporando las extensiones tanto de expresiones booleanas
sencillas, como la implicación, como los cuantificadores, expresiones comparativas y aritméticas,
y ası́ hasta que el software esté completo, incluyendo instrucciones, clases, modelos, etcétera, de
manera que sea capaz de trabajar con programas completos.
4.5.1.
Plan de Testing
Como referencias para la elaboración de este plan se tomaron los lineamientos de Rational,
en especı́fico de su metodologı́a Rational Unified Process (RUP)[23, 25], en donde se expone la
problemática inherente a la elaboración de los planes de testing. Siguiendo estos lineamientos,
se plantea la elaboración de un plan de testing para cada una de las versiones a completar en
el transcurso de la elaboración de este proyecto. Cada plan consta de una especificación de los
aspectos a evaluar del software (qué queremos probar en cada caso), y el conjunto de casos de
prueba especı́ficos. Además, el proceso de testing se desea automatizar, mediante una herramienta
de testing que se plantea realizar, ya que aunque el proceso de testing fue realizado con JUnit[3]
en tempranas etapas del proyecto, con su uso se pierde parte del testing planteado para este
proyecto, como lo es el reconocimiento sintáctico. Luego, gracias a la automatización, la ejecución
y verificación de los resultados será más rápida y las pruebas pueden ser acumulativas, esto es,
21
que luego de cada iteración en la cual se solventen algunas fallas, se pueda con facilidad retomar
todos los casos con los cuales el programa funcionaba bien y comprobar que sigue trabajando bien,
además de verificar si los casos en los que fallaba se ha solucionado dicha falla; también se puede
incluir nuevos casos de prueba que se crean convenientes y necesarios.
4.5.2.
Usando JUnit para realizar las pruebas
JUnit es un software que facilita el proceso de testing del software en desarrollo. Está diseñado
para que su uso en repetidas ocasiones sea sencillo. Una vez que se han creados los casos de prueba,
basta con solicitar que se vuelvan a evaluar, incluso luego de que se le han hecho modificaciones
al código de las clases que se están desarrollando, sin tener que reescribir los casos de prueba, en
la mayorı́a de los casos (a menos claro, que se modifiquen la estructura o nombres de las clases
involucradas en la prueba).
Las pruebas, para hacer uso de esta herramienta, deben estructurarse en clases, subtipo de
TestCase, y en ellas definirse como métodos, tal y como se ilustra en el apéndice A, donde se
encuentra el código correspondiente a algunos de los casos planteados.
4.5.3.
Usando TestJCR
JCR contiene varios puntos de inicio para la gramática, lo cual facilita el testing de cada una
de las subpartes del reconocedor de la misma y, por supuesto, su uso en diversos contextos, ya que
se puede contar sólo con un reconocedor de expresiones, o con un reconocedor de instrucciones
(aunque ya incluirı́an expresiones), o un reconocedor de clases completas. Se tiene en un archivo el
conjunto de casos de prueba con sus respectivas respuestas en un formato adecuado (ver apéndice
B), y por salida estándar nos indica para cada caso si los resultados fueron los esperados y el
porcentaje y la cantidad de casos acertados y fallados.
Capı́tulo 5
Implementación
La verificación dinámica planteada en la sección 4.2 del capı́tulo 4 se realizó en este proyecto
mediante la traducción del código JCR (Java extendido) a Java básico. Luego la verificación
dinámica, una vez realizada la traducción, recae sobre el compilador original de Java.
A medida que se realiza el reconocimiento del código de JCR se van construyendo los árboles
abstractos correspondientes a las expresiones e instrucciones. La estructura del árbol resultante
luego es traducida a Java, y éste es el código que luego es ejecutado y que por tanto verifica, para
la instancia particular, que se cumplan las especificaciones formales incluidas en las anotaciones
tipo JML que se encontraban en el código original correspondiente.
Para la realización de este proyecto se contó con la librerı́a de árboles abstractos del lenguaje
GaCeLa[24]. GaCeLa, como se mencionó en el capı́tulo 4, es una implementación del lenguaje
GCL[8], y su compilador al reconocer este lenguaje como entrada, construye los árboles abstractos
de sus estructuras (expresiones, instrucciones y funciones entre otros) correspondientes; para su
ejecución se traducen estos árboles a código Java y luego es el código Java el que se compila y
ejecuta al momento de querer ejecutar el programa que originalmente estaba en GaCeLa. Esta
librerı́a sirvió de punto de partida y muchas de sus estructuras son reutilizadas ı́ntegramente; sin
embargo algunas de ellas fue necesario adaptarlas a las necesidades de JCR, puesto que en GaCeLa
no se contemplan las mismas instrucciones y expresiones. Por esto se tuvieron que incluir algunos
22
23
tipos de árboles nuevos y modificar algunos de los existentes para que contemplaran las nuevas
estructuras.
Se utilizó la herramienta Claire para elaborar el reconocedor de Java extendido, tal como se
propuso en el capı́tulo 1. El esquema de reconocimiento LALR(1) de dicha herramienta limitó el
reconocimiento del lenguaje; las modificaciones que se tuvieron que hacer al lenguaje originalmente
diseñado se presentan en el apéndice D.
Como referencia principal, se tomaron The Java Language Specification[15] y el manual de
referencia de JML[18]; esto para seguir JML como modelo de extensión, pero adaptado a la versión
1.5 de Java (JML actualmente está disponible para Java 1.4). Además se tomó como referencia
inicial el reconocedor de C. Scott Ananian de Java 1.5, construido con la herramienta JavaCup[2];
algunas maneras particulares de solucionar conflictos respecto al reconocimiento fueron adaptadas
del mismo. Las últimas modificaciones a esta versión son de julio de 2003; por esto y quizás
por algunos conflictos en JavaCup, hay elementos de Java 1.5 que no están presentes en ese
reconocedor, como lo son las anotaciones de Java.
En el desarrollo de JCR se llegó hasta reconocer todo el lenguaje extendido contemplado
en el diseño (ver ’Propuesta’, capı́tulo 4), con las limitaciones especificadas en el apéndice D,
pero sólo se llegó a construir los árboles (y por tanto generar el código Java correspondiente)
de las expresiones e instrucciones convencionales (entendiendo por esto que se dejaron de lado
las instrucciones que incluyen declaración de clases, instrucciones sincronizadas, etiquetadas, de
manejo de excepciones try-catch/throws, enumeraciones y las complicaciones intrı́nsecas a las
llamadas de métodos). En relación con las cláusulas de JML, se reconocen todas mas sólo se
incluyen en la traducción a árboles (y por tanto a código Java) las más básicas, entre las cuales se
consideraron las correspondientes a especificación de precondición, postcondición, invariantes (de
ciclo y clase) y cota (de ciclo).
24
Respecto al testing, se realizaron pruebas con códigos de diversos tamaños (menos de 500 lı́neas,
entre 1000 y 2000 y más de 2000 lı́neas), mas por razones de tiempo no se concretó el desarrollo de
TestJCR y por tanto tampoco la automatización del proceso, lo cual queda pendiente para futuras
etapas del desarrollo de JCR.
Capı́tulo 6
Conclusiones y Recomendaciones
A lo largo del desarrollo de este proyecto se pudo evidenciar la cantidad de implementaciones,
de lenguajes (en particular Java) con especificaciones, que han habı́do en los últimos años, aunque
por razones de simplicidad, se mostraron sólo las que se consideraron pertinentes bien por su gran
desarrollo o uso, por su particular enfoque o por su importancia referencial.
La gramática de Java es bastante extensa y nada sencilla de convertir en un reconocedor. En
relación a esto, aunque se logró estructurar toda la gramática de Java-extendido con la herramienta Claire, se evidenciaron limitaciones en el reconocimiento debido al algoritmo LALR(1),
utilizado por la herramienta, y por tanto se tuvieron que hacer algunas concesiones, las cuales,
lamentablemente, alteran el lenguaje en reconocimiento (ver apéndice D).
Se debe tener especial cuidado al tomar temas que están en pleno desarrollo, como JML, ya
que su documentación es limitada y en constante cambio. Ası́ mismo, se debe estar muy seguro de
cómo se implementan los modelos teóricos, ya que a pesar de tener conocimiento de las condiciones
de refinamiento que se deben cumplir en las clases y subclases, se pasó por alto su manejo adecuado
en la implementación de JML[17] y jContractor[16] respectivamente.
Con el desarrollo de este proyecto de grado se ha profundizado y puesto en práctica una
serie de conocimientos adquiridos a lo largo de la carrera, ası́ como la ejercitación del proceso de
investigación.
25
26
Recomendaciones
Se recomienda realizar una modificación a la herramienta Claire para permitir, aunque sea
a nivel lexicográfico, tener acceso al contexto derecho de los sı́mbolos para ası́ lograr un mejor
reconocimiento.
Ası́ mismo, para darle continuidad a JCR se recomienda mejorar este reconocedor solucionando los detalles de implementación documentados en el apéndice D, y ası́ mismo tomar en
consideración las complicaciones aquı́ analizadas y otras posibles que se escaparon del presente
estudio, y plantear posibles soluciones. También puede ser interesante llevar a cabo una fase de
testing más amplia, y utilizar JCR para programas de mayor tamaño.
Bibliografı́a
[1] Parker Abercrombie. Libreria JaQuaL, 2002.
URL http://jcontractor.sourceforge.net/doc/jaqual/api/index.html
[2] C. Scott Ananian. Gramática CUP de Java versión 1.5, Julio 2003.
URL http://www.cs.princeton.edu/~appel/modern/java/CUP/javagrm.tar.gz
[3] Kent Beck y Erich Gammma. JUnit Cookbook.
URL http://junit.sourceforge.net/doc/cookbook/cookbook.htm
[4] Richard Bird. Introduction to Functional Programming using Haskell. Prentice Hall, segunda
edición, 1998.
[5] C. Boyapati, S. Khurshid y D. Marinov. Korat: Automated testing based on Java predicates. En Proceedings of the 2002 International Symposium on Software Testing and Analysis
(ISSTA), Rome, Italy, July 22–24, 2002. 2002.
URL http://citeseer.ist.psu.edu/boyapati02korat.html
[6] Alistair Cockburn. Crystal Clear : A Human-Powered Methodology for Small Teams. Addison
Wesley Professional, 2004.
[7] Compaq Research Center. Extended static checker ESC/Java.
URL http://www.research.digital.com/SRC/esc/Esc.html
27
28
[8] Edsger Wybe Dijkstra. Guarded commands, nondeterminancy and formal derivation of programs. Communications of the ACM, 18(8):453–457, Agosto 1975. ISSN 0001-0782.
[9] Edsger Wybe Dijkstra. A Discipline of Programming. Prentice-Hall series in Automatic
Computation, Englewood Cliffs, N.J., 1976.
[10] Robert Bruce Findler y Matthias Felleisen. Contract soundness for object-oriented languages.
En ACM Conference on Object Oriented Programming, Systems, Languages and Applications
(OOPSLA’01), páginas 1–15. 2001.
URL http://www.ccs.neu.edu/scheme/pubs/oopsla01-ff.pdf
[11] Robert Floyd. Assigning meanings to programs. En J. T. Schwartz, editor, Mathematical
Aspects of Computer Science, Proceedings of Symposia in Applied Mathematics 19, páginas
19–32. American Mathematical Society, Providence, 1967.
[12] Charles Antony Richard Hoare. An axiomatic basis for computer programming. Communications of the ACM, 12(10):576–583, Octubre 1969.
[13] Bart Jacobs. A formalisation of java’s exception mechanism. Informe Técnico CSI-R0015,
University of Nijmegen, 2000.
URL citeseer.ist.psu.edu/article/jacobs00formalisation.html
[14] Bart Jacobs, Hans Meijer y Erik Poll. VerifiCard: A European Project for Smart Card
Verification. Newsletter 5 of the Dutch Association for Theoretical Computer Science (NVTI),
2001.
URL http://www.cs.kun.nl/~bart/PAPERS/nvti01.ps.Z
[15] Bill Joy, James Gosling, Guy Steele y Gilad Bracha. The Java Language Specification. Addison
29
Wesley Professional, tercera edición, 2005.
URL http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html
[16] Murat Karaorman, Urs Hölzle y John Bruno. jcontractor: A reflective java library to support
design by contract. En P. Cointe, editor, Meta-Level Architectures and Reflection, 2nd Int’l
Conf. Reflection, tomo 1616 de LNCS, páginas 175–196. Springer Verlag, Berlin, Alemania,
1999.
URL http://www.cs.ucsb.edu/labs/oocsb/papers/TRCS98-31.pdf
[17] Gary Leavens y Yoonsik Cheon. Design by Contract with JML, Enero 2005.
URL ftp://ftp.cs.iastate.edu/pub/leavens/JML/jmldbc.pdf
[18] Gary Leavens, Erik Poll, Curtis Clifton, Yoonsik Cheon, Clyde Ruby, David Cok, Peter Müller
y Joseph Kiniry. JML Reference Manual, Julio 2005.
URL http://www.cs.iastate.edu/~leavens/JML/jmlrefman/jmlrefman_toc.html
[19] Barbara Liskov y Jeannette Wing. Behavioral subtyping using invariants and constraints.
Informe Técnico CS99-156, School of Computer Science, Carnegie Mellon University, Julio
1999.
URL http://citeseer.ist.psu.edu/liskov99behavioral.html
[20] Bertrand Meyer. Object-oriented Software Construction. Prentice Hall, 1988.
[21] Carroll Morgan. On the Refinement Calculus. Springer-Verlag, 1994.
[22] John Reynolds. Separation Logic: A Logic for Shared Mutable Data Structures. En Symposium on Logic in Computer Science, 2002. 29. 2002.
URL http://citeseer.ist.psu.edu/reynolds02separation.html
30
[23] Rational Software. Rational unified process: Best practices for software development teams.
White Paper, diciembre 2003.
URL http://www-106.ibm.com/developerworks/rational/library/253.html
[24] Ascánder Suárez. Implementación de GCL: GaCeLa. Caracas, Venezuela, 2004.
URL http://gacela.labf.usb.ve/GCLWiki/Wiki.jsp
[25] Paul Szymkowiak. Testing: The rup philosophy. The Rational Edge, Febrero 2003.
[26] Joachim van den Berg y Bart Jacobs. The LOOP compiler for Java and JML. Lecture Notes
in Computer Science, 2031:299, 2001.
URL http://citeseer.ist.psu.edu/vandenberg01loop.html
Glosario
Aliasing: fenómeno que ocurre cuando un objeto tiene más de un nombre, y que suele traer
como consecuencia cambios inesperados en la instancia; un caso particular de esto sucede
cuando un objeto es referenciado por más de un apuntador.
Efecto de borde (o secundario): consecuencia inesperada y generalmente indeseada, se considera un efecto extra de la ejecución de una acción o la evaluación de una expresión; en
particular, el caso de modificar variables mientras se evalúan expresiones correspondientes
a la especificación.
Extender gramática: crear una gramática cuyo lenguaje asociado sea un superconjunto del
asociado a la gramática original; la nueva presenta ciertas facilidades adicionales que la
anterior no tenı́a.
Invariante: 1. I. de ciclo aserción que se debe cumplir al inicio y al final de cada vuelta de
un ciclo 2. I. de clase aserción que se debe cumplir en cada uno de los puntos visibles de
una clase (al inicio y final de los métodos y al final de los constructores).
Precondición: aserción que se debe cumplir al inicio de un método.
Predicado: expresión booleana.
Postcondición: aserción que se debe cumplir al final de un método.
31
32
Smart Card: tarjetas que tienen un pequeño procesador, cuya funcionalidad es permitir el
acceso a diversos servicios, alguno de los cuales puede requerir un alto grado de confidencialidad y seguridad.
Apéndice A
Código de los casos de prueba para JUnit
Estos casos de prueba se usaron para evaluar una versión temprana del reconocedor, la cual
sólo reconocia expresiones y además de reconocerlas, permitı́a la posibilidad de evaluarlas.
A continuación se presenta el código de los casos de prueba relativos a la lista de variables:
import junit.framework.*;
public class ExprBooleanaTest extends TestCase {
private ExprBooleanaConstante verdadera;
private ExprBooleanaConstante falsa;
private ExprBooleanaIdentif x1, x2, x3, x4;
public static void main(String args[]) {
junit.textui.TestRunner.run(suite());
}
public static Test suite() {
TestSuite suite = new TestSuite(ExprBooleanaTest.class);
return suite;
}
public void setUp() {
verdadera = new ExprBooleanaConstante(true);
falsa = new ExprBooleanaConstante(false);
x1 = new ExprBooleanaIdentif("x1");
x2 = new ExprBooleanaIdentif("x2");
x3 = new ExprBooleanaIdentif("x3");
x4 = new ExprBooleanaIdentif("x4");
}
33
34
public void testListaVar1() throws ExcepcionIdentifNoEnTabla {
ExprBooleana expresion, esperada;
expresion = new ExprBooleanaBinaria(’&’, verdadera, falsa);
esperada = falsa;
assertTrue(expresion.evaluar(new TablaIdentif())==esperada.evaluar(new TablaIdentif()));
}
public void testListaVar2() throws ExcepcionIdentifNoEnTabla {
ExprBooleana expresion, esperada;
expresion = new ExprBooleanaBinaria(’y’, new ExprBooleanaBinaria(’y’, verdadera, x1),
new ExprBooleanaUnaria(’!’, x2));
esperada = verdadera;
TablaIdentif tabla = new TablaIdentif();
tabla.asignar(new Entrada("x1", true));
assertTrue(expresion.evaluar(tabla)==esperada.evaluar(tabla));
}
public void testListaVar3() throws ExcepcionIdentifNoEnTabla {
ExprBooleana expresion, esperada;
expresion = new ExprBooleanaBinaria(’y’, new ExprBooleanaBinaria(’y’, verdadera, x1),
new ExprBooleanaUnaria(’!’, x2));
esperada = verdadera;
TablaIdentif tabla = new TablaIdentif();
tabla.asignar(new Entrada("x1", true));
tabla.asignar(new Entrada("x2", false));
assertTrue(expresion.evaluar(tabla)==esperada.evaluar(tabla));
}
public void testListaVar4() throws ExcepcionIdentifNoEnTabla {
ExprBooleana expresion, esperada;
expresion = new ExprBooleanaBinaria(’^’, verdadera, falsa);
esperada = verdadera;
TablaIdentif tabla = new TablaIdentif();
tabla.asignar(new Entrada("x1", true));
tabla.asignar(new Entrada("x2", false));
tabla.asignar(new Entrada("x3", true));
assertTrue(expresion.evaluar(tabla)==esperada.evaluar(tabla));
}
public void testListaVar5() throws ExcepcionIdentifNoEnTabla {
ExprBooleana expresion, esperada, auxiliar;
35
auxiliar = new ExprBooleanaBinaria(’y’, new ExprBooleanaUnaria(’!’, x1), falsa);
expresion = new ExprBooleanaBinaria(’o’, new ExprBooleanaUnaria(’!’, auxiliar), x2);
esperada = verdadera;
TablaIdentif tabla = new TablaIdentif();
tabla.asignar(new Entrada("x1", true));
tabla.asignar(new Entrada("x2", false));
tabla.asignar(new Entrada("x3", true));
tabla.asignar(new Entrada("x4", true));
assertTrue(expresion.evaluar(tabla)==esperada.evaluar(tabla));
}
public void testListaVar6() throws ExcepcionIdentifNoEnTabla {
ExprBooleana expresion, esperada, auxiliar;
auxiliar = new ExprBooleanaBinaria(’&’, x1, x2);
expresion = new ExprBooleanaBinaria(’o’, auxiliar, falsa);
esperada = falsa;
TablaIdentif tabla = new TablaIdentif();
tabla.asignar(new Entrada("x1", true));
tabla.asignar(new Entrada("x2", true));
tabla.asignar(new Entrada("x1", false));
assertTrue(expresion.evaluar(tabla)==esperada.evaluar(tabla));
}
}
Apéndice B
Formato de los casos de prueba de TestJCR
El formato del archivo es:
<nombreDelPrograma>
%
<datosDeEntrada0>
#
<resultados0>
%
<datosDeEntrada1>
#
<resultados1>
...
<datosDeEntradaN>
#
<resultadosN>
@
Se compilará y se ejecutará muchas veces el programa llamado <nombreDelPrograma>, y se le
pasará en cada ocasión los <datosDeEntrada> correspondientes y se verificará que se produzcan
los <resultados> esperados.
Nota0: si los resultados se reflejan en una estructura de datos compleja, previamente se determinará en que formato se representarán.
Nota1: esto fácilmente se usa para programas que lean los datos de entrada por entrada estándar e imprima por salida estándar los de salida, pero es análogo su uso para programas que
interactúan con archivos, leyendo y escribiendo respectivamente datos de entrada o salida.
36
Apéndice C
Programas en JML
C.1.
Programa tipo Algoritmos I: Factorial
/**
* Devuelve el valor correspondiente al factorial del entero pasado como par’ametro
*
* @param i valor entero al que se le calcularar’a el factorial
* @return el valor del factorial de i
*/
/*@
@ requires i >= 0;
@ ensures (\result == (\product int k; 1 <= k && k <= i; k));
@*/
public static int fact(int i) {
if (i == 0) {
return 1;
} else {
return (i * fact(i - 1));
}
}
C.2.
Programa tipo Algoritmos I: Ordenamiento Burbuja
/**
* Dado un arreglo, devuelve el arreglo ordenado ascendentemente correspondiente
* a los valores que conten’ia el original.
*
* @param a arreglo de valores a ordenar
*
* @return el arreglo ordenado
*/
37
38
/*@
@ ensures
@
(arreglo.length == \old(arreglo.length))
@
&&
@
(\forall int i
@
;0 <= i && i < arreglo.length
@
;(\num_of int j; 0 <= j && j < arreglo.length; arreglo[i] == arreglo[j])
@
==
@
(\num_of int j; 0 <= j && j < \old(arreglo.length); arreglo[i] == \old(arreglo[j]))
@
)
@
&&
@
(\forall int i; 0 <= i && i < arreglo.length - 1; arreglo[i] <= arreglo[i + 1]);
@*/
public static void ordenar(int arreglo[]) {
int i = 0;
/*@
@ loop_invariant
@
(0 <= i && (arreglo.length > 0 ==> i <= arreglo.length - 1))
@
&&
@
(\forall int l
@
;arreglo.length - 1 - i <= l && l < arreglo.length - 1
@
;arreglo[l] <= arreglo[l + 1]
@
)
@
&&
@
(\forall int m, l
@
;0 <= m && m <= arreglo.length - 1 - i && arreglo.length - 1 - i < l && l <arreglo.length
@
;arreglo[m] <= arreglo[l]
@
);
@ decreasing (arreglo.length - i);
@*/
for (; i < arreglo.length - 1; i++) {
int j = 0;
/*@
@ loop_invariant
@
(0 <= j && j <= arreglo.length - 1 - i)
@
&&
@
(\forall int k; 0 <= k && k < j; arreglo[k] <= arreglo[j]);
@ decreasing (arreglo.length - 1 - i - j);
@*/
for (; j < arreglo.length - 1 - i; j++) {
39
if (arreglo[j] > arreglo[j + 1]) {
swap(arreglo, j, j + 1);
}
}
}
}
C.3.
Programa tipo Algoritmos II: TAD Diccionario
import java.util.Vector;
class Diccionario {
private /*@ spec_public @*/ Vector claves;
private /*@ spec_public @*/ Vector valores;
private /*@ spec_public @*/ int tam;
/*@
@ invariant 0 <= tam
@
&&
@
(\forall int i, j
@
;0 <= i && i < tam && 0 <= j && j < tam
@
;claves.elementAt(i).equals(claves.elementAt(j)) ==> i == j
@
);
@*/
//@ ensures tam == 0 && valores.size() == 0 && claves.size() == 0;
public Diccionario() {
tam = 0;
claves = new Vector();
valores = new Vector();
}
/*@
@ requires !(conocida(clave));
@ ensures
((String) claves.elementAt(tam - 1)).equals(clave)
@
&&
@
((String) valores.elementAt(tam - 1)).equals(valor)
@
&&
@
\old(tam) + 1 == tam
@
&&
@
(\forall int i
@
;0 <= i && i < tam - 1
40
@
;((String) claves.elementAt(i)).equals((String) \old(claves).elementAt(i))
@
&&
@
((String) valores.elementAt(i)).equals((String) \old(valores).elementAt(i))
@
);
@*/
public void agregar(String clave, String valor) {
claves.add(clave);
valores.add(valor);
tam++;
}
/*@
@ requires (conocida(clave));
@ ensures (\forall int i
@
;0 <= i && i < posicion(clave)
@
;((String) claves.elementAt(i)).equals((String) \old(claves.elementAt(i)))
@
)
@
&&
@
(\forall int i
@
;posicion(clave) <= i && i < tam
@
;((String) claves.elementAt(i)).equals((String) \old(claves.elementAt(i + 1)))
@
)
@
&&
\old(tam) - 1
== tam;
@*/
public void eliminar(String clave) {
valores.remove(claves.indexOf(clave));
claves.remove(claves.indexOf(clave));
tam--;
}
/*@
@ requires conocida(clave);
@ ensures (\exists int i
@
;0 <= i && i < tam
@
;claves.elementAt(i).equals(clave)
@
&&
@
\result.equals((String) valores.elementAt(i))
@
);
@*/
public /*@ pure @*/ String obtener(String clave) {
return((String) valores.elementAt(claves.indexOf(clave)));
41
}
//@ ensures \result == (\exists int i; 0 <= i && i < tam; claves.elementAt(i).equals(clave));
public /*@ pure @*/ boolean conocida(String clave) {
return(claves.contains(clave));
}
//@ ensures \result == (tam == 0);
public /*@ pure @*/ boolean esVacio() {
return(tam == 0);
}
/*@
@ requires conocida(clave);
@ ensures claves.elementAt(\result) == clave;
@*/
private /*@ pure spec_public @*/ int posicion(String clave) {
return(claves.indexOf(clave));
}
}
Apéndice D
Detalles de Implementación
El esquema de análisis sintáctico LALR(1), que utiliza la herramienta Claire, generaba conflictos con la gramática propuesta para JCR, y por tanto se tuvo que recurrir a una modificación
del lenguaje que, imponiendo restricciones extra al lenguaje, logra eliminar las ambigüedades
existentes bajo este esquema. A continuación se presentan las concesiones que se tuvieron que
hacer:
Prohibir los comentarios entre @ e interface en la declaración de un tipo de anotación-Java,
esto debido a que al punto de estar reconociendo los modificadores que puede tener esta
declaración y que el próximo terminal sea un @ no se puede determinar si es una anotación
ya creada o que se está declarando el tipo de anotación.
Agrupar en una regla varios no-terminales opcionales, y usarla en algunas reglas donde
algunos de esos no terminales no deberı́an aparecer. Se debe a que al tener elementos en
común y ser opcionales no se puede determinar a cuál de las posibles reglas corresponde.
Esta limitación sin embargo puede ser solucionada a través del uso de atributos en el tipo
de retorno de esa regla que indiquen cuáles no terminales se reconocieron y dando un error
en caso de que se reconozca alguno que no corresponde a ese punto de reconocimiento.
42
43
Las especificaciones de los métodos y constructores se deben colocar luego del encabezado.
Esto limita las opciones de JML de poder colocarlo al inicio o al final del encabezado.
Esto se debe a que los posibles modificadores del encabezado causan confictos con la regla
privacy que es parte de los no-terminales de la regla model_program, la cual es una posible
especificación.
Obligar a tener la separación de rango y predicado en las cuantificaciones, ası́ el rango sea
vacı́o. Esto debido a que, teniendo todo esto opcional, se generan conflictos al momento de
reconocer una expresión: no se identifica si se está reconociendo el rango o el predicado (y
no habı́a rango).
Inclusión de una serie de reglas adicionales sobre las expresiones que evitan que éstas vayan
al no-terminal name antes de que sea la última opción, ya que con la gran cantidad de reglas
que van a name, no se está seguro de a cuál de tantas corresponde.
Eliminar la coma opcional en las reglas: element_value_array_initializer, enum_body,
array_initializer; ya que al estar ésta luego de una lista, no se puede determinar con
certeza si la lista terminó o no.
Colocar información un poco repetitiva en la regla explicit_constructor_invocation
y no hacer buen uso de las macros disponibles, esto debido a que, si se utilizan para
mejorar esto, se crean conflictos con la regla primary_no_new_array, por el no-terminal
opt(type_arguments).
No permitir colocar una etiqueta (name :) en la regla possibly_annotated_loop, ya que
esto generı́a confusión con la regla labeled_statement.
44
Además de las anteriores concesiones, también se cambiaron otros aspectos. A pesar de que
estos cambios se hicieron principalmente por no lograr el reconocimiento por limitaciones de la
herramienta, se determinó que conservar estos cambios es más conveniente que tener la versión
original de JML. Esos aspectos son los siguientes:
No permitir el tipo arreglo como resultado de la creación de un conjunto.
No utilizar el modificador weakly de JML, hasta que no se compruebe que es conveniente
tener subclases que no sean behavioral subtypes
Y por falta de documentación no se incluyeron las reglas normal_spec_clause y redundant_spec, la primera de ellas por no contar con la especificación de cómo reducir ese no terminal
y la segunda por depender de la primera.
Anexo 1
Ejemplo de programa en JContractor
Programa: PriorityQueue
package edu.ucsb.ccs.jcontractor.examples.heap;
/**
* Abstract description of a datastructure that allows easy access to
* its largest element.
*
* @author Parker Abercrombie
*
* @invariant size () >= 0
* @invariant isFull() implies !isEmpty()
* @invariant isEmpty() implies !isFull()
*/
public interface PriorityQueue {
/**
* Is the queue empty?
*
* @return <code>true</code> if there are no items in the queue,
*
otherwise <code>false</code>.
*/
public boolean isEmpty ();
/**
* Is the queue full?
*
* @return <code>true</code> if no more items may be added to the
*
queue.
*/
public boolean isFull ();
/**
45
46
* Get the largest element in the queue.
*
* @return the largest element in the queue.
*
* @pre !isEmpty ()
*
* @post return != null
* @post size() == size()@pre
*/
public Comparable item ();
/**
* Remove all elements from the queue.
*
* @post isEmpty ()
*/
public void clear ();
/**
* Get the number of elements in the queue.
*
* @return the number of elements in the queue.
*
* @post return >= 0
*/
public int size ();
/**
* Determine how many times ‘key’ appears in the queue.
*
* @param key the item to search for.
*
* @return the number of times <code>key</code> appears in the
* queue.
*/
public int occurences (Comparable key);
/**
* Add an item to the queue.
*
* @param newItem the item to add.
*
* @pre newItem != null
* @pre !isFull()
*
* @post !isEmpty ()
* @post size () == size()@pre + 1
47
* @post occurences (newItem) == occurences(newItem)@pre + 1
*/
public void put (Comparable newItem);
/**
* Remove and return the largest item in the queue.
*
* @return the item that was removed.
*
* @pre !isEmpty ()
*
* @post !isFull()
* @post size() == size()@pre - 1
* @post return == item()@pre
* @post occurences (return) == occurences(item())@pre - 1
* @post return != null
*/
public Comparable remove ();
}
Programa que contiene los contratos de la clase PriorityQueue
package edu.ucsb.ccs.jcontractor.examples.heap;
public abstract class PriorityQueue_CONTRACT implements PriorityQueue {
protected boolean item_Precondition () {
return !isEmpty();
}
protected boolean item_Postcondition (Comparable RESULT) {
return (RESULT != null);
}
protected boolean clear_Postcondition (boolean RESULT) {
return isEmpty();
}
protected boolean size_Postcondition (int RESULT) {
return RESULT >= 0;
}
protected boolean put_Precondition (Comparable newItem) {
return (newItem != null) && !isFull();
}
48
protected boolean put_Postcondition (Comparable newItem,
Void RESULT) {
return !isEmpty();
}
protected boolean remove_Postcondition (Comparable RESULT) {
return !isFull() && (RESULT != null);
}
}
Códigos de Parker Abercrombie, distribuidos con la última versión de JContractor
(jcontractor-0.1/src/edu/ucsb/ccs/jcontractor/examples/heap).
Anexo 2
Ejemplo de programa en JML
Programa: LinearSearch
// @(#)$Id: LinearSearch.java,v 1.4 2005/02/17 17:05:32 leavens Exp $
// Copyright (C) 1998, 1999 Iowa State University
// This file is part of JML
// JML is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2, or (at your option)
// any later version.
// JML is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with JML; see the file COPYING.
If not, write to
// the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
package org.jmlspecs.samples.misc;
/** A linear search component, intended to demonstrate verification in
* JML specifications.
This class has two abstract methods that
* describe the search, which need to be filled in to instantiate
* it.
The formulation of the search and the verification
* is based on Edward Cohen’s book
* <cite>Programming in the 1990s</cite> (Springer-Verlag, 1990).
* @author Gary T. Leavens
*/
public abstract class LinearSearch
{
49
50
/** The function that describes what is being sought. */
//@ requires j >= 0;
public abstract /*@ pure @*/ boolean f(int j);
/** The last integer in the search space, this describes the
* domain of f, which goes from 0 to the result.
*/
//@ ensures 0 <= \result;
//@ ensures (\exists int j; 0 <= j && j <= \result; f(j));
public abstract /*@ pure @*/ int limit();
/** Find a solution to the searching problem. */
/*@ public normal_behavior
@
requires (\exists int i; 0 <= i && i <= limit(); f(i));
@
assignable \nothing;
@
ensures \result == (\min int i; 0 <= i && f(i); i);
@*/
public int find() {
int x = 0;
//@ maintaining 0 <= x && (\forall int i; 0 <= i && i < x; !f(i));
//@ decreasing limit() - x;
while (!f(x)) {
/*@ assert 0 <= x && !f(x)
@
&& (\forall int i; 0 <= i && i < x; !f(i));
@*/
//@ hence_by (* arithmetic *);
/*@ assert 0 <= (x+1)-1 && !f((x+1)-1)
@
&& (\forall int i; 0 <= i && i < (x+1)-1; !f(i));
@*/
x = x + 1;
/*@ assert 0 <= x-1 && !f((int)(x-1))
@
&& (\forall int i; 0 <= i && i < x-1; !f(i));
@*/
//@ hence_by (* arithmetic, constant term rule *);
/*@ assert 0 <= x && (\forall int i; i == x-1; !f(i))
@
&& (\forall int i; 0 <= i && i < x-1; !f(i));
@*/
//@ hence_by (* joining the range *);
/*@ assert 0 <= x &&
@
(\forall int i; (0 <= i && i < x-1) || i == x-1; !f(i));
@*/
//@ hence_by (* arithmetic *);
//@ assert 0 <= x && (\forall int i; 0 <= i && i < x; !f(i));
}
/*@ assert 0 <= x && f(x)
@
@*/
&& (\forall int i; 0 <= i && i < x; !f(i));
51
//@ hence_by (* definition of \min *);
//@ assert x == (\min int i; 0 <= i && f(i); i);
return x;
}
}
Código de Gary Leavens, distribuido con la versión actual de JML
(JML/org/jmlspecs/samples/misc/LinearSearch.java).