60
i cenidet Centro Nacional de Investigación y Desarrollo Tecnológico Departamento de Ciencias Computacionales TESIS DE MAESTRÍA EN CIENCIAS Análisis del Problema para Determinar un Procedimiento que Deduzca a Partir de Código Legado Pre y Pos Condiciones e Invariantes Presentada por Liliana Badillo Sánchez L.M.A. por la Universidad Juárez del Estado de Durango Como requisito para la obtención del grado de: Maestro en Ciencias en Ciencias de la Computación Director de tesis: Dr. René Santaolaya Salgado Co-Director de tesis: M.C. Olivia Graciela Fragoso Díaz Cuernavaca, Morelos, México. Noviembre de 2008

Centro Nacional de Investigación y Desarrollo Tecnológico

  • Upload
    others

  • View
    2

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Centro Nacional de Investigación y Desarrollo Tecnológico

i

cenidet

Centro Nacional de Investigación y Desarrollo Tecnológico Departamento de Ciencias Computacionales

TESIS DE MAESTRÍA EN CIENCIAS

Análisis del Problema para Determinar un Procedimiento que Deduzca a Partir de Código Legado

Pre y Pos Condiciones e Invariantes

Presentada por

Liliana Badillo Sánchez L.M.A. por la Universidad Juárez del Estado de Durango

Como requisito para la obtención del grado de: Maestro en Ciencias en Ciencias de la Computación

Director de tesis: Dr. René Santaolaya Salgado

Co-Director de tesis: M.C. Olivia Graciela Fragoso Díaz

Cuernavaca, Morelos, México. Noviembre de 2008

Page 2: Centro Nacional de Investigación y Desarrollo Tecnológico

ii

ÍNDICE

ÍNDICE DE FIGURAS ............................................................................................................. iv

ÍNDICE DE TABLAS ............................................................................................................... v

SIMBOLOGÍA ......................................................................................................................... ivi

GLOSARIO .............................................................................................................................. vii

Capítulo INTRODUCCIÓN ....................................................................................................... 1

1.1. Introducción. ....................................................................................................................... 2

1.2. Objetivo. .............................................................................................................................. 2

1.3. Descripción del problema. ................................................................................................... 2

1.4. Justificación. ........................................................................................................................ 2

1.5. Beneficios. ........................................................................................................................... 2

1.6. Alcances. ............................................................................................................................. 2

1.7. Limitaciones. ....................................................................................................................... 3

1.8. Organización de esta tesis. .................................................................................................. 3

Capítulo 2 MARCO TEÓRICO ............................................................................................... 21

2.1. Aserción ............................................................................................................................ 21

2.2. Código anotado. .................................................................................................................. 4

2.3. Lógica de Hoare. ................................................................................................................. 5

2.4. Axiomas y reglas de la lógica de Hoare. ............................................................................. 6

2.4.1. Axioma de asignación. ............................................................................................. 7

2.4.2. Regla de composición .............................................................................................. 7

2.4.3. Regla del comando if ................................................................................................ 8

2.4.4. Reforzamiento de la precondición. ........................................................................... 8

2.4.5. Debilitamiento de la poscondición. .......................................................................... 8

2.4.6. Regla del comando while. ........................................................................................ 9

2.4.7. Otras reglas de la lógica de Hoare. .......................................................................... 9

2.5. Cálculo de la precondición más débil wp ............................................................................ 9

2.5.1. Ejemplo del cálculo de la precondición más débil wp ....................................... 10

2.6. Extensión de la lógica de Hoare para programación orientada a objetos ......................... 12

2.6.1. Diseño por contratos ........................................................................................... 12

2.6.2. Invariante de clase .............................................................................................. 16

2.7. Correctitud en programación orientada a objetos ............................................................. 16

2.8. Mecanización de la verificación de programas ................................................................. 17

2.9. Verificación de Modelos ................................................................................................... 18

2.9.1. Verificación de Modelos Temporal ................................................................... 18

2.9.2. Lógica Temporal Lineal (LTL) ........................................................................... 19

2.9.3. Sintaxis de LTL................................................................................................... 19

2.9.4. Semática de LTL. ................................................................................................ 19

Capítulo 3 ANTECEDENTES ................................................................................................. 21

3.1. Antecedentes. .................................................................................................................... 21

3.1.1. Identificación de funciones recurrentes en software legado ................................. 21

3.2. Estado del arte. .................................................................................................................. 22

3.3. Trabajos relacionados. ....................................................................................................... 23

3.3.1. Análisis de código fuente ............................................................................................ 23

3.3.1.1. Extended Static Checking for Java ESC/Java ..................................................... 23

3.3.1.2. Houdini, An annotation assistant for ESC/Java . ................................................ 23

3.3.2. Análisis de documentación.......................................................................................... 23

3.3.2.1. Uncovering hidden contracts: The .NET example ............................................. 24

3.3.3. Análisis dinámico ........................................................................................................ 24

Page 3: Centro Nacional de Investigación y Desarrollo Tecnológico

iii

3.3.3.1. The Daikon system for dynamic detection of likely invariants .......................... 24

3.3.4. Análisis de documentación y análisis dinámico .......................................................... 24

3.3.4.1. Extracting functional and Non functional contracts from Java Classes and

Enterprise Java Beans ...................................................................................................... 25

3.3.4.2. Discern: Towards the Automatic Discovery of Software Contracts .................. 25

Capítulo 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN PROPUESTA ............................. 26

4.1. Técnicas y herramientas que pueden ayudar en la extracción .......................................... 27

4.1.1. Análisis de documentación ..................................................................................... 27

4.1.2. Análisis del código fuente ...................................................................................... 27

4.1.3. Lógica de Hoare y cálculo de la precondición más débil wp ................................. 27

4.1.4. Herramientas disponibles en Internet ..................................................................... 28

4.1.4.1. Análisis dinámico ........................................................................................... 28

4.1.4.2. Generación automática de invariantes ............................................................ 28

4.2. Etapas del problema de la extracción ................................................................................ 29

4.3. Solución propuesta ............................................................................................................ 30

4.4. Formalización de la solución propuesta ............................................................................ 32

Capítulo 5 PRUEBAS .............................................................................................................. 34

5.1. Caso de prueba 1 ............................................................................................................... 34

5.2. Caso de prueba 2 ............................................................................................................... 38

5.3. Caso de prueba 3 ............................................................................................................... 40

5.4. Caso de prueba 4 ............................................................................................................... 42

Capítulo 6 CONCLUSIONES .................................................................................................. 44

6.1. Conclusiones. .................................................................................................................... 44

6.2. Aportaciones. ..................................................................................................................... 44

6.2. Trabajo futuro. ................................................................................................................... 45

REFERENCIAS ....................................................................................................................... 46

ANEXO A ................................................................................................................................ 48

Page 4: Centro Nacional de Investigación y Desarrollo Tecnológico

iv

ÍNDICE DE FIGURAS

Figura 1. Especificación de un contrato de software.. ............................................................. 12

Figura 2. Ejemplo de la declaración de una rutina en el lenguaje de programación Eiffel ......13

Figura 3. Atributos de un contracto en CDL. ........................................................................... 15

Figura 4. Estructura de especificación de un contrato.. ........................................................... 16

Figura 5. Ejemplo de salida de Daikon.. .................................................................................. 28

Figura 6. Tareas de la extracción de especificaciones formales.. ............................................ 29

Figura 7. Algoritmo propuesto para la extracción de especificaciones formales. .................... 31

Figura 8. Código del never claim generado en Promela. ......................................................... 32

Figura 9. Salida de Spin para el algoritmo propuesto .............................................................. 33

Page 5: Centro Nacional de Investigación y Desarrollo Tecnológico

v

ÍNDICE DE TABLAS

Tabla 1. Capacidad de automatización de técnicas y herramientas. ........................................ 30

Tabla 2. Especificación formal de la clase Rectangle. ............................................................. 37

Tabla 3. Especificación formal de la clase Square. .................................................................. 37

Tabla 4. Especificación formal de la función factorial. ........................................................... 40

Tabla 5. Especificación formal de la función multiplic. .......................................................... 42

Tabla 6. Especificación formal de la clase Stack. .................................................................... 43

Page 6: Centro Nacional de Investigación y Desarrollo Tecnológico

vi

SIMBOLOGÍA

Vc Condición de verificación

⊢ Inferencia ≡ Equivalencia

i.e. Es decir

∧ Y, “and”

∨ O, “or” ∃ Existe al menos un elemento

Siguiente

◊ Eventualmente

□ Siempre

U Fuerte hasta

W Débil hasta

⊤ Tautología

⊥ Contradicción

∀ Para todo

Page 7: Centro Nacional de Investigación y Desarrollo Tecnológico

vii

GLOSARIO

ASERCIÓN Una aserción es una terna {P}S{Q}, donde S es un programa

y P ,Q son formulas de la lógica de predicados (conocidas

como precondición y poscondición respectivamente). Una

aserción es verdadera si y sólo si, S inicia en el estado que

satisface P y si la computación de S termina, entonces la

computación termina en el estado que satisface Q.

CÓDIGO ANOTADO Un código o comando anotado tiene aserciones embebidas

(precondición, poscondición e invariantes).

CÓDIGO LEGADO Código fuente heredado de alguien más.

CENIDET Centro Nacional de Investigación y Desarrollo Tecnológico.

CONDICIONES DE

VERIFICACIÓN

De las especificaciones anotadas en el programa se genera

un conjunto de sentencias puramente matemáticas llamadas

verification conditions (condiciones de verificación o vc’s).

Las vc’s se verifican automáticamente en un demostrador de

teoremas.

CONTRATO Involucra generalmente dos partes, cada una de ellas espera

algún beneficio y está preparada a cumplir algunas

obligaciones en intercambio. Los beneficios y las

obligaciones son plasmados en un documento. En Ingeniería

de Software se aplica éste concepto como: “si la ejecución

de una cierta tarea recae en una llamada de rutina que

maneja sus sub-tareas, es necesario especificar la relación

entre el cliente (el que llama a la rutina) y el proveedor (la

rutina llamada) lo más preciso posible”.

CORRECTITUD Un algoritmo es correcto si cumple con lo siguiente:

resuelve el problema computacional para el cual fue

diseñado; para cada entrada produce la salida deseada; y su

ejecución termina en un tiempo finito.

CORRECTITUD

PARCIAL

Si se cumple que ⊢ {P}S{Q} entonces la aserción es

parcialmente correcta con respecto a P y Q.

DEMOSTRACIÓN

AUTOMÁTICA DE

TEOREMAS

Demostración de teoremas matemáticos mediante una

computadora.

DISEÑO POR

CONTRATOS

Es una metodología para el diseño e implementación de

aplicaciones y componentes. En ésta se definen

precondiciones, poscondiciones e invariantes para cada

método de una clase.

Page 8: Centro Nacional de Investigación y Desarrollo Tecnológico

viii

ESPECIFICACIÓN

FORMAL

Descripción matemática de software o hardware que se

utiliza para desarrollar una implementación.

INVARIANTE DE

CICLO

Es una condición que se cumple al terminar cada iteración

de un ciclo.

INVARIANTE DE

CLASE

Expresa propiedades globales inmutables de las instancias de

una clase.

LÓGICA DE HOARE

Permite probar la verdad o falsedad de propiedades de

programas imperativos (especialmente correctitud y

terminación).

CÁLCULO WP(S,Q) También conocido como cálculo de la precondición más

débil wp. Es un transformador de predicados que define la

transformación de un predicado de poscondición en un

predicado de precondición para cualquier fragmento de un

programa.

MODEL CHECKING Técnica que se utiliza para demostrar que se mantengan

algunas propiedades a través del tiempo de un modelo

formalmente especificado.

PRECONDICIÓN Sentencia lógica que hace referencia al estado inicial de la

parte de un código.

POSCONDICIÓN Sentencia lógica que hace referencia al estado final de la

parte de un código

Page 9: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 1 INTRODUCCIÓN

1

Capítulo 1) INTRODUCCIÓN

En este capítulo se da una breve introducción al tema de tesis, el objetivo, alcances y

limitaciones, entre otros. Además, se lista el contenido de los capítulos siguientes.

1.1. Introducción Una de las actuales corrientes de investigación está orientada hacia la búsqueda, clasificación

y recuperación de servicios y componentes de reuso utilizando la especificación de su

funcionalidad. Para esto, es necesario que cada componente de reuso y servicio tenga definida

explícitamente su funcionalidad.

Los elementos que constituyen la especificación formal de la funcionalidad son:

precondición, poscondición e invariantes.

La especificación formal de la funcionalidad de código fuente no es común en la

práctica debido a la complejidad que representa para el desarrollador. Documentar

correctamente código fuente ayuda a prevenir muchos errores. Desafortunadamente escribir y

mantener dicha documentación requiere una inversión considerable de tiempo y esfuerzo por

parte de los programadores. Sin embargo, esta inversión no es un desperdicio, por el

contrario, ayuda a encontrar defectos que tienen un costo elevado (dinero y esfuerzo).

Sería de gran beneficio una herramienta que analizara código fuente existente y

extrajera de manera automática sus especificaciones (precondiciones, poscondiciones e

invariantes). Sin embargo, se considera imposible el desarrollo de dicha herramienta debido a

razones teóricas y prácticas.

En esta tesis se analizó el problema de extraer especificaciones a partir de código

fuente. Se analizaron las técnicas y herramientas existentes que ayudan en el proceso y se

identificaron las etapas que deben seguirse para deducir precondiciones, poscondiciones e

Page 10: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 1 INTRODUCCIÓN

2

