2

I cannot seem to figure out why the following fails to establish termination;

datatype Expr = Const(nat) | Add(Expr,Expr)

function eval(e: Expr) : nat
decreases e {
    match e
        case Const(v) => v
        case Add(e1,e2) => evalAdd(e1,e2)
}

function evalAdd(e1: Expr, e2: Expr) : nat
decreases e1,e2 {
    var n1 := eval(e1);
    var n2 := eval(e2);
    n1 + n2
}

I am aware that there are several ways I could prove termination here:

  1. Inlining evalAdd() into eval() solves the problem. However, this is part of a larger example where I want to split out functionality rather than have a single large match statement.
  2. Defining a notion of height(Expr) also solves the problem. However, to my mind, this should not be necessary.

Thoughts?

redjamjar
  • 535
  • 2
  • 11

1 Answers1

3

Here is one way:

datatype Expr = Const(nat) | Add(Expr,Expr)

function eval(e: Expr) : nat
decreases e,1 {
    match e
        case Const(v) => v
        case Add(e1,e2) => evalAdd(e1,e2)
}

function evalAdd(e1: Expr, e2: Expr) : nat
decreases Add(e1,e2),0 {
    var n1 := eval(e1);
    var n2 := eval(e2);
    n1 + n2
}

The rule of decreases clauses for mutually recursive functions is that every call to another function in the mutually recursive bunch must have a callee-decreases clause less than the caller-decreases clause. So it helps to make all the decreases clauses "similar". Your original clause for evalAdd was e1, e2 which has two elements, but the clause for eval had only one. There's nothing wrong with that in principle, except that it means that when evalAdd calls eval, it must decrease, which it doesn't.

So first, I changed your clause for evalAdd to Add(e1, e2), which fixes that problem. But then the call to evalAdd from eval has an error, since it also needs to decrease. There I use a trick to basically convince Dafny to allow it to do the inlining for you: use a constant 0/1 to allow a single "extra" call that leaves the first element of the clause the same.

James Wilcox
  • 5,307
  • 16
  • 25