11

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 是否以及如何与他们相关。

4

3 回答 3

6

我无法回答您的 SPARK 问题,但我可以回答您的一些子问题。

子问题 1:由于您使用"<"的是整数,子表达式也将是整数类型。对于Positive'Last( 2 ** 31 - 1with GNAT),你的函数结果应该是 30,子表达式会溢出。(这是从 SPARK 的角度来看的;编译器在评估表达式时允许使用更大范围的类型以获得数学/逻辑上正确的结果,即使子表达式会溢出,并且 GNAT 将对 -gnato 的某些值执行此操作。 )

子问题4:2 ** (X'Size - 1)可以溢出。原因与 2 个含义有关'SizePositive'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 thenor else只应在实际需要时使用。现代处理器在机器代码级别进行各种优化,这些优化必须关闭才能进行短路评估。尽管它们可能看起来像优化,但实际上它们往往相反。

HTH。

(您可能想用 [ada] 标记它。我只看到它是因为您在 clada 中引用了它。)

于 2018-12-16T11:19:28.853 回答
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 中按以下方式证明后置条件(请注意,与问题中所述的算法相比,我使用了不同的算法):

实用程序广告

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;

实用程序.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;

然而不幸的是,证明者在非​​线性算术(即求幂)方面存在困难,并且所有证明会话(在我的计算机上)都以超时结束。事实上,如果我gnatprove以 0 级努力运行,那么我只能在限制 to 的上限时证明后置条件Pos2**7 - 1

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

增加努力水平(或超时)允许我证明较大值的后置条件Pos'Last

第二种方法

为了解决证明者的局限性,我通过重新定义幂函数应用了一个小技巧。Pos然后,我可以使用以下代码来证明我以 1 级努力运行时的全部后置条件gnatprove

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;

实用程序广告

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;

实用程序.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;
于 2019-07-16T20:09:06.677 回答
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 不能证明 commented 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)

在反例中,它表示“ when I = 0 and X = 0”,但I第一个不能为 0 Loop_Invariant

此外,如果我初始化Prod2**I而不是2**30,我得到:

util.adb:6:26: medium: overflow check might fail (e.g. when I = 30 and Prod = 0)

我怀疑 gnatprove 与**操作员有一些基本问题。我希望用来Prev_Prod帮助证明您的先决条件,但我不知道如何解决我遇到的上述问题。

于 2019-04-07T20:30:18.013 回答