invariantes. A partir de este análisis se propuso un algoritmo de decisión y se formalizó

utilizando una herramienta de verificación de modelos.

1.2. Objetivo El objetivo de esta tesis es determinar la forma de extraer la especificación formal de

componentes de reuso o servicios a partir de código fuente. Para evitar que el desarrollador,

invierta una considerable cantidad de tiempo y esfuerzo, en la especificación manual de

precondiciones, poscondiciones e invariantes de clase.

1.3. Descripción del problema La especificación formal de la funcionalidad de código fuente no es común en la práctica.

Requiere una considerable inversión de tiempo y esfuerzo por parte del programador de la

aplicación. El tiempo y esfuerzo invertidos en la especificación formal de la funcionalidad no

representa una pérdida, por el contrario ayuda en la prevención y detección de errores en el

desarrollo de software.

Una herramienta que analizara y extrajera especificaciones formales a partir de código

fuente, sería de gran beneficio en la reducción de tiempo y esfuerzo requeridos para la

realización de esta tarea.

El problema radica en que no se conoce la forma de determinar las precondiciones,

poscondiciones e invariantes de clase de manera semiautomática o automática a partir de

código fuente. No se tiene conocimiento de la existencia de un procedimiento que sea

completamente automatizable, para que pueda ser implementado en una herramienta que

determine automáticamente especificaciones formales de código fuente.

1.4. Justificación Varios investigadores han observado en sus reportes que conocer formalmente las

precondiciones, poscondiciones e invariantes de operaciones relacionadas con diferentes

dominios ayudaría a resolver muchos de los problemas de recuperación y empate de

información, prueba de correctitud y eliminación de errores. Así mismo, la especificación

formal de la funcionalidad del código fuente es de gran ayuda en la prevención y detección de

errores en etapas tempranas del desarrollo de software. Además, asegura de manera formal la

correctitud del producto final. Sin embargo no se sabe si es factible ni se conoce la

complejidad a la que se enfrentaría un sistema que automatice la extracción de

especificaciones formales a partir de código fuente.

1.5. Beneficios Al analizar la viabilidad y los diferentes escenarios de éxito y fracaso, en un proceso de

extracción de la especificación formal de código fuente se podría plantear una técnica o

procedimiento para la automatización de esta tarea. Dicha técnica o procedimiento tendría

como posible aplicación la localización automática tanto de servicios como de componentes

de reuso. Además de ayudar en la detección de errores en etapas tempranas del desarrollo y el

aseguramiento de la calidad del software.

1.6. Alcances El alcance que presenta esta tesis, es un reporte de alternativas viables para la obtención de

especificaciones formales a partir de código fuente. Y la propuesta de un procedimiento, que

involucra técnicas encontradas en la literatura especializada.

Page 11: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 1 INTRODUCCIÓN

3

1.7. Limitaciones En esta sección se mencionan las limitaciones que presenta el procedimiento propuesto:

• Al ser un trabajo de análisis no hay desarrollo de una herramienta.

• El procedimiento planteado requiere de conocimiento especializado para su ejecución.

• Al no existir desarrollo de una herramienta, se realizaron pruebas de forma manual.

1.8. Organización de esta tesis. A continuación se describe un panorama de la organización de este documento de tesis.

Capítulo 2.

Se presentan los conceptos y temas relacionados con la presente investigación,

necesarios para dar el fundamento teórico para que el lector o las personas interesadas

en esta investigación se familiaricen con el tema.

Capítulo 3.

Se presentan los antecedentes de esta tesis. El antecedente es un trabajo de

investigación llevado a cabo en el CENIDET que está relacionado con esta

investigación. Se documenta el estado del arte y los trabajos relacionados a esta tesis.

Capítulo 4.

Se presenta un estudio detallado de las técnicas y herramientas que fueron analizadas y

que son la base para el desarrollo del algoritmo de decisión que se propone en este

trabajo. Además se describe el procedimiento para la formalización del algoritmo de

decisión propuesto.

Capítulo 5.

Se presentan cuatro casos de prueba que se realizaron para ilustrar el funcionamiento

del algoritmo propuesto.

Capítulo 6.

Se describen las conclusiones de la tesis, así como también las aportaciones y los

trabajos futuros.

Page 12: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

4

Capítulo 2) MARCO TÉORICO

En este capítulo se describen: la lógica de Hoare; el método de la precondición más débil

(wp); la generación de condiciones de verificación; verificación de modelos para validación;

entre otros. Estos elementos se encuentran fuertemente ligados con el desarrollo de este

trabajo y en conjunto forman el marco teórico que respalda esta tesis.

2.1. Aserción

Una aserción es una terna {P}S{Q}, donde S es un programa y P ,Q son formulas de la lógica

de predicados (conocidas como precondición y poscondición respectivamente). Una aserción

es verdadera si y sólo si, S inicia en el estado que satisface P y si la computación de S termina,

entonces la computación termina en el estado que satisface Q. Lo anterior se denota

⊢{P}S{Q}. Si se cumple que ⊢{P}S{Q} entonces la aserción es parcialmente correcta (i.e.

correctitud parcial) con respecto a P y Q. A esta notación también se le conoce como terna

de Hoare.

2.2. Código anotado Un código o comando anotado tiene aserciones embebidas (precondición, poscondición e

invariantes). Un comando esta anotado correctamente si las aserciones han sido insertadas en

los siguientes lugares:

Antes de cada comando Ci (donde i>1) en la secuencia C1;C2;…;Cn.

Después de la palabra do en el comando while y en el for en el caso de las invariantes

de ciclo.

De aquí en adelante las anotaciones en código fuente se representan con { }.

Page 13: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

5

Ejemplo: En el siguiente código las aserciones deben insertarse en los puntos 1 y 2 para

estar propiamente anotado. La precondición y la poscondición (indicadas entre { }) fueron

anotadas previamente por el programador.

Donde 1 la precondición del ciclo while se expresa como {Y=1 ∧ X=n} y 2 la invariante de

ciclo como {Y * X! = n!}.

2.3. Lógica de Hoare Fue desarrollada por C.A.R. Hoare [4] y permite probar la verdad o falsedad de propiedades

de programas imperativos (especialmente corrección y terminación). Está basada en la idea de

diagrama de flujo anotado.

Para demostrar las propiedades mencionadas anteriormente se utilizan axiomas y

reglas de inferencia como en cualquier sistema deductivo.

Se verifica cada comando c de un programa. Si el control de un programa ejecuta el

comando c con entrada a con P (precondición) verdadero, entonces el control debe dejar el

comando con salida b con Q verdadero (poscondición). Lo anterior se demuestra verdadero

mediante condiciones de verificación Vc(P;Q) con los antecedentes y consecuentes de c como

premisas. Los axiomas utilizados se mencionan a continuación:

Sean Vc(P;Q) y Vc(P0;Q0) entonces:

Axioma 1. Vc ( P ∧ P’; Q ∧ Q’)

El axioma 1 significa que si existen pruebas separadas de que un programa tiene ciertas

propiedades, es posible formar una conjunción de ambas. Ya que es bien sabido que en lógica

de predicados si se tienen dos premisas válidas, entonces su conjunción también lo es.

Axioma 2. Vc(P ∨ P’; Q ∨ Q’)

El axioma 2 es útil para combinar los resultados de un caso de análisis.

Axioma 3. Vc ( ( ∃x)( P ) ; ( ∃x ) ( Q ) )

El axioma 3 establece que si la variable x tiene la propiedad P antes de ejecutar el comando c,

entonces tiene la propiedad Q después de la ejecución.

Axioma 4. Si Vc ( P ; Q ) y R⊢P, Q⊢S, entonces Vc ( R ; S ).

El axioma 4 dice que si P es antecedente y Q consecuente del comando c, entonces también

son válidos cualquier antecedente más fuerte que P y cualquier consecuente más débil que Q.

Corolario 1. Si Vc ( P ; Q ) y ⊢( P ≡ R), ⊢( Q ≡ S ), entonces Vc ( R ; S ).

El primer paso para hacer un razonamiento válido acerca de un programa es conocer

las operaciones elementales que realiza (suma, multiplicación y división de enteros). La

aritmética de las computadoras no es la misma que la aritmética matemática y es necesario

poner cuidado en la selección de los axiomas apropiados.

Page 14: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

6

Una vez seleccionado alguno los axiomas presentados anteriormente, es posible

utilizarlo para la deducción de propiedades de programas. Estas propiedades se especifican

con sentencias en lógica de primer orden (aserciones). Las aserciones no atribuyen valores

particulares las variables de un programa, especifican propiedades generales sus posibles

valores y las relaciones entre ellos.

La validez de los resultados de un programa (o parte de él) depende de los valores que

tomen las variables antes de su ejecución. Ésta precondición inicial de éxito, es especificada

por el mismo tipo de aserciones usadas, para describir los resultados obtenidos al término del

programa.

Se utiliza la notación {P} Q {R} para establecer la conexión entre la precondición

(P), un programa (Q) y la descripción del resultado de la ejecución (R) [4]. La notación

anterior se interpreta como: "si la aserción P es verdadera al inicio del programa Q, entonces

la aserción R es verdadera al término de la ejecución del mismo". Si no existen

precondiciones impuestas, se escribe Q{R}. Los axiomas para el razonamiento sobre

programas se presentan a continuación.

2.4. Axiomas y reglas de la lógica de Hoare Partiendo de una especificación (una poscondición y posiblemente alguna precondición), se

demuestra que el programa cumple con la funcionalidad requerida. Generalmente, después de

analizar unas cuantas líneas de código, la prueba se hace demasiado compleja para

representarla en forma de inferencias encadenadas. Para resolver este problema se insertan en

el código anotaciones con la siguiente representación:

Dado S ≡ S1;S2;…Sn, si queremos demostrar {P0}S{Pn} es necesario probar:

{P0}S1{P1} {P1}S2{P2} . . . {Pn-1}Sn{Pn}

Y se usa la regla de composición n-1 veces.

Se representa la prueba de {P0}S{Pn} como:

{P0} anotación

S1

{P1} anotación

S2

. . .

{Pn-1} anotación

Sn

{Pn} anotación

Las fórmulas P1,…,Pn-1 son condiciones intermedias y cada paso: {Pi-1} Si {Pi} será

obtenido a través de alguna de las reglas de la lógica de Hoare.

Se obtiene una proposición mediante las reglas de inferencia pertenecientes a esta

lógica P’0 al principio de la secuencia de comandos. Ésta garantiza que al ejecutar S se

verifica la poscondición Pn. Se verifica si es posible obtener P’0 a partir de la precondición P0

mediante la regla de reforzamiento de la precondición.

Se demuestra que P0 ⊢ P’0 usando cualquiera de los sistemas deductivos de la lógica

de predicados. Este tipo de demostraciones suelen omitirse en la secuencia de anotaciones e

instrucciones.

Page 15: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

7

Los axiomas en la lógica de Hoare se especifican por esquemas, los cuales son

instanciados para obtener especificaciones particulares de correctitud parcial. Las reglas de

inferencia son especificadas con la siguiente notación:

La cual significa que la conclusión ⊢S es deducida de la hipótesis ⊢S1, … ,Sn.

Las hipótesis son los teoremas de la lógica de Hoare o una mezcla entre ellos y

diversos teoremas de otras ramas de las matemáticas.

2.4.1. Axioma de asignación Si el estado inicial es s, entonces el estado s’ después de la asignación es igual a s,

sustituyendo la variable V por el resultado de evaluar E. Si s’ satisface P (la hace cierta), la

fórmula que satisface s sería P[E/V], el resultado de sustituir todas las ocurrencias libres de V

en P por E.

Ejemplo: Sea a =b/2-1 un comando de asignación y sea { a < 10 } su poscondición. La

precondición es computada sustituyendo b /2 -1 en la aserción {a<10}, como sigue:

b/2 -1 < 10

b/2 < 10+1

b < 11 * 2

Por lo tanto b < 22

Así la precondición para la asignación dada es {b < 22}.

2.4.2. Regla de composición

Esta regla aplica la transitividad sobre dos comandos para poder obtener las

precondiciones y poscondiciones de la concatenación de ambas.

Ejemplo:

y = 3 * x +1

x = y + 3;

{x<10}

La precondición para el último comando de asignación es y<7, entonces la usamos

como poscondición para el primer estatuto.

3 * x +1<7

3*x<7-1

x<6/3

x<2

Page 16: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

8

2.4.3. Regla del comando if

Los bloques S1 y S2 tienen la misma poscondición Q. La conjunción de la fórmula P con la

condición B y respectivamente con su negación ¬B son sus precondiciones. P aparece como

precondición de la selección y Q como su poscondición.

Ejemplo:

if (x>0)

y = y – 1;

else

y = y +1;

Supongamos que Q={y>0} es la poscondición para este estatuto. Podemos usar el

axioma para la asignación en la cláusula then:

y = y – 1 {y > 0} esto produce {y-1>0} o {y>1}.

Ahora aplicamos el mismo axioma a la clausula else:

y = y +1 {y>0} esto produce la precondición {y + 1>0} o {y > -1}.

Ya que {y > 1}=>{y > -1}, la regla del if permite usar P={y>1} para la precondición del

estatuto de selección.

2.4.4. Reforzamiento de la precondición (implicación izquierda)

Esta regla asume una precondición más fuerte que la existente sin perder la corrección

de la demostración del programa anotado. Es decir, se puede tomar una precondición más

fuerte que la inicial para la prueba de propiedades de un programa. Se importan pruebas de la

lógica de predicados, concretamente R ⊢ P (lo cual significa que de R se deduce P).

2.4.5. Debilitamiento de la poscondición (implicación derecha)

