Featured image of post ¿Por qué podemos demostrar matemáticamente que un programa no tiene ningún bug sin ejecutarlo ni una sola vez? La ciencia de la verificación formal

¿Por qué podemos demostrar matemáticamente que un programa no tiene ningún bug sin ejecutarlo ni una sola vez? La ciencia de la verificación formal

Desde la máxima de Dijkstra y el teorema de Rice hasta la lógica de Floyd-Hoare, los solucionadores SMT Z3, el Model Checking temporal (TLA+), el desastre del cohete Ariane 5, la interpretación abstracta (conexiones de Galois) y los asistentes de pruebas (Coq, Lean 4, CompCert, seL4).

1. Los límites del testing y el muro de la indecidibilidad: por qué pasar 1.000.000 de pruebas no demuestra nada

1.1 La máxima inmortal de Dijkstra y la explosión combinatoria

En 1969, el ganador del Premio Turing Edsger W. Dijkstra pronunció en la conferencia de Ingeniería de Software de la OTAN la sentencia más célebre de la informática:

“Program testing can be used to show the presence of bugs, but never to show their absence!" (¡Las pruebas de programas pueden usarse para mostrar la presencia de errores, pero nunca para mostrar su ausencia!)

Consideremos una función pura elemental f(x, y) que recibe dos enteros con signo de 64 bits (int64_t). El número total de entradas posibles es:

$$N_{\text{inputs}} = 2^{64} \times 2^{64} = 2^{128} \approx 3,4028 \times 10^{38}$$

Incluso con una supercomputadora capaz de ejecutar mil millones ($10^9$) de pruebas por segundo, probar todas las combinaciones llevaría $1,08 \times 10^{22}$ años, ¡unas 780 mil millones de veces la edad del universo! Si añadimos memoria dinámica, red e intercalaciones (interleavings) no deterministas de hilos concurrentes, el número de rutas de ejecución diverge hacia el infinito ($\infty$).

En 2006, Joshua Bloch (Google) reveló un error que permaneció oculto durante 9 años en la biblioteca estándar de Java (java.util.Arrays.binarySearch): el famoso bug de desbordamiento de enteros en la búsqueda binaria:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static int binarySearch(int[] a, int key) {
    int low = 0;
    int high = a.length - 1;
    while (low <= high) {
        int mid = (low + high) / 2; // ★ BUG FATAL: ¡desbordamiento negativo cuando low + high >= 2^31!
        int midVal = a[mid];
        if (midVal < key) low = mid + 1;
        else if (midVal > key) high = mid - 1;
        else return mid;
    }
    return -(low + 1);
}

Cuando low + high supera el máximo de un entero con signo de 32 bits ($2^{31} - 1 = 2.147.483.647$), la suma se desborda hacia un valor negativo en complemento a dos y lanza ArrayIndexOutOfBoundsException.

1.2 El problema de la parada de Turing (1936) y el teorema de Rice (1953)

¿Por qué no construir un analizador estático universal que lea el código de cualquier programa $P$ y decida con 100 % de exactitud si tiene errores? El problema de la parada de Alan Turing (1936) y el teorema de Rice (1953) demuestran que es imposible:

$$\text{Teorema (Rice, 1953): Toda propiedad semántica no trivial } \mathcal{S} \text{ de las funciones computables es indecidible.}$$

Ningún verificador puede ofrecer simultáneamente (1) automatización total, (2) corrección / solidez (Soundness: cero falsos negativos) y (3) completitud (Completeness: cero falsos positivos).

1.3 Cómo supera la verificación formal el teorema de Rice: sobreaproximación sólida

La verificación formal mantiene de forma innegociable la solidez (Soundness): si certifica que un programa es "SAFE", está matemáticamente garantizado al 100 % libre de errores en sus $\infty$ ejecuciones. Para lograrlo automáticamente, calcula una sobreaproximación sólida (Sound Over-Approximation) $\mathcal{R}_{\text{approx}} \supseteq \mathcal{R}_{\text{exact}}$ y demuestra que no interseca el conjunto de estados peligrosos $\mathcal{B}$:

