Skip to article frontmatterSkip to article content

Diseño por Contratos y Lógica de Primer Orden

Fundamentos formales de la especificación y verificación de software

Introducción

El Diseño por Contratos (Design by Contract, DbC) es una metodología formal de desarrollo de software introducida por Bertrand Meyer en el lenguaje Eiffel. Se fundamenta en la metáfora de un contrato legal entre partes: cada componente de software tiene obligaciones (precondiciones que debe garantizar el cliente) y beneficios (postcondiciones que garantiza el proveedor). Este enfoque transforma el desarrollo de software de una actividad artesanal a una disciplina ingenieril rigurosa.

Metáfora del contrato: cliente y proveedor tienen obligaciones y derechos mutuos, formalizados mediante precondiciones y postcondiciones.

Figure 1:Metáfora del contrato: cliente y proveedor tienen obligaciones y derechos mutuos, formalizados mediante precondiciones y postcondiciones.

La formalización mediante Lógica de Primer Orden (LPO) proporciona el rigor matemático necesario para especificar, verificar y razonar sobre la corrección de programas.

Fundamentos de Lógica de Primer Orden

Sintaxis de la Lógica de Primer Orden

La Lógica de Primer Orden (First-Order Logic, FOL) extiende la lógica proposicional con cuantificadores sobre individuos.

Alfabeto

Símbolos lógicos:

Símbolos no lógicos (específicos del dominio):

Términos

Los términos denotan objetos del dominio:

t::=xcf(t1,,tn)t ::= x \mid c \mid f(t_1, \ldots, t_n)

donde xx es una variable, cc una constante, y ff una función de aridad nn.

Ejemplos:

Fórmulas

Las fórmulas expresan propiedades y relaciones:

ϕ::=P(t1,,tn)t1=t2¬ϕϕ1ϕ2ϕ1ϕ2ϕ1ϕ2x.ϕx.ϕ\phi ::= P(t_1, \ldots, t_n) \mid t_1 = t_2 \mid \neg \phi \mid \phi_1 \land \phi_2 \mid \phi_1 \lor \phi_2 \mid \phi_1 \rightarrow \phi_2 \mid \forall x.\phi \mid \exists x.\phi

Ejemplos:

Semántica

La interpretación I\mathcal{I} asigna significado a símbolos no lógicos:

Una asignación σ\sigma mapea variables a elementos del dominio: σ:VarD\sigma: \text{Var} \rightarrow D.

Satisfacción

La relación de satisfacción I,σϕ\mathcal{I}, \sigma \models \phi se define inductivamente:

I,σP(t1,,tn)si (t1I,σ,,tnI,σ)I(P)I,σt1=t2si t1I,σ=t2I,σI,σ¬ϕsi I,σ⊭ϕI,σϕ1ϕ2si I,σϕ1 y I,σϕ2I,σϕ1ϕ2si I,σϕ1 o I,σϕ2I,σϕ1ϕ2si I,σ⊭ϕ1 o I,σϕ2I,σx.ϕsi para todo dD:I,σ[xd]ϕI,σx.ϕsi existe dD:I,σ[xd]ϕ\begin{align} \mathcal{I}, \sigma &\models P(t_1, \ldots, t_n) &&\text{si } (\llbracket t_1 \rrbracket_{\mathcal{I},\sigma}, \ldots, \llbracket t_n \rrbracket_{\mathcal{I},\sigma}) \in \mathcal{I}(P) \\ \mathcal{I}, \sigma &\models t_1 = t_2 &&\text{si } \llbracket t_1 \rrbracket_{\mathcal{I},\sigma} = \llbracket t_2 \rrbracket_{\mathcal{I},\sigma} \\ \mathcal{I}, \sigma &\models \neg \phi &&\text{si } \mathcal{I}, \sigma \not\models \phi \\ \mathcal{I}, \sigma &\models \phi_1 \land \phi_2 &&\text{si } \mathcal{I}, \sigma \models \phi_1 \text{ y } \mathcal{I}, \sigma \models \phi_2 \\ \mathcal{I}, \sigma &\models \phi_1 \lor \phi_2 &&\text{si } \mathcal{I}, \sigma \models \phi_1 \text{ o } \mathcal{I}, \sigma \models \phi_2 \\ \mathcal{I}, \sigma &\models \phi_1 \rightarrow \phi_2 &&\text{si } \mathcal{I}, \sigma \not\models \phi_1 \text{ o } \mathcal{I}, \sigma \models \phi_2 \\ \mathcal{I}, \sigma &\models \forall x.\phi &&\text{si para todo } d \in D: \mathcal{I}, \sigma[x \mapsto d] \models \phi \\ \mathcal{I}, \sigma &\models \exists x.\phi &&\text{si existe } d \in D: \mathcal{I}, \sigma[x \mapsto d] \models \phi \end{align}

