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

2

5

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
Conundrum answered 11/9, 2021 at 23:12 Comment(7)
Realize my answer might be lurking in here: (https://mcmap.net/q/2035950/-spark-integer-overflow-check). Too tired to make sense of it, will sleep and try again tomorrow.Conundrum
docs.adacore.com/spark2014-docs/html/ug/en/source/…Corbet
SPARK does better with expression functions than regular functions, and with subtypes rather than preconditions. So declaring subtype Summable is Natural range 0 .. 2; might help. Also, Post => Sum'Result = N * (N + 1) / 2 will probably work. Finally, as N is an in parameter, it cannot be decreased, but I'm not clear what that is supposed to mean in this context.Ockham
@JeffreyR.Carter Using a subtype should work nicely for the exercise. The alternate implementation/specification however sort of defeats the purpose of the exercise. (Though I suppose handling overflow isn't part of the original Dafny exercise, so maybe I could just turn off those checks and call it a day...)Conundrum
@Corbet Looking closer at the Fibonacci example in the documentation, it uses a saturating subtype of Natural, thus "avoiding" any overflow issue.Conundrum
@JeffreyR.Carter As I understand Decreases => n, it says that any recursive invocation is done using a smaller input n for each invocation. This is necessary for ensuring the recursion is finite, however it would not seem to apply on recursive expressions inside postconditions, which is unfortunate in my scenario.Conundrum
Found something interesting. gnatprove default prover is CVC4. Changing prover to Z3 makes the error go away with just the Subprogram_Variant (for recursion) and precondition (for avoiding overflow). No postcondition required.Conundrum
H
4

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
Hereto answered 12/9, 2021 at 21:15 Comment(5)
Accepting answer, despite the Post condition not being recursive in itself. Works for my current needs. In case I do end up being stuck on a function that does not have an inductive form (Fibonacci?) I'll open a new question.Conundrum
@Conundrum I expanded my answer a little bit and added a second example in response to your comment in the hope it might be useful to you.Hereto
You are too kind.Conundrum
Moving on to translating other assignments, I'm tempted using pragma Annotate or pragma Assume for things SPARK cannot (easily) figure out, and reference to an external proof having used other techniques/tools. E.g. for fibonacci setting a precondition and annotate/assume the expression cannot overflow, based on separate test runs getting the next fib and throw exception on overflow.Conundrum
@Conundrum For Fibonacci, the approach is largely the same, but after trying it myself, I figured that you do hit some unintelligible solver behavior. By applying the trail-and-error method on the details you can eventuality get a proof for N = 0..30, but I can imagine that you want to stay away from trail-and-error approaches in your class/exercises as they might obscure the main point. The current solvers, whilst powerful, are sometimes still somewhat if a black box for those not building them (like me). If you want me to post the solution I found, just drop a comment or open a new question.Hereto
C
2

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.
Conundrum answered 12/9, 2021 at 18:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.