跳至主要內容

Hoare逻辑

Mr.Lexon大约 6 分钟proof

Hoare逻辑

简介

Hoare逻辑(Hoare Logic)是一种被广泛应用的工具。由英国计算机科学家C.A.R. Hoare于20世纪60年代提出,Hoare逻辑通过引入前置条件和后置条件的形式化描述,为程序的部分正确性提供了系统化的证明方法。Hoare逻辑的优势在于其简洁和高效。通过明确的前置条件和后置条件,开发者可以对程序的行为进行描述和验证。

三元组

三元组是Hoare逻辑的核心,其中P表示前置条件,Q表示后置条件,C表示程序或程序片段。

{P} C {Q}

该三元组的含义是:如果在满足前置条件P的情况下执行程序C,那么在程序C执行完毕后,后置条件Q应当成立。就是在函数调用时需要满足P条件才能调用,并且返回的一定是Q条件,注意在实际使用时,他是静态调用的,如果对应定义的函数的输入是由IO所决定的,那么这个Hoare就没用了。因为他能检测的都是编译期就能发现出来的问题,但是如果是运行时的,那么他就无能为力了。以下是示例:

有效示例

// 导入 Prusti 的 prelude,包含 #[requires] 和 #[ensures] 等宏 
use prusti_contracts::*; 
/// 这个函数计算输入加一 
/// { P: x >= 0 } 
/// fn add_one(x: i32) -> i32 { 
///     let y = x + 1; 
///     y // return_value 
/// } 
/// { Q: return_value == x + 1 } 

#[requires(x >= 0)] // P: 前置条件 - 要求输入的 x 是非负数 
#[ensures(return_value == x + 1)] // Q: 后置条件 - 保证返回的值等于 x 加一 
fn add_one(x: i32) -> i32 { 
	// C: 程序片段 - 这里是函数的实现 
	let y = x + 1; 
	y 
	// 函数的返回值对应后置条件中的return_value 
} 
// Prusti 会从 main 函数开始验证,或者验证你指定的函数 
fn main() { 
	// 可以在这里调用 add_one,Prusti 会检查调用点是否满足前置条件 
	// 如果你只关心 add_one 函数本身的验证,可以保持 main 函数为空或简单 
	let a = 10;
	let b = add_one(a); 
	// Prusti 会检查是否满足 add_one 的 requires(a >= 0) 
	println!("{} + 1 = {}", a, b); 
	// b == 11,满足 ensures(b == a + 1) 
	// 尝试一个不满足前置条件的调用 (这行本身不会让 add_one 函数验证失败, 
	// 但 Prusti 会在 main 函数的验证中指出调用 add_one(-5) 违反了其 requires) 
	// let c = add_one(-5); // 这里 Prusti 会报告错误:违反了 requires(x >= 0) 
}

说明:

  • #[requires(x >= 0)] 定义了前置条件 P:函数执行前,参数 x 必须大于或等于 0。
  • #[ensures(return_value == x + 1)] 定义了后置条件 Q:函数执行后,返回值(用 return_value 表示)必须等于 x + 1
  • 函数体 let y = x + 1; y 是程序片段 C。
  • 整个结构 { x >= 0 } add_one(x) { return\_value == x + 1 } 形成了一个 Hoare 三元组。
  • Prusti 会静态分析这个函数,并使用 SMT 求解器来证明:如果在函数入口处 x >= 0 成立,那么执行完函数体后,return_value == x + 1 一定成立。对于这个简单的例子,这是显然成立的,Prusti 应该能成功验证通过。

无效示例

// main.rs

use prusti_contracts::*;
use std::io;

