Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Diseño por Contratos Formal y Verificación

Lógica de primer orden, tripletas de Hoare, invariantes de TADs y cálculo de wp en C

Universidad Nacional de Río Negro

3. Contratos de Estructuras de Datos e Invariantes de Clase

Introducción

Desarrollo

Fundamentos Matemáticos del Diseño por Contratos

Para sistemas de software de alta integridad o complejidad (como al implementar las estructuras dinámicas internas de un Tipo de Dato Abstracto), las especificaciones en lenguaje natural o aserciones en tiempo de ejecución resultan insuficientes. Se requiere un marco formal sustentado en la lógica matemática para probar de forma rigurosa la correctitud y coherencia de las rutinas de software.


1. Fundamentos de Lógica de Primer Orden

La Lógica de Primer Orden (First-Order Logic, FOL) extiende la lógica proposicional clásica introduciendo cuantificadores sobre elementos individuales del dominio de discurso.

Sintaxis y Alfabeto de la LPO

Un lenguaje en Lógica de Primer Orden consta de:

Variables Libres y Ligadas

Una ocurrencia de una variable en una fórmula está ligada si se encuentra dentro del alcance directo de un cuantificador (x\forall x o x\exists x). De lo contrario, se considera una variable libre. Una fórmula que carece de variables libres se denomina sentencia.

La operación de sustitución ϕ[t/x]\phi[t/x] representa el reemplazo de todas las ocurrencias libres de la variable xx en la fórmula ϕ\phi por el término tt, previniendo la colisión de identificadores.


2. Lógica de Hoare y Cálculo de Precondición Más Débil (wpwp)

La Lógica de Hoare provee un sistema axiomático formal para razonar sobre la corrección de algoritmos imperativos mediante el uso de Tripletas de Hoare:

{P} S {Q}\{P\}\ S\ \{Q\}

Donde:

Regla de la Asignación

La regla de asignación calcula analíticamente la precondición mínima requerida para asegurar una postcondición QQ tras asignar una expresión EE a la variable xx:

{Q[E/x]} x:=E {Q}\frac{}{\{Q[E/x]\}\ x := E\ \{Q\}}
Precondición Más Débil (Weakest Precondition - wpwp)

La precondición más débil wp(S,Q)wp(S, Q) describe la condición más general (menos restrictiva) sobre el estado inicial del sistema que garantiza que la ejecución de SS finalice en un estado que satisfaga la postcondición QQ.

wp(x := E,Q)Q[E/x]wp(\texttt{x := E}, Q) \equiv Q[E/x]

3. Contratos de Estructuras de Datos e Invariantes de Clase

En el diseño de Tipos Datos Abstractos (TADs), un invariante de clase o de estructura es una propiedad lógica fundamental que describe la validez interna de la representación física de los datos. Debe ser verdadera tras la construcción del objeto y preservarse antes y después de cada llamada a métodos o funciones públicas del TAD.

1
2
3
4
5
6
7
8
typedef struct
{
    int *elementos;
    int tope;
    int capacidad;
} pila_t;
// Invariante de estructura Pila:
// elementos != NULL ∧ capacidad > 0 ∧ 0 <= tope <= capacidad
Verificación Práctica: assert.h y la Regla 0x2003h

Aunque lenguajes de especificación formal como ACSL son valiosos para verificación estática matemática, en el desarrollo práctico de C (y cumpliendo con las directivas de la cátedra) se utiliza un enfoque pragmático basado en la verificación dinámica con la biblioteca <assert.h> y la documentación estructurada de la Regla Regla 0x2003h: Todas las funciones deben incluir documentación completa y estructurada.