Validez: ϕ\phi es válida (ϕ\models \phi) si I,σϕ\mathcal{I}, \sigma \models \phi para toda interpretación I\mathcal{I} y asignación σ\sigma.

Variables Libres y Ligadas

En x.ϕ\forall x.\phi y x.ϕ\exists x.\phi, las ocurrencias de xx en ϕ\phi están ligadas.

Una variable es libre si no está bajo el alcance de un cuantificador.

Ejemplo: En x.(x<y)\forall x.(x < y), xx está ligada y yy es libre.

Una fórmula sin variables libres es una sentencia.

Sustitución

La sustitución ϕ[t/x]\phi[t/x] reemplaza todas las ocurrencias libres de xx en ϕ\phi por el término tt, evitando captura de variables.

Ejemplo:

(y.(x<y))[z+1/x]=y.(z+1<y)(\forall y.(x < y))[z+1/x] = \forall y.(z+1 < y)

Teorías de Primer Orden

Una teoría TT es un conjunto de axiomas (fórmulas) en LPO.

Ejemplos:

Diseño por Contratos: Formalización

Componentes de un contrato: precondición, postcondición e invariante, con sus roles en la verificación.

Figure 2:Componentes de un contrato: precondición, postcondición e invariante, con sus roles en la verificación.

Precondiciones

Una precondición Pre(x1,,xn)\text{Pre}(x_1, \ldots, x_n) es una fórmula LPO sobre los parámetros de entrada que debe ser verdadera antes de ejecutar la función.

Definición formal:

Pre:Estadoin{true,false}\text{Pre}: \text{Estado}_{\text{in}} \rightarrow \{\text{true}, \text{false}\}

donde Estadoin\text{Estado}_{\text{in}} representa el estado del programa antes de la llamada.

Interpretación:

Ejemplo: Búsqueda binaria

// Pre: arr != NULL ∧ n > 0 ∧ sorted(arr, 0, n-1)
int busqueda_binaria(int arr[], int n, int clave);

Formalización:

Pre(arrNULL)(n>0)(i,j. 0i<j<narr[i]arr[j])\text{Pre} \equiv (\text{arr} \neq \text{NULL}) \land (n > 0) \land \left(\forall i, j.\ 0 \leq i < j < n \rightarrow \text{arr}[i] \leq \text{arr}[j]\right)

Postcondiciones

Una postcondición Post(x1,,xn,r)\text{Post}(x_1, \ldots, x_n, r) es una fórmula LPO que debe ser verdadera después de ejecutar la función, relacionando entradas, salidas y estado.

Definición formal:

Post:Estadoin×Estadoout{true,false}\text{Post}: \text{Estado}_{\text{in}} \times \text{Estado}_{\text{out}} \rightarrow \{\text{true}, \text{false}\}

Interpretación:

Ejemplo: Búsqueda binaria

// Post: (r = -1 ∧ ∀i. 0 ≤ i < n → arr[i] ≠ clave) ∨
//       (0 ≤ r < n ∧ arr[r] = clave)
int busqueda_binaria(int arr[], int n, int clave);

Formalización: $$

Post (r=1i. 0i<narr[i]clave)(0r<narr[r]=clave)\begin{align} \text{Post} \equiv\ &(r = -1 \land \forall i.\ 0 \leq i < n \rightarrow \text{arr}[i] \neq \text{clave}) \\ &\lor (0 \leq r < n \land \text{arr}[r] = \text{clave}) \end{align}

$$

Invariantes

Un invariante de lazo II es una fórmula que es verdadera antes y después de cada iteración.

Definición formal:

I:Estado{true,false}I: \text{Estado} \rightarrow \{\text{true}, \text{false}\}

Regla de inferencia para lazos:

{IB} S {I}{I} while B do S {I¬B}\frac{\{I \land B\}\ S\ \{I\}}{\{I\}\ \textbf{while}\ B\ \textbf{do}\ S\ \{I \land \neg B\}}

donde BB es la guarda del lazo y SS el cuerpo.

Comportamiento del invariante de lazo a través de las iteraciones, mostrando su preservación.

Figure 3:Comportamiento del invariante de lazo a través de las iteraciones, mostrando su preservación.

Ejemplo: Suma de arreglo

// Pre: arr != NULL ∧ n ≥ 0
// Post: result = Σ(arr[i]) para i ∈ [0, n)
int sumar_arreglo(int arr[], int n) {
    int suma = 0;
    int i = 0;
    
    // Invariante: suma = Σ(arr[j]) para j ∈ [0, i) ∧ 0 ≤ i ≤ n
    while (i < n) {
        suma += arr[i];
        i++;
    }
    
    return suma;
}

