在Spark中证明Floor_Log2

12

作为我第一次接触Spark和Ada,所以这个问题可能过于宽泛。然而,这是出于诚意的提问,旨在帮助理解Spark。除了以下问题的直接答案之外,我也欢迎对样式、工作流程等方面进行批评。

在我第一次尝试Spark时,我选择尝试实现(简单的)并证明(目前不成功)函数\left \lfloor{log_2(x)}\right \rfloor

问题:如何正确地实现和证明此函数的正确性?

我从以下util.ads开始:

package Util is
   function Floor_Log2(X : Positive) return Natural with
     Post => 2**Floor_Log2'Result <= X and then X < 2**(Floor_Log2'Result + 1);    
end Util;

我的先决条件是没有的,因为输入范围完全表达了唯一有趣的先决条件。我写的后置条件基于数学定义;然而,我在这里有一个直接的关注点。如果XPositive'Last,那么2**(Floor_Log2'Result + 1)会超过Positive'LastNatural'Last。 我在这里遇到了我对Ada的有限了解,所以:子问题1:后置条件中的子表达式的类型是什么?这个溢出是个问题吗?有没有一般的方法来解决它?为了避免在这种特定情况下出现问题,我将规范修改为以下不太直观但等效的规范:

package Util is
   function Floor_Log2(X : Positive) return Natural with
     Post => 2**Floor_Log2'Result <= X and then X/2 < 2**Floor_Log2'Result;
end Util;

有很多方法可以实现这个函数,目前我不特别关注性能,所以我会对其中任何一种方法感到满意。根据我的 C 背景,我认为“自然”的实现方式类似于以下代码:

util.adb
package body Util is
   function Floor_Log2 (X : Positive) return Natural is
      I : Natural := 0;
      Remaining : Positive := X;
   begin
      while Remaining > 1 loop
         I := I + 1;
         Remaining := Remaining / 2;
      end loop;
      return I;
   end Floor_Log2;
end Util;

尝试不使用循环不变量来证明这一点是失败的,符合预期。结果(此结果以及所有结果均为GNATprove 4级,从GPS调用gnatprove -P%PP -j0%X --ide-progress-bar -u%fp --level = 4 --report = all ):