Se asume una poscondición más débil que la existente de manera análoga a la regla anterior.

Es posible tomar una poscondición más débil que la inicial para la prueba de propiedades de

un programa. Las pruebas de la lógica de predicados que se importan son de la forma Q ⊢ R.

Page 17: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

9

2.4.6. Regla del comando while (correctitud parcial)

Con esta regla se calculan las precondiciones y poscondiciones de un ciclo while

satisfaciendo la propiedad de correctitud parcial. Si la conjunción de las condiciones P y B

es la precondición de S y P es su poscondición, quiere decir que cuando P y B son ciertas, P

sería cierta tras la ejecución de S. Si B es la condición del ciclo while se demuestra que siendo

cierta la precondición P, se cumple la pos condición P∧¬B, dado que P era poscondición de

la(s) instrucción(es) S del interior del ciclo y ¬B debe cumplirse para salir del while.

Para demostrar {R} while B do S {Q}, se debe encontrar una proposición P (invariante de

ciclo i.e. lo que no varía durante su ejecución) que verifique:

1. R ⊢ P para el reforzamiento de la precondición (i.e. P se deduce de R).

2. P∧¬B ⊢Q para el debilitamiento de la poscondición (i.e. de P∧¬B se deduce Q).

3. {P} while B do S { P∧¬B } (utilizando la regla del while de la lógica de Hoare).

Encontrar invariantes de ciclo requiere ingenio. Para esto, hay algunas reglas heurísticas:

1. Modificar la poscondición del while para hacerla dependiente del índice del ciclo (la

variable que crece o decrece en cada iteración).

2. Si la invariante P aún no verifica, se debe reforzar P para que se cumpla.

Existen diversos enfoques para inferir invariantes de ciclo de manera semiautomática o

automática como: el uso de ecuaciones diferenciales; análisis dinámico; análisis estático; y

ejecución simbólica.

2.4.7. Otras reglas de la lógica de Hoare

2.5. Cálculo de la precondición más débil (wp) Éste cálculo requiere de las siguientes definiciones:

Definición 2.1. Una fórmula A es más débil que la fórmula B si B → A. Dado un conjunto de fórmulas

{A1, A2,…}, Ai es la más débil en el conjunto si Aj → Ai para toda j.

Se puede reforzar una premisa y debilitar una consecuencia. Por ejemplo, si P → Q,

entonces (P ∧ R) → Q y P → (Q ∨ R).

Page 18: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

10

Definición 2.2. Dado el programa S y la fórmula Q, la precondición más débil de S y Q wp(S,Q) es la

fórmula más débil P tal que ⊢ {P}S{Q}.

Lema 2.1. ⊢ {P}S{Q} si y solo si ⊢ P → wp(S,Q) .

wp es un transformador de predicados que define la transformación de un predicado de

poscondición en un predicado de precondición para cualquier fragmento de un programa. El

razonamiento que utiliza wp es conocido como “hacia atrás” (backwards). Comienza a partir

de la especificación del resultado del programa y se calcula hacia atrás la derivación de cada

comando individualmente, hasta encontrar el predicado de precondición más débil. El cálculo

de la precondición más débil es la base para la derivación de programas a partir de su

especificación (refinamiento de programas) y su demostración formal.

2.5.1 Ejemplo del cálculo de la precondición más débil. Se tiene el siguiente programa que calcula la división de dos números:

Se observa que el programa tiene dos asignaciones y un ciclo while (éste con dos

asignaciones más). Se calculan las anotaciones de abajo hacia arriba (método de la

precondición más débil wp) con las reglas de asignación y composición. El primer paso es

calcular la invariante del ciclo, una fórmula P que verifique la regla del while parcial.

Utilizando una de las heurísticas propuestas se toma la poscondición del programa. Se

propone la invariante x=a*y+b ∧ b≤0.

Ahora se demuestra la invariante propuesta con la regla del while parcial. Se verifica

que se cumpla {P∧B}S{P} donde P es la invariante de ciclo. El primer paso es aplicar la regla

de asignación:

{x=(a+1)*y+b ∧ b≥0} a=a+1; {x=a*y+b ∧ b≥0}

Se aplica de nuevo la regla de asignación al siguiente comando:

{x=(a+1)*y+b-y ∧ b-y≥0} b=b-y;{ x=(a+1)*y+b ∧ b≥0}

Aplicando la regla de la composición:

{x = (a+1) ∗ y+b−y ∧ b−y ≥ 0} b = b−y; a = a+1; {x = a ∗ y+b ∧ b ≥ 0}

Haciendo reforzamiento de la precondición se demuestra {P ∧ B}S{P}:

P B

Page 19: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

11

(x =a∗y+b ∧ b ≥ 0 ∧ b ≥ y) → (x=(a+1)∗y+b−y ∧ b−y ≥ 0)

Aplicando técnicas algebraicas se llega a la conclusión de que es cierta la proposición

anterior. Se factoriza el valor de a y se despeja el valor de y en la primera sentencia. Se

deduce entonces que ambas proposiciones son equivalentes. Ahora se utiliza la regla del while

parcial para demostrar:

A partir de la invariante, se asciende en el programa aplicando la regla de asignación

dos veces:

{x=a∗y+x ∧ x ≥ 0} b = x; {x=a∗y+b ∧ b ≥ 0}

{x=x ∧ x ≥ 0} a = 0; {x=a∗y+x ∧ x ≥ 0}

Se aplica composición en las anteriores:

{x=x ∧ x ≥ 0} a = 0; b = x; {x=a∗y+b ∧ b ≥ 0}

Como la poscondición del ciclo coincide con la del programa, queda por demostrar la

precondición. Con la regla de reforzamiento se obtiene la siguiente condición de verificación:

(x ≥ 0 ∧ y ≥ 0) → (x=x ∧ x ≥ 0)}

Lo anterior se demuestra de la siguiente manera:

1. x ≥ 0 ∧ y ≥ 0 Premisa

2. x ≥ 0 Eliminación de ∧ de 1

3. x=x Axioma de números naturales

4. x=x ∧ x ≥ 0 Conjunción de 2 y 3

Se resume la demostración intercalando las anotaciones en el programa:

Page 20: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

12

2.6. Extensión de la lógica de Hoare para programación orientada a objetos Un programa orientado a objetos es una colección de clases. Cada clase define una colección

de objetos de cierto tipo y un conjunto de funciones (métodos). Además de la lógica de Hoare

son necesarias herramientas adicionales para asegurar que programas orientados a objetos

sean correctos. La primera de ellas es la metodología de Diseño por Contratos y la segunda es

el concepto de invariante de clase.

2.6.1. Diseño por Contratos El contrato humano involucra generalmente dos partes, cada una de ellas espera algún

beneficio y está preparada a cumplir algunas obligaciones en intercambio. Los beneficios y

las obligaciones (i.e. contrato) son plasmados en un documento. Éste protege ambas partes: al

cliente especificando cuánto puede obtener y al proveedor diciéndole qué tan poco es

aceptable. Se observa que lo que es una obligación para una parte, es un beneficio para la otra.

Esta noción de contrato se aplica en el desarrollo de software (ver figura 1). Si la

ejecución de una cierta tarea recae en una llamada de rutina que maneja sus sub-tareas, es

necesario especificar la relación entre el cliente (el que llama a la rutina) y el proveedor (la

rutina llamada) lo más preciso posible.

Los contratos de software se especifican con aserciones. El Diseño por contratos

contempla tres tipos de proposiciones:

Precondición

Poscondición

Invariante de clase

Figura 1. Especificación de un contrato de software.

Page 21: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

13

El principio de no-redundancia establece que “bajo ninguna circunstancia el cuerpo de

una rutina deberá checar el cumplimiento de la precondición”. La precondición compromete

al cliente ya que define las condiciones por las cuales una llamada a la rutina es válida. Las

poscondiciones comprometen a la clase (donde se implementa la rutina) ya que establecen las

obligaciones de esa rutina. Es muy importante notar que la precondición y la poscondición

que definen el contrato forman parte del elemento de software en sí. Es importante incluir las

precondiciones y poscondiciones como parte de la declaración de una rutina.

Figura 2. Ejemplo de la declaración de una rutina en el lenguaje de programación Eiffel con

precondiciones y poscondiciones explícitas.

La figura 2 es un ejemplo de la notación Eiffel [9]. Cada aserción es una lista de

expresiones booleanas, separadas por “;”. El “;” es equivalente al “and” booleano, permite

identificadores individuales de las aserciones. La precondición expresa requerimientos que

cualquier cliente debe cumplir para que la transacción sea correcta. La poscondición expresa

propiedades que son aseguradas al momento de ejecutarse el procedimiento.

Es posible establecer unas aserciones más fuertes que otras. ¿Qué significa que una

aserción es más fuerte que otra? Lo anterior se define de la siguiente manera: dadas dos

aserciones P y Q, P es más fuerte o igual que Q si P implica Q, como es establecido en [4]. El

concepto de fortificar o debilitar aserciones se utiliza en la herencia cuando es necesario

redefinir rutinas.

En Eiffel (y en general en las herramientas disponibles para otros lenguajes) el

lenguaje para soportar aserciones tiene algunas diferencias con el cálculo de predicados, en

primer lugar no tiene cuantificadores (aunque el concepto de agentes provee un mecanismo

para especificarlos). Además soporta llamadas a funciones y existe la palabra reservada old

para indicar el valor anterior a la ejecución de la rutina de algún elemento.

Las invariantes de clase sirven para expresar propiedades globales de las instancias de

una clase, mientras que las precondiciones y poscondiciones describen las propiedades de

rutinas o funciones particulares. Desde el punto de vista de la metáfora de los contratos, las

invariantes de clase establecen regulaciones generales aplicables a todos los contratos.

Provienen del concepto de invariante de datos de [4] y deben satisfacerse en dos momentos de

la ejecución: después de la creación del objeto y después de la llamada a una rutina por un

cliente.

Existe una serie de principios a seguir cuando se diseñan clases utilizando contratos de

software [12]:

1. Separar consultas de comandos. Las rutinas de una clase deben ser (en lo posible) o

comandos o consultas pero no ambas cosas. Las consultas devuelven un valor (ej.

funciones) y los comandos pueden cambiar el estado interno de un objeto.

Nombre de la rutina (declaraciones de los argumentos) is

--Comentarios de cabecera

require

Precondición

ensure

Pos condición

end

Page 22: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

14

2. Separar consultas básicas de consultas derivadas. La intención es conseguir un

conjunto de especificaciones formado por consultas que denominamos básicas, de

manera que el resto de las consultas puedan derivarse de las básicas.

3. Para cada consulta derivada escribir una poscondición especificando su resultado en

términos de una o más consultas básicas. Esto permite conocer el valor de las

consultas derivadas conociendo el valor de las consultas básicas. Idealmente, sólo el

conjunto mínimo de especificación tiene la obligación de ser exportado públicamente.

4. Para cada comando escribir una precondición que especifique el valor de cada

consulta básica. Dado que el resultado de todas las consultas puede visualizarse a

través de las consultas básicas, con este principio se garantiza el total conocimiento

de los efectos visibles de cada comando.

5. Para toda consulta o comando decidir una precondición adecuada. Este principio se

auto-explica ya que permite definir claramente el contrato de cada rutina.

6. Escribir el invariante para definir propiedades de los objetos.

Los contratos se categorizan en cuatro diferentes niveles [13]. Cada nivel de contrato

define otro contrato y la combinación de los cuatro define un contrato global. Los cuatro

diferentes niveles son los siguientes:

1. Contratos sintácticos, la firma de los tipos de datos.

2. Contratos de comportamiento, descripción semántica de los tipos de datos.

3. Contratos de sincronización, los que tratan con la concurrencia.

4. Contratos de calidad de servicio (QoS), todos los requerimientos no funcionales y

garantías.

El nivel sintáctico es provisto por el tipo de firmas de un servicio descrito por una

interface. Reglas comunes de subtipos permiten substitución. El contrato de comportamiento

se expresa en interfaces mediante aserciones. Los aspectos de sincronización de contratos aún

necesitan ser estudiados en el marco de componentes referentes a concurrencia. En QoS su

especificación se hace con QML (QoS Modeling Language) [14]. Existen tres mecanismos:

tipo de contrato, contrato propiamente dicho y su perfil.

Para combinar correctamente los cuatro niveles para definir un contrato global, se

debe determinar qué propiedades o características debe tener el contrato. Las tres mínimas

propiedades son: una especificación formal, una regla de conformidad (para permitir

substitución) y una técnica de monitoreo en tiempo de ejecución [15].

En [16] los autores definen un lenguaje de especificación de contratos. Contract

Definition Language (CDL) permite la descripción de atributos de un contrato (ver figura 3).

Se identifican diferentes tipos de contratos: contrato base que denota información básica

acerca del componente; contrato de método que describe los métodos del componente o

servicio; y contrato de evento que se usa para exponer eventos a los cuales otros componentes

pueden suscribirse.

Dentro del apartado method se especifica la información acerca de parámetros,

invocaciones, precondiciones, poscondiciones, invariantes de clase, eventos, clasificación de

aserciones y localización de los métodos. Para cada parámetro es posible definir: nombre,

tipo, restricción e inicialización. Las precondiciones, poscondiciones e invariantes de clase

comparten la misma estructura de especificación (ver figura 4).

Page 23: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

15

Figura 3. Atributos de un contracto en CDL [16].

En [16] los contratos se modelan como máquinas abstractas con el objetivo de

especificarlos formalmente y asegurar que sean correctos. Una máquina abstracta se

caracteriza por tener elementos dinámicos y estáticos. Los elementos estáticos son la