$$\mathcal{R}_{\text{approx}} \cap \mathcal{B} = \emptyset \implies \mathcal{R}_{\text{exact}} \cap \mathcal{B} = \emptyset$$
  flowchart TD
    A["Conjunto exacto de estados alcanzables R_exact de P (Indecidible por Rice)"] --> B["Los 4 Pilares de la Verificación Formal"]
    B --> C["Pilar 1: Verificación Deductiva (Lógica de Hoare y SMT)"]
    B --> D["Pilar 2: Model Checking (Lógica Temporal y TLA+)"]
    B --> E["Pilar 3: Interpretación Abstracta (Conexiones de Galois y Astrée)"]
    B --> F["Pilar 4: Demostradores Interactivos (Coq / Lean 4 / Isabelle)"]
    C --> C1["Invariantes de bucle I + prueba automática de VCs con el solver SMT Z3"]
    D --> D1["Estructura de Kripke M; exploración simbólica de estados vía BDDs/SAT/IC3"]
    E --> E1["Sobreaproximación automática R_exact ⊆ γ(a#) probando γ(a#) ∩ Bad = ∅"]
    F --> F1["Isomorfismo de Curry-Howard: pruebas matemáticas verificadas por un micronúcleo"]

2. Pilar 1: Verificación deductiva — Lógica de Floyd-Hoare, precondición más débil y solucionadores SMT

2.1 La terna de Hoare, invariantes de bucle y funciones de rango

En 1969, C. A. R. Hoare formuló la lógica de Floyd-Hoare basada en la terna de Hoare:

$$\{P\}\; C \;\{Q\}$$
  • Corrección parcial: Si la precondición $P$ se cumple antes de ejecutar $C$ y si $C$ termina, entonces la postcondición $Q$ se cumple al finalizar.
  • Corrección total: Bajo $P$, $C$ termina garantizadamente y cumple $Q$.

Las reglas fundamentales incluyen el axioma de asignación $\{\,Q[E/x]\,\}\; x := E \;\{\,Q\,\}$ y la regla del bucle while con un invariante de bucle $I$ y una función de rango (variante) $V \in \mathbb{N}$ estrictamente decreciente:

$$\frac{\{I \land B \land V = v_0\}\; S \;\{I \land V < v_0\} \qquad (I \land B) \implies V \ge 0}{[I]\; \text{while } B \text{ do } S \;[I \land \neg B]}$$

Mediante el cálculo de la precondición más débil (Weakest Precondition, $\text{wp}(C, Q)$, Dijkstra 1975), la verificación se transforma en condiciones de verificación (VCs) puramente matemáticas: $P \implies \text{wp}(C, Q)$.

2.2 Verificación formal de la búsqueda binaria en Dafny y Z3

Al corregir el punto medio a mid := low + (high - low) / 2 en Dafny, el solucionador SMT Z3 (arquitectura DPLL(T) sobre funciones no interpretadas EUF, aritmética lineal entera LIA, vectores de bits $\mathcal{T}_{\text{BV}}$ y arrays $\mathcal{T}_{\text{A}}$) demuestra automáticamente en milisegundos la ausencia total de desbordamientos y errores de índice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
method BinarySearch(a: array<int>, key: int) returns (index: int)
  requires forall i, j :: 0 <= i < j < a.Length ==> a[i] <= a[j]
  ensures 0 <= index ==> index < a.Length && a[index] == key
  ensures index < 0 ==> forall k :: 0 <= k < a.Length ==> a[k] != key
{
  var low := 0;
  var high := a.Length;
  while low < high
    invariant 0 <= low <= high <= a.Length
    invariant forall i :: 0 <= i < a.Length && !(low <= i < high) ==> a[i] != key
    decreases high - low
  {
    var mid := low + (high - low) / 2; // ¡Z3 demuestra que no hay overflow y 0 <= mid < a.Length!
    if a[mid] < key { low := mid + 1; }
    else if key < a[mid] { high := mid; }
    else { return mid; }
  }
  return -1;
}

