如果您想证明某个尾递归求和函数的结果等于某个值的给定递归求和函数的结果N
,那么原则上,只定义递归函数(作为表达式函数)就足够了后置条件。然后,您只需在尾递归函数的后置条件中提及递归(表达式)函数(请注意,ensures
Dafny 中的递归函数也没有后置条件 ( ))。
但是,由于 SPARK 的主要目标之一是证明不存在运行时错误,因此您必须证明不会发生溢出,因此,您确实需要递归函数的后置条件。正如@Jeffrey Carter 在评论中已经建议的那样,这种后置条件的合理选择是算术级数的显式求和公式:
Sum (N) = N * (1 + N) / 2
这个选择实际上非常有吸引力,因为有了这个公式,我们现在还可以根据用于计算一系列自然数之和的著名数学显式表达式在功能上验证递归函数本身。
不幸的是,按原样使用这个公式只会把你带到一半的地方。在 SPARK(以及 Ada)中,前置条件和后置条件是可选的可执行的(另请参见SPARK 参考指南中的RM 11.4.2和第5.11.1节),因此它们本身必须没有任何运行时错误。因此,按原样使用公式只会让您证明任何正数都不会发生溢出,直到
max N s.t. N * (1 + N) <= Integer'Last <-> N = 46340
与后置条件一样,乘法也不允许溢出(注意Natural'Last
= Integer'Last
= 2**31 - 1
)。
要解决此问题,您需要使用已在 Ada 202x 标准库中引入的大整数包(另请参阅RM A.5.6;此包已包含在 GNAT CE 2021 和 GNAT FSF 11.2 中)。大整数是无界的,使用这些整数的计算永远不会溢出。使用这些整数,可以证明任何正数都不会发生溢出,直到
max N s.t. N * (1 + N) / 2 <= Natural'Last <-> N = 65535 = 2**16 - 1
下面的示例说明了这些整数在后置条件中的用法。
一些最后的笔记:
该Subprogram_Variant
方面只需要证明递归子程序最终将终止。必须通过向函数添加注释来明确请求此类终止证明(也显示在下面的示例中,并且如注释中 @egilhh 指出的 SPARK 文档中所讨论的那样)。但是,您的初始目的不需要该Subprogram_Variant
方面:证明某些尾递归求和函数的结果等于某个给定递归求和函数的结果 value N
。
要编译使用新 Ada 202x 标准库中的函数的程序,请使用编译器选项-gnat2020
。
虽然我使用子类型来限制 的允许值范围N
,但您也可以使用前提条件。这不应该有任何区别。但是,在 SPARK(以及 Ada)中,通常认为尽可能使用(子)类型来表达约束是一种最佳实践。
将反例视为可能的线索而不是事实。它们可能有意义,也可能没有意义。反例是由一些求解器可选地生成的,可能没有意义。另请参阅SPARK 用户指南中的第 7.2.6 节。
主文件
with Ada.Numerics.Big_Numbers.Big_Integers;
procedure Main with SPARK_Mode is
package BI renames Ada.Numerics.Big_Numbers.Big_Integers;
use type BI.Valid_Big_Integer;
-- Conversion functions.
function To_Big (Arg : Integer) return BI.Valid_Big_Integer renames BI.To_Big_Integer;
function To_Int (Arg : BI.Valid_Big_Integer) return Integer renames BI.To_Integer;
subtype Domain is Natural range 0 .. 2**16 - 1;
function Sum (N : Domain) return Natural is
(if N = 0 then 0 else N + Sum (N - 1))
with
Post => Sum'Result = To_Int (To_Big (N) * (1 + To_Big (N)) / 2),
Subprogram_Variant => (Decreases => N);
-- Request a proof that Sum will terminate for all possible values of N.
pragma Annotate (GNATprove, Terminating, Sum);
begin
null;
end Main;
输出(gnatprove)
$ gnatprove -Pdefault.gpr --output=oneline --report=all --level=1 --prover=z3
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
main.adb:13:13: info: subprogram "Sum" will terminate, terminating annotation has been proved
main.adb:14:30: info: overflow check proved
main.adb:14:32: info: subprogram variant proved
main.adb:14:39: info: range check proved
main.adb:16:18: info: postcondition proved
main.adb:16:31: info: range check proved
main.adb:16:53: info: predicate check proved
main.adb:16:69: info: division check proved
main.adb:16:71: info: predicate check proved
Summary logged in [...]/gnatprove.out
附录(回应评论)
因此,您可以将后置条件添加为递归函数,但这无助于证明不存在溢出;您仍然必须在函数结果上提供一些上限,以使证明者相信表达式N + Sum (N - 1)
不会导致溢出。
为了检查加法过程中是否存在溢出,证明者将根据其规范考虑所有Sum
可能返回的值,并查看这些值中的至少一个是否可能导致加法溢出。在后置条件中没有显式绑定的情况下,Sum
可能会根据其返回类型返回 range 中的任何值Natural'Range
。该范围包括Natural'Last
并且该值肯定会导致溢出。因此,证明者将报告加法可能溢出。鉴于其允许的输入值,从不返回该值的事实Sum
在这里无关紧要(这就是它报告可能的原因)。因此,需要更精确的返回值上限。
如果没有确切的上限,那么您通常会退回到更保守的界限,例如,在这种情况下, (或使用 SPARK 用户手册第 5.2.7 节N * N
中的斐波那契示例中所示的饱和数学,但那方法确实会改变您的功能,这可能是不可取的)。
这是一个替代示例:
示例.ads
package Example with SPARK_Mode is
subtype Domain is Natural range 0 .. 2**15;
function Sum (N : Domain) return Natural
with Post =>
Sum'Result = (if N = 0 then 0 else N + Sum (N - 1)) and
Sum'Result <= N * N; -- conservative upper bound if the closed form
-- solution to the recursive function would
-- not exist.
end Example;
例子.adb
package body Example with SPARK_Mode is
function Sum (N : Domain) return Natural is
begin
if N = 0 then
return N;
else
return N + Sum (N - 1);
end if;
end Sum;
end Example;
输出(gnatprove)
$ gnatprove -Pdefault.gpr --output=oneline --report=all
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
example.adb:8:19: info: overflow check proved
example.adb:8:28: info: range check proved
example.ads:7:08: info: postcondition proved
example.ads:7:45: info: overflow check proved
example.ads:7:54: info: range check proved
Summary logged in [...]/gnatprove.out