definición del estado y los dinámicos corresponden a las operaciones.

En el mapeo de CDL a máquinas abstractas los parámetros y las propiedades no

funcionales se convierten en variables de estado y los métodos corresponden a funciones de

estado. Las precondiciones, poscondiciones e invariantes se mapean directamente de CDL.

Para cada variable de estado se define su dominio.

Page 24: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

16

Figura 4. Estructura de especificación de un contrato [16].

2.6.2. Invariante de clase Asegura que todos los objetos mantengan su integridad durante su ciclo de vida, sin importar

qué método se les aplique. Es una expresión booleana que especifica las condiciones bajo las

cuales un objeto se mantiene “bien definido”. Describe el estado interno del objeto usando las

variables públicas y las variables privadas de instancia de la clase. Una expresión INV es una

invariante correcta para la clase C si cumple con las siguientes condiciones:

Cada llamada a un constructor C con argumentos que satisfacen su precondición, crea

un nuevo objeto con un estado que satisface INV.

Cada llamada a un método M con argumentos que satisfacen su precondición, deja el

objeto en un estado que satisface INV.

Lo anterior quiere decir que la invariante de clase debe ser verdadera cuando el objeto es

creado por un constructor y debe permanecer así después de la llamada a cualquier método.

2.7. Correctitud en programación orientada a objetos Utilizando la notación de terna de Hoare, se define formalmente “clase correcta” como: sea R

la invariante de clase, Pi la precondición y Qi la poscondición para el i-ésimo constructor Ci o

método Mi, entonces una clase es correcta respecto a sus aserciones si se tiene:

1. Para el conjunto de argumentos válidos x para cada constructor Ci, se cumple {Pi(x)}

Ci.body {Qi(x) ∧ INV}.

Page 25: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

17

2. Para el conjunto de argumentos válidos x para cada método Mi, se cumple {Pi(x) ∧

INV }Mi.body{Qi(x) ∧ INV}.

La regla 1 significa que la ejecución del constructor para cualquier objeto establece la

validez de la invariante de clase. La regla 2 dice que la ejecución de métodos en donde la

invariante de clase es válida, ésta se preserva así durante el transcurso y término de los

métodos.

2.8. Mecanización de la verificación de programas Se han hecho varios intentos para automatizar la prueba de correctitud de programas mediante

sistemas automáticos que generan demostraciones formales en la lógica de Floyd-Hoare.

Desafortunadamente se ha demostrado que, en principio, es imposible diseñar un

procedimiento para decidir automáticamente la verdad o falsedad de una sentencia

matemática arbitraria.

Sin embargo, es posible probar la validez de una demostración formal. Esto consiste

en checar que los resultados de cada línea de la demostración son axiomas o consecuencias de

líneas anteriores. Las demostraciones de correctitud de programas son largas y tediosas por lo

que es común cometer errores cuando son generadas manualmente. Razón por la cual es útil

comprobar de manera automática su validez, aún cuando sólo puedan ser generadas mediante

el análisis humano.

Un sistema de verificación de demostraciones toma como entrada una especificación

de correctitud parcial de un programa. De las especificaciones anotadas en el programa el

sistema genera un conjunto de sentencias puramente matemáticas llamadas condiciones de

verificación (vc’s verification conditions). Las vc’s se verifican automáticamente en un

demostrador de teoremas.

Para demostrar que una terna de Hoare {P}S{Q} es correcta, se siguen tres pasos:

1. El programa S se anota (se insertan anotaciones). Esta parte es difícil y necesita buen

entendimiento del programa.

2. Se genera un conjunto de sentencias lógicas (vc’s) a partir de la especificaciones

anotadas. Este proceso es puramente mecánico y puede ser fácilmente realizado por un

demostrador de teoremas.

3. Las condiciones de verificación son probadas.

Si se demuestra {P}S{Q} entonces la terna ⊢{P}S{Q} es válida. El programa S es

correcto parcialmente y cumple con sus especificaciones.

A continuación se mostrará un ejemplo sencillo de cómo se generan las condiciones de

verificación manualmente.

Supóngase que se quiere demostrar el siguiente programa:

El primer paso es insertar las anotaciones, se tiene:

Page 26: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

18

El siguiente paso es generar las condiciones de verificación:

Las condiciones de verificación se generan al aplicar las reglas y axiomas de la lógica

de Hoare. Nótese que son sentencias aritméticas. El último paso consiste en probarlas en un

demostrador de teoremas, lo cual es una tarea relativamente sencilla.

2.9. Verificación de modelos Otro enfoque de razonamiento formal de programas es la verificación de modelos (model

checking). Esta técnica se utiliza para demostrar que se mantengan algunas propiedades a

través del tiempo de un modelo formalmente especificado. El modelo puede ser alguno de los

siguientes:

Del programa (cada comando es un estado).

Una abstracción del programa.

Un modelo de la especificación.

Un modelo del dominio.

Verificación de modelos está basada en la lógica temporal lineal (LTL). Busca una

solución entre todos los posibles estados del modelo dado. Da respuesta a algunas preguntas

acerca de propiedades temporales de un programa.

Se construye el modelo A de un problema o sistema. L(A) denota todos los posibles

comportamientos y L(P) el conjunto de comportamientos válidos (i.e. aquellos donde la

propiedad P es satisfactible). Para demostrar que el modelo A siempre satisface P, es

suficiente mostrar que L(A) ⊆ L(P) o equivalentemente L(A) ∩ L(P) = ∅. Si la intersección

anterior es no vacía, se busca un contraejemplo al comportamiento válido.

2.9.1 Verificación de modelos temporal En la mayoría de los sistemas, la verdad de algunas fórmulas es dinámica, es decir, cambia

con el tiempo.

Existen dos tipos de lógica temporal:

Árbol lógico de computación (computation tree logic CTL). El tiempo se representa

como un árbol que tiene raíz en el momento actual y se extiende hacia el futuro.

Page 27: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

19

Lógica temporal lineal (linear-time temporal logic LTL). El tiempo es un conjunto de

trayectorias. Una trayectoria es una secuencia de instantes en el tiempo. La lógica

temporal lineal se relaciona con la teoría de autómatas finitos, la cual se utiliza para

modelar sistemas.

2.9.2 Lógica temporal lineal (LTL) La lógica temporal (LT) es una extensión de la lógica clásica y es usada para describir un

sistema de reglas y simbolismo, para la representación y razonamiento de proposiciones

cuantificadas en términos de tiempo. La lógica temporal lineal (LTL) modela el tiempo como

una secuencia de estados, que se extienden infinitamente. Esta secuencia de estados se conoce

como camino. En general el futuro no está determinado, por lo que se consideran diferentes

caminos que representan posibles futuros. LTL contiene a los siguientes operadores:

Siguiente

Eventualmente

Siempre

Fuerte hasta

Débil hasta

2.9.3. Sintaxis de LTL Una fórmula bien formada (wff) en LTL se define recursivamente como:

⊤ y ⊥ son fórmulas bien formadas.

Si p es un símbolo proposicional que representa una propiedad falsa o verdadera en

cualquier estado del modelo A, p es una fórmula bien formada.

Si p y q son fórmulas bien formadas entonces también:

Son todas.

2.9.4. Semántica de LTL El símbolo ⊨ expresa que una secuencia de estados satisface una fórmula bien formada en

LTL. Lo anterior significa que:

σ[i] ⊨ p ⇔ la propiedad p permanece en la secuencia de estados { σi, σi+1,…}.

σ ⊨ f ⇔ la fórmula temporal f permanece en la secuencia de estados σ.

Con esta notación se definen formalmente los operadores temporales de LTL a

continuación.

Siempre: σ ⊨ □p ⇔ ∀i ≥ 0. (σ[i] ⊨ p)

□p establece que la propiedad p permanece invariantemente verdadera a través de una

secuencia de estados.

Eventualmente: σ[i] ⊨ ◊p ⇔ ∃i ≥ 0. (σ[i] ⊨ p)

Page 28: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 2 MARCO TEÓRICO

20

◊p establece la garantía de que la propiedad p eventualmente se convertirá verdadera al menos

una vez.

Siguiente: σ[i] ⊨ p ⇔ σ[i+1] ⊨ p

p establece que la propiedad p es verdadera en el estado siguiente.

Page 29: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 3 ANTECEDENTES

21

Capítulo 3) ANTECEDENTES

En el presente capítulo se describe el antecedente de este trabajo de tesis, un estudio de la

evolución de las especificaciones formales y el análisis de algunos trabajos relacionados.

3.1. Antecedentes La tesis que se presenta en este documento tiene como antecedente un trabajo de

investigación desarrollado por un estudiante del grupo de ingeniería de software del

CENIDET.

A continuación se describe la tesis de maestría que es el antecedente de este trabajo.

3.1.1. Identificación de funciones recurrentes en software legado [1]

El objetivo general de [1] fue definir un mecanismo de identificación de funciones

implementadas una y otra vez en diferentes aplicaciones aún con diferente comportamiento.

Dada su naturaleza recurrente significa que estas funciones representan componentes

reusables, por lo tanto podrían ser consideradas para incorporarlas a un marco de

componentes reusables para evitar su duplicación. Este tipo de funciones se identificaron en

software legado a partir del análisis de precondiciones y poscondiciones. Las precondiciones

y poscondiciones de funciones se convirtieron a predicados lógicos de primer orden y se

evaluaron con un demostrador automático de teoremas.

Los resultados obtenidos al evaluar el desempeño de la herramienta desarrollada en

este trabajo confirmaron que es posible implementar el análisis de precondiciones y

poscondiciones para determinar la recurrencia de funciones. Además de que existe una fuerte

Page 30: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 3 ANTECEDENTES

22

dependencia entre la especificación formal correcta de las precondiciones y poscondciones y

el nivel de desempeño de la herramienta propuesta en [1].

3.2. Estado del arte A mediados de los años 60’s del siglo pasado apareció el primer trabajo sobre especificación

formal. Debido a que la complejidad de los sistemas se hizo creciente, surgió la necesidad de

tener un estándar riguroso para definir formalmente el significado de programas. Esto con el

objetivo de realizar pruebas sobre ciertas propiedades, particularmente de la forma: si los

valores iniciales de las variables de un programa satisfacen la relación R1, entonces los

valores finales una vez terminada su ejecución, satisfacen a la relación R2 [2].

Para demostrar las propiedades mencionadas anteriormente, se hizo evidente la

necesidad de contar con un sistema lógico formal. Es cuando se desarrolló la lógica de Hoare

[4] para hacer razonamiento válido acerca de programas. En esta lógica se utilizan axiomas y

reglas de inferencia como en cualquier sistema deductivo.

Es en este punto donde surge un nuevo problema: encontrar aserciones adecuadas a

cada estado del programa que se está analizando. Se han propuesto diversos métodos para

generar aserciones semiautomática o automáticamente [3]. Un ejemplo de las técnicas usadas

es el uso de ecuaciones diferenciales finitas para caracterizar la acción de las variables al

ejecutar el control de un ciclo. En algunos casos significativos, las ecuaciones diferenciales

pueden ser resueltas ya sea de manera manual o con la ayuda de un sistema deductivo

mecánico. Las aserciones también se pueden determinar con técnicas heurísticas.

Poco tiempo después, surge una estrategia para la deducción de precondiciones basada

en la lógica de Hoare conocida como el método de la precondición más débil (weakest

precondition) wp(S,R) [5]. El transformador de predicados wp(S,R) es una función de mapeo

entre una lista de comandos S y un predicado R (poscondición). Esta función da como

resultado la precondición más débil para S (S termina) con R verdadero. Análogamente existe

otro transformador de predicados de poscondición más fuerte (strongest postcondition) [6].

Establece que “dada una precondición Q y una secuencia de comandos S, sp deriva un

predicado sp(S,Q)”.

Al mismo tiempo que se exploran técnicas para la generación de invariantes aparece

Ejecución simbólica [7]. Es otra herramienta para deducción de propiedades de programas. En

vez de entradas triviales para un programa (por ejemplo números), se utilizan símbolos que

representan valores arbitrarios. La ejecución procede de manera normal, excepto que los

valores pueden ser fórmulas simbólicas. El resultado obtenido es un árbol de ejecución que

caracteriza los caminos tomados durante la ejecución simbólica de un programa. En el árbol

que se obtiene, se asocia un nodo con cada comando ejecutado etiquetado con el número en la

secuencia de la ejecución que le corresponde. Cada transición entre comandos es un arco

dirigido que conecta los nodos asociados. Este método tiene como ventaja que puede

representar una clase infinita (en la mayoría de los casos) de ejecuciones normales.

Conforme la programación orientada a objetos ganó terreno en el mundo de

desarrollo de software, sus usuarios vieron la necesidad de contar con una metodología

estructurada con el objetivo de desarrollar software de una manera confiable, robusta y con

correctitud. Ésta metodología es conocida como Diseño por contratos [8].

Se vuelve evidente la necesidad de contar con lenguajes de especificación formal que

puedan ser embebidos en el código fuente para su verificación. Eiffel [9] es uno de los pocos

lenguajes que ofrece soporte nativo para aserciones, sin embargo otros lenguajes como Java

[10] y C# [11] cuentan con extensiones para la especificación formal.

El problema central que prevalece desde los inicios de la especificación formal, es la

deducción de invariantes de ciclo. La generación de invariantes de ciclo ha sido estudiada

desde los inicios de la especificación formal y se han propuesto diversos métodos para su

generación semiautomática o automáticamente. Un ejemplo, es el uso de ecuaciones

Page 31: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 3 ANTECEDENTES

23