Bajo esta regla, los contratos se establecen de la siguiente manera:

  1. Documentación estructurada (@pre y @post): En la cabecera de la función (.h), declarando explícitamente qué asunciones se hacen y qué se garantiza.

  2. Verificación dinámica de precondiciones (assert): Al inicio de la implementación de la función (.c), para abortar inmediatamente la ejecución si el cliente viola el contrato en modo desarrollo, evitando que un estado inválido corrompa la memoria.

  3. Funciones de validación de invariantes: Implementar una función interna del módulo (ej: bool pila_es_valida(const pila_t *p)) que evalúe y retorne verdadero si todas las invariantes de la estructura de datos se cumplen.

Ejemplo Práctico de Contrato Seguro

Archivo de Cabecera (pila.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct pila pila_t;
/**
 * Inserta un elemento en el tope de la pila.
 *
 * @param p Puntero a la pila (debe estar inicializada y no estar llena).
 * @param dato Elemento entero a apilar.
 *
 * @pre p != NULL (Regla 0x2003h)
 * @pre p->tope < p->capacidad (La pila no debe estar llena)
 * @post El elemento queda en el tope de la pila y el tamaño se incrementa
 * en 1.
 */
void pila_push(pila_t *p, int dato);

Archivo de Implementación (pila.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "pila.h"
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
struct pila
{
    int *elementos;
    size_t tope;
    size_t capacidad;
};
// Función auxiliar para verificar el invariante del TAD
static bool pila_es_valida(const pila_t *p)
{
    if (p == NULL)
        return false;
    if (p->elementos == NULL)
        return false;
    if (p->capacidad == 0)
        return false;
    if (p->tope > p->capacidad)
        return false;
    return true;
}
void pila_push(pila_t *p, int dato)
{
    // Verificación defensiva y obligatoria de precondiciones en desarrollo
    assert(p != NULL);
    assert(pila_es_valida(p));
    assert(p->tope < p->capacidad);
    // Operación
    p->elementos[p->tope] = dato;
    p->tope++;
    // Verificación de postcondición/invariante
    assert(pila_es_valida(p));
}
El Frame Problem y la directiva assigns

El Frame Problem consiste en la dificultad de especificar formalmente qué partes del estado del sistema no cambian durante la ejecución de una función. Sin una solución, las especificaciones deberían listar exhaustivamente cada variable del programa que permanece igual.

En ACSL, esto se resuelve mediante la directiva assigns, la cual especifica con exactitud las únicas variables o posiciones de memoria que la función tiene permitido modificar. El analizador formal asume de manera automática que todo lo que no figure en dicha cláusula permanece inalterado.

Ejercicios de Autoevaluación

Solution to Exercise 1

La fórmula en LPO utilizando cuantificación universal es:

i. (0ilimit<n)(arr[i]>K)\forall i.\ (0 \leq i \leq \text{limit} < n) \rightarrow (\text{arr}[i] > K)
Solution to Exercise 2
  • Ligadas: Las variables xx y zz son variables ligadas debido a que sus ocurrencias se encuentran bajo el alcance directo de los cuantificadores universal x\forall x y existencial z\exists z respectivamente.

  • Libres: La variable yy es libre porque no se encuentra bajo el alcance de ningún cuantificador lógico en la fórmula.

Solution to Exercise 3

La sustitución reemplaza la ocurrencia libre de xx por el término sin alterar la estructura ligada de yy:

(y. xy>10)[w+2/x]y. (w+2)y>10(\exists y.\ x * y > 10)[w+2/x] \equiv \exists y.\ (w + 2) * y > 10
Solution to Exercise 4

Aplicamos la regla de asignación de Hoare de atrás hacia adelante:

  1. Para la última instrucción y:=ty := t con postcondición Q=(x=By=A)Q = (x = B \land y = A):

    Q[t/y](x=Bt=A)Q[t/y] \equiv (x = B \land t = A)
  2. Para la instrucción anterior x:=yx := y sobre la condición anterior:

    (x=Bt=A)[y/x](y=Bt=A)(x = B \land t = A)[y/x] \equiv (y = B \land t = A)
  3. Para la primera instrucción t:=xt := x:

    (y=Bt=A)[x/t](y=Bx=A)(y = B \land t = A)[x/t] \equiv (y = B \land x = A)

La precondición obtenida de forma analítica es exactamente {y=Bx=A}\{y = B \land x = A\}, lo cual demuestra la corrección de la terna.

Solution to Exercise 5

Utilizando la regla de la precondición más débil para la asignación:

wp(x := x * 2,x>10)(x2>10)(x>5)wp(\texttt{x := x * 2}, x > 10) \equiv (x * 2 > 10) \equiv (x > 5)
Solution to Exercise 6

La Regla de Consecuencia permite adaptar tripletas de Hoare preexistentes a contextos más estrictos o específicos.

PP{P} S {Q}QQ{P} S {Q}\frac{P' \rightarrow P \quad \{P\}\ S\ \{Q\} \quad Q \rightarrow Q'}{\{P'\}\ S\ \{Q'\}}
  • Debilitar la precondición (PPP' \rightarrow P): Reemplazar la precondición PP por una condición PP' que es más restrictiva. Si el programa funciona asumiendo una precondición débil, seguirá siendo correcto con una más fuerte.

  • Fortalecer la postcondición (QQQ \rightarrow Q'): Reemplazar la postcondición QQ por una condición QQ' que sea menos restrictiva, garantizando más de lo inicialmente especificado.

Solution to Exercise 7

El Frame Problem es la dificultad de expresar formalmente qué variables y recursos del sistema no se modifican tras la ejecución de una rutina sin tener que listar explícitamente el infinito número de variables inalteradas.

La directiva assigns soluciona esto en ACSL permitiendo declarar un marco delimitado de variables de escritura. El analizador estático (como Frama-C) asume automáticamente que cualquier variable o celda del heap que no esté listada de forma explícita en la directiva assigns permanece inalterada, lo que simplifica drásticamente el cálculo de las condiciones de verificación.

Solution to Exercise 8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <assert.h>
#include <stddef.h>
/**
 * Resetea el valor de la variable apuntada a cero.
 *
 * @param ptr Puntero a la variable entera a resetear.
 *
 * @pre ptr != NULL (Regla 0x2003h)
 * @post El valor apuntado por ptr es igual a 0.
 */
void resetear(int *ptr)
{
    // Verificación dinámica de la precondición
    assert(ptr != NULL);
    *ptr = 0;
    // Verificación de la postcondición
    assert(*ptr == 0);
}
Solution to Exercise 9

En un Rectangulo mutable, la operación set_ancho(w) tiene la postcondición de que el ancho se actualiza a w y el alto permanece inalterado.

Si Cuadrado es un subtipo, para mantener su invariante de estructura (ancho == alto), la operación set_ancho(w) debe forzosamente alterar el alto también. Esto viola la postcondición heredada de Rectangulo (que garantiza que el alto no se modifica), rompiendo el Principio de Sustitución de Liskov ya que un código cliente diseñado para Rectangulo fallaría si se le pasa un objeto de tipo Cuadrado.

Glosario

Precondición
Condición que debe cumplirse antes de invocar una función.
Postcondición
Garantía que ofrece una función al finalizar si se cumplieron sus precondiciones.
Invariante
Propiedad que debe permanecer verdadera durante el ciclo de vida de un objeto o ejecución.
Invariante de lazo (Loop Invariant)
Condición o propiedad lógica asociada a una estructura iterativa que permanece verdadera antes de ingresar al lazo, antes y después de cada vuelta, y al salir de este.

Síntesis y Resumen

En este apunte se han presentado los conceptos fundamentales del tema.

Referencias y Lecturas Complementarias

References
  1. Meyer, B. (1988). Design by Contract. Advances in Object-Oriented Software Engineering.
  2. Meyer, B. (1992). Applying “Design by Contract.” Computer, 25(10), 40–51. 10.1109/2.161279
  3. Meyer, B. (1997). Object-Oriented Software Construction (2nd ed.). Prentice Hall.