recursionadadafnyspark-2014

How to make Pre and Post conditions for recursive functions in SPARK?


I'm translating an exercise I made in Dafny into SPARK, where one verifies a tail recursive function against a recursive one. The Dafny source (censored, because it might still be used for classes):

function Sum(n:nat):nat 
    decreases n
{ 
    if n==0 then n else n+Sum(n-1)
}

method ComputeSum(n:nat) returns (s:nat) 
    ensures s == Sum(n) 
{
    s := 0;
    // ...censored...
}

What I got in SPARK so far:

function Sum (n : in Natural) return Natural
is
begin
   if n = 0 then
      return n;
   else
      return n + Sum(n - 1);
   end if;
end Sum;

function ComputeSum(n : in Natural) return Natural
  with
    Post => ComputeSum'Result = Sum(n)
is
   s : Natural := 0;
begin
   -- ...censored...
   return s;
end ComputeSum;

I cannot seem to figure out how to express the decreases n condition (which now that I think about it might be a little odd... but I got graded for it a few years back so who am I to judge, and the question remains how to get it done). As a result I get warnings of possible overflow and/or infinite recursion.

I'm guessing there is a pre or post condition to be added. Tried Pre => n <= 1 which obviously does not overflow, but I still get the warning. Adding Post => Sum'Result <= n**n on top of that makes the warning go away, but that condition gets a "postcondition might fail" warning, which isn't right, but guess the prover can't tell. Also not really the expression I should check against, but I cannot seem to figure what other Post I'm looking for. Possibly something very close to the recursive expression, but none of my attempts work. Must be missing out on some language construct...

So, how could I express the recursive constraints?


Edit 1:

Following links to this SO answer and this SPARK doc section, I tried this:

function Sum (n : in Natural) return Natural
is
  (if n = 0 then 0 else n + Sum(n - 1))
    with
      Pre => (n in 0 .. 2),
      Contract_Cases => (n = 0 => Sum'Result = 0,
                         n >= 1 => Sum'Result = n + Sum(n - 1)),
      Subprogram_Variant => (Decreases => n);

However getting these warnings from SPARK:

spark.adb:32:30: medium: overflow check might fail [reason for check: result of addition must fit in a 32-bits machine integer][#0]
spark.adb:36:56: warning: call to "Sum" within its postcondition will lead to infinite recursion

Solution

  • If you want to prove that the result of some tail-recursive summation function equals the result of a given recursive summation function for some value N, then it should, in principle, suffice to only define the recursive function (as an expression function) without any post-condition. You then only need to mention the recursive (expression) function in the post-condition of the tail-recursive function (note that there was no post-condition (ensures) on the recursive function in Dafny either).

    However, as one of SPARK's primary goal is to proof the absence of runtime errors, you must have to prove that overflow cannot occur and for this reason, you do need a post-condition on the recursive function. A reasonable choice for such a post-condition is, as @Jeffrey Carter already suggested in the comments, the explicit summation formula for arithmetic progression:

    Sum (N) = N * (1 + N) / 2
    

    The choice is actually very attractive as with this formula we can now also functionally validate the recursive function itself against a well-known mathematically explicit expression for computing the sum of a series of natural numbers.

    Unfortunately, using this formula as-is will only bring you somewhere half-way. In SPARK (and Ada as well), pre- and post-conditions are optionally executable (see also RM 11.4.2 and section 5.11.1 in the SPARK Reference Guide) and must therefore themselves be free of any runtime errors. Therefore, using the formula as-is will only allow you to prove that no overflow occurs for any positive number up until

    max N   s.t.   N * (1 + N) <= Integer'Last        <->    N = 46340
    

    as in the post-condition, the multiplication is not allowed to overflow either (note that Natural'Last = Integer'Last = 2**31 - 1).

    To work around this, you'll need to make use of the big integers package that has been introduced in the Ada 202x standard library (see also RM A.5.6; this package is already included in GNAT CE 2021 and GNAT FSF 11.2). Big integers are unbounded and computations with these integers never overflow. Using these integers, one can proof that overflow will not occur for any positive number up until

    max N   s.t.   N * (1 + N) / 2 <= Natural'Last    <->    N = 65535 = 2**16 - 1
    

    The usage of these integers in a post-condition is illustrated in the example below.

    Some final notes:

    main.adb

    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;
    

    output (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
    

    ADDENDUM (in response to comment)

    So you can add the post condition as a recursive function, but that does not help in proving the absence of overflow; you will still have to provide some upper bound on the function result in order to convince the prover that the expression N + Sum (N - 1) will not cause an overflow.

    To check the absence of overflow during the addition, the prover will consider all possible values that Sum might return according to it's specification and see if at least one of those value might cause the addition to overflow. In the absence of an explicit bound in the post condition, Sum might, according to its return type, return any value in the range Natural'Range. That range includes Natural'Last and that value will definitely cause an overflow. Therefore, the prover will report that the addition might overflow. The fact that Sum never returns that value given its allowable input values is irrelevant here (that's why it reports might). Hence, a more precise upper bound on the return value is required.

    If an exact upper bound is not available, then you'll typically fallback onto a more conservative bound like, in this case, N * N (or use saturation math as shown in the Fibonacci example from the SPARK user manual, section 5.2.7, but that approach does change your function which might not be desirable).

    Here's an alternative example:

    example.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;
    

    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;
    

    output (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