SE 504
Notes on Richard Denman's Chapter 20: Procedure Declarations and Calls
from his unpublished manuscript

Work in progress ...


Subprograms and Argument Passing

First we review the concept of a subprogram, which, in different programming languages, is known by different names. Subprograms having a return value (so that invocations of which serve as expressions, but not necessarily as stand-alone commands/statements) are known as functions in several languages, including FORTRAN, Pascal, Ada, and C/C++. Subprograms having no return value (which in C/C++ (respectively, Java) would correspond to a function (respectively, method) whose return type is specified as being void) are referred to as procedures in Pascal and Ada and as subroutines in FORTRAN. An invocation of such a subprogram serves as a complete command/statement.

One of the features that makes subprograms particularly useful is their ability to receive input (and possibly produce output) in the form of arguments (also referred to as parameters) that are "passed" between the caller and the subprogram.

For example, consider the following Java method:

public static int square(int k) { return k*k; }

We invoke (or call) the method using an expression of the form

square(E)

where E is itself an expression of type int. This expression (i.e., the call to the method) is evaluated as follows:

  1. E is evaluated in the current state, yielding, say, v.
  2. v is assigned to the method's formal argument, k, which thereafter plays the role of a local variable within the method as it executes.
  3. The body of the method is executed, which produces a return value (in this case, v2), which is taken to be the value of the invocation expression.

Thus, if variable s has value 5 when the command/statement

r = square(s+1) - 4;

is executed, the result will be that the value 32 is assigned to r. In particular, the subexpression square(s+1) evaluates to 36.

The above example illustrates the use of argument passing for the purpose of providing input to a subprogram (i.e., for sending data from caller to subprogram). In some languages, one also may use arguments to send information in the opposite direction, from subprogram to caller. Consider the following procedure subprogram:

procedure increment(in k : int; out m : int)
   |[ m := k+1 ]|

Its intended purpose is to receive as input from the caller one value (via the formal argument k) and to pass back (via the formal argument m) the value one greater than that received. Notice that we use in (respectively, out) to identify a formal argument used strictly for input (respectively, output).

In terms of Hoare Triples, the semantics of the invocation increment(x,y) is given by

{x=C} increment(x,y) {x=C ∧ y=C+1}