Formalización del invariante:

I(suma=j=0i1arr[j])(0in)I \equiv \left(\text{suma} = \sum_{j=0}^{i-1} \text{arr}[j]\right) \land (0 \leq i \leq n)

Demostración de corrección:

  1. Inicialización: Antes del lazo, i=0i = 0 y suma=0\text{suma} = 0

    I[0/i,0/suma]=(0=j=01arr[j])(00n)=trueI[0/i, 0/\text{suma}] = \left(0 = \sum_{j=0}^{-1} \text{arr}[j]\right) \land (0 \leq 0 \leq n) = \text{true}
  2. Preservación: Si I(i<n)I \land (i < n) antes de la iteración, entonces I[i+1/i,suma+arr[i]/suma]I[i+1/i, \text{suma}+\text{arr}[i]/\text{suma}] después: $$

    suma+arr[i]=j=0i1arr[j]+arr[i]=j=0iarr[j]\begin{align} \text{suma} + \text{arr}[i] &= \sum_{j=0}^{i-1} \text{arr}[j] + \text{arr}[i] \\ &= \sum_{j=0}^{i} \text{arr}[j] \end{align}

    $Y Y (i+1) \leq nporque porque i < n$.

  3. Terminación: Al salir del lazo, I¬(i<n)=I(in)I \land \neg(i < n) = I \land (i \geq n). Por el invariante, ini \leq n, entonces i=ni = n.

    suma=j=0n1arr[j]\text{suma} = \sum_{j=0}^{n-1} \text{arr}[j]

    que es la postcondición deseada.

Invariantes de Clase

Un invariante de clase es una propiedad que debe ser verdadera para todos los objetos en todos los estados observables.

Definición formal:

ClassInv:Estadoobjeto{true,false}\text{ClassInv}: \text{Estado}_{\text{objeto}} \rightarrow \{\text{true}, \text{false}\}

Propiedades:

  1. Debe ser verdadero después del constructor

  2. Debe preservarse por todos los métodos públicos

  3. Puede violarse temporalmente en métodos privados

Ejemplo: Cola circular con arreglo

typedef struct {
    int *elementos;
    int capacidad;
    int frente;
    int final;
    int tamanio;
} cola_t;

// Invariante de clase:
// 1. elementos != NULL
// 2. capacidad > 0
// 3. 0 ≤ frente < capacidad
// 4. 0 ≤ final < capacidad
// 5. 0 ≤ tamanio ≤ capacidad
// 6. Si tamanio > 0: elementos[(frente + i) % capacidad] está definido para 0 ≤ i < tamanio

Formalización: $$

Invcola (elementosNULL)(capacidad>0)(0frente<capacidad)(0final<capacidad)(0tamaniocapacidad)(tamanio>0i. 0i<tamanioelementos[(frente+i)modcapacidad]Z)\begin{align} \text{Inv}_{\text{cola}} \equiv\ &(\text{elementos} \neq \text{NULL}) \\ &\land (\text{capacidad} > 0) \\ &\land (0 \leq \text{frente} < \text{capacidad}) \\ &\land (0 \leq \text{final} < \text{capacidad}) \\ &\land (0 \leq \text{tamanio} \leq \text{capacidad}) \\ &\land (\text{tamanio} > 0 \rightarrow \forall i.\ 0 \leq i < \text{tamanio} \rightarrow \text{elementos}[(\text{frente} + i) \bmod \text{capacidad}] \in \mathbb{Z}) \end{align}

$$

Lógica de Hoare

La Lógica de Hoare proporciona un sistema formal para razonar sobre la corrección de programas.

Tripletas de Hoare

Una tripleta de Hoare tiene la forma:

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

donde:

Interpretación (corrección parcial):

Si PP es verdadera antes de ejecutar SS, y SS termina, entonces QQ es verdadera después.

Corrección total: [P] S [Q][P]\ S\ [Q] garantiza además que SS termina.

Representación de una tripleta de Hoare mostrando la transformación de estados.

Figure 4:Representación de una tripleta de Hoare mostrando la transformación de estados.

Reglas de Inferencia

Regla de Asignación

{Q[E/x]} x:=E {Q}\frac{}{\{Q[E/x]\}\ x := E\ \{Q\}}

Ejemplo:

{y+1>0} x:=y+1 {x>0}\{y + 1 > 0\}\ x := y + 1\ \{x > 0\}

Regla de Composición Secuencial

{P} S1 {R}{R} S2 {Q}{P} S1;S2 {Q}\frac{\{P\}\ S_1\ \{R\} \quad \{R\}\ S_2\ \{Q\}}{\{P\}\ S_1; S_2\ \{Q\}}

Regla de Condicional

