Home > Software design >  How to make Pre and Post conditions for recursive functions in SPARK?
How to make Pre and Post conditions for recursive functions in SPARK?

Time:09-17

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

CodePudding user response:

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:

  • The Subprogram_Variant aspect is only needed to prove that a recursive subprogram will eventually terminate. Such a proof of termination must be requested explicitly by adding an annotation to the function (also shown in the example below and as discussed in the SPARK documentation pointed out by @egilhh in the comments). The Subprogram_Variant aspect is, however, not needed for your initial purpose: proving that the result of some tail-recursive summation function equals the result of a given recursive summation function for some value N.

  • To compile a program that uses functions from the new Ada 202x standard library, use compiler option -gnat2020.

  • While I use a subtype to constrain the range of permissible values for N, you could also use a precondition. This should not make any difference. However, in SPARK (and Ada as well), it is in general considered to be a best practise to express contraints using (sub)types as much as possible.

  • Consider counterexamples as possible clues rather than facts. They may or may not make sense. Counterexamples are optionally generated by some solvers and may not make sense. See also the section 7.2.6 in the SPARK user’s guide.

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

CodePudding user response:

I landed in something that sometimes works, which I think is enough for closing the title question:

function Sum (n : in Natural) return Natural
is
  (if n = 0 then 0 else n   Sum(n - 1))
    with
      Pre => (n in 0 .. 10), -- works with --prover=z3, not Default (CVC4)
      -- Pre => (n in 0 .. 100), -- not working - "overflow check might fail, e.g. when n = 2"
      Subprogram_Variant => (Decreases => n),
      Post => ((n = 0 and then Sum'Result = 0)
               or (n > 0 and then Sum'Result = n   Sum(n - 1)));
      -- Contract_Cases => (n = 0 => Sum'Result = 0,
      --                    n > 0 => Sum'Result = n   Sum(n - 1)); -- warning: call to "Sum" within its postcondition will lead to infinite recursion
      -- Contract_Cases => (n = 0 => Sum'Result = 0,
      --                    n > 0 => n   Sum(n - 1) = Sum'Result); -- works
      -- Contract_Cases => (n = 0 => Sum'Result = 0,
      --                    n > 0 => Sum'Result = n * (n   1) / 2); -- works and gives good overflow counterexamples for high n, but isn't really recursive

Command line invocation in GNAT Studio (Ctrl Alt F), --counterproof=on and --prover=z3 my additions to it:

gnatprove -P%PP -j0 %X --output=oneline --ide-progress-bar --level=0 -u %fp --counterexamples=on --prover=z3

Takeaways:

  • Subprogram_Variant => (Decreases => n) is required to tell the prover n decreases for each recursive invocation, just like the Dafny version.
    • Works inconsistently for similar contracts, see commented Contract_Cases.
  • Default prover (CVC4) fails, using Z3 succeeds.
  • Counterproof on fail makes no sense.
    • n = 2 presented as counterproof for range 0 .. 100, but not for 0 .. 10.
    • Possibly related to this mention in the SPARK user guide: However, note that since the counterexample is always generated only using CVC4 prover, it can just explain why this prover cannot prove the property.
  • Cleaning between changing options required, e.g. --prover.
  • Related