Functions

A function in Scala is analogous to a method in Java or C# – it is a named body of commands that does significant work. It may take one or more parameters and/or return a value. Recall the syntax for a function in Scala:

def functionName(paramList): returnType = {

}

We will use the keyword Unit (like void in other languages) for a function that does not return a value. If a function has a non-Unit return type, then all paths through the function must end in a return statement:

return expression

Where expression is a variable, literal, or expression matching returnType.

Function contracts

In order to prove the correctness of a function, it must include a function contract just inside the function header. This function contract specifies what we need to know to use the function correctly. While we will use these specifications to help us prove correctness, they are good to include in ANY library function that will be used by someone else (even more informally in other languages). The person/program who calls the function is not supposed to read the function’s code to know how to use the function. A function contract shows them what parameters to pass (including what range of values are acceptable), what the function will return in terms of the parameter, and whether/how the function will modify things like sequences (arrays/lists in other languages) or global variables.

Here is the syntax for a function contract in Logika:

Contract(
    Requires (  preconditions   ),
    Ensures (   postconditions  )
)

Here is a summary of the different function contract clauses:

  • Requires: lists the preconditions for the function in a comma-separated list. If there are no preconditions, we can skip this clause.
  • Ensures: lists the postconditions for the function in a comma-separated list.

Preconditions

The preconditions for a function are requirements the function has in order to operate correctly. Generally, preconditions constrain the values of the parameters and/or global variables. For example, this function returns the integer division between two parameters. The function can only operate correctly when the denominator (the second parameter, b) is non-zero:

def div(a: Z, b: Z) : Z =  {
    Contract(
        Requires (  b != 0  ),
        ...
    )

    val ans: Z = a/b
    return ans
}

Logika will display an error if any preconditions are not proven before calling a function. Because we are required to prove the preconditions before any function call, the function itself can list the preconditions as premises in a proof block just after the function contract:

def example(a: Z, b: Z) : Z =  {
    Contract(
        Requires (  Precondition1, Precondition2, ...),
        ...
    )

    //we can list the preconditions as premises
    Deduce(
        1 ( precondition1   )   by Premise,
        2 ( precondition2   )   by Premise
        ...
    )

    ...
}

Postconditions

The postconditions of a function state what the function has accomplished when it terminates. In particular, postconditions should include:

  • A formalization of what the function promises to return in terms of the parameters/global variables. We use the keyword Res[returnType] to refer to the object returned by the function (we will only use this keyword in the function contract). For example, in a function that returns an integer (Z), we can use the keyword Res[Z] in a postcondition to refer to the return value.

  • A description of how any global variables and/or sequence parameters will be modified by the function (we will not use global variables or sequences until chapter 10).

For example, the div function above should promise to return the integer division of its two parameters, like this:

def div(a: Z, b: Z) : Z =  {
    Contract(
        Requires(   b != 0  ),
        Ensures(    Res[Z] == a/b   )
    )

    val ans: Z = a/b
    return ans
}

In order to prove a postcondition involving the return value, we must have proof blocks just before returning that demonstrate each postcondition claim, using the variable name being returned instead of the Res[returnType] keyword. In the example above, since we are returning a variable named ans, then we must prove the claim ans == a/b in order to satisfy the postcondition. We can complete the verification of the div function as follows:

def div(a: Z, b: Z) : Z =  {
    Contract(
        Requires(   b != 0  ),
        Ensures(    Res[Z] == a/b   )
    )

    val ans: Z = a/b

    Deduce(
        1 ( b != 0      )   by Premise, //precondition (needed for division)
        2 ( ans == a/b  )   by Premise  //satisifes the postcondition
                                        //(from the "ans = a/b" assignment)
    )

    return ans
}

Logika will display an error if postconditions are not proven before leaving a function. Because we are required to prove the postconditions before the function ends, any calling code can list those postconditions as premises after calling the function. We will see this in more detail in the next section.

Work of the calling code

We saw above that when writing code that calls a function, we must PROVE the preconditions before the function call (since the function requires that we meet those preconditions in order to work correctly). After the function terminates, the calling code can list the function’s postconditions as PREMISES (since the function ensured that certain things would happen).

The “calling code” in Scala goes outside of any function definition. Typically, I place the calling code at the bottom of the Scala file, after all functions. This is the code executed first by Scala, just like in Python programs.

Proving preconditions

Suppose we wish to call the div function to divide two numbers:

val x: Z = 10
val y: Z = 2

val num: Z = div(x, y)

If we included that calling code in a file with our div function, we would see an error that we had not yet proved the precondition. To prove each precondition, we must have a proof block just before the function call that demonstrate each precondition claim, using value being passed instead of the parameter name. Since we are passing the value y as our second parameter, and since the div function requires that b != 0 (where b is the second parameter), then we must demonstrate that y != 0:

val x: Z = 10
val y: Z = 2

Deduce(
    1 ( x == 10     )   by Premise,     //from the "x = 10" assignment
    2 ( y == 2      )   by Premise,     //from the "y = 2" assignment
    3 ( y != 0      )   by Algebra*(2)  //satisfies the precondition for div
)

val num: Z = div(x, y)

Note that Logika is picky about proving the precondition using EXACTLY the value being passed for the corresponding parameter. For example, suppose instead that we wanted to divide x-1 and y+1:

val x: Z = 10
val y: Z = 2

Deduce(
    1 ( x == 10     )   by Premise,     //from the "x = 10" assignment
    2 ( y == 2      )   by Premise,     //from the "y = 2" assignment
    3 ( y != 0      )   by Algebra*(2)  //NO! precondition is not satisfied!
)

val num: Z = div(x-1, y+1)

If we made no changes to our logic block, Logika would complain that we had not satisfied the precondition. And indeed we haven’t – while we’ve shown that y isn’t 0, we haven’t shown that our second parameter y+1 isn’t 0. Here is the correction:

val x: Z = 10
val y: Z = 2

Deduce(
    1 ( x == 10     )   by Premise,     //from the "x = 10" assignment
    2 ( y == 2      )   by Premise,     //from the "y = 2" assignment
    3 ( y+1 != 0    )   by Algebra*(2)  //Yes! Satisfies the precondition for our second parameter (y+1)
)

val num: Z = div(x-1, y+1)

Using postconditions

Recall that since the function is ensuring that it will do/return specific things (its postconditions), then the calling code can list those postconditions as premises after the function call. If a postcondition uses the keyword Res[returnType], then the calling code can list exactly that postcondition using whatever variable it used to store the return value in place of Res[returnType] and using whatever values were passed in place of the parameter names. In the div example above where we divide x-1 by y+1, the calling code stores the value returned by div in a variable called num. Since the div postcondition is result == a/b, then we can claim the premise num == (x-1)/(y+1):

val x: Z = 10
val y: Z = 2

Deduce(
    1 ( x == 10     )   by Premise,     //from the "x = 10" assignment
    2 ( y == 2      )   by Premise,     //from the "y = 2" assignment
    3 ( y+1 != 0    )   by Algebra*(2)  //Yes! Satisfies the precondition for our second parameter (y+1)
)

val num: Z = div(x-1, y+1)

Deduce(
    1 ( num == (x-1)/(y+1)  )   by Premise  //postcondition of div
)

We know from looking at this example that (x-1)/(y+1) is really 9/3, which is 3. We would like to be able to assert that num, the value returned from div, equals 3. We can do this by adding a few more steps to our final proof block, plugging in the values for x and y and doing some algebra:

val x: Z = 10
val y: Z = 2

Deduce(
    1 ( x == 10     )   by Premise,     //from the "x = 10" assignment
    2 ( y == 2      )   by Premise,     //from the "y = 2" assignment
    3 ( y+1 != 0    )   by Algebra*(2)  //Yes! Satisfies the precondition for our second parameter (y+1)
)

val num: Z = div(x-1, y+1)

Deduce(
    1 ( num == (x-1)/(y+1)  )   by Premise,     //postcondition of div
    2 ( x == 10             )   by Premise,     //previous variable assignment
    3 ( y == 2              )   by Premise,     //previous variable assignment
    4 ( num == 9/3          )   by Algebra*(1,2,3),
    5 ( num == 3            )   by Algebra*(4)  //needed for assert 
)

assert(num == 3)

Using a function contract and our deduction rules, we have PROVED that the div function will return 3 in our example (without needing to test the code at all).

Examples

In this section, we will see two completed examples of Logika programs with a function and calling code.

Example 1

In this example, we write a plusOne function that takes a non-negative parameter and returns one more than that parameter:

// #Sireum #Logika
//@Logika: --manual --background save
import org.sireum._
import org.sireum.justification._
import org.sireum.justification.natded.prop._

def plusOne(n: Z): Z = {
    Contract(
        Requires(   n >= 0  ),
        Ensures(
            Res[Z] == n + 1,
            Res[Z] > 0
        )
    )

    val answer: Z = n + 1
  
    Deduce(
        1  (    n >= 0          )   by Premise,
        2  (    answer == n + 1 )   by Premise,
        3  (    answer > 0      )   by Algebra*(1, 2)
    )

    return answer
}

////////// Test code ///////////////

var x: Z = 5
Deduce(
    1  (    x == 5  )   by Premise,
    2  (    x >= 0  )   by Algebra*(1)
)