diferenciales finitas para caracterizar la acción de las variables al ejecutar el control de un

ciclo. Otro ejemplo es la generación automática de invariantes de ciclo usando la técnica de

suma simbólica (symbolic summation) combinada con álgebra polinomial [23].

Actualmente se encuentran en desarrollo técnicas y herramientas que tienen como

propósito la extracción de especificaciones formales a partir de código fuente, ejemplo de ello

son las herramientas Discern [20] y Daikon [21].

3.3. Trabajos relacionados Mantener y especificar contratos es una tarea costosa en cuanto a tiempo y esfuerzo para los

desarrolladores. La gran mayoría del código fuente no cuenta con la especificación de su

contrato. Por lo que surge el problema de extraer contratos después del ciclo de desarrollo de

software. Algunos trabajos se enfocan al análisis del código fuente (análisis estático)

mientras que otros analizan los resultados obtenidos después de una serie de ejecuciones

experimentales (análisis dinámico). Finalmente, otros combinan ambos enfoques para obtener

mejores resultados.

3.3.1. Análisis de código fuente Es posible extraer algunas especificaciones por simple inspección de código fuente. Aunque

no se deducen todos los elementos que constituyen un contrato siguiendo esta técnica, la

información recabada es valiosa. A partir de ella se pueden deducir las aserciones faltantes. A

continuación se describen dos trabajos que utilizan esta técnica.

3.3.1.1. Extended Static Checking for Java ESC/Java [17] En este trabajo se presentó la herramienta ESC/Java que permite encontrar errores comunes

en programas. ESC/Java tiene un lenguaje de anotación semejante a Java para expresar

formalmente el diseño. ESC/Java examina el código anotado y genera condiciones de

verificación utilizando técnicas de demostración automática de teoremas. La salida de esta

herramienta es un conjunto de advertencias de inconsistencias entre diseño y código.

Los programadores que probaron ESC/Java reportaron que la herramienta aun no ha

alcanzado el nivel deseado de efectividad. En particular, da una excesiva cantidad de

advertencias sin fundamento en códigos sin anotación o parcialmente anotados. Lo anterior

significa que los programadores deben proveer anotaciones iníciales y no es una herramienta

completamente automática.

3.3.1.2. Houdini, An annotation assistant for ESC/Java [18] El objetivo de este trabajo es ayudar a reducir el costo de escribir especificaciones formales

para los programadores. La herramienta Houdini es un asistente para la anotación de código

fuente. Mediante un exhaustivo análisis al código fuente, genera un gran número de

candidatos a anotaciones y utiliza ESC/Java para verificar o refutar, cada uno de los

candidatos a anotación.

Para el uso de esta herramienta se recomienda como estrategia mantener el código

base con unas cuantas anotaciones insertadas manualmente. Una limitante de Houdini es que

no puede inferir todas las anotaciones relevantes. Además, el programador debe proveer

anotaciones iníciales.

3.3.2. Análisis de documentación El primer lugar donde se comienza a buscar información acerca de especificaciones formales

es la documentación. Es posible inferir algunas aserciones si se cuenta con una

documentación adecuada del código fuente. Algunas etapas de este tipo de análisis pueden ser

automatizadas. A continuación se presenta un trabajo que describe el análisis de

Page 32: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 3 ANTECEDENTES

24

documentación en lenguajes orientados a objetos, así como la experiencia obtenida en su

estudio y las lecciones aprendidas.

3.3.2.1. Uncovering hidden contracts: The .NET example [19] En [19] se analizó la colección de librerías de estructuras de datos y algoritmos en la .NET

Framework Class Library. Fue posible extraer automáticamente algunos elementos que

intervienen en su especificación formal. También se detectaron algunos patrones en el código

y la documentación que apuntan a posibles contratos.

La justificación que los autores dieron de porqué utilizaron librerías de componentes

de reuso en .NET es porque están desarrolladas bajo el concepto de Metadata. Éste brinda

información substancial acerca de su especificación y provee documentación que hace auto

descriptivo cada componente al estilo de diseño por contratos.

Después de realizar pruebas con la clase ArrayList que pertenece a esta librería

encontraron como resultado que las poscondiciones de las rutinas se encuentran expresadas en

.NET pero no de manera explícita. Es necesario el análisis humano en el análisis ya que estas

poscondiciones se encuentran dispersas en toda la documentación. Las precondiciones tienen

posibilidad de inferirse automáticamente a partir de excepciones. Las clases que implementan

interfaces comparten un conjunto de propiedades que podrían ser candidatas a invariantes de

interface, pero el análisis a la documentación no reveló dichas propiedades.

3.3.3. Análisis dinámico El análisis dinámico consiste en realizar una serie de ejecuciones experimentales de un

programa con el propósito de inferir algunas propiedades. A continuación se presenta un

trabajo que utiliza este tipo de análisis para extraer especificaciones formales a partir de

código fuente.

3.3.3.1. The Daikon system for dynamic detection of likely invariants [21] En este trabajo se describe la herramienta Daikon. Esta herramienta es la implementación de

detección dinámica de invariantes (dynamic detection of likely invariants). La salida de

Daikon es un conjunto de posibles invariantes que son justificadas estadísticamente por

rastreo de ejecuciones. Primero descubre invariantes de ejecuciones mediante la

instrumentación del programa objetivo para el rastreo de ciertas variables. Después se ejecuta

el programa instrumentado y se infieren las invariantes de ambos programas (instrumentado y

no instrumentado).

La tarea básica del instrumentador es agregar instrucciones al programa objetivo para

rastrear los valores de salida de las variables. El resultado es enviado directamente a la

máquina de inferencias (inference engine) para la inferencia de las invariantes. La máquina

de inferencias lee los datos producidos por el instrumentador y produce posibles invariantes.

Utiliza un algoritmo genera y prueba (generate and check) para probar un conjunto de

potenciales invariantes contra los valores de el rastreo de las variables. La precisión de los

resultados depende en gran parte de la calidad de los casos de prueba lo cual es una

importante limitante.

3.3.4. Análisis de documentación y análisis dinámico Los trabajos que se describen a continuación combinan ambas técnicas para obtener mejores

resultados en la extracción de especificaciones formales a partir de código fuente.

Page 33: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 3 ANTECEDENTES

25

3.3.4.1. Extracting functional and Non Functional Contracts from Java Classes and Enterprise Java Beans [16] El trabajo presentado en [19] motivó a otros para analizar software escrito en otros lenguajes

de programación para encontrar su especificación formal.

El objetivo de este trabajo es analizar código fuente escrito en Java y su

documentación. Primero, se analiza la documentación para encontrar candidatos a

especificaciones. Después, se construye un modelo de ejecución del programa (i.e. posibles

valores de las variables) como máquina abstracta. Finalmente, se monitorean los cambios y

se infiere el contrato.

Para encontrar una lista inicial de candidatos a anotación se analiza la documentación.

Las precondiciones se extraen de las condiciones de los métodos que checan los parámetros

de entrada y excepciones. En un lenguaje que soporta manejo de excepciones las

precondiciones generalmente van ligadas a ellas. Las poscondiciones se encuentran en la

documentación y los comandos return de cada método. Se usan comentarios en Javadoc para

extraer información de contratos de clases de Java. Las etiquetas @throws y @exception son

de ayuda para extraer precondiciones. De @param se extraen firmas de métodos y candidatos

a precondiciones. Si un constructor está comentado con alguna de estas etiquetas, se utiliza

esta información para inferir invariantes. La etiqueta @return es punto de partida para formar

candidatos a poscondiciones.

Después de completar el paso anterior se refinan los candidatos con análisis estático.

Mediante una inspección al código fuente se prueba que las aserciones identificadas son

correctas. Para lo anterior, se asume la negación de las aserciones y se prueba que no es

posible o no se cubren todos los posibles estados modelados con el candidato asumido. En

este paso se podrían identificar candidatos adicionales y se construye un contrato temporal.

El siguiente paso es evaluar los candidatos usando una función de aptitud, si no son

válidos se ejecuta análisis dinámico con otro conjunto de pruebas. Una vez que los candidatos

han sido evaluados y validados se proponen como contrato final.

3.3.4.2. Discern: Towards the automatic discovery of software contracts [20] En este trabajo se propone un método para deducir semiautomáticamente especificaciones

formales y se encuentra aún en fase de implementación.

Recurre al análisis estático para descubrir posibles contratos. Una de las dificultades

en este tipo de análisis son los ciclos, pero se utilizan heurísticas para reconocer sus formas

más comunes y extraer información útil de ellos.

La herramienta Discern utiliza un conjunto de especificaciones de librerías integradas

en los lenguajes de programación más usados.

Además, utiliza los métodos formales de precondición más débil y poscondición más

fuerte en la deducción de especificaciones formales. Internamente representa el programa con

un lenguaje de especificación y utiliza demostradores automáticos de teoremas para la

validación de las condiciones de verificación.

La limitante de este trabajo es que el análisis requiere de ayuda humana ya que

Discern deriva posibles contratos con la adición manual de una poscondición y el etiquetado

de tres aserciones como invariantes.

Page 34: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

26

Capítulo 4) ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

Para extraer especificaciones formales (contratos) a partir de código fuente orientado a

objetos, es necesario el análisis humano. Algunas partes de la tarea son relativamente fáciles

pero otras resultan ser complejas, como el caso de la deducción de invariantes de ciclo.

Además, se requiere amplio conocimiento del problema y su dominio, lo cual es otra limitante

ya que por lo general, quien analiza el código fuente para deducir especificaciones formales

no lo diseñó originalmente. Es posible automatizar algunas partes del proceso, pero depende

del lenguaje en el que se encuentre implementado el código fuente.

En este capítulo se presenta el análisis realizado a las diferentes propuestas en los

trabajos relacionados. Se identificaron las técnicas y herramientas identificadas que pueden

ayudar en el proceso de extraer especificaciones formales a partir de código fuente, las cuales

se enlistan a continuación:

1. Análisis de documentación

2. Análisis de código fuente

3. Lógica de Hoare y cálculo de la precondición más débil wp

4. Análisis dinámico

5. Generación automática de invariantes.

A partir de este análisis se construyó un algoritmo de decisión para realizar la

extracción de especificaciones formales. Además, este algoritmo se demostró formalmente

con la técnica automática de verificación de modelos.

Page 35: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

27

4.1. Técnicas y herramientas que pueden ayudar en la extracción de especificaciones formales

4.1.1. Análisis de documentación Javadoc [22] es un generador automático de documentación de código fuente escrito en Java.

La documentación generada está en formato HTML. Javadoc no tiene semántica formal y no

puede ser verificado formalmente. Se utilizan los caracteres /** . . . */ para indicar al

compilador que es una anotación en Javadoc. Cuenta con palabras reservadas como @param

y @return.

Si se analiza la documentación generada con Javadoc, es posible extraer información

implícita de especificaciones formales de clases. No es posible desarrollar una herramienta

formal para analizar automáticamente la documentación, por lo que es imprescindible la

intervención humana.

Las precondiciones se extraen de las anotaciones fe las condiciones que verifican los

parámetros de entrada y en los comandos que provocan la ejecución de una excepción. Las

precondiciones generalmente van ligadas a las excepciones. Las precondiciones se deducen

del comando try...catch (es posible automatizar este paso). Las poscondiciones se buscan en

las anotaciones del comando return de cada método.

A continuación se enlistan las etiquetas de Javadoc de las cuales se extrae información

relevante

1. @throws y @exception. Contienen información acerca de precondiciones, se identifican

excepciones que se encuentran en métodos.

2. @param. Se refiere a firmas de métodos y precondiciones.

3. @return. Provee información acerca de poscondiciones.

4. @see. Con la información extraída de esta etiqueta, se analiza la herencia y dependencia

en el comportamiento de una clase, para verificar si existen conflictos entre las

precondiciones, poscondiciones e invariantes identificadas.

Si un constructor esta comentado con cualquiera de las etiquetas: @throws,

@exception y @param, es posible que contenga información referente a la invariante de

clase.

En [19] se describe la extracción de información referente a especificaciones formales

de programas escritos en .NET.

4.1.2. Análisis de código fuente Cuando la documentación no existe o no es suficientemente explícita, se analiza el código

fuente para encontrar elementos que pudieran estar implícitos en él. Posibles fuentes de

invariantes son: constructores; interfaces implementadas y clases base.

Las precondiciones usualmente están ligadas a las excepciones (si el lenguaje lo

soporta). Se consideran como precondiciones las condiciones en un método en las cuales no

se lanzan excepciones.

Las poscondiciones se encuentran implícitas en el comando return. En este paso es

imprescindible el análisis humano ya que es necesario conocer el dominio del problema para

poder inferir poscondiciones a partir del comando return.

4.1.3. Lógica de Hoare y cálculo de la precondición más débil wp No es posible automatizar algunas etapas del cálculo de la precondición más débil wp y la

lógica de Hoare debido a razones prácticas y teóricas. Debido a que el usuario debe proveer

Page 36: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

28

algunas aserciones como invariantes de ciclo y poscondiciones. Sin embargo, la generación de

condiciones de verificación y su prueba en un demostrador de teoremas si es automatizable.

4.1.4. Herramientas disponibles en Internet A continuación se presentan dos herramientas de código libre que se encuentran disponibles

en Internet y que pueden ser de ayuda en la extracción de especificaciones formales a partir de

código fuente:

4.1.4.1. Análisis dinámico La herramienta Daikon [21] es una implementación de análisis dinámico. Detecta en tiempo

de ejecución, posibles precondiciones, poscondiciones e invariantes de código fuente escrito

en los lenguajes de programación: C; C++; Java; Perl; entre otros. La salida de Daikon es un