util.adb:6:13: info: initialization of "Remaining" proved[#2]
util.adb:7:15: info: initialization of "I" proved[#0]
util.adb:7:17: medium: overflow check might fail[#5]
util.adb:8:23: info: initialization of "Remaining" proved[#1]
util.adb:8:33: info: range check proved[#4]
util.adb:8:33: info: division check proved[#8]
util.adb:10:14: info: initialization of "I" proved[#3]
util.ads:3:14: medium: postcondition might fail, cannot prove 2**Floor_Log2'Result <= X[#7]
util.ads:3:15: medium: overflow check might fail[#9]
util.ads:3:50: info: division check proved[#6]
util.ads:3:56: info: overflow check proved[#10]

这里大部分错误对我来说都很基础。从第一个溢出检查开始,GNATprove不能证明循环在少于 Natural'Last 次迭代(或者根本没有)中终止,因此它无法证明 I := I + 1 不会溢出。我们知道这不是事实,因为 Remaining 是递减的。我尝试通过添加循环变量 pragma Loop_Variant (Decreases => Remaining), 来表达这一点,同时GNATprove能够证明循环变量,但是 I := I + 1 的潜在溢出没有改变,可能是因为证明循环是否终止并不等同于证明它在少于Positive'Last 次迭代中终止。更严格的约束将显示循环在最多Positive'Size次迭代后终止,但我不确定如何证明这一点。相反,我通过添加 pragma Assume (I <= Remaining'Size) 来"强制"终止; 我知道这是不好的做法,这里的目的纯粹是让我看看我可以解决这个问题的程度。如预期所示,这个假设使证明器证明了实现文件中的所有范围检查。子问题2:在这个循环中证明I不会溢出的正确方法是什么?

然而,我们仍然没有就证明后置条件取得任何进展。显然需要一个循环不变量。一种在循环顶部成立的循环不变量是 pragma Loop_Invariant (Remaining * 2**I <= X and then X/2 < Remaining * 2**I);除了为真之外,这个不变量具有良好的特性,即当循环终止条件为真时,它显然等价于后置条件。然而,如预期所见,GNATprove无法证明这个不变量: medium: loop invariant might fail after first iteration, cannot prove Remaining * 2**I <= X[#20]。这是有道理的,因为这里的归纳步骤是非显然的。对于实数上的除法,人们可以想象一个直接的引理,即 for all I, X * 2**I = (X/2) * 2**(I+1),但是(a) 我不指望GNATprove在没有提供引理的情况下知道这一点,(b) 在整数除法中更加混乱。所以,子问题3a:这是尝试用来证明这个实现的适当循环不变量吗?子问题3b:如果是,正确的证明方法是什么?外部证明引理并使用它吗?如果是,那具体是什么意思?

此时,我想探索一个完全不同的实现,只是为了看看它是否导致了不同的结果:

package body Util is
   function Floor_Log2 (X : Positive) return Natural is
   begin
      for I in 1 .. X'Size - 1 loop
         if 2**I > X then
            return I - 1;
         end if;
      end loop;
      return X'Size - 1;
   end Floor_Log2;
end Util;

对我来说,这是一种不太直观的实现。我没有深入探索这个第二个实现,但我把它留在这里以展示我的尝试;为了给主要问题提供潜在的解决方案,并引发额外的子问题。

这里的想法是通过使终止和范围显式来绕过关于I溢出和终止条件的部分证明。有点意外的是,证明者首先在检查表达式2 ** I的溢出时陷入了困境。我本来期望2 **(X'Size - 1)可证明在X的范围内 -- 但是,我又遇到了我Ada知识的极限。子问题4:这个表达式在Ada中真的是无溢出的吗,如何证明?

这已经成为一个很长的问题...但我认为我所提出的问题,在一个几乎微不足道的例子的背景下,是相对通用的,可能对其他像我一样试图理解Spark是否与他们相关的人有用。


1
请注意,您可以这样做:type Big is mod 2 ** Integer'Size; 并将您的条件更改为 2 ** Floor_Log2'Result <= Big (X) and Big (X) < 2 ** (Floor_Log2'Result + 1) 等等,以避免溢出警告。 - Jeffrey R. Carter
3个回答

6
我无法回答你的SPARK问题,但我可以回答你的一些子问题。
子问题1:由于你在整数上使用“<”,子表达式也将是整数类型。对于Positive'Last(在GNAT中为2 ** 31-1),你的函数结果应该是30,子表达式将溢出。(从SPARK的角度来看,编译器允许在计算表达式以获得数学/逻辑上正确的结果时使用更大的范围类型,即使子表达式会溢出,GNAT也会为某些-gnato值这样做。)
子问题4:2 **(X'Size-1)可能会溢出。原因与“Size”的2个含义有关:Positive'Size是存储Positive子类型值所需的最小位数;X'Size是分配给X的实际位数。由于你正在使用GNAT,
Integer'Last = Positive'Last = 2 ** 31-1。X'Size = 32。Positive'Size = 31。
因此,2 **(X'Size-1)= 2 ** 31> Positive'Last。你可能想使用Positive'Size而不是X'Size。
(同样,从SPARK的角度来看;编译器允许获得逻辑上正确的结果。)
另外:短路形式and then和or else只有在实际需要时才应使用。现代处理器在机器代码级别上执行各种优化,必须关闭短路评估。尽管它们看起来像优化,但实际上它们经常是相反的。
希望对你有所帮助。
(你可能想用[ada]标记这个问题。我只是因为你在c.l.ada中引用了它才看到它。)

1

避免后置条件中的溢出

假设有原始函数签名:

function Floor_Log2 (X : Positive) return Natural with
   Post => 2**Floor_Log2'Result <= X and then X < 2**(Floor_Log2'Result + 1);  

我注意到我需要限制X的域,以防止后置条件中第二项的溢出。鉴于Standard.ads中的定义,即:
type Integer is range -(2**31) .. +(2**31 - 1);
for Integer'Size use 32;

subtype Natural  is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last;

我得出结论,为了防止溢出,

X < 2**(Floor_Log2'Result + 1) <= 2**31 - 1

因此,X <= 2**30 - 1。因此,我更改了函数签名为:

subtype Pos is Positive range 1 .. 2**30 - 1
      
function Floor_Log2 (X : Pos) return Natural with
  Post => 2**Floor_Log2'Result <= X and then X < 2**(Floor_Log2'Result + 1);

第一种方法

原则上,我现在可以在GNAT CE 2019中使用以下方式证明后置条件(请注意,与问题中所述的算法不同):

util.ads

package Util with SPARK_Mode is
   
   subtype Pos is Positive range 1 .. 2**30 - 1
      
   function Floor_Log2 (X : Pos) return Natural with
     Post => 2**Floor_Log2'Result <= X and then X < 2**(Floor_Log2'Result + 1);
   
end Util;

util.adb

package body Util with SPARK_Mode is  
  
   ----------------
   -- Floor_Log2 --
   ----------------
   
   function Floor_Log2 (X : Pos) return Natural is      
      L : Positive := 1;
      H : Positive := L * 2;
      I : Natural  := 0;
   begin
            
      while not (L <= X and then X < H) loop
         
         pragma Loop_Invariant
           (L = 2 ** I and H = 2 ** (I+1));

         pragma Loop_Invariant
           (for all J in 0 .. I =>
               not (2 ** J <= X and then X < 2 ** (J+1)));
         
         L := H;         
         H := H * 2;    
         I := I + 1;
         
      end loop;
      
      return I;
      
   end Floor_Log2;  

end Util;

不幸的是,证明器在处理非线性算术(即指数运算)时遇到了困难,所有证明会话(在我的计算机上)都以超时结束。实际上,如果我使用努力级别0运行gnatprove,那么当我将Pos的上限限制为2 ** 7-1时,我只能证明后置条件。

subtype Pos is Positive range 1 .. 2**7 - 1;

提高努力程度(或超时时间)使我能够证明Pos'Last的更大值的后置条件。

第二种方法

为了绕过证明器的限制,我采用了一个小技巧,重新定义了指数函数。然后,当我使用努力程度为1的gnatprove运行以下代码时,我可以证明对于完整范围的Pos的后置条件:

spark_exp.ads

generic
   type Int is range <>;   
   Base  : Int;
   N_Max : Natural;
package SPARK_Exp with SPARK_Mode is
   
   subtype Exp_T is Natural range 0 .. N_Max;
   
   function Exp (N : Exp_T) return Int with Ghost;

private
   
   type Seq_T is array (Exp_T range <>) of Int;
   
   function Exp_Seq return Seq_T with
     Ghost,
     Post =>  (Exp_Seq'Result'First = 0)
     and then (Exp_Seq'Result'Last  = N_Max)
     and then (Exp_Seq'Result (0) = 1)
     and then (for all I in 1 .. N_Max =>
                 Exp_Seq'Result (I) = Base * Exp_Seq'Result (I - 1) and
                   Int'First < Exp_Seq'Result (I) and Exp_Seq'Result (I) < Int'Last);
      
   function Exp (N : Exp_T) return Int is (Exp_Seq (N));   
   
end SPARK_Exp;

spark_exp.adb

package body SPARK_Exp with SPARK_Mode is

   -------------
   -- Exp_Seq --
   -------------

   function Exp_Seq return Seq_T is
      S : Seq_T (Exp_T'Range) := (others => 1);
   begin

      for I in 1 .. N_Max loop

         pragma Loop_Invariant
           (for all J in 1 .. I - 1 =>
              S (J) = Base * S (J - 1) and
                (Int'First / Base) < S (J) and S (J) < (Int'Last / Base));

         S (I) := Base * S (I - 1);

      end loop;

      return S;

   end Exp_Seq;

end SPARK_Exp;

util.ads

with SPARK_Exp;

package Util with SPARK_Mode is
   
   subtype Pos is Positive range 1 .. 2**30 - 1;
   
   
   package SPARK_Exp_2 is
     new SPARK_Exp (Positive, 2, 30);
   
   function Exp2 (N : SPARK_Exp_2.Exp_T) return Positive
     renames SPARK_Exp_2.Exp;   
   
   
   function Floor_Log2 (X : Pos) return Natural with
     Post => (Exp2 (Floor_Log2'Result) <= X) and then
                (X < Exp2 (Floor_Log2'Result + 1));

end Util;

util.adb

package body Util with SPARK_Mode is
   
   ----------------
   -- Floor_Log2 --
   ----------------
   
   function Floor_Log2 (X : Pos) return Natural is      
      L : Positive := 1;
      H : Positive := L * 2;
      I : Natural  := 0;
   begin
            
      while not (L <= X and then X < H) loop
         
         pragma Loop_Invariant
           (L = Exp2 (I) and H = Exp2 (I + 1));

         pragma Loop_Invariant
           (for all J in 0 .. I =>
               not (Exp2 (J) <= X and then X < Exp2 (J + 1)));
         
         L := H;         
         H := H * 2;    
         I := I + 1;
         
      end loop;
      
      return I;
      
   end Floor_Log2;

end Util;

0

这个实现证明了所有主体内的检查,但前提条件仍未得到证明:

package body Util is
    pragma SPARK_Mode (On);

    function Floor_Log2 (X : Positive) return Natural is
       I : Natural := 30;
       Prod : Natural := 2**30;
       type Large_Natural is range 0 .. 2**31;
       Prev_Prod : Large_Natural := Large_Natural'Last with Ghost;
    begin
       while I > 0 loop
          if X >= Prod then
             pragma Assert (Large_Natural (X) < Prev_Prod);
             return I;
          end if;
          pragma Loop_Invariant (I > 0);
          pragma Loop_Invariant (Prod >= X and Prev_Prod >= Large_Natural (X));
          --  pragma Loop_Invariant (2**I > X);
          Prod := Prod / 2;
          I := I - 1;
       end loop;

       pragma Assert (I = 0);

       return 0;
    end Floor_Log2;

end Util;

使用gnatprove,将会得到以下输出:

gnatprove -P/Users/pnoffke/projects/ada/spark/floor_log2/floor_log2.gpr -j0 --ide-progress-bar -u util.adb --level=2 --report=all
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
util.adb:10:13: info: initialization of "I" proved
util.adb:11:18: info: initialization of "Prod" proved
util.adb:12:28: info: assertion proved
util.adb:12:48: info: initialization of "Prev_Prod" proved
util.adb:13:20: info: initialization of "I" proved
util.adb:15:33: info: initialization of "I" proved
util.adb:15:33: info: loop invariant preservation proved
util.adb:15:33: info: loop invariant initialization proved
util.adb:16:33: info: initialization of "Prod" proved
util.adb:16:33: info: loop invariant preservation proved
util.adb:16:33: info: loop invariant initialization proved
util.adb:16:47: info: initialization of "Prev_Prod" proved
util.adb:18:18: info: initialization of "Prod" proved
util.adb:18:23: info: division check proved
util.adb:19:15: info: initialization of "I" proved
util.adb:19:17: info: range check proved
util.adb:22:22: info: initialization of "I" proved
util.adb:22:22: info: assertion proved
util.ads:5:15: info: overflow check proved
util.ads:5:44: medium: postcondition might fail, cannot prove X / 2 < 2**Floor_Log2'result (e.g. when Floor_Log2'Result = 0 and X = 2)
util.ads:5:46: info: division check proved
util.ads:5:53: medium: overflow check might fail (e.g. when Floor_Log2'Result = 30)

我不明白为什么gnatprove无法证明被注释的Loop_Invariant。如果我尝试这样做,我会得到以下额外输出:

util.adb:17:33: medium: loop invariant might fail after first iteration, cannot prove 2**I > X (e.g. when I = 0 and X = 0)
util.adb:17:33: medium: loop invariant might fail in first iteration, cannot prove 2**I > X (e.g. when I = 30 and X = 1)
util.adb:17:34: medium: overflow check might fail (e.g. when I = 0)

在这个反例中,它说“当I = 0和X = 0”,但是根据第一个Loop_InvariantI不能为0。
此外,如果我将Prod的初始值设置为2 ** I而不是2 ** 30,我会得到:
util.adb:6:26: medium: overflow check might fail (e.g. when I = 30 and Prod = 0)

我怀疑gnatprove在使用**运算符时存在一些根本性问题。我希望使用Prev_Prod来帮助证明您的前提条件,但是由于上述问题,我不知道如何实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接