Search code examples
verificationdafny

Dafny recursive assertion violation


I am new to dafny and am trying to get this simple piece of code to work. I want to count the occurrences of a char in a string. I am receiving an assertion violation on line 4. I know my function is finding the right amount of characters, but clearly there are some holes in this assertion. I'm trying to get the basics down before I start using pre and post conditions and what not, and this should be possible without them. The function simply checks the last character in the string and returns a 1 or 0, along with calling the function again which cuts off the tail of the string until it is empty.

method Main() {
  var s:string := "hello world";
  print tally(s, 'l');
  assert tally(s,'l') == 3;
}
function method tally(s: string, letter: char): nat
{
  if |s| == 0 then 0
  else if s[|s|-1] == letter then 1+tally(s[..|s|-1], letter)
  else 0 + tally(s[..|s|-1], letter)
}

http://rise4fun.com/Dafny/2lvt Here is the link to my code.


Solution

  • It would be natural to think that the Dafny static verifier can evaluate any code, like the expression in your assert statement. The verifier does try to evaluate expressions like these where the arguments are given as constants (like your "hello world", 'l', and 3). However, the static verifier wants to avoid recursing forever (or even recursing for too long), so it doesn't always fully go through these expressions. In your case, there are also limits to what the verifier is able to do with the sequence operations. So, in short, although the verifier tries to be helpful, it does not always get to the bottom of the recursions.

    There are two ways you can work around these limits to get the verifier to accept your assertion.

    The most reliable way to debug the situation is to start with smaller inputs and build up to the longer inputs you're using. This is quite tedious, however, and it makes you appreciate when Dafny is able to do these things automatically. The following (which verifies) illustrates what you would do:

    var s := "hello world";
    assert tally("he",'l') == 0;
    assert tally("hel",'l') == 1;
    assert "hell"[..3] == "hel";
    assert tally("hell",'l') == 2;
    assert "hello"[..4] == "hell";
    assert tally("hello",'l') == 2;
    assert "hello "[..5] == "hello";
    assert tally("hello ",'l') == 2;
    assert "hello w"[..6] == "hello ";
    assert tally("hello w",'l') == 2;
    assert "hello wo"[..7] == "hello w";
    assert tally("hello wo",'l') == 2;
    assert "hello wor"[..8] == "hello wo";
    assert tally("hello wor",'l') == 2;
    assert "hello worl"[..9] == "hello wor";
    assert tally("hello worl",'l') == 3;
    assert s[..10] == "hello worl";
    assert tally(s,'l') == 3;
    

    In fact, the thing that the Dafny verifier does not expand (too many times) for you are the "take" operations (that is, the expressions of the form s[..E]). The following intermediate assertions will also verify themselves and will help to verify the final assertion. These intermediate assertions show what the verifier doesn't think to do automatically for you.

    var s := "hello world";
    assert "he"[..1] == "h";
    assert "hel"[..2] == "he";
    assert "hell"[..3] == "hel";
    assert "hello"[..4] == "hell";
    assert "hello "[..5] == "hello";
    assert "hello w"[..6] == "hello ";
    assert "hello wo"[..7] == "hello w";
    assert "hello wor"[..8] == "hello wo";
    assert "hello worl"[..9] == "hello wor";
    assert s[..10] == "hello worl";
    assert tally(s,'l') == 3;
    

    You might wonder, "how in the world would I come up with this?". The most systematic way would be to start like in my first example above. Then, you could try pruning the many assertions there to see what it is the verifier needs.

    (I'm now thinking that perhaps the Dafny verifier could be enhanced to do these operations, too. It may cause performance problems elsewhere. I shall take a look.)

    The other way to work around the verifier's limits is to define function tally in a different way, in particular avoiding the "take" operations, which the verifier does not want to expand a lot. It is not clear what to change to make the verifier happy in these situations, or if that's even possible at all, so this workaround may not be the best. Nevertheless, I tried the following definition of tally and it makes your assertion go through:

    function method tally'(s: string, letter: char): nat
    {
      tally_from(s, letter, 0)
    }
    function method tally_from(s: string, letter: char, start: nat): nat
      requires start <= |s|
      decreases |s| - start
    {
      if start == |s| then 0
      else (if s[start] == letter then 1 else 0) + tally_from(s, letter, start+1)
    }
    

    Note that these definitions do not use any "take" operations. Here, the verifier is happy to expand all the recursive calls until the final answer is found.

    Rustan