conjunto de posibles especificaciones formales que son justificadas estadísticamente por

rastreo de ejecuciones. La figura 5 es un ejemplo de salida que contiene posibles

especificaciones para una implementación de la estructura de datos pila.

Figura 5. Ejemplo de salida de Daikon.

Las aserciones encontradas en la figura 5 son candidatas a la especificación formal de

un contrato. Es necesario demostrar su validez formalmente. La manera de verificar

manualmente la validez de aserciones es mediante la lógica de Hoare y el cálculo de la

precondición más débil wp.

4.1.4.2. Generación automática de invariantes ALIGATOR [23] es un paquete de la herramienta Mathematica [24], su función es razonar

algebraicamente sobre una amplia clase de ciclos imperativos llamados P-solvable (tiene

solución). Estos ciclos contienen asignaciones, secuencias y condicionales.

ALIGATOR combina symbolic summation (suma simbólica) y álgebra polinomial. Su

salida es un conjunto finito de invariantes que es base para formar el ideal de invariantes.

Contiene rutinas para:

Verificar que un ciclo sea P-solvable.

Transformar ciclos en un sistema de ecuaciones recurrentes.

Resolver recurrencias y derivar formas cerradas de invariantes de ciclo.

Page 37: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

29

Computar el ideal de invariantes.

Esta herramienta es útil en la búsqueda de invariantes de ciclo, cuando las heurísticas

comúnmente utilizadas son ineficaces.

4.2. Etapas del problema de la extracción de especificaciones formales De acuerdo al análisis realizado a los trabajos relacionados con este problema, existen tres

tareas en la extracción de precondiciones, poscondiciones e invariantes de código fuente.

Éstas son independientes del lenguaje en el que esté implementado el código fuente. En la

figura 6 se mencionan las tareas

Figura 6. Tareas de la extracción de especificaciones formales.

Primero se analiza la documentación y código fuente en busca de precondiciones,

poscondiciones e invariantes.

Para la deducción de especificaciones formales es necesario contar con poscondiciones

para cada método e invariantes de ciclo, las cuales deben anotarse en el código fuente.

Después se deducen las especificaciones formales. La deducción de precondiciones y

poscondiciones mediante wp se realiza manualmente a “lápiz y papel” como las

demostraciones matemáticas tradicionales. Este cálculo es complejo y tedioso, además de que

requiere amplio conocimiento del dominio del problema.

Al finalizar la generación de especificaciones ya sea de forma manual o automática, se

debe anotar el programa manualmente en algún lenguaje de especificación.

Por último se verifica que las especificaciones formales generadas sean correctas. A partir

del código anotado se generan las condiciones de verificación para ser demostradas. Al

término de este proceso se anotan las especificaciones correctas.

En la tabla 1 se presenta un resumen del análisis de las técnicas y herramientas que

ayudan en el proceso de deducir especificaciones formales, contrastando su capacidad de

automatización. Además se especifica el producto esperado de cada uno de ellos:

Deducir

especificaciones

formales

Analizar

documentación y

código fuente

Verificar correctitud de

especificaciones formales

deducidas

Page 38: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

30

Tabla 1. Capacidad de automatización de técnicas y herramientas.

Técnica o herramienta Automatizable Producto

Análisis de documentación Parcialmente Pre/Pos/Inv clase

Análisis de código fuente Parcialmente Pre/Pos/Inv clase

Lógica de Hoare y wp Parcialmente Pre

Análisis dinámico Si Pre/Pos

Generación de invariantes Parcialmente Inv ciclo

Verificación de correctitud de

contratos extraídos

Si Contrato correcto

Como conclusión del análisis anterior se tiene que no es posible la construcción de un

sistema completamente automatizado a partir de las técnicas y herramientas existentes.

Debido a esto fue necesario deducir un algoritmo que ayude en la elección de técnicas o

herramientas a utilizar para la extracción de especificaciones formales a partir de código

fuente.

4.3. Solución propuesta La figura 7 muestra el algoritmo de decisión propuesto. El algoritmo incluye las herramientas

y técnicas listadas anteriormente. Los pasos del algoritmo están enumerados para efectos

ilustrativos, pero no son necesariamente ejecutados todos o secuencialmente. La entrada del

algoritmo es un código fuente S (puede ser un programa completo o una fracción de él) que ha

sido previamente compilado y su documentación D, en caso de que exista. La salida es el

contrato {P}Q{S} extraído de la documentación o deducido del código fuente, si el algoritmo

tiene éxito. Después de la extracción del contrato, éste se valida generando las condiciones de

verificación correspondientes.

Paso 1. Evaluación de la documentación.

El lugar más lógico para comenzar la búsqueda de especificaciones formales es la

documentación. Esta tarea requiere de esfuerzo humano y de decisiones subjetivas, ya que se

evalúa la existencia y la calidad de la documentación. La mayoría del tiempo el código

fuente no se encuentra debidamente documentado y si lo está quizá no sea lo suficientemente

explícito. Si la documentación cuenta con suficientes anotaciones explícitas, se continúa al

paso 2, en caso contrario al paso 4.

Paso 2. Análisis de la documentación.

Después de que se ha evaluado la documentación y se considera que cuenta con suficiente

información, se realiza un análisis estático. No es posible desarrollar una herramienta que

automatice completamente esta tarea. Es posible encontrar algunos patrones que apunten a

posibles contratos [19]. La documentación asociada también se analiza. Ésta incluye:

procedimientos; algoritmos; documentación del dominio; reglas del negocio; manuales de

operación; entre otros. De aquí se sigue al paso 3.

Paso 3. Evaluación de especificaciones extraídas de la documentación.

Un contrato está “completo” si contiene precondiciones, poscondiciones para todos los

métodos e invariantes de clase. Esta tarea requiere de decisiones subjetivas. Si las

especificaciones extraídas de la documentación son suficientes se considera como éxito. Por

el contrario, si no son suficientes se sigue al paso 4.

Paso 4. Análisis de código fuente.

Cuando la documentación no contiene información acerca de especificaciones, se analiza el

código fuente. Los elementos que por lo general tienen información implícita acerca de

Page 39: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

31

invariantes de clase son: constructores; interfaces implementadas; y clases base. Las

precondiciones se encuentran implícitas en las excepciones y las poscondiciones en las

sentencias return. Una vez analizado el código fuente se continúa al paso 5.

Paso 5. Deducción de especificaciones.

Es posible utilizar la lógica de Hoare y el cálculo de la precondición más débil wp para

deducir especificaciones finales, si se extrajeron algunas especificaciones de la

documentación y el código fuente. Las invariantes de ciclo se pueden generar

automáticamente o encontrar con las heurísticas propuestas. En el caso de que no se haya

extraído ninguna información significativa acerca de especificaciones, se realiza análisis

dinámico para encontrar aserciones. Al terminar la deducción de especificaciones se procede

al paso 6.

Paso 6. Correctitud de las especificaciones deducidas.

Es necesario verificar que las especificaciones deducidas son correctas. Lo anterior se realiza

generando condiciones de verificación y demostrando su validez automáticamente en un

demostrador de teoremas. Si se tiene éxito en esta tarea, las especificaciones finales son

correctas parcialmente y por lo tanto formales. En caso contrario no se dedujeron

especificaciones correctas.

Figura 7. Algoritmo propuesto para la extracción de especificaciones formales.

Page 40: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

32

4.4. Formalización de la solución propuesta Se utilizó el método formal verificación de modelos para verificar que el algoritmo propuesto

es correcto (i.e. termina). El verificador de modelos que se utilizó es la herramienta Spin [25].

Ésta utiliza el lenguaje de programación Promela (Process Meta Language) [26] para

especificar algoritmos. Los pasos para verificar un algoritmo en Spin son los siguientes:

1. Se codifica el algoritmo en el lenguaje Promela (similar al lenguaje C).

2. Spin mapea automáticamente el algoritmo a un autómata finito.

3. El estado del algoritmo no deseado (never claim) se expresa en lógica temporal (LTL).

4. Spin traduce a un autómata la fórmula en LTL para el algoritmo y se verifica el

algoritmo.

Spin verifica que el lenguaje del autómata del algoritmo está contenido en el lenguaje del

autómata de la propiedad a verificar. Si se cumple lo anterior el algoritmo es correcto. En caso

contrario, Spin muestra la secuencia de líneas de código que contiene el error.

De acuerdo con el proceso anterior, primero se codificó el algoritmo propuesto en el

lenguaje Promela (ver anexo A). Spin genera internamente el autómata finito a partir del

código en Promela (la figura ejemplo de este automáta se encuentra en el anexo A).

Después se especificó la propiedad a verificar del algoritmo (si termina) como:

“¿siempre ocurre que eventualmente se alcanza el estado de éxito o no éxito?”. El

comportamiento deseado se expresa en LTL con la fórmula ◊(estado== exito ∨ estado==no_exito). El comportamiento no deseado (never claim) que es verificado por Spin

es la negación de la fórmula anterior y se expresa en LTL como □(estado!=éxito ∧

estado!=no_exito).

En la figura 8 se muestra el código del never claim generado por Spin en el lenguaje

Promela del comportamiento no deseado del algoritmo. Spin toma como entrada la fórmula

en LTL □(estado!=éxito ∧ estado!=no_exito) y la mapea al lenguaje Promela.

Figura 8. Código del never claim generado en Promela.

Spin efectuó automáticamente el último paso para la verificación del algoritmo. Por lo

tanto se demostró que no se alcanza el estado no deseado en cualquier ejecución del

algoritmo, por lo tanto el algoritmo termina (i.e. es correcto). La figura 9 muestra la salida de

Spin para el algoritmo propuesto, donde se observa que toma como entrada el código del

modelo y del never claim. Para el modelo del algoritmo propuesto Spin reportó que tiene 0

errores. El espacio de búsqueda contiene 71 estados en donde se verifica que no ocurra el

comportamiento no deseado del algoritmo.

Page 41: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 4 ANÁLISIS DEL PROBLEMA Y SOLUCIÓN

33

Figura 9. Salida de Spin para el algoritmo propuesto.

Page 42: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

34

Capítulo 5) Pruebas

Para mostrar el funcionamiento del algoritmo de decisión en la deducción de especificaciones

correctas y que el resultado de su ejecución corresponde a lo modelado, se realizaron cuatro

casos de prueba. El primero es un conocido caso de estudio que ilustra el principio de

substitución de Liskov: el problema Square-Rectangle [27]. Los siguientes dos casos son

tomados de [1]. El último es un caso donde el código se encuentra debidamente documentado.

En este capítulo se presentan los casos de prueba, los resultados obtenidos y el análisis

que se realizó en base a ellos.

5.1. Caso de prueba 1 Supóngase que se tiene el siguiente código fuente [28]:

class Rectangle

{

double itsHeight, itsWidth;

public:

Rectangle(double h, double w)

{

itsHeight = h;

itsWidth = w;

}

virtual void SetHeight(double h)

{

itsHeight = h;

}

virtual void SetWidth(double w)

Page 43: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

35

{

itsWidth = w;

}

double double GetHeight() const

{

return itsHeight;

}

double GetWidth() const

{

return itsWidth;

}

};

class Square: public Rectangle

{

public:

Square(double w):Rectangle(h, h)

{

}

virtual void SetHeight (double h)

{

Rectangle::SetHeight(h);

Rectangle::SetWidth(h);

}

virtual void SetWidth(double w)

{

Rectangle:: SetHeight(w);

Rectangle:: SetWidth(w);

}

};

Primero se evalúa la existencia de documentación (Paso 1). En este caso en particular

no existe documentación alguna que acompañe al código fuente. El código fuente no tiene

comentarios.

En la documentación asociada (teoría elemental de geometría) se encuentran algunos

elementos del contrato (Paso 2). Es bien conocido que el alto y ancho de un rectángulo no

necesariamente tienen el mismo valor. En el caso de un cuadrado, su alto y ancho si deben

tener el mismo valor. Por lo tanto, las invariantes de las clases Rectangle y Square son las

siguientes:

class Rectangle

//invariant itsHeight != itsWidth

class Square: public Rectangle

//invariant itsHeight = = itsWidth

Todos los posibles estados de los objetos tipo Rectangle deben mantener la propiedad

itsWidth != itsHeight y los objetos tipo Square conserva itsWidth = = itsHeight. Nótese que

estas invariantes de clase no están implícitas en el código fuente. Los dos elementos del

contrato extraídos hasta este punto (invariantes) son insuficientes (Paso 3).

Para encontrar los elementos faltantes se inspecciona el código fuente (Paso 4). En

esta etapa las precondiciones y poscondiciones para cada método son deducidas. La

precondición para el constructor de la clase Rectangle, es que los parámetros que toma como

Page 44: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

36

entrada (h y w) deben existir y tener valores reales, para evitar que se lance una excepción. La

poscondición para el constructor de la clase Rectangle es itsHeight = = h y itsWidth = = w ya

que son los valores esperados de cómputo. La deducción de precondiciones y poscondiciones

para los demás métodos de la clase Rectangle se extraen de manera similar. A continuación se

muestra el código con los elementos del contrato deducidos y anotados en el código fuente:

class Rectangle

//invariant itsHeight != itsWidth

{

public:

//requires exists: double h, double w

//ensures itsHeight = =h and itsWidth = = w

Rectangle(double h, double w)

{

itsHeight = h;

itsWidth = w;

}

//requires exists: double h

//ensures itsHeight = =h

virtual void SetHeight(double h)

{

itsHeight = h;

}

//requires exists: double w

//ensures itsWidth = = w

virtual void SetWidth(double w)

{

itsWidth = w;

}

//requires true

//ensures result = = itsHeight

double double GetHeight () const

{

return itsHeight;

}

//requires true

//ensures result = = itsWidth

double GetWidth () const

{

return itsWidth;

}

};

