Ternas de Hoare y demostración formal

Recursos útiles:

Pregunta central

¿Cómo se demuestra, mediante lógica, que un programa transforma todo estado de entrada permitido en un estado de salida que cumple su especificación?

Mapa de la clase

flowchart TB
    A[Especificar el problema] --> P[Precondición P]
    A --> Q[Postcondición Q]
    P --> H[Terna de Hoare P C Q]
    Q --> H
    C[Código C] --> H
    H --> CP[Corrección parcial]
    CP --> T{¿C siempre termina?}
    T -->|Sí| CT[Corrección total]
    T -->|No demostrado| SOLO[Solo corrección parcial]
    H --> R[Reglas de inferencia]
    R --> CON[Consecuencia]
    R --> CD[Conjunción y disyunción]
    R --> ASIG[Asignación]
    ASIG --> WP[Verificación hacia atrás]

La clase introduce la verificación formal mediante ternas de Hoare. El punto central es separar tres piezas: qué estados iniciales son válidos, qué código se ejecuta y qué estados finales se aceptan. Después se estudian reglas para transformar especificaciones y, finalmente, la regla de asignación que permite razonar desde la salida deseada hacia la entrada necesaria. (Diap. 1–27; láminas 55–81.)


1. Qué significa que un programa sea correcto

Un programa es correcto respecto de una especificación cuando, para toda entrada admitida, produce la salida esperada. Esta noción se concentra en el comportamiento funcional; por sí sola no afirma que el programa sea rápido, use poca memoria o tenga código optimizado. (Diap. 1.)

La demostración formal aplica reglas lógicas al código en lugar de depender únicamente de ejecuciones de prueba. Las pruebas siguen siendo útiles para encontrar fallos, pero solo cubren casos concretos; una demostración pretende abarcar todos los estados que satisfacen la especificación. Las ternas fueron propuestas por C. A. R. Hoare, también creador de QuickSort. (Diap. 2.)