{PB} S1 {Q}{P¬B} S2 {Q}{P} if B then S1 else S2 {Q}\frac{\{P \land B\}\ S_1\ \{Q\} \quad \{P \land \neg B\}\ S_2\ \{Q\}}{\{P\}\ \textbf{if}\ B\ \textbf{then}\ S_1\ \textbf{else}\ S_2\ \{Q\}}

Regla de Lazo (While)

{IB} S {I}{I} while B do S {I¬B}\frac{\{I \land B\}\ S\ \{I\}}{\{I\}\ \textbf{while}\ B\ \textbf{do}\ S\ \{I \land \neg B\}}

donde II es el invariante de lazo.

Regla de Consecuencia

PP{P} S {Q}QQ{P} S {Q}\frac{P' \rightarrow P \quad \{P\}\ S\ \{Q\} \quad Q \rightarrow Q'}{\{P'\}\ S\ \{Q'\}}

Esta regla permite debilitar la precondición y fortalecer la postcondición.

Debilidad de Precondiciones

Una precondición P1P_1 es más débil que P2P_2 si:

P2P1P_2 \rightarrow P_1

Es decir, P1P_1 es verdadera en más estados que P2P_2.

Precondición más débil (weakest precondition, wpwp):

wp(S,Q)=la precondicioˊn maˊs deˊbil tal que {wp(S,Q)} S {Q}wp(S, Q) = \text{la precondición más débil tal que } \{wp(S, Q)\}\ S\ \{Q\}

Ejemplo: Para S=x := x + 1S = \texttt{x := x + 1} y Q=(x>5)Q = (x > 5):

wp(x := x + 1,x>5)=(x+1>5)=(x>4)wp(\texttt{x := x + 1}, x > 5) = (x + 1 > 5) = (x > 4)

Ejemplo Completo: Máximo de Dos Números

// Pre: true
// Post: r = max(a, b)
int maximo(int a, int b) {
    int r;
    if (a >= b) {
        r = a;
    } else {
        r = b;
    }
    return r;
}

Demostración:

  1. Rama then: {ab} r:=a {r=max(a,b)}\{a \geq b\}\ r := a\ \{r = \max(a, b)\}

    • Por la regla de asignación: debemos probar ab(a=max(a,b))a \geq b \rightarrow (a = \max(a, b))

    • Esto es verdadero porque abmax(a,b)=aa \geq b \rightarrow \max(a, b) = a

  2. Rama else: {a<b} r:=b {r=max(a,b)}\{a < b\}\ r := b\ \{r = \max(a, b)\}

    • Similarmente: a<b(b=max(a,b))a < b \rightarrow (b = \max(a, b)) es verdadero

  3. Condicional completo: Por la regla de condicional:

    {true} if (ab)  {r=max(a,b)}\{\text{true}\}\ \textbf{if}\ (a \geq b)\ \cdots\ \{r = \max(a, b)\}

Verificación Automática y Herramientas

Aserciones en C

En C, las aserciones se implementan con assert.h:

#include <assert.h>

// Pre: arr != NULL ∧ n > 0
void procesar(int arr[], int n) {
    assert(arr != NULL);
    assert(n > 0);
    
    // ... código ...
}

Limitaciones:

Anotaciones Avanzadas: ACSL

ACSL (ANSI/ISO C Specification Language) extiende C con contratos formales para Frama-C:

/*@ requires n > 0;
    requires \valid(arr + (0..n-1));
    ensures \result >= 0;
    ensures \result < n;
    ensures \forall integer i; 0 <= i < n ==> arr[\result] >= arr[i];
  */
int buscar_maximo(int arr[], int n);

Cuantificadores en ACSL:

Verificadores de Modelos

Frama-C: Framework para análisis estático de C

Flujo de verificación automática: desde código anotado hasta prueba formal.

Figure 5:Flujo de verificación automática: desde código anotado hasta prueba formal.

Ejemplo completo verificable:

/*@ requires n >= 0;
    requires \valid(arr + (0..n-1));
    ensures \result == \sum(0, n-1, \lambda integer i; arr[i]);
  */
int sumar_arreglo(int arr[], int n) {
    int suma = 0;
    
    /*@ loop invariant 0 <= i <= n;
        loop invariant suma == \sum(0, i-1, \lambda integer j; arr[j]);
        loop assigns i, suma;
        loop variant n - i;
      */
    for (int i = 0; i < n; i++) {
        suma += arr[i];
    }
    
    return suma;
}

Contratos en Estructuras de Datos

Ejemplo 1: Pila

typedef struct {
    int *elementos;
    int tope;
    int capacidad;
} pila_t;

// Invariante de clase:
// 1. elementos != NULL
// 2. capacidad > 0
// 3. 0 ≤ tope ≤ capacidad
// 4. ∀i. 0 ≤ i < tope → elementos[i] está definido