Of course, here C is a rigid variable (in Gries's terminology) or specification constant (as it is called by some others).

One may also wish to use an argument to pass data first from caller to subprogram, and then, when the subprogram has terminated, in the other direction. As an example, consider this procedure:

procedure increment2(inout k : int)
   |[ k := k+1 ]| 

Its semantics are given by the Hoare Triple

{x=C} increment2(x) {x=C+1}

Notice that we use inout to identify a formal argument used for both input and output.

Let us now provide an operational description of how a procedure invocation is carried out. Assume that we have the following procedure declaration:

procedure proc(in x : T1; out y : T2; inout z : T3)
   |[ S ]|  

Of course, S is the body of the procedure. For each of the three argument-passing modes, we have one formal argument. Generalizing it to more (or fewer!) is straightforward.

The invocation proc(E,b,c) (where E is an expression of type T1 and b and c are variables of types T2 and T3, respectively, is executed as follows:

  1. E is evaluated and its value is assigned to formal argument x
  2. The value of c is assigned to formal argument z
  3. The body of the method, S, is executed, at the conclusion of which
  4. Execution resumes in the calling program.

Notice that an out formal argument is not assigned a starting value. Hence, it must be initialized within the subprogram.

Also notice that the actual argument corresponding to an out or inout formal argument must be a variable, whereas the actual argument corresponding to an in formal argument can be, more generally, an expression (and need not be a variable). Otherwise, the assignment of formal argument values to actual arguments (as a procedure ends) would not make sense.

The mechanism that we describe above for how values are passed between out and inout formal arguments and their corresponding actual arguments is sometimes called pass-by-value-return. A different (and more commonly used) mechanism that can be used, but that can have a different effect if aliasing (see below) occurs, is pass-by-reference, in which the formal argument shares the same memory space as the corresponding actual argument.

If you are a Java or C programmer, you may be wondering how your language is able to get by without having out or in out modes of argument passing. (Both of these languages use the pass-by-value mechanism exclusively, which corresponds to how our in formal arguments work.) The answer is that, in C, you can pass the address of a variable to a subprogram, giving the subprogram the ability to access (and/or change) what is stored there. In effect, then, you get the inout mode. In Java, when you pass a reference to an object (i.e., any value of a non-primitive type) to a method, the called method can itself invoke methods upon that object, potentially changing its state.

For the sake of simplicity, here we are going to focus exclusively upon procedure subprograms, and ignore function subprograms. (As noted earlier, in Java (and C/C++), the former correspond to methods with return type void.)


Aliasing Complicates Matters

One of the things that makes it difficult to deal with proving the correctness of programs that invoke subprograms is aliasing, which refers to a situation in which the same item has multiple names.

To illustrate, imagine that we have the following procedure

procedure bump(inout x, y : int)
   |[ x := x+1; y := y+1; z := z+1 ]| 

You are to understand z to be a variable whose scope includes the procedure. (That is, from the point of view of the procedure, z is a "global" variable.) (In Java, of course, the scope of a class's instance variables includes the bodies of all non-static methods in the class.)

Now, consider the following code segments, each of which includes a call to this procedure:

In the first, there is no aliasing, so no problems arise. But in each of the others, a troublesome kind of aliasing occurs. In order to make the proof rule for procedures simple, we shall not allow any aliasing of these kinds. Specifically, one cannot pass the same actual argument to two different formal arguments, each of which is designated as either inout or out. Nor are we going to allow non-local variables to be mentioned inside a subprogram. (Note: This restriction is really too strict!)

We shall, however, allow a benign type of aliasing in which the same actual argument is passed to any number of in formal arguments along with at most one formal argument that is either out or inout.

For example, recall the procedure increment() described earlier:

procedure increment(in k : int; out m : int)
      |[ m := k+1 ]| 

The call increment(a,a) is legal and has the effect of increasing the value of variable a by one.


Proof Rule for Procedures

The proof rule for procedures has two parts

The first of these rules is nothing more than what you've been doing in the course already, except here the input and output variables are going to be the formal arguments (rather than what we have been declaring as local constants and variables).

As an example, take our increment() procedure, here augmented with a pre- and post-condition (so as to provide its specification in the form of a Hoare Triple):

procedure increment(in k : int; out m : int)
   |[ { true } m := k+1 { m=k+1} ]| 

The second rule says something like this: If a procedure proc with formal arguments x1, ..., xm has as its specification the Hoare Triple {P} S {Q} then the call proc(E1,E2,...,Em) should satisfy the Hoare Triple

{P(x1,x2,...,xm := E1,E2,...,Em)} proc(E1,E2,...,Em) {Q(x1,x2,...,xm := E1,E2,...,Em)}

Here it is to be understood that if xi is designated as out or inout, then Ei must be a variable.

In other words, it would be nice if the Hoare Triple obtained by substituting actuals for formals in the subprogram's specification were guaranteed to be valid.

For example, {true} increment(a, b) {b = a+1} (which we obtained by applying our proposed second rule (in particular, using the textual substitution k,m := a,b)) should be valid.

But if we apply this rule to the invocation increment(a,a), we get the erroneous Hoare triple

{true} increment(a,a) {a = a+1}

Hence we see that even this kind of "benign" aliasing seems to lead to problems. But we can fix it if we forbid mentioning any in arguments in the postcondition of a procedure's specification. Instead, for every in formal argument, we introduce a rigid variable to capture the argument's initial value.

In our example, we would use the following specification:

procedure increment(in k : int; inout m : int)
    |[ {pre: k = C} m := k+1; {post: m = C+1} ]| 

Now if we do the straightforward substitution of actuals for formals, as above, we get the (valid) Hoare triple

{a = C} increment(a,a) {a = C+1}

Note that we might also need to rename the rigid variables according to the context of the procedure call. For example, our proof rule should allow us to show that

{a = B} increment(a,a) {a = B+1}

is correct, or even

{a = B+z-5} increment(a,a) {a = (B+z-5)+1}

Here we used, respectively, the substitutions C:=B and C:=B+z-5. Such a substitution will be valid provided that its "right-hand side" does not mention any actual argument.

Now we summarize and formalize. For the sake of brevity, we show procedures having only one formal argument of each of the three modes and we omit any indication of the formal arguments' data types.

Proper Specification: A procedure specification

procedure f(in x; out y; inout z) 
   |[ {P} S {Q} ]| 

is proper if P is independent of y (i.e., fails to mention y) and Q is independent of x (i.e., fails to mention x).

Valid Procedure Declaration: The procedure declaration above is valid if its specification is proper and the Hoare Triple {P} S {Q} is valid.

Procedure Call Rule: If procedure f has a valid declaration (as shown above), then

{P(x,z := E,c)ψ} f(E,b,c) {Q(y,z := b,c)ψ} 

is valid for all E (expression) and b,c (variables) and all substitutions ψ for constants in P and Q, provided that no variable occurs within b or c more than once and substitution ψ refers to none of the actual arguments.

Example

Click here.