Se sigue el mismo proceso para la clase Square ya que hereda los atributos y métodos

de la clase Rectangle. Las precondiciones, poscondiciones e invariantes se anotan en el código

fuente:

class Square: public Rectangle

//invariant itsHeight = = itsWidth

{

public:

Page 45: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

37

//requires exists: h

//ensures itsHeight = =h and itsWidth = = h

Square(double w):Rectangle(h, h)

{

}

//requires exists: h

//ensures itsHeight = =h and itsWidth = = h

virtual void SetHeight (double h)

{

Rectangle::SetHeight(h);

Rectangle::SetWidth(h);

}

//requires exists: w

//ensures itsHeight = =w and itsWidth = = w

virtual void SetWidth(double w)

{

Rectangle:: SetHeight(w);

Rectangle:: SetWidth(w);

}

};

La siguiente tarea (Paso 6) contiene una decisión subjetiva: ¿se demuestra la

correctitud parcial del contrato deducido ó se considera el proceso terminado? Como no

existen estructuras complejas en el código fuente tales como while o if, no es necesario

utilizar la lógica de Hoare y el cálculo de la precondición más débil wp. Por lo tanto se

consideran completos los elementos del contrato deducidos. En las tablas 2 y 3 se resumen los

elementos del contrato extraídos para la clase Rectangle y la clase Square.

Tabla 2. Especificación formal de la clase Rectangle.

Método Precondición Poscondición Invariante de clase

Rectangle //requires exists:

double h, double w

//ensures itsHeight =

=h and itsWidth = = w

//invariant itsHeight !=

itsWidth

SetHeight //requires exists:

double h

//ensures itsHeight =

=h

SetWidth //requires exists:

double w

//ensures itsWidth = =

w

GetHeight //requires true //ensures result = =

itsHeight

GetWidth //requires true //ensures result = =

itsWidth

Tabla 3. Especificación formal de la clase Square.

Método Precondición Poscondición Invariante de clase

Square //requires exists: h //ensures itsHeight =

=h and itsWidth = = h

//invariant itsHeight =

= itsWidth

SetHeight //requires exists: h //ensures itsHeight =

=h and itsWidth = = h

SetWidth //requires exists: w //ensures itsHeight =

=w and itsWidth = = w

Considérese ahora una nueva función, conocida como the real problem of the Square-

Rectangle problem [27]:

void g(Rectangle &r)

{

r.SetWidth(5);

Page 46: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

38

r.SetHeight(4);

assert(r.GetWidth() * r.GetHeight()) == 20);

}

La función anterior toma como parámetro de entrada un objeto de tipo Rectangle y le

da valores a sus atributos. El código fuente no tiene documentación que ayude a deducir sus

especificaciones pero el comando assert provee información importante para la deducción de

especificaciones.

Es posible utilizar las especificaciones encontradas anteriormente. Se analiza el código

fuente (Paso 4) comenzando por el último comando. Es sabido que el método SetHeight

requiere la existencia del parámetro que toma como entrada. Continuado con el siguiente

comando hacia arriba, se observa que el método SetWidth también requiere de la existencia

del parámetro de entrada. De lo anterior se deduce que la precondición del método g es que el

objeto r exista (r es no nulo y es un rectángulo).

El método g funciona sin problemas para un objeto de tipo Rectangle. El comando

assert declara un error si se pasa como parámetro un objeto de tipo Square y en este caso se

viola la invariante de clase itsHeight = = itsWidth. Se deduce que el contrato extraído para el

método g es incorrecto. En este caso particular el contrato es incorrecto por un mal diseño de

la clase y no es necesario seguir el proceso otra vez más.

5.2. Caso de prueba 2 En [1] se presenta el siguiente código que calcula el factorial de un número entero cualquiera:

int factorial (int n)

{

int i;

int prod=1;

if(n>1)

for(i=2;i<=n;i++)

prod*=i;

return(prod);

}

No existe documentación para el código fuente anterior (Paso 1). Se analiza el código

fuente (Paso 4) y se observa que la función regresa el valor de la variable prod. Esta variable

siempre tendrá valor positivo ya que en el ciclo se incrementa en cada iteración. Además, al

ser el parámetro de entrada un valor de tipo integer, se garantiza que siempre habrá al menos

una iteración del ciclo for. Debido a lo anterior se deduce que la poscondición de la función

factorial es prod>0.

El siguiente paso es utilizar la lógica de Hoare y el cálculo de la precondición más

débil wp para deducir las demás especificaciones (Paso 5). El cálculo se realiza siguiendo la

siguiente serie de pasos

1. Se debe reescribir parte del código para aplicar el cálculo de la precondición más débil

wp, ya que no existe una regla de inferencia definida en la lógica de Hoare para el

comando for. El código queda de la siguiente manera:

int factorial (int n)

{

int i;

int prod=1;

if(n>1)

{

i=2;

Page 47: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

39

while(i<=n;i++)

{

prod*=i;

i++;

}

}

return(prod);

}

2. Se calcula ahora la precondición más débil comenzando por el último comando de la

función. De éste comando se dedujo que la poscondición de la función es prod>0.

3. Continuando con el siguiente comando (while), se debe encontrar una invariante de

ciclo apropiada. Siguiendo una de las heurísticas más sencillas, se propone como

invariante de ciclo la poscondición prod>0 quedando así el cálculo de wp para el

while:

{prod>0 ∧ i>0} prod*=i; {prod>0}

El cálculo anterior considera el comando i++; y la precondición se obtiene utilizando

la regla del while con correctitud parcial de la lógica de Hoare.

Para comprobar lo anterior se debe mostrar que (condición de verificación):

{prod>0 ∧ i<=n} → { prod>0 ∧ i>0}

Lo cual resulta evidente ya que si i<=n entonces se cumple que i>0 ya que n es un

número natural.

4. Continuando con el comando siguiente al while, se calcula la precondición de la

asignación i=2:

{prod>0 ∧ 2>0} i=2; { prod>0 ∧ i>0}

5. Se calcula la precondición más débil para el comando if, para comprobar el resultado

se debe demostrar:

{prod>0 ∧ n>1} → { prod>0}

Se observa claramente que a partir de la primera proposición se deduce sin problemas

la segunda.

6. El último cálculo corresponde a una asignación que queda como:

{1>0} prod=1; {prod>0}

7. Como se tiene la proposición 1>0 como precondición de la función entonces se

deduce que la precondición es T (cualquier valor). La función acepta cualquier número

natural n sin más condiciones.

Las especificaciones extraídas fueron comprobadas con la generación de condiciones de

verificación, por lo tanto son correctas parcialmente. El código anotado queda de la siguiente

manera

Page 48: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

40

int factorial (int n)

//requires T

//ensures return>0

{

int i;

int prod=1;

if(n>1)

for(i=2;i<=n;i++)

prod*=i;

return(prod);

}

Los elementos del contrato extraídos de la función factorial se resumen en la tabla 4.

Tabla 4. Especificación formal de la función factorial

Precondición Poscondición

//requires T //ensures return>0

5.3. Caso de prueba 3 El siguiente código [1] multiplica un número cierto número de veces:

int multiplic (int num, int cont)

{

int result;

result++;

while((num>1) && (cont<num))

{

result*=cont;

cont++;

}

return(result);

}

Como no contiene documentación alguna se analiza el código fuente (Paso 4) y se

propone como posible poscondición result>0. Con esta especificación se aplica el método de

la precondición más débil a todos los comandos, para encontrar una precondición para la

función multiplic

1. Se toma la poscondición anterior como invariante de ciclo (siguiendo la heurística más

simple) y se calcula la precondición de los dos comandos de asignación que contiene

el while:

{result>0} cont++; {result>0}

{ cont>0} result*= cont; {result>0}

2. Se aplica la regla de inferencia de composición a los comandos anteriores:

{result>0 ∧ cont>0 } result*= cont; cont++; {result>0}

3. Para demostrar la validez de lo anterior se genera la condición de verificación

utilizando la regla del comando while para la correctitud parcial de la lógica de Hoare

Page 49: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

41

(recuérdese que en la premisa incluye a la precondición y a la condición de ejecución

del while):

{(result>0 ∧ num>1) ∧ (cont<num) } → {result>0 ∧ cont>0}

Lo anterior se demuestra de la siguiente manera:

1. result>0 ∧ num>1 ∧ cont<num Premisa

2. result>0 Eliminación de ∧ de 1

3. num>1 ∧ cont<num Eliminación de ∧ de 1

4. cont<1 De 3

5. cont>0 De 4 ya previamente se dedujo

como precondición en una

asignación

6. result>0 ∧ cont>0 Conjunción de 2 y 5

4. Con el resultado anterior como precondición, se asciende en el código fuente para

calcular la precondición general de la función y se tiene:

{result>0 ∧ cont>0} result++; { result>0 ∧ num>1}

5. Como la precondición obtenida no condiciona a ninguno de los parámetros de entrada

a cumplir con alguna restricción, la precondición es T (verdadero ó válido para

cualquier valor entero n).

Con la generación de condiciones de verificación las especificaciones deducidas fueron

demostradas formalmente, las especificaciones deducidas son parcialmente correctas.

A continuación se presenta el código anotado:

int multiplic (int num, int cont)

//requires T

//ensures result>0

{

int result;

result++;

while((num>1) && (cont<num))

{

result*=cont;

cont++;

}

return(result);

}

La tabla 5 resume la especificación formal extraída a partir del código fuente de la

función multiplic.

Tabla 5. Especificación formal de la función multiplic

Precondición Poscondición

//requires T //ensures result>0

Page 50: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

42

5.4. Caso de prueba 4 El siguiente código es una implementación de la estructura de datos “pila” y cuenta con

documentación en Javadoc:

package jb;

import java.util.NoSuchElementException;

import java.util.ArrayList;

/**

* The Stack class represents a last-in-first-out stack of objects.

* @author Joseph Bergin

* @version 1.0, May 2000

* Note that this version is not thread safe.

*/

public class Stack

{

/**

* Pushes an item on to the top of this stack.

* @param item the item to be pushed.

*/

public void push(Object item){this.elements.add(item);}

/**

* Removes the object at the top of this stack and returns that object.

* @return The object at the top of this stack.

* @exception NoSuchElementException if this stack is empty.

*/

public Object pop() throws NoSuchElementException

{ int length = this.elements.size();

if (length == 0) throw new NoSuchElementException();

return this.elements.remove(length - 1);

}

/**

* Returns the object at the top of this stack without removing it.

* @return the object at the top of this stack.

* @exception NoSuchElementException if this stack is empty.

*/

public Object peek() throws NoSuchElementException

{ int length = this.elements.size();

if (length == 0) throw new NoSuchElementException();

return this.elements.get(length - 1);

}

/**

* Tests if this stack is empty.

* @return true if this stack is empty and false otherwise.

*/

public boolean isEmpty()

{ return this.elements.isEmpty();

}

private ArrayList elements = new ArrayList();

}

El primer paso es evaluar la documentación existente (Paso 1). En este caso se

considera que la documentación es fuente suficiente para extraer elementos de su

especificación. Se analiza cada uno de los métodos individualmente y se deducen las

Page 51: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 5 PRUEBAS

43

precondiciones y poscondiciones (Paso 3). El análisis realizado utilizando el enfoque de [16]

a cada uno de los métodos del código comentado es el siguiente:

1. Método push(). La etiqueta @param no dice nada explícito sobre alguna precondición

y al ser de tipo void, no se puede extraer una poscondición de la sentencia return.

2. Método pop(). De @return y de la sentencia return se deduce que la poscondición es:

tamaño actual de la pila = tamaño anterior de la pila − 1. Es decir, el tamaño de la

pila después de la ejecución del método pop() es el tamaño que tenía previamente

menos un elemento. La etiqueta @exception tiene implícita la precondición: la pila es

no vacía, por lo que tamaño de la pila ≥ 0.

3. Método peek(). Se analiza la sentencia return y ésta se toma como poscondición. Este

método hace una llamada a un método de la librería ArrayList y no se tienen

referencias sobre sus precondiciones, poscondiciones e invariantes. La etiqueta

@exception tiene implícita la precondición: la pila es no vacía, tamaño actual de la

pila = tamaño anterior de la pila y tamaño de la pila ≥ 0.

4. Método isEmpty(). No se puede extraer algún tipo de información relevante, ya que

hace llamada a un método de la Liberia ArrayList y no contiene etiquetas que apunten

a una posible precondición.

No se pueden extraer invariantes de clase porque no hay etiquetas en el constructor. Como

se mencionó en el análisis de las precondiciones y poscondiciones, es necesario contar con

librerías propias de Java etiquetadas mediante el mismo método. De ese modo se podría

extraer información relevante de las llamadas a métodos de librerías. Por lo que es evidente

que la información recabada no es completa ya que no se tienen precondiciones y

poscondiciones para cada uno de los métodos, ni invariantes de clase. Por lo que es necesario

realizar otro tipo de análisis para completar las especificaciones formales correspondientes a

este código fuente, ya que no se pueden inferir con la lógica de Hoare por no contar con las

estructuras contempladas (tales como if, while, entre otras) en ésta. La tabla 6 resume los

elementos extraídos del contrato correspondiente a la clase Stack.

Tabla 6. Especificación formal de la clase Stack.

Método Precondición Poscondición Invariante de clase

push Elemento no

encontrado

Elemento no

encontrado

No existe información

referente a invariante

de clase en la