/*@ predicate pila_valida(pila_t *p) =
        \valid(p) &&
        p->elementos != NULL &&
        p->capacidad > 0 &&
        0 <= p->tope <= p->capacidad &&
        \valid(p->elementos + (0..p->capacidad-1));
  */

/*@ requires pila_valida(p);
    requires p->tope < p->capacidad;
    assigns p->tope, p->elementos[p->tope];
    ensures pila_valida(p);
    ensures p->tope == \old(p->tope) + 1;
    ensures p->elementos[p->tope - 1] == dato;
  */
void push(pila_t *p, int dato) {
    p->elementos[p->tope] = dato;
    p->tope++;
}

/*@ requires pila_valida(p);
    requires p->tope > 0;
    assigns p->tope;
    ensures pila_valida(p);
    ensures p->tope == \old(p->tope) - 1;
    ensures \result == \old(p->elementos[p->tope - 1]);
  */
int pop(pila_t *p) {
    p->tope--;
    return p->elementos[p->tope];
}

Ejemplo 2: Lista Enlazada

typedef struct nodo {
    int dato;
    struct nodo *siguiente;
} nodo_t;

typedef struct {
    nodo_t *cabeza;
    int longitud;
} lista_t;

/*@ predicate lista_acíclica(nodo_t *n) =
        n == NULL ||
        (\valid(n) && lista_acíclica(n->siguiente) && !alcanzable(n->siguiente, n));
  */

/*@ predicate longitud_correcta(lista_t *l) =
        l->longitud == contar_nodos(l->cabeza);
  */

/*@ predicate lista_valida(lista_t *l) =
        \valid(l) &&
        lista_acíclica(l->cabeza) &&
        l->longitud >= 0 &&
        longitud_correcta(l);
  */

/*@ requires lista_valida(l);
    assigns l->cabeza, l->longitud, \allocable;
    ensures lista_valida(l);
    ensures l->longitud == \old(l->longitud) + 1;
    ensures l->cabeza->dato == dato;
    ensures l->cabeza->siguiente == \old(l->cabeza);
  */
void insertar_inicio(lista_t *l, int dato) {
    nodo_t *nuevo = malloc(sizeof(nodo_t));
    //@ assert nuevo != NULL; // Asumiendo éxito de malloc
    
    nuevo->dato = dato;
    nuevo->siguiente = l->cabeza;
    l->cabeza = nuevo;
    l->longitud++;
}

Subtipos y Contratos

Principio de Sustitución de Liskov

El Principio de Sustitución de Liskov (LSP) establece que un subtipo debe poder usarse donde se espera el supertipo sin alterar la corrección.

Formalización mediante contratos:

Si SS es un subtipo de TT, entonces para todo método mm:

PreT(m)PreS(m)(precondicioˊn maˊs deˊbil)PostS(m)PostT(m)(postcondicioˊn maˊs fuerte)InvTInvS(invariante maˊs fuerte)\begin{align} \text{Pre}_T(m) &\rightarrow \text{Pre}_S(m) \quad \text{(precondición más débil)} \\ \text{Post}_S(m) &\rightarrow \text{Post}_T(m) \quad \text{(postcondición más fuerte)} \\ \text{Inv}_T &\rightarrow \text{Inv}_S \quad \text{(invariante más fuerte)} \end{align}
Diagrama del Principio de Sustitución de Liskov mostrando la relación entre contratos de super y subtipos.

Figure 6:Diagrama del Principio de Sustitución de Liskov mostrando la relación entre contratos de super y subtipos.

Interpretación:

Ejemplo: Rectángulo vs Cuadrado

// Rectángulo (supertipo)
typedef struct {
    int ancho;
    int alto;
} rectangulo_t;

/*@ requires \valid(r);
    requires ancho > 0 && alto > 0;
    assigns r->ancho, r->alto;
    ensures r->ancho == ancho && r->alto == alto;
  */
void set_dimensiones_rect(rectangulo_t *r, int ancho, int alto);

// Cuadrado (subtipo problemático)
typedef struct {
    int lado;
} cuadrado_t;

// ¡Viola LSP! Precondición más fuerte: ancho debe ser igual a alto
void set_dimensiones_cuad(cuadrado_t *c, int ancho, int alto) {
    assert(ancho == alto); // Precondición adicional
    c->lado = ancho;
}

Este es el problema clásico del cuadrado-rectángulo: un cuadrado matemáticamente es un rectángulo, pero como tipo mutable viola LSP porque tiene restricciones adicionales.

Frame Problem y Especificación de Cambios

El Frame Problem consiste en especificar qué no cambia en una operación. Es fundamental para contratos precisos.

Cláusulas de Frame

Sintaxis:

//@ assigns x, y, z;

Semántica: Solo las variables listadas pueden cambiar; todo lo demás permanece igual.

Ejemplo:

/*@ requires \valid(arr + (0..n-1));
    requires 0 <= i < n;
    assigns arr[i];
    ensures arr[i] == valor;
    ensures \forall integer j; 0 <= j < n && j != i ==> arr[j] == \old(arr[j]);
  */
void actualizar(int arr[], int n, int i, int valor) {
    arr[i] = valor;
}

La cláusula assigns arr[i] especifica que solo arr[i] puede cambiar. El resto del arreglo permanece igual, lo cual se hace explícito en la postcondición.

Separación Lógica

La Lógica de Separación (Separation Logic) extiende Lógica de Hoare con conectivos espaciales para razonar sobre heap:

Ejemplo:

{xayb} swap(x,y) {xbya}\{x \mapsto a * y \mapsto b\}\ \texttt{swap}(x, y)\ \{x \mapsto b * y \mapsto a\}

Esto especifica que xx e yy apuntan a celdas distintas (separación) y que swap intercambia sus contenidos.

Casos de Estudio

Caso 1: Ordenamiento por Inserción

/*@ predicate sorted{L}(int *arr, integer i, integer j) =
        \forall integer k, l; i <= k < l <= j ==> arr[k] <= arr[l];
  */

/*@ predicate permutation{L1,L2}(int *arr, integer n) =
        \forall integer i; 0 <= i < n ==>
            cuenta{L1}(arr, n, arr[i]) == cuenta{L2}(arr, n, arr[i]);
  */

/*@ requires n >= 0;
    requires \valid(arr + (0..n-1));
    assigns arr[0..n-1];
    ensures sorted(arr, 0, n-1);
    ensures permutation{Pre,Post}(arr, n);
  */
void insertion_sort(int arr[], int n) {
    /*@ loop invariant 1 <= i <= n;
        loop invariant sorted(arr, 0, i-1);
        loop invariant permutation{Pre,Here}(arr, n);
        loop assigns i, arr[0..n-1];
        loop variant n - i;
      */
    for (int i = 1; i < n; i++) {
        int clave = arr[i];
        int j = i - 1;
        
        /*@ loop invariant -1 <= j < i;
            loop invariant sorted(arr, 0, j) && sorted(arr, j+2, i);
            loop invariant \forall integer k; j+2 <= k <= i ==> arr[k] >= clave;
            loop assigns j, arr[0..i];
            loop variant j + 1;
          */
        while (j >= 0 && arr[j] > clave) {
            arr[j + 1] = arr[j];
            j--;
        }
        
        arr[j + 1] = clave;
    }
}

Invariantes clave:

  1. Lazo externo: Los primeros ii elementos están ordenados

  2. Lazo interno: Se mantiene espacio para insertar clave

  3. Permutación: Los elementos del arreglo son los mismos (orden distinto)

Caso 2: Búsqueda Binaria con Demostración

/*@ requires n > 0;
    requires \valid_read(arr + (0..n-1));
    requires sorted(arr, 0, n-1);
    assigns \nothing;
    behavior found:
        assumes \exists integer i; 0 <= i < n && arr[i] == clave;
        ensures 0 <= \result < n;
        ensures arr[\result] == clave;
    behavior not_found:
        assumes \forall integer i; 0 <= i < n ==> arr[i] != clave;
        ensures \result == -1;
    complete behaviors;
    disjoint behaviors;
  */
int busqueda_binaria(const int arr[], int n, int clave) {
    int izq = 0;
    int der = n - 1;
    
    /*@ loop invariant 0 <= izq <= der + 1 <= n;
        loop invariant \forall integer i; 0 <= i < izq ==> arr[i] < clave;
        loop invariant \forall integer i; der < i < n ==> arr[i] > clave;
        loop assigns izq, der;
        loop variant der - izq;
      */
    while (izq <= der) {
        // Previene overflow: medio = (izq + der) / 2
        int medio = izq + (der - izq) / 2;
        
        if (arr[medio] == clave) {
            return medio;
        }
        
        if (arr[medio] < clave) {
            izq = medio + 1;
        } else {
            der = medio - 1;
        }
    }
    
    return -1;
}

Demostración de corrección:

Invariante:

I(0izqder+1n)I1I2I \equiv (0 \leq \text{izq} \leq \text{der} + 1 \leq n) \land I_1 \land I_2

donde: $$

I1i. 0i<izqarr[i]<claveI2i. der<i<narr[i]>clave\begin{align} I_1 &\equiv \forall i.\ 0 \leq i < \text{izq} \rightarrow \text{arr}[i] < \text{clave} \\ I_2 &\equiv \forall i.\ \text{der} < i < n \rightarrow \text{arr}[i] > \text{clave} \end{align}

