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

Prerrequisitos: lógica proposicional, funciones, assert.h, TAD y sus invariantes. Este capítulo formaliza los contratos informales del bloque 1.

Objetivo: expresar precondiciones, poscondiciones e invariantes y usarlas para justificar una operación sobre una estructura de datos.

Comprobación de salida: escribí una precondición y una postcondición para una operación de pila y verificá que no se contradigan.

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 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

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.