La semántica de transformadores de predicados fue introducida por Edsger Dijkstra en su artículo seminal " Ordenes guardadas, no determinación y derivación formal de programas ". Define la semántica de un paradigma de programación imperativa asignando a cada enunciado en este lenguaje un transformador de predicados correspondiente : una función total entre dos predicados en el espacio de estados del enunciado. En este sentido, la semántica de transformadores de predicados es un tipo de semántica denotacional . En realidad, en los comandos guardados , Dijkstra utiliza solo un tipo de transformador de predicados: las conocidas precondiciones más débiles (ver más abajo).
Además, la semántica de transformadores de predicados es una reformulación de la lógica de Floyd-Hoare . Mientras que la lógica de Hoare se presenta como un sistema deductivo , la semántica de transformadores de predicados (ya sea por precondiciones más débiles o por poscondiciones más fuertes, ver más abajo) son estrategias completas para construir deducciones válidas de la lógica de Hoare. En otras palabras, proporcionan un algoritmo eficaz para reducir el problema de verificar un triple de Hoare al problema de probar una fórmula de primer orden . Técnicamente, la semántica de transformadores de predicados realiza una especie de ejecución simbólica de enunciados en predicados: la ejecución se ejecuta hacia atrás en el caso de las precondiciones más débiles, o hacia adelante en el caso de las poscondiciones más fuertes.
Para un enunciado S y una poscondición R , una precondición más débil es un predicado Q tal que para cualquier precondición P , si y solo si . En otras palabras, es el requisito "más flexible" o menos restrictivo necesario para garantizar que R se cumple después de S . La unicidad se deduce fácilmente de la definición: si tanto Q como Q' son precondiciones más débiles, entonces por la definición tal y tal , y por lo tanto . A menudo usamos para denotar la precondición más débil para el enunciado S con respecto a una poscondición R .
Usamos T para denotar el predicado que es verdadero en todas partes y F para denotar el que es falso en todas partes. No deberíamos, al menos conceptualmente, confundirnos con una expresión booleana definida por alguna sintaxis del lenguaje, que también podría contener verdadero y falso como escalares booleanos. Para tales escalares, necesitamos hacer una conversión de tipo de modo que tengamos T = predicado(verdadero) y F = predicado(falso). Tal promoción se lleva a cabo a menudo de manera casual, por lo que la gente tiende a tomar T como verdadero y F como falso.
A continuación, damos dos condiciones previas más débiles equivalentes para la declaración de asignación. En estas fórmulas, es una copia de R donde las ocurrencias libres de x se reemplazan por E . Por lo tanto, aquí, la expresión E se convierte implícitamente en un término válido de la lógica subyacente: es, por lo tanto, una expresión pura , totalmente definida, terminante y sin efectos secundarios.
Siempre que E esté bien definido, simplemente aplicamos la llamada regla de un punto en la versión 1. Entonces
La primera versión evita una posible duplicación de x en R , mientras que la segunda versión es más simple cuando hay como máximo una única ocurrencia de x en R. La primera versión también revela una profunda dualidad entre la precondición más débil y la poscondición más fuerte (ver más abajo).
Un ejemplo de un cálculo válido de wp (usando la versión 2) para asignaciones con una variable x de valor entero es:
Esto significa que para que la poscondición x > 10 sea verdadera después de la asignación, la precondición x > 15 debe ser verdadera antes de la asignación. Esta es también la "precondición más débil", ya que es la restricción "más débil" sobre el valor de x que hace que x > 10 sea verdadero después de la asignación.
Por ejemplo,
A modo de ejemplo:
Ignorando la terminación por un momento, podemos definir la regla para la precondición liberal más débil , denotada wlp , usando un predicado INV , llamado Loop INV ariant , típicamente suministrado por el programador:
Para demostrar que esto es totalmente correcto, también tenemos que demostrar que el bucle termina. Para ello, definimos una relación bien fundada en el espacio de estados denotada como ( wfs , <) y definimos una función variante vf , de modo que tengamos:
De manera informal, en la conjunción anterior de tres fórmulas:
Sin embargo, la conjunción de esos tres no es una condición necesaria. Exactamente, tenemos
En realidad, el lenguaje de órdenes vigiladas de Dijkstra (GCL) es una extensión del lenguaje imperativo simple dado hasta aquí con enunciados no deterministas. De hecho, el GCL pretende ser una notación formal para definir algoritmos. Los enunciados no deterministas representan elecciones que quedan en manos de la implementación real (en un lenguaje de programación eficaz): las propiedades probadas en enunciados no deterministas están aseguradas para todas las posibles elecciones de implementación. En otras palabras, las precondiciones más débiles de los enunciados no deterministas aseguran
Tenga en cuenta que las definiciones de la condición previa más débil dadas anteriormente (en particular para el bucle while ) preservan esta propiedad.
La selección es una generalización de la declaración if :
[ cita requerida ]
Aquí, cuando dos guardias y son simultáneamente verdaderos, entonces la ejecución de esta declaración puede ejecutar cualquiera de las declaraciones asociadas o .
La repetición es una generalización de la declaración while de manera similar.
El cálculo de refinamiento extiende GCL con la noción de enunciado de especificación . Sintácticamente, preferimos escribir un enunciado de especificación como
que especifica un cálculo que comienza en un estado que satisface pre y se garantiza que terminará en un estado que satisface post cambiando solo x . Llamamos constante lógica a una constante empleada para ayudar en una especificación. Por ejemplo, podemos especificar un cálculo que incremente x en 1 como
Otro ejemplo es el cálculo de la raíz cuadrada de un número entero.
La declaración de especificación parece una primitiva en el sentido de que no contiene otras declaraciones. Sin embargo, es muy expresiva, ya que pre y post son predicados arbitrarios. Su condición previa más débil es la siguiente:
Combina la idea sintáctica de Morgan con la idea de nitidez de Bijlsma, Matthews y Wiltink. [1] La ventaja de esto es su capacidad de definir wp de goto L y otras declaraciones de salto. [2]
La formalización de sentencias de salto como goto L es un proceso muy largo y accidentado. Una creencia común parece indicar que la sentencia goto solo podría argumentarse de manera operacional. Esto probablemente se deba a que no se reconoce que goto L es realmente milagroso (es decir, no estricto) y no sigue la Ley de los Milagros Excluidos acuñada por Dijkstra, tal como se define en sí misma. Pero disfruta de una visión operacional extremadamente simple desde la perspectiva de la condición previa más débil, lo cual fue inesperado. Definimos
Para goto L la ejecución transfiere el control a la etiqueta L en la que la precondición más débil tiene que cumplirse. La forma en que se hace referencia a wpL en la regla no debe tomarse como una gran sorpresa. Es solo para algún Q calculado hasta ese punto. Esto es como cualquier regla wp, que usa declaraciones constituyentes para dar definiciones de wp, aunque goto L parece un primitivo. La regla no requiere la unicidad de las ubicaciones donde wpL se cumple dentro de un programa, por lo que teóricamente permite que la misma etiqueta aparezca en múltiples ubicaciones siempre que la precondición más débil en cada ubicación sea la misma wpL. La declaración goto puede saltar a cualquiera de esas ubicaciones. Esto realmente justifica que podríamos colocar las mismas etiquetas en la misma ubicación varias veces, como , que es lo mismo que . Además, no implica ninguna regla de alcance, lo que permite un salto al cuerpo de un bucle, por ejemplo. Calculemos wp del siguiente programa S, que tiene un salto al cuerpo del bucle.
wp(hacer x > 0 → L: x := x-1 od; si x < 0 → x := -x; ir a L ⫿ x ≥ 0 → omitir fi, publicar) = { reglas de composición secuencial y alternancia } wp(do x > 0 → L: x := x-1 od, (x<0 ∧ wp(x := -x; ir a L, publicar)) ∨ (x ≥ 0 ∧ publicar) = {composición secuencial, goto, reglas de asignación} wp(haz x > 0 → L: x := x-1 od, x<0 ∧ wpL(x ← -x) ∨ x≥0 ∧ publicación) = { regla de repetición } La solución más fuerte de Z: [ Z ≡ x > 0 ∧ wp(L: x := x-1, Z) ∨ x < 0 ∧ wpL(x ← -x) ∨ x=0 ∧ publicación ] = { regla de asignación, encontrada wpL = Z(x ← x-1) } La solución más fuerte de Z: [ Z ≡ x > 0 ∧ Z(x ← x-1) ∨ x < 0 ∧ Z(x ← x-1) (x ← -x) ∨ x=0 ∧ publicación] = { sustitución } La solución más fuerte de Z:[ Z ≡ x > 0 ∧ Z(x ← x-1) ∨ x < 0 ∧ Z(x ← -x-1) ∨ x=0 ∧ publicación ] = { resolver la ecuación por aproximación } publicación(x ← 0)
Por lo tanto,
wp(S, publicación) = publicación(x ← 0).
Una variante importante de la precondición más débil es la precondición liberal más débil , que produce la condición más débil bajo la cual S no termina o establece R. Por lo tanto, se diferencia de wp en que no garantiza la terminación. Por lo tanto, corresponde a la lógica de Hoare en corrección parcial: para el lenguaje de enunciados dado anteriormente, wlp difiere de wp solo en while-loop , en que no requiere una variante (ver arriba).
Dado S una declaración y R una precondición (un predicado sobre el estado inicial), entonces es su postcondición más fuerte : implica cualquier postcondición satisfecha por el estado final de cualquier ejecución de S, para cualquier estado inicial que satisfaga R. En otras palabras, un triple de Hoare es demostrable en la lógica de Hoare si y solo si se cumple el predicado siguiente:
Por lo general, las poscondiciones más fuertes se utilizan con corrección parcial. Por lo tanto, tenemos la siguiente relación entre las precondiciones liberales más débiles y las poscondiciones más fuertes:
Por ejemplo, en la tarea tenemos:
Arriba, la variable lógica y representa el valor inicial de la variable x . Por lo tanto,
En secuencia, parece que sp corre hacia adelante (mientras que wp corre hacia atrás):
Leslie Lamport ha sugerido win y sin como transformadores de predicados para la programación concurrente . [3]
En esta sección se presentan algunas propiedades características de los transformadores de predicados. [4] A continuación, S denota un transformador de predicados (una función entre dos predicados en el espacio de estados) y P un predicado. Por ejemplo, S(P) puede denotar wp(S,P) o sp(S,P) . Mantenemos x como la variable del espacio de estados.
Los transformadores de predicado de interés ( wp , wlp y sp ) son monótonos . Un transformador de predicado S es monótono si y solo si:
Esta propiedad está relacionada con la regla de consecuencia de la lógica de Hoare .
Un transformador de predicado S es estricto si y solo si:
Por ejemplo, wp se hace estricto artificialmente, mientras que wlp generalmente no lo es. En particular, si la declaración S no puede terminar, entonces es satisfacible. Tenemos
De hecho, T es un invariante válido de ese bucle.
Los transformadores de predicados no estrictos pero monótonos o conjuntivos se denominan milagrosos y también se pueden utilizar para definir una clase de construcciones de programación, en particular, sentencias de salto, por las que Dijkstra no se preocupaba tanto. Esas sentencias de salto incluyen goto L directo, break y continue en un bucle y sentencias de retorno en un cuerpo de procedimiento, manejo de excepciones, etc. Resulta que todas las sentencias de salto son milagros ejecutables, [5] es decir, se pueden implementar pero no son estrictas.
Un transformador de predicado S está terminando si:
En realidad, esta terminología sólo tiene sentido para transformadores de predicados estrictos: de hecho, es la condición previa más débil que garantiza la terminación de S.
Parece que sería más apropiado llamar a esta propiedad no abortante : con total corrección, la no terminación es aborto, mientras que con corrección parcial, no lo es.
Un transformador de predicado S es conjuntivo si y solo si:
Este es el caso de , incluso si la declaración S no es determinista como una declaración de selección o una declaración de especificación.
Un transformador de predicado S es disyuntivo si y solo si:
En general, este no es el caso cuando S no es determinista. De hecho, considere una declaración no determinista S que elige un booleano arbitrario. Esta declaración se da aquí como la siguiente declaración de selección :
Luego, se reduce a la fórmula .
Por lo tanto, se reduce a la tautología
Considerando que la fórmula se reduce a la proposición errónea .
En la semántica de los transformadores de predicados, las expresiones están restringidas a términos de la lógica (ver arriba). Sin embargo, esta restricción parece demasiado fuerte para la mayoría de los lenguajes de programación existentes, donde las expresiones pueden tener efectos secundarios (llamada a una función que tiene un efecto secundario), pueden no terminar o abortar (como la división por cero ). Hay muchas propuestas para extender las precondiciones más débiles o las poscondiciones más fuertes para los lenguajes de expresión imperativa y en particular para las mónadas .
Entre ellos, la teoría de tipos de Hoare combina la lógica de Hoare para un lenguaje similar a Haskell , la lógica de separación y la teoría de tipos . [9] Este sistema está implementado actualmente como una biblioteca Coq llamada Ynot . [10] En este lenguaje, la evaluación de expresiones corresponde a los cálculos de las postcondiciones más fuertes .
Los transformadores de predicados probabilísticos son una extensión de los transformadores de predicados para programas probabilísticos . De hecho, estos programas tienen muchas aplicaciones en criptografía (ocultación de información mediante ruido aleatorio) y sistemas distribuidos (ruptura de simetría). [11]