$$

Inicialización: Con izq=0\text{izq} = 0 y der=n1\text{der} = n-1:

Preservación:

Terminación: (derizq)(\text{der} - \text{izq}) decrece en cada iteración y es no negativo.

Corrección: Al salir, izq>der\text{izq} > \text{der}, entonces:

Limitaciones y Desafíos

Indecidibilidad

Teorema: La corrección de programas con respecto a especificaciones en LPO es indecidible (consecuencia del Teorema de Rice).

Implicaciones:

Complejidad de las Especificaciones

Escribir especificaciones completas puede ser más difícil que escribir el código:

Ejemplo: Especificar “ordenamiento estable” requiere razonar sobre permutaciones preservando orden relativo:

sorted(arr,0,n1) permutation(arrin,arrout) i,j. (0i<j<narrin[i]=arrin[j])(π(i)<π(j))\begin{align} &\text{sorted}(\text{arr}, 0, n-1) \\ \land\ &\text{permutation}(\text{arr}_{\text{in}}, \text{arr}_{\text{out}}) \\ \land\ &\forall i, j.\ (0 \leq i < j < n \land \text{arr}_{\text{in}}[i] = \text{arr}_{\text{in}}[j]) \\ &\quad\quad \rightarrow (\pi(i) < \pi(j)) \end{align}

donde π\pi es la permutación aplicada.

Límites de la Verificación Automática

SMT Solvers tienen dificultades con:

Mejores Prácticas

Diagrama de flujo para aplicar diseño por contratos efectivamente en proyectos.

Figure 7:Diagrama de flujo para aplicar diseño por contratos efectivamente en proyectos.

Granularidad de Contratos

  1. Contratos públicos completos: APIs públicas deben tener Pre/Post completas

  2. Contratos privados opcionales: Funciones auxiliares pueden tener contratos más relajados

  3. Invariantes de clase siempre: Estructuras de datos deben tener invariantes claros

Orden de Desarrollo

Test-Driven Design by Contract:

  1. Especificar contratos: Pre/Post/Invariantes primero

  2. Escribir tests: Basados en los contratos

  3. Implementar: Código que satisface contratos

  4. Verificar: Con herramientas automáticas cuando sea posible

Contratos como Documentación

Los contratos son la mejor documentación:

Balance Costo-Beneficio

Tipo de ProyectoNivel de Contratos Recomendado
Prototipos rápidosAserciones básicas en código crítico
Software comercialPre/Post en APIs públicas, invariantes de clase
Sistemas críticosEspecificación formal completa, verificación automática
Software certificadoPruebas formales de corrección con asistentes de prueba (Coq, Isabelle)

Herramientas y Ecosistema

Lenguajes con Soporte Nativo

Herramientas para C

Frameworks de Testing

Ejercicios

Demostrá que el invariante se preserva en cada iteración.


````{solution} ejer-contratos-1
:class: dropdown

**Especificación**:

```c
/*@ requires n >= 0;
    requires n <= 12;  // Para evitar overflow en int de 32 bits
    assigns \nothing;
    ensures \result == factorial_matematico(n);
  */
int factorial(int n);

Invariante de lazo:

I(1in+1)(resultado=i1!)I \equiv (1 \leq i \leq n+1) \land (\text{resultado} = i-1!)

Demostración:

  1. Inicialización: Antes del lazo, i=1i = 1 y resultado=1\text{resultado} = 1

    I[1/i,1/resultado]=(11n+1)(1=0!)=trueI[1/i, 1/\text{resultado}] = (1 \leq 1 \leq n+1) \land (1 = 0!) = \text{true}
  2. Preservación: Asumiendo I(in)I \land (i \leq n) antes de la iteración:

    • Tenemos: resultado=(i1)!\text{resultado} = (i-1)!

    • Después de resultado *= i: resultado=(i1)!×i=i!\text{resultado}' = (i-1)! \times i = i!

    • Después de i++: i=i+1i' = i + 1

    • Nuevo invariante: resultado=(i1)!=i!\text{resultado}' = (i'-1)! = i!

  3. Terminación: Al salir, I(i>n)I \land (i > n), entonces i=n+1i = n+1 y resultado=n!\text{resultado} = n!

Postcondición satisfecha: resultado=n!\text{resultado} = n!


```{exercise}
:label: ejer-contratos-2

Encontrá el error en la siguiente especificación e implementación:

```c
/*@ requires n > 0;
    requires \valid(arr + (0..n-1));
    ensures \forall integer i; 0 <= i < n-1 ==> arr[i] <= arr[i+1];
  */
void ordenar_ascendente(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        arr[i] = i;
    }
}
```