3. Pilar 2: Model Checking y lógica temporal — Exploración exhaustiva de sistemas concurrentes

3.1 Estructuras de Kripke y lógica temporal LTL / CTL

En 1981, Clarke, Emerson y Sifakis (Premio Turing 2007) crearon el Model Checking, mientras Leslie Lamport (Premio Turing 2013) desarrolló TLA+. El sistema se modela como una estructura de Kripke $M = (S, S_0, R, L)$ y sus propiedades se especifican en LTL o CTL:

  1. Seguridad (Safety — «Nada malo ocurre jamás»): Exclusión mutua: $$\text{LTL}: \quad \mathbf{G}\, \neg (\text{crit}_1 \land \text{crit}_2) \qquad \text{CTL}: \quad \mathbf{AG}\, \neg (\text{crit}_1 \land \text{crit}_2)$$
  2. Vivacidad (Liveness — «Algo bueno acaba ocurriendo siempre»): Ausencia de inanición: $$\text{LTL}: \quad \mathbf{G}\, (\text{req}_1 \implies \mathbf{F}\, \text{crit}_1) \qquad \text{CTL}: \quad \mathbf{AG}\, (\text{req}_1 \implies \mathbf{AF}\, \text{crit}_1)$$

3.2 Autómatas de Büchi, BDDs, IC3 y el éxito de TLA+ en AWS

El verificador traduce $\neg \varphi$ en un autómata de Büchi $A_{\neg \varphi}$ y comprueba si el producto síncrono es vacío ($\mathcal{L}(M \times A_{\neg \varphi}) = \emptyset$). Para superar la explosión de estados, se emplean diagramas de decisión binaria (BDDs, Bryant 1986 / McMillan 1990) capaces de explorar $10^{20}$ estados, Bounded Model Checking (BMC) e IC3 / PDR.

En Amazon Web Services (AWS), el uso de TLA+ (TLC) en S3 y DynamoDB permitió descubrir, antes de escribir una sola línea de código en producción, un sutil bug distribuido que requería una secuencia exacta de 35 pasos intercalados de fallos de red y retrasos de mensajes.


4. Pilar 3: Interpretación abstracta y la lección del desastre del cohete Ariane 5

4.1 4 de junio de 1996: Por qué el cohete Ariane 5 (370 millones $) se autodestruyó a los 37 segundos

El 4 de junio de 1996, el vuelo inaugural (Vuelo 501) del cohete europeo Ariane 5 explotó 37 segundos después del despegue, causando pérdidas por 370 millones de dólares. El sistema de referencia inercial (SRI) reutilizaba código Ada del Ariane 4 que convertía una variable en coma flotante de 64 bits de velocidad horizontal (BH) en un entero con signo de 16 bits ($-32.768 \dots +32.767$). Debido a la mayor aceleración del Ariane 5, BH superó $32.767$ y disparó una excepción hardware de desbordamiento (Operand Error) no capturada. Tanto el computador de respaldo como el principal se apagaron y enviaron un patrón de bits de diagnóstico que el computador de a bordo (OBC) interpretó erróneamente como un ángulo extremo de actitud, girando las toberas al máximo y desintegrando el cohete.

4.2 La interpretación abstracta de Cousot (1977), conexiones de Galois y Astrée en el Airbus A380