// 这个函数从标准输入读取一个数字,并声称它大于 10
// Prusti 无法验证这个断言,因为它无法预知用户的输入。
// 但核心思想是:Prusti 无法对 `io::stdin().read_line()` 的结果值做任意假设或证明
#[ensures(return_value > 10)] // Q: 后置条件 - 我们“声称”返回的值大于 10
fn read_number_and_claim_greater_than_10() -> i32 {
    // P: 前置条件 - 在这个例子中,我们没有关于外部输入的有意义的前置条件,可以认为是 { true }
    println!("请输入一个数字:");
    let mut input = String::new();

    // C: 程序片段 - 读取用户输入并尝试解析
    io::stdin().read_line(&mut input)
        .expect("无法读取用户的输入"); 

    // 尝试将输入字符串解析为 i32
    let number: i32 = input.trim().parse()
        .expect("请输入一个有效的整数"); // 忽略解析错误处理的复杂性

    // Prusti 在验证时,无法知道 `number` 这个变量在执行到这里时会是什么具体的值。
    // 它的值完全取决于用户在程序运行时输入了什么。

    number // 函数返回读取到的数字
    // { Q: return_value > 10 } - Prusti 需要在这里证明 return_value > 10 成立
    // 但由于 return_value 就是 number,而 number 的值不可预测,Prusti 无法完成证明。
}

fn main() {
    // 运行时调用这个函数
    // 如果用户输入 5,那么函数返回 5。在函数调用结束时,我们声称的后置条件 (5 > 10) 是假的。
    // 但是 Prusti 在验证时无法知道用户会输入 5,因此它无法静态地捕获这个运行时错误。
    println!("调用 read_number_and_claim_greater_than_10 函数...");
    let value = read_number_and_claim_greater_than_10();
    println!("函数返回的值是: {}", value);
}

在这个例子中,Prusti是一定会报错的,因为无法证明这个代码一定成立,就算将运算单独列出去,将函数纯化,他也无法证明这个是对的。这是第二个例子:

// main.rs

use prusti_contracts::*;
use std::io;

// 这个函数从标准输入读取一个数字。
fn get_number_from_user() -> i32 {
    println!("请输入一个整数:");
    let mut input = String::new();
    io::stdin().read_line(&mut input).expect("读取输入失败");
    let number: i32 = input.trim().parse().expect("请输入有效的整数");
    number // 返回用户输入的数字
}

// 这是一个纯函数,它的输出完全由输入决定,没有副作用。
// Prusti 可以轻松验证这个纯函数自身的正确性。
#[requires(x < 1000)] // 假设我们对输入加十有一个约束(只是为了演示 requires)
#[ensures(return_value == x + 10)] // Prusti 可以验证:如果输入 x 满足 requires,则返回值一定等于 x + 10
#[pure] // 标记这是一个纯函数(没有副作用)
fn add_ten(x: i32) -> i32 {
    x + 10 // 纯粹的计算
}

// 现在我们在一个函数中调用上述两个函数,并尝试对最终结果进行断言。
// 这里的 #[ensures] 应用于 process_and_verify 函数的返回值。
// { P: true } // process_and_verify 函数本身没有关于外部 IO 的前置条件
#[ensures(return_value > 20)] // Q: 后置条件 - 我们希望验证最终结果大于 20
fn process_and_verify() -> i32 {
    // 调用不纯函数获取输入
    let user_input = get_number_from_user(); // user_input 的值在验证期是未知的

    // 调用纯函数进行计算
    // Prusti 知道 add_ten(user_input) 的结果是 user_input + 10
    // 但是,因为 user_input 是未知的,所以 Prusti 仍然无法确定 user_input + 10 的具体值。
    let result = add_ten(user_input);

    // 在这里,Prusti 需要证明 result > 20
    // 即需要证明 user_input + 10 > 20
    // 这等价于证明 user_input > 10
    // 但是,Prusti 无法证明 user_input > 10,因为 user_input 的值来自不可控的 IO。

    result 
}

fn main() {
    println!("开始执行组合函数...");
    let final_value = process_and_verify();
    println!("最终结果是: {}", final_value);

    // 在运行时,如果用户输入 5,user_input 是 5,result 是 15。
    // 此时后置条件 (15 > 20) 是假的。Prusti 无法静态地发现这一点。
}

上次编辑于:
贡献者: Lexon