¿La implementación satisface la especificación? ¿Es esto lo que se quería especificar?
```

````{solution} ejer-contratos-2
:class: dropdown

**Problema 1**: La implementación **sí satisface** la especificación técnicamente:
- Postcondición: $\forall i.\ 0 \leq i < n-1 \rightarrow \text{arr}[i] \leq \text{arr}[i+1]$
- Después del código: `arr = [0, 1, 2, ..., n-2, ???]`
- La postcondición es verdadera

**Problema 2**: La especificación es **demasiado débil**. Falta:
1. **Permutación**: Los elementos finales deben ser los mismos que los iniciales
2. **Frame**: No especifica qué no debe cambiar

**Especificación corregida**:

```c
/*@ requires n > 0;
    requires \valid(arr + (0..n-1));
    assigns arr[0..n-1];
    ensures sorted(arr, 0, n-1);
    ensures permutation{Pre,Post}(arr, n);
  */
void ordenar_ascendente(int arr[], int n);
```

donde:
```c
/*@ predicate sorted(int *arr, integer i, integer j) =
        \forall integer k; i <= k < j ==> arr[k] <= arr[k+1];
  */

/*@ predicate permutation{L1,L2}(int *arr, integer n) =
        multiset_same{L1,L2}(arr, 0, n-1);
  */
```

**Lección**: Especificaciones incompletas permiten implementaciones "correctas" pero inútiles.
Solution to Exercise 2

Especificación:

/*@ requires n >= 0;
    requires \valid(arr + (0..n-1));
    assigns arr[0..n-1];
    ensures \forall integer i; 0 <= i < n ==> arr[i] == \old(arr[n-1-i]);
  */
void invertir(int arr[], int n) {
    int i = 0;
    int j = n - 1;
    
    /*@ loop invariant 0 <= i <= j+1 <= n;
        loop invariant \forall integer k; 0 <= k < i ==>
            arr[k] == \old(arr[n-1-k]);
        loop invariant \forall integer k; j < k < n ==>
            arr[k] == \old(arr[n-1-k]);
        loop invariant \forall integer k; i <= k <= j ==>
            arr[k] == \old(arr[k]);
        loop assigns i, j, arr[0..n-1];
        loop variant j - i + 1;
      */
    while (i < j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
        i++;
        j--;
    }
}

Invariante: $$

I (0ij+1n) k. 0k<iarr[k]=arrold[n1k] k. j<k<narr[k]=arrold[n1k] k. ikjarr[k]=arrold[k]\begin{align} I \equiv\ &(0 \leq i \leq j+1 \leq n) \\ \land\ &\forall k.\ 0 \leq k < i \rightarrow \text{arr}[k] = \text{arr}_{\text{old}}[n-1-k] \\ \land\ &\forall k.\ j < k < n \rightarrow \text{arr}[k] = \text{arr}_{\text{old}}[n-1-k] \\ \land\ &\forall k.\ i \leq k \leq j \rightarrow \text{arr}[k] = \text{arr}_{\text{old}}[k] \end{align}

$$

Interpretación:

  • Los elementos antes de ii ya están invertidos

  • Los elementos después de jj ya están invertidos

  • Los elementos entre ii y jj aún no se han tocado

Demostración de preservación: En cada iteración:

  1. Intercambiamos arr[i] y arr[j]

  2. Incrementamos ii y decrementamos jj

  3. Los elementos recién intercambiados ahora satisfacen la propiedad de inversión

  4. Los elementos previamente intercambiados no se modifican

Al terminar (iji \geq j), todos los elementos han sido procesados y están invertidos.

Referencias y Lecturas Complementarias

Textos Fundamentales

Lógica de Primer Orden

Verificación Formal

Separation Logic

Recursos en Línea

Resumen

El Diseño por Contratos es una metodología que transforma el desarrollo de software en una disciplina ingenieril rigurosa mediante especificaciones formales basadas en Lógica de Primer Orden.

Conceptos Clave

Beneficios Prácticos

  1. Documentación precisa: Sin ambigüedades

  2. Detección temprana de errores: En el punto exacto de violación

  3. Testing dirigido: Los contratos generan casos de prueba

  4. Mantenibilidad: El comportamiento esperado es explícito

  5. Confianza: Pruebas matemáticas de corrección

Desafíos

  1. Curva de aprendizaje: Requiere familiaridad con lógica formal

  2. Costo inicial: Escribir especificaciones toma tiempo

  3. Límites de automatización: No todo es decidible

  4. Especificaciones complejas: A veces más difíciles que el código

Cuándo Usar DbC

El Diseño por Contratos no es solo una técnica, es una filosofía de desarrollo que eleva el software de artesanía a ingeniería, proporcionando las bases matemáticas para razonar rigurosamente sobre la corrección de programas.