var added: Z = plusOne(x)

Deduce(
    1  (    x == 5                  ) by Premise,
    2  (    added == x + 1          ) by Premise,
    3  (    added > 0               ) by Premise,
    4  (    added == 6              ) by Algebra*(1, 2),
    5  (    added == ∧ & added > 0  ) by AndI(4, 3)
)
assert(added == 6 ∧ added > 0)

var x: Z = 5

Deduce(
    1 (     x == 5      )   by Premise,     //from the "x=5" assignment
    2 (     x >= 0      )   by Algebra*(1)  //proves the plusOne precondition
)

var added: Z = plusOne(x)

Deduce(
    //I can list the postcondition (what is returned) as a premise
    1 (     x == 5                  )   by Premise, //x is unchanged 
    2 (     added == x+1            )   by Premise, //plusOne postcondition 1
    3 (     added > 0               )   by Premise, //plusOne postcondition 2
    4 (     added == 6              )   by Algebra*(1,2),
    5 (     added == 6 ∧ added > 0  )   by AndI(4,3)
)

assert(added == 6 ∧ added > 0)

Note that when we have more than one postcondition, we must prove all postconditions inside the function, and we can list all postconditions as premises in the calling code after the function call.

Example 2

In this example, we write a findMax function that returns the biggest between two integer parameters. This is very similar to an example from section 8.7, which used an if/else statement to find the max. In that example, our assert that we had indeed found the max was: assert((max >= x & max >= y) & (max == x | max == y)). We will see that our postconditions for findMax come directly from the different claims in that assert. In our calling code, we call findMax with num1 (which has the value 3) and num2 (which has the value 2). We are able to prove that findMax returns 2:

// #Sireum #Logika
//@Logika: --manual --background save
import org.sireum._
import org.sireum.justification._
import org.sireum.justification.natded.prop._

//find the max between x and y
def findMax(x: Z, y: Z): Z = {
    Contract(
        //no precondition needed
        Ensures(
            Res[Z] >= x,                //postcondition 1
            Res[Z] >= y,                //postcondition 2
            Res[Z] == x v Res[Z] == y   //postcondition 3
        )
    )

    var max: Z = 0

    if (x > y) {
        max = x

        Deduce(
            1 (     max == x    )   by Premise,
            2 (     max >= x    )   by Algebra*(1),     //build to postcondition 1
            3 (     x > y       )   by Premise,         //IF condition is true
            4 (     max >= y    )   by Algebra*(1,3)    //build to postcondition 2
        )

    } else {
        max = y

        Deduce(
            1 (     max == y    )   by Premise,
            2 (     ¬(x > y)    )   by Premise,         //IF condition is false
            3 (     x <= y      )   by Algebra*(2),
            4 (     max >= x    )   by Algebra*(1, 2),  //build to postcondition 1
            5 (     max >= y    )   by Algebra*(1)      //build to postcondition 2
        )
    }

    //prove the postconditions
    Deduce(
        //true in both the if and the else
        1 (     max >= x            )   by Premise,     //proves postcondition 1 
        2 (     max >= y            )   by Premise,     //proves postcondition 2

        //first was true in if, second true in else
        3 (     max == x v max == y )   by Premise     //proves postcondition 3
    )

    return max
}

////////////// Test code /////////////////

val num1: Z = 3
val num2: Z = 2

//findMax has no preconditions, so nothing to prove here

val biggest: Z = findMax(num1, num2)

Deduce(
    1 (     biggest >= num1                     )   by Premise,     //findMax postcondition 1
    2 (     biggest >= num2                     )   by Premise,     //findMax postcondition 2
    3 (     biggest == num1 v biggest == num2   )   by Premise,     //findMax postcondition 3

    //pull in the initial values
    4 (     num1 == 3                           )   by Premise,
    5 (     num2 == 2                           )   by Premise,

    6 (     biggest >= 3                        )   by Algebra*(1, 4),
    7 (     biggest >= 2                        )   by Algebra*(2, 5),
    8 (     biggest == 3 v biggest == num2      )   by Subst_<(4, 3),
    9 (     biggest == 3 v biggest == 2         )   by Subst_<(5, 8),

    //OR-elimination
    10 SubProof(
        11  Assume( biggest == 3 ) 
    ),
    12 SubProof(
        13  Assume( biggest == 2 ) 
        14 (    ¬(biggest >= 3)                 )   by Algebra*(13),
        15 (    F                               )   by NegE(6, 14),
        16 (    biggest == 3                    )   by BottomE(15)
    ),
    17 (        biggest == 3                    )   by OrE(9,10,12) //needed for assert
)

assert(biggest == 3)