flowchart TB
    E[Estado inicial s] -->|s satisface P| C[Ejecutar C]
    C --> F[Estado final s']
    F -->|s' satisface Q| OK[Resultado permitido]
    F -->|s' no satisface Q| FAIL[Contraejemplo de corrección]

Ejemplo: para un procedimiento que calcula una raíz cuadrada real puede especificarse:

La precondición excluye entradas fuera del dominio real y la postcondición describe exactamente qué salida se considera correcta. (Diap. 6.)

raiz(n):
    requiere n >= 0
    r <- calcular_raiz(n)
    asegura r >= 0 y r*r = n
    devolver r

Important

La corrección siempre es relativa a una especificación. Un programa no es “correcto en abstracto”: es correcto con respecto a una precondición y una postcondición determinadas.


2. Aserciones, precondiciones, postcondiciones y estados

Una aserción es una fórmula lógica sobre las variables del programa. Puede aparecer antes de un fragmento como precondición o después como postcondición . Las reglas de inferencia parten de aserciones conocidas y obtienen otras aserciones válidas. (Diap. 3–4.)

Ejemplos:

Un estado es una asignación de valores a todas las variables relevantes. Se escribe cuando el estado satisface la aserción . El código transforma un estado inicial en uno final; la concatenación ejecuta primero y luego . (Diap. 5 y 7.)

Ejemplos de satisfacción:

  • .
  • Un estado descrito solo por no garantiza : podría tener .
flowchart TD
    S[Estado s: x=2, y=5] --> P1{x >= 2}
    S --> P2{y = x + 3}
    S --> P3{x = 7}
    P1 -->|Verdadera| SAT[s satisface la aserción]
    P2 -->|Verdadera| SAT
    P3 -->|Falsa| NSAT[s no la satisface]

La aserción vacía

Las diapositivas escriben { } para la aserción que no restringe ningún estado. Para evitar confundirla con un conjunto vacío, aquí también se denota por (true):

También son equivalentes a las tautologías o . (Diap. 8–9.)

def satisface(estado, asercion):
    """Una aserción se interpreta como un predicado del estado."""
    return asercion(estado)
 
estado = {"x": 2, "y": 5}
assert satisface(estado, lambda s: s["x"] >= 2)
assert satisface(estado, lambda s: s["y"] == s["x"] + 3)

3. Terna de Hoare

Una terna tiene la forma:

Su lectura es: si la ejecución de comienza en un estado que satisface y termina, el estado final satisface . (Diap. 8 y 10.).

Una manera intuitiva de entender esta tripleta es como:

Donde en lenguaje coloquial podemos decir que:

Si es verdadera antes de ejecutar , y termina, entonces será verdadera después.

Ejemplo de la presentación:

Para evitar ambigüedad cuando una variable cambia, es preferible distinguir el valor inicial :

Corrección parcial y total

La corrección parcial afirma que, si el programa termina, su resultado cumple . La corrección total añade una demostración de que el programa termina para todas las entradas permitidas por . (Diap. 10–11.)

flowchart TD
    I[Estado inicial satisface P] --> EX[Ejecutar C]
    EX --> D{¿La ejecución termina?}
    D -->|Sí| Q{¿El estado final satisface Q?}
    Q -->|Sí | PAR[Corrección parcial]
    Q -->|No| INC[Programa incorrecto]
    D -->|No| PEND[La corrección parcial no decide]
    PAR --> TERM{¿Además siempre termina?}
    TERM -->|Sí| TOTAL[Corrección total]

Un fragmento finito sin ciclos ni recursión termina si sus instrucciones primitivas terminan; en ese caso, demostrar corrección parcial basta para obtener corrección total.

Formalmente la regla de fortalecimiento cumple:

Entonces se cumple:

Que en lenguaje formal se traduce a:


4. Fortaleza lógica de las aserciones

La aserción es más fuerte que cuando todo estado que satisface también satisface :

Fortalecer reduce el conjunto de estados admitidos; debilitar lo amplía. Por ejemplo, , pero la implicación inversa es falsa. (Diap. 11–12.)

AserciónEstados admitidosFuerza relativa
(false)ningunola más fuerte
pocosmás fuerte que
másmás débil que
o { }todosla más débil

(Diap. 12–13.)

flowchart TD
    F[false: ningún estado] --> A[i > 5]
    A --> B[i > 1]
    B --> C[i > 0]
    C --> T[true: todos los estados]

Cada flecha representa implicación. Cuanto más arriba, más restrictiva es la condición.

Propiedades usadas en las diapositivas: (Diap. 13–15.)

Que no obliga a que . Por ejemplo, , pero . Solo cuando ambas implicaciones son válidas, y son lógicamente equivalentes.

Notación de Inferencia

A lo largo de las notas se empleará la convención siguiente, partiendo del esquemátco:

El cual se lee como :

Si puedo demostrar las dos cosas que están arriba de la línea, entonces puedo concluir lo que está debajo de la línea.

5. Regla de consecuencia

La regla de consecuencia permite reforzar la precondición o debilitar la postcondición sin cambiar el código. (Diap. 16–19.)

Fortalecimiento de la precondición

Si el código funciona para todos los estados de , también funciona para el subconjunto descrito por .

Ejemplo: como ,

(Diap. 17.)

Como cualquier implica , la precondición vacía puede reforzarse con cualquier aserción:

(Diap. 18.)

Debilitamiento de la postcondición

Si el resultado cumple una propiedad específica, también cumple cualquier consecuencia más general. Como :

(Diap. 19.)

flowchart TB
    P1[Precondición fuerte P1] -->|P1 implica P| P[Precondición P]
    P --> H[Terna conocida P C Q]
    H --> Q[Postcondición Q]
    Q -->|Q implica Q1| Q1[Postcondición débil Q1]
    P1 -. permite concluir .-> N[Nueva terna P1 C Q1]
    Q1 -. permite concluir .-> N

6. Reglas de conjunción y disyunción

Cuando ya existen dos demostraciones para el mismo código , pueden combinarse. (Diap. 20–22.)

Conjunción

La entrada debe satisfacer ambas precondiciones y, por ello, el resultado satisface ambas postcondiciones. (Diap. 20.)

La diapositiva 21 aplica esta regla a i := i + 1. Con notación explícita para el valor previo :

Por conjunción se obtiene ; como implica , se concluye mediante debilitamiento de la postcondición:

Disyunción

Si el estado inicial cae en cualquiera de los dos casos demostrados, el estado final cumple la postcondición correspondiente y, por tanto, su disyunción. (Diap. 22.)

flowchart TD
    I[Estado inicial] --> D{¿Qué condición satisface?}
    D -->|P1| C1[Aplicar corrección P1 C Q1]
    D -->|P2| C2[Aplicar corrección P2 C Q2]
    C1 --> O[Estado final satisface Q1 o Q2]
    C2 --> O

7. Verificación hacia atrás y asignación

Para demostrar un programa secuencial se parte de la postcondición final y se calculan, en orden inverso, las condiciones necesarias antes de cada instrucción. El código se ejecuta hacia adelante, pero se verifica hacia atrás. (Diap. 23.)

flowchart TB
    P0[Precondición inicial P0] --> C1[Código C1]
    C1 --> P1[Condición intermedia P1]
    P1 --> C2[Código C2]
    C2 --> P2[Condición intermedia P2]
    P2 --> C3[Código C3]
    C3 --> Q[Postcondición final Q]
    Q -. calcular hacia atrás .-> P2
    P2 -. calcular hacia atrás .-> P1
    P1 -. calcular hacia atrás .-> P0

Axioma de asignación

Para una asignación y una postcondición , se sustituye en cada aparición libre de por :

La notación adoptada en estas notas es:

y se lee “ con sustituida por ”. Por tanto,

Las diapositivas escriben el esquema auxiliar para indicar que se usa como regla de reemplazo dentro de . No debe interpretarse como la conjunción en el estado inicial. El resultado es la precondición más débil que garantiza para esa asignación. (Diap. 24–25.)

Ejemplo 1: k := 4*a

Buscamos responder:

Es decir, se desea al final:

Sustituir por produce la precondición:

Por tanto:

(Diap. 25.)

Ejemplo 2: i := 2*i

Determinar la precondición para que la siguiente terna sea correcta:

Para obtener después de la asignación:

Luego:

(Diap. 26.)

Cálculo hacia adelante de una postcondición

Las diapositivas 26–27 presentan también el caso en que se conoce una precondición que fija los valores usados por la expresión . Si denota la igualdad producida por la asignación, , se usa la notación:

Por tanto, en esta convención:

se lee “ con los valores dados por sustituidos en ”. Por ejemplo, si y :

Así se obtiene:

Alcance de esta notación

Las diapositivas representan el reemplazo mediante el esquema auxiliar . Se trata de una instrucción de sustitución, no de una conjunción lógica. Este cálculo directo funciona cuando proporciona valores suficientes para evaluar , como en el ejemplo de las diapositivas. Para una precondición arbitraria, la postcondición más fuerte puede necesitar conservar valores anteriores mediante una variable fresca o un cuantificador existencial; no se obtiene aplicando mecánicamente .

Ejemplo 3: varias instrucciones

x := x + 1
y := 2 * x

Queremos la postcondición :

  1. Antes de y := 2*x se necesita , es decir, .
  2. Antes de x := x+1 se necesita , es decir, .
flowchart RL
    Q[y > 10] --> S2[Sustituir y por 2*x]
    S2 --> M[x > 5]
    M --> S1[Sustituir x por x+1]
    S1 --> P[x > 4]

Un esquema para un verificador simbólico de asignaciones y secuencias sería:

wp(código, Q):
    si código es asignación V := E:
        devolver sustituir(Q, V, E)
 
    si código es secuencia C1 ; C2:
        condición_intermedia <- wp(C2, Q)
        devolver wp(C1, condición_intermedia)

Las diapositivas 26–27 también ilustran este razonamiento hacia adelante con y k := 4*a, que permite concluir . En general, el cálculo hacia atrás mediante es la regla básica del axioma de asignación y evita confundir valores anteriores y posteriores.


8. Precisiones de notación y correcciones

Diapositiva 11

Dice que la distinción entre corrección parcial y total es esencial para programas que “no incluyan” ciclos o recursión. Por el contexto debe decir que incluyan ciclos o recursión: ahí es donde la terminación deja de ser inmediata.

Aserción vacía, diapositivas 4, 8, 9, 12 y 18

{ } significa , una fórmula verdadera en todos los estados; no significa “falso” ni el conjunto matemático sin elementos. La aserción más fuerte es , porque ningún estado la satisface.

Valores viejos y nuevos, diapositiva 21

La expresión posterior i = i + 1 es ambigua si ambos i designan el estado final. Debe escribirse , o evitar variables históricas usando directamente el axioma de asignación.

Condición inicial, diapositiva 24

No es necesario que la precondición declarada sea literalmente igual a la calculada. Basta que sea suficientemente fuerte: si , entonces es válida por la regla de consecuencia.

Cálculo hacia adelante, diapositivas 26–27

Sustituir directamente valores conocidos funciona en el ejemplo a=3; k:=4*a, escrito como . Para programas generales, la postcondición más fuerte puede requerir conservar el valor anterior mediante una variable fresca o un cuantificador existencial.


9. Referencia diapositiva por diapositiva

La numeración “diap.” cuenta las 27 páginas del PDF; “lámina” conserva el número impreso por la presentación.

Diap.LáminaContenido principalSección del apunte
155Demostración formal y sentido funcional de corrección§1
256Ternas de Hoare y nota biográfica§1
357Inferencia, precondiciones y postcondiciones§2
458Aserciones y notación §2
559Código, composición secuencial y estado§2
660Condiciones de entrada/salida; raíz cuadrada§1
761Satisfacción de una aserción por un estado§2
862Forma y aserción vacía§2–3
963Propiedades de la aserción vacía§2
1064Corrección parcial y total§3
1165Terminación sin ciclos; relación de fortalecimiento§3–4 y precisiones
1266Aserciones fuertes/débiles y conjuntos de estados§4
1367, transitividad y conjunción contextual§4
1468Reflexividad y ausencia de implicación inversa§4
1569Negación, equivalencia y preservación por conjunción§4
1670Fortalecimiento de precondiciones§5
1771Ejemplo §5
1872Reforzar la precondición vacía§5
1973Debilitamiento de postcondiciones§5
2074Regla de conjunción§6
2175Ejercicio de incremento y conjunción§6 y precisiones
2276Regla de disyunción§6
2377Verificación en sentido inverso a la ejecución§7
2478Asignación y comienzo de la regla de sustitución§7 y precisiones
2579Sustitución; ejemplo k := 4*a§7
2680Ejemplo i := 2*i; cálculo hacia adelante§7 y precisiones
2781Ejemplo de postcondición k=12§7 y precisiones

Complemento de AllLectures: la sustitución sin atajos

Las diapositivas nuevas distinguen dos afirmaciones que conviene no mezclar:

  • : la terna es válida según la semántica.
  • : la terna es demostrable con las reglas de Hoare.

Un sistema correcto (sound) no permite demostrar ternas inválidas. Esta distinción explica por qué no basta que una regla “parezca intuitiva”.

Ejercicio guiado: detectar la falacia hacia adelante

Se propone erróneamente:

[ {P}\ V:=E\ {P^{V}_{E}}. ]

Toma , y :

  1. La supuesta regla produciría .
  2. La postcondición es falsa en todo estado.
  3. Por tanto, la regla propuesta no puede ser correcta.
  4. La regla válida parte de la meta y calcula antes de la asignación.

Práctica inmediata

Para y meta , sustituye : , luego . Comprueba con y con el valor frontera .

Continúa con ejercicios graduados en la guía paso a paso.

Conceptos atómicos

Dudas para revisar

  • ¿Qué reglas de Hoare se usarán después para condicionales y ciclos? Véanse condicionales y ciclos.
  • ¿Cómo se construye un invariante de ciclo y una función variante para demostrar terminación? Véase Función variante.
  • ¿El curso empleará o para la aserción verdadera en ejercicios futuros?

Resumen después de clase

Una terna de Hoare relaciona una precondición, un fragmento de código y una postcondición. La corrección parcial garantiza el resultado cuando el código termina; la total agrega terminación. La implicación entre aserciones permite reforzar entradas y debilitar salidas, mientras que las reglas de conjunción y disyunción combinan demostraciones. Para asignaciones, la precondición se calcula sustituyendo hacia atrás desde la postcondición, lo que convierte la verificación de una secuencia en una cadena de obligaciones lógicas.

Referencia