documentación pop tamaño de la pila ≥ 0 tamaño actual de la

pila = tamaño anterior

de la pila − 1

peek tamaño actual de la

pila = tamaño anterior

de la pila y tamaño de

la pila ≥ 0

tamaño actual de la

pila = tamaño anterior

de la pila − 1

isEmpty Elemento no

encontrado

Elemento no

encontrado

Page 52: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 6 CONCLUSIONES

44

Capítulo 6) CONCLUSIONES

En este capítulo se describen las conclusiones generadas a partir del trabajo de tesis y algunas

ideas que pueden ser consideradas para futuras investigaciones.

6.1. Conclusiones La especificación formal de la funcionalidad de código fuente no es común en la práctica.

Requiere una considerable inversión de tiempo y esfuerzo por parte del programador de la

aplicación. La especificación formal de la funcionalidad ayuda en la prevención y detección

de errores en el desarrollo de software. El objetivo de este trabajo fue “determinar la

factibilidad de extraer automáticamente la especificación formal de la funcionalidad de

componentes de reuso o servicios a partir de código fuente, para evitar que el desarrollador

invierta una considerable cantidad de tiempo y esfuerzo en la especificación manual de

precondiciones, poscondiciones e invariantes de clase”.

Para encontrar solución al problema planteado en el objetivo, fue necesario realizar un

análisis de: etapas, herramientas y técnicas que ayudan en el proceso de extraer

especificaciones formales a partir de código orientado a objetos. A partir de este análisis se

propuso un algoritmo de decisión generalizado y se formalizó con la herramienta Spin.

En el análisis del problema se determinó que las técnicas que ayudan en el proceso

son: análisis de documentación; análisis de código fuente; lógica de Hoare; cálculo de la

precondición más débil wp; análisis dinámico; y generación automática de invariantes.

De los anteriores solo análisis dinámico y generación dinámica de invariantes son

completamente automatizables. Las demás técnicas son parcialmente automatizables.

Se identificaron tres tareas para la extracción de especificaciones a partir de código

fuente: analizar documentación y código fuente; deducir especificaciones formales; y verificar

correctitud de las especificaciones deducidas.

Page 53: Centro Nacional de Investigación y Desarrollo Tecnológico

CAPÍTULO 6 CONCLUSIONES

45

El algoritmo incluye las herramientas y técnicas listadas anteriormente. Éste presenta

dos limitaciones principales: no es completamente automatizable debido a la naturaleza

indecidible del problema; y es imprescindible el análisis humano con conocimientos

especializados, debido a que se requieren decisiones subjetivas en el proceso.

Las especificaciones deducidas son parcialmente correctas por definición al utilizar la

lógica de Hoare y el cálculo de la precondición más débil wp. Además se demostró

automáticamente que el algoritmo propuesto siempre llega a un estado de éxito o no éxito.

Al analizar el funcionamiento del algoritmo propuesto con los casos de prueba

realizados se encontró que es necesario contar con librerías propias del lenguaje de

programación con el que se encuentra implementado el código fuente previamente anotadas.

Es imprescindible el paso anterior ya que es necesario conocer las especificaciones formales

de estas librerías, para deducir precondiciones y poscondiciones a partir del código fuente.

Una vez que el contrato ha sido generado (especificaciones formales) es necesario refinarlo

con análisis humano para remover elementos que pudieran ser redundantes.

La extracción de especificaciones formales es una tarea tediosa y en algunas ocasiones

casi imposible de realizar debido a razones teóricas y prácticas. Los contratos deberían ser

anotados a priori en el proceso de desarrollo de software y así se evitaría el tener que

extraerlos a posteriori.

6.2. Aportaciones La aportación principal de este trabajo de tesis es el algoritmo de decisión generalizado, que

como se demostró en las pruebas realizadas ayuda en el proceso de la extracción de

especificaciones formales a partir de código fuente.

Entre otras aportaciones se encuentran:

Análisis de herramientas y técnicas existentes que ayudan en el proceso.

Análisis de las tareas que son realizadas durante el proceso.

Ejemplificación de la formalización de un proceso con una herramienta automática.

El material obtenido de esta investigación permitió la redacción de un artículo para un

congreso internacional y una exposición en un congreso nacional.

“Process for contract extraction”. International Conference on Software

Engineering Advances, 2008. ICSEA, Sliema, Malta 2008.

“Extracción de especificaciones formales”. XLI Congreso Nacional de la

Sociedad Matemática Mexicana, Valle de Bravo, Estado de México 2008.

6.3. Trabajo futuro Para facilitar la semiautomatización del algoritmo, se propone implementar las técnicas

existentes para la extracción de especificaciones formales que pueden ser automatizadas:

generación automática de invariantes y generación de condiciones de verificación. Además se

propone la integración de la generación de condiciones de verificación con algún demostrador

automático de teoremas que cuente con soporte directo para probar que sean correctas.

También se propone analizar los elementos faltantes para aplicar el algoritmo en el

análisis de código fuente para la búsqueda y selección de componentes y servicios web.

Además, sería necesario proponer un lenguaje de especificación para expresar contratos de

componentes y servicios web.

Page 54: Centro Nacional de Investigación y Desarrollo Tecnológico

REFERENCIAS

46

REFERENCIAS

[1] Zamudio López , Sheydi Anel. “Identificación de funciones recurrentes en software

legado”. Tesis Maestría, Departamento de Ciencias Computacionales, Centro Nacional de

Investigación y Desarrollo Tecnológico. Diciembre 2001.

[2] Floyd, R.W. “Assigning Meanings to Programs”, Pmt. Am. Math. Sot. Symp. in Applied

Math., Vol. 19, J.T. Schwartz, ed.. American Mathematical Society, Providence, RI., 1967,

pp. 19-31.

[3] Nimmer , Jeremy W. y Ernst, Michael D. “Automatic generation of program

specifications”, In ISSTA 2002, Proceedings of the 2002 International Symposium on

Software Testing and Analysis, (Rome, Italy), July 22-24, 2002, pp. 232-242.

[4] Hoare, C.A.R., “An axiomatic basis for computer programming”, Communications of the

ACM, 12, pp. 576-583, Oct. 1969.

[5] Dijkstra, E.W., A Discipline of Programming, Prentice Hall, Englewood Cliffs, N.J., 1976.

[6] Gannod, G. C. y Cheng, B. H. C. “Strongest postcondition semantics as the formal basis

for reverse engineering”. Proceedings of the Second Working Conference on Reverse

Engineering, pag. 166, 1995.

[7] King, James C. “Symbolic execution and program testing". Comm. ACM, pags. 385-394,

1976.

[8] Meyer, Bertrand, “Applying Design by Contract". Computer, pags. 40-51 ,1992.

[9] http://www.eiffel.com . Consultada el 03 de noviembre 2007.

[10] Leavens, Gary T. y Cheon , Yoonsik. “Design by Contract with JML".

http://www.cs.iastate.edu/ leavens/JML/. Consultada el 03 de noviembre 2007 .

[11] Microsoft Research “Spec #". http://research.microsoft.com/specsharp/ . Consultada el 05

de noviembre de 2007.

[12] Mitchell, Richard y McKim, Jim. “Design by Contract, by example". Addison-Wesley,

2002.

[13] Beugnard, A. et al. “Making components contract aware". Computer 32(7). Julio 1999.

[14] Miguel, M.A. “QoS modeling language for high quality systems”. Proceedings of the

Eighth International Workshop on Object-Oriented Real-Time Dependable Systems,

2003. (WORDS 2003). Páginas 210-216, 2003.

[15] Collet , Philippe. “Functional and Non-Functional Contracts Support for Component-

Oriented Programming". Comm. ACM, 2001.

Page 55: Centro Nacional de Investigación y Desarrollo Tecnológico

REFERENCIAS

47

[16] Milanovic, N. y Malek, M. “Extracting Functional and Nonfunctional Contracts From

Java Classes and Enterprise Java Beans" Proceedings of the Workshop on Architecting

Dependable Systems (WADS 2004), Florencia, Italia 2004.

[17] Flanagan, C. y Leino, K. R.M. et al. “Extended static checking for Java”. In Proc. Conf.

Programming Language Design and Implementation, pages 234–245, 2002.

[18] Flanagan, C. y Leino, K. R.M. “Houdini, an annotation assistant for ESC/Java”. In Proc.

Int’l Symp. Formal Methods Europe on Formal Methods for Increasing Software

Productivity, pages 500–517, 2001.

[19] Bertrand Meyer y Karine Arnout, “Uncovering hidden contracts: The .net example”.

IEEE Computer, 36, No. 11, pp 48-55, November 2003.

[20] Feldman, Yishai A. y Gendler, Leon. “Discern: “Towards the Automatic Discovery of

Software Contracts”. SEFM 2006 Fourth IEEE International Conference on

Software Engineering and Formal Methods, pp. 90 – 99, 2006.

[21] Ernst, Michael D. et al. “The Daikon system for dynamic detection of likely invariants”.

Science of Computer Programming, vol. 69, no. 1--3, Dec. 2007, pp. 35-45.

[22] Javadoc Tool Homepage. http://java.sun.com/j2se/javadoc/. Consultada el 10 de

noviembre de 2007.

[23] L. Kovács. "Aligator: A Mathematica Package for Invariant Generation". Proc. of

IJCAR 2008, LNCS 5195, pp. 275-282, 2008.

[24] Mathematica. http://www.wolfram.com/. Consultada el 04 de agosto de 2008.

[25] Holzmann, Gerard J. “The Model Checker Spin”. IEEE Trans. on Software Engineering,

Vol. 23, No. 5, May 1997, pp. 279-295

[26] Holzmann, G.J. “Design and Validation of Computer Protocols”. Englewood Cliffs, N.J.:

Prentice Hall, 1991.

[27] Martin, Robert C. “The Liskov Substitution Principle”. The C++ Report, 1996.

http://www.objectmentor.com/resources/articles/lsp.pdf. Consultada el 23 de mayo de

2008.

Page 56: Centro Nacional de Investigación y Desarrollo Tecnológico

ANEXO A

48

Anexo A Código del algoritmo propuesto en el lenguaje Promela En este anexo se presenta el código del algoritmo para extraer especificaciones a partir de

código fuente propuesto en este trabajo en el lenguaje Promela, para su formalización en la

herramienta Spin. Se adjunta además el código de la propiedad que se probó de este

algoritmo: ¿siempre ocurre que eventualmente el algoritmo alcanza el estado de éxito o el

estado de no éxito? Por último se presentan algunas simulaciones del algoritmo propuesto en

Spin.

El siguiente es el código del algoritmo propuesto en el lenguaje Promela.

mtype={ S_SC_Commented, S_Doc_Ana, S_SC_Ana,

S_EvalContracts, S_DeductContracts, S_PartialCorrectContracts,

S_Success,S_Failure}

mtype state;

inline setState(x) {

atomic {

state = x;

/* printf("El estado de la deduccion de contratos es: ");

printm(state);*/

printf("\n");

}

}

active proctype Contract_Extraction_Model() {

s_sc_commented: setState(S_SC_Commented);

if

:: true -> { printf("El codigo fuente esta documentado\n");

goto s_doc_ana; }

:: true -> { printf("El codigo fuente no esta documentado\n");

goto s_sc_ana; }

fi;

s_doc_ana: setState(S_Doc_Ana);

printf("Se analiza la documentacion el codigo fuente \n");

goto s_evalcontracts;

s_sc_ana: setState(S_SC_Ana);

printf("Se analiza el codigo fuente \n");

goto s_deductcontracts;

s_evalcontracts: setState(S_EvalContracts);

if

:: true -> { printf("El contrato deducido (a partir de documentacion y codigo fuente) es

correcto\n");

goto s_success; }

:: true -> { printf("El contrato deducido (a partir de documentacion y codigo fuente) es

incorrecto\n");

goto s_sc_ana; }

fi;

s_deductcontracts: setState(S_DeductContracts);

printf("Se deducen contratos mediante metodos formales (calculo de la precondicion mas debil y

logica de Hoare) \n");

goto s_partialcorrectcontracts;

Page 57: Centro Nacional de Investigación y Desarrollo Tecnológico

ANEXO A

49

s_partialcorrectcontracts: setState(S_PartialCorrectContracts);

if

:: true -> { printf("El contrato es parcialmente correcto\n");

goto s_success; }

:: true -> { printf("El contrato es parcialmente incorrecto\n");

goto s_failure; }

fi;

s_success: setState(S_Success);

goto end;

s_failure: setState(S_Failure);

end: skip;

}

El código que corresponde a la propiedad que fue demostrada del agoritmo es el

siguiente:

#define p (state==S_Success)

#define q (state==S_Failure)

never { /* [] (!p && !q) */

accept_init:

T0_init:

if

:: (! ((p)) && ! ((q))) -> goto T0_init

fi;

}

En las figuras 1, 2 y 3 se ilustra la simulación del algoritmo en Spin. En la figura 4 se

observa la representación interna (autómata de estados finitos) que Spin realiza del modelo

que toma como entrada.

Page 58: Centro Nacional de Investigación y Desarrollo Tecnológico

ANEXO A

50

Figura 1. Simulación del algoritmo propuesto en Spin (parte 1).

Figura 2. Simulación del algoritmo propuesto en Spin (parte 2).

Page 59: Centro Nacional de Investigación y Desarrollo Tecnológico

ANEXO A

51

Figura 3. Simulación del algoritmo propuesto en Spin (parte 3).

Page 60: Centro Nacional de Investigación y Desarrollo Tecnológico

ANEXO A

52

Figura 4. Representación interna del modelo del algoritmo propuesto en Spin.