Creada en 1977 por Patrick Cousot y Radhia Cousot, la interpretación abstracta conecta el retículo concreto $(\mathcal{C}, \subseteq) = (\mathcal{P}(\Sigma), \subseteq)$ con un dominio abstracto $(\mathcal{A}, \sqsubseteq^\#)$ (intervalos $[a,b]$, octágonos $\pm x \pm y \le c$, poliedros convexos) mediante una conexión de Galois:

$$(\mathcal{C}, \subseteq) \underset{\alpha}{\overset{\gamma}{\leftrightarrows}} (\mathcal{A}, \sqsubseteq^\#) \quad \text{tal que} \quad \alpha(S) \sqsubseteq^\# a^\# \iff S \subseteq \gamma(a^\#)$$

Con la condición de solidez local $\alpha(F(\gamma(a^\#))) \sqsubseteq^\# F^\#(a^\#)$ y el operador de ensanchamiento (Widening $\nabla$), el punto fijo abstracto envuelve de forma 100 % segura todos los estados alcanzables:

$$\mathcal{R}_{\text{exact}} = \text{lfp}\, F \subseteq \gamma(\text{lfp}\, F^\#)$$
  flowchart TD
    subgraph Concrete["Dominio Concreto C = P(Σ) (Infinito / Indecidible)"]
        S["Conjunto de estados concretos S"] -->|"Transición concreta F"| FS["Estados siguientes F(S)"]
        LFP_C["Estados alcanzables exactos R_exact = lfp F"]
    end
    subgraph Abstract["Dominio Abstracto A (Intervalos / Octágonos / Elipsoides)"]
        A_elem["Estado abstracto a#"] -->|"Transición abstracta F#"| FA["Siguiente estado abstracto F#(a#)"]
        LFP_A["Punto fijo abstracto lfp F# (Convergencia vía Widening ∇)"]
    end
    S -->|"Abstracción α(S)"| A_elem
    FA -->|"Concretización γ(F#(a#)) ⊇ F(S)"| FS
    LFP_A -->|"Garantía sólida: R_exact ⊆ γ(lfp F#)"| LFP_C

El analizador estático Astrée verificó automáticamente el software de control de vuelo Fly-by-Wire del Airbus A380 (cerca de 1.000.000 de líneas de código C) con cero falsas alarmas, demostrando matemáticamente la ausencia total de desbordamientos, divisiones por cero o accesos fuera de límites.


5. Pilar 4: Demostradores interactivos de teoremas (Coq / Lean 4 / Isabelle) y comparativa global

Basados en el isomorfismo de Curry-Howard (las proposiciones son tipos, las pruebas son programas), Coq (Rocq), Lean 4 e Isabelle/HOL lograron dos hitos históricos:

  1. CompCert (Xavier Leroy, INRIA): Compilador optimizador de C verificado en Coq. En el estudio de fuzzing Csmith (PLDI 2011), se hallaron más de 325 errores de compilación en GCC y LLVM, ¡pero cero errores en el middle-end verificado de CompCert!
  2. seL4 (Gerwin Klein et al.): Primer micronúcleo de sistema operativo verificado formalmente en Isabelle/HOL hasta el código binario ARM/RISC-V.
Paradigma de verificaciónBase matemáticaHerramientas y lenguajesAutomatizaciónCasos industriales claveFortaleza y compromiso
1. Verificación deductivaLógica de Hoare, cálculo $\text{wp}$, SMTDafny, Frama-C, SPARK Ada, Verus, Z3Semiautomática (Invariantes + SMT)Criptografía, control aéreo, AWS CedarDemuestra corrección funcional profunda en el código.
2. Model CheckingEstructuras de Kripke, LTL/CTL, Büchi, BDDsTLA+ (TLC), SPIN, NuSMV, CBMCAutomática (en modelos finitos)AWS S3 y DynamoDB, Paxos/Raft, CPUsGenera contraejemplos exactos para errores concurrentes.
3. Interpretación abstractaRetículos, conexiones de Galois $(\alpha,\gamma)$, $\nabla$Astrée, Polyspace, Infer (Meta)Totalmente automáticaAirbus A380/A350 Fly-by-Wire, C embebidoEscala a más de 1.000.000 de líneas de C sin anotaciones.
4. Prueba interactivaIsomorfismo de Curry-Howard, teoría de tiposCoq (Rocq), Lean 4, Isabelle/HOLInteractiva (Humano + Tácticas + IA)Compilador CompCert, micronúcleo seL4Expresividad ilimitada y certeza absoluta vía micronúcleo.
comments powered by Disqus