Lock-and-key Strategies for Handling Undefined Variables

Page created by Brian Osborne
 
CONTINUE READING
SOFTWARE—PRACTICE AND EXPERIENCE, VOL. 23(7), 693–710 (JULY 1993)

              Lock-and-key Strategies for Handling
                      Undefined Variables

                         richard b. borie and allen s. parrish
       Department of Computer Science, University of Alabama, Box 870290, Tuscaloosa, AL
                                      35487-0290, U.S.A.
                                                and
                                       srinivas mandyam
                 BusLogic, Inc., 4151 Burton Drive, Santa Clara, CA 95054, U.S.A.

                                            SUMMARY
Most programming languages ignore the problem of undefined variables and permit compilers to
allow the leftover contents of the memory cells belonging to such variables to be referenced.
Although efficient, this type of semantics does not support software engineering, because defects
might arise in subtle ways and might be difficult to locate. Two other types of semantics that
better support software engineering are to either initialize all variables to some default, e.g. zero,
or to require that all references to undefined variables be treated as errors. However, these types
of semantics are obviously more expensive than simply ignoring the undefined variables entirely.
   In this paper, we propose a simple technique that works equally well for both of these latter
two types of semantics, and whose efficiency compares favorably for certain realistic programs
with more traditional implementations of these semantics. Furthermore, we provide a mechanism
for using this technique through Ada implementations of two abstract data types where undefined
variables respectively exhibit these two types of semantics, and whose implementations of these
semantics use our technique. These abstract data types allow our technique to be selectively used
in strictly those situations where the cost of the technique is justified. We provide practical
examples illustrating these situations.
key words: Undefined variables   Programming languages    Software engineering   Algorithms   Language
design Sparse arrays

                                  INTRODUCTION
The method of handling variables that are referenced before being defined represents
one of many decisions that have to be made by programming language designers.
At the semantic level, there are three approaches:1
  I1. Allow values of undefined variables to be referenced. This means that bit
      patterns that happen to be ‘leftover’ in memory from previous executions
      could be used by the program.
  I2. Initialize all variables to default values, e.g. zero for integers, blank for
      characters, etc.
  I3. Define rules prohibiting references to undefined variables.

0038–0644/93/070693–18$14.00                                            Received 10 August 1992
 1993 by John Wiley & Sons, Ltd.                                        Revised 5 January 1993
694                  r. b. borie, a. s. parrish and s. mandyam
   I1 effectively means that the whole issue is ignored, and undefined variables are
treated just like any other variable. Among these different semantics, I1 can be
‘implemented’ most efficiently, since actually no implementation is required. Conse-
quently, I1 is the most frequently adopted semantics by language designers.1,2
However, I1 is generally acknowledged to be the most problematic semantics in
terms of software engineering.1–5 For example, both Reference 2 and Reference 3
observe that I1 can hide defects during testing. Consider a program which contains
a counter that was not properly initialized to zero at the beginning of the program.
If the counter happens to initially contain a zero coincidentally during the testing
process, testing might not reveal any defects. But if the program then later executes
in an environment where the counter is coincidentally initialized to something other
than zero, a defect might then appear.
   This example seems to offer some support for I2 or I3, despite additional
implementation costs. Between these two approaches, there is some disagreement as
to which one better supports software engineering. Some authors, e.g. Meyer1 and
Weide, Ogden and Zweben4, argue in favor of I2, because it simplifies the program-
mer’s tasks if the initialization values are well-chosen. On the other hand, Reference
2 argues in favor of I3, because despite the use of well-chosen initialization values,
there might be some cases where a default initialization value was not really intended
to be used. In such cases, the programmer might have made a mistake in either
failing to properly assign the variable, or in referencing a variable that should not
have been referenced. As an example from Reference 2, consider a program that is
supposed to find the minimum value in an array (1. .N) of natural numbers that has
been filled by the programmer in positions (1. .i), where i is allowed to be strictly
less than N. If the program is ‘off-by-one’ and accidentally looks at position i+1 in
searching for the minimum, and I2 semantics have been used to initialize the array
to zeros, then the program could conceivably indicate that the 0 in the (i+1)th
position is the minimum. Yet the (i+1)th position is irrelevant, because it is not part
of the data set. Depending on the context, this error might be difficult to find during
testing and debugging.
   Regardless of whether I2 or I3 better supports software engineering, it is clear
that both offer more support than I1, yet both are more expensive than I1. Conse-
quently, reducing the cost of implementations of these semantics seems to be a
desirable goal. In this paper, we propose a simple technique that works equally well
for both of these latter two types of semantics, and whose efficiency compares
favorably for certain realistic programs with more traditional implementations of
these semantics. We offer an analysis of our approach that concretely identifies the
types of programs for which the approach outperforms existing implementations.
Furthermore, we provide a mechanism for using this technique through Ada
implementations of two abstract data types where undefined variables respectively
exhibit these two types of semantics, and whose implementations of these semantics
use our technique. These abstract data types allow the programmer to selectively
use our technique in strictly those situations where the cost of the technique is
justified. We provide practical examples illustrating these situations. Although there
are advantages in building our technique directly into compilers, our use of abstract
data types has the advantage of permitting our technique to be used with existing
compilers.
lock-and-key strategies                              695
                           EXISTING IMPLEMENTATIONS
In this section, we consider existing, typical implementations of I2 and I3. Our
interest is in informally analyzing the run-time requirements of these implementations
beyond what is already required by normal program execution, i.e. with I1 semantics.
To make our analysis as widely applicable as possible, we assume a typical machine
architecture where no special provisions have been made for efficiently implementing
these semantics. We also conduct our analysis in the context of local procedure
variables in a stack-based language such as Pascal. Thus, although the load image
for a program may include a static memory map allowing global variables to be
initialized without run-time overhead, our discussion is in the context of local
procedure variables, for which run-time initialization overhead is necessary.
   For simplicity, we define a variable to be any scalar variable of simple type, such
as integer, character, real or boolean, or any elementary component of a structure,
such as a single integer element from an array of integers. Also, we use the term
program to mean any program unit, such as a procedure or block, for which variables
can be separately allocated at initiation and destroyed at termination. We then refer
to the collection of all variables in a program as the program’s variable space. A
variable definition is a statement that assigns a value to the variable, i.e. through
assignment or user input. A variable reference is a statement that refers to an
existing value of the variable, i.e. through the right side of an assignment statement
or through a predicate.
   Throughout this work, it is important to classify variable references as follows:
  1. Variable references for which the compiler can determine whether or not the
     variable has been defined. These variable references fall into two subcategories:
        (a) Variable   references for which there is no path from the beginning of the
            program    to the reference that contains a definition of the variable.
        (b) Variable    references for which every path from the beginning of the
            program    to the reference contains a definition of the variable.
  2. Variable references for which the compiler cannot determine whether or not the
     variable has been defined. These variable references fall into two subcategories:
        (a) Variable references for which some, but not all paths from the beginning
            of the program to the reference contain definitions of the variable.
        (b) References where the memory location is based entirely on run time
            events. This includes most references to array elements that have variable
            subscripts, and to dereferenced pointers.
   The compiler is unable to determine whether references in 2(a) are guaranteed to
be defined, because it cannot determine whether the paths containing definitions are
executable. A similar determination is impossible for 2(b) because the compiler
cannot determine the value of the subscript variable. We note that because of 2(b),
variable references falling into categories 1(a) and 1(b) are usually to locations where
the relative address can be computed at compile time, such as normal scalar variables.
   We first consider I3 semantics, which makes references to undefined variables
illegal. Because static analysis can detect references falling into category 1, such
references are either guaranteed errors 1(a), and can be flagged at compile time with
696                  r. b. borie, a. s. parrish and s. mandyam
a fatal error, or are guaranteed not to be errors 1(b), and can be flagged and ignored
at run-time. However, since the compiler cannot determine whether variable references
in category 2 have been defined, run-time analysis is necessary for variables in this
category. Also in the absence of static analysis to identify variables falling into the
first category, such run-time analysis must be performed with respect to all variables.
   There are several implementations of tun-time analysis for I3.2 Two representative
implementations are as follows:

  1. Undefined values method. There is an ‘undefined’ value for every type, that is
     outside the set of legal values for the type. The compiler must generate code
     to initialize every variable to the undefined value for its type. This code is
     executed at program start-up time. Then, every time a variable is referenced,
     if the variable contains the undefined value for its type, an error message is
     generated. This requires time at start-up to perform the initialization, proportional
     to the number of variables with references in category 2, and constant time
     for every variable reference in category 2.
  2. Extra bit method. For every variable, an extra ‘defined’ bit denotes whether or
     not the variable has been assigned a value. All of the bits must be set to
     ‘undefined’ initially, requiring time at start-up to perform the initialization
     proportional to the number of variables with references in category 2. Then
     constant time is required for every variable reference in category 2 to check
     the assigned bit, and also for every variable definition with a subsequent
     reference in category 2 to check and/or update this bit. If the assigned bit still
     has its ‘undefined’ value when the corresponding variable is referenced, an
     error message is generated.

   Other existing implementations of the run-time analysis component of I3 possess
the same general form. Essentially, for any of these implementations, an initialization
must first be performed, requiring time proportional to the number of variables with
references in category 2, to set every variable to an undefined state. Then when the
user code is executed, every variable reference must be checked to see whether or
not the variable is in its undefined state. If a variable is referenced while in its
undefined state, an error message is generated.
   To implement I2 semantics, the compiler must generate additional code to initialize
some or all program variables. In the absence of static analysis, all program variables
must be initialized. If static analysis is performed to categorize variable references
as above, then an optimization is possible: it is not necessary to generate initialization
code for variables where every reference falls into category 1(b), since such variables
are always guaranteed to be initialized. Of course, both of these approaches require
time proportional to the number of variables being initialized.
   An alternative approach to implementing I2 involves generating code to initialize
only those variables having references in category 1(a). As pointed out already, it
is unnecessary to generate initialization code for variables whose only references are
in category 1(b). Rather than initializing variables having references in category 2,
one of the strategies for detecting undefined variables at run-time could be employed.
However, instead of generating an error, a default value for the type could be
returned. This approach takes initialization time proportional to the number of
lock-and-key strategies                               697
variables having references in category 1(a), plus the time required to detect undefined
variables in category 2.
   All of these implementations of I2 and I3 share a potentially non-trivial initializ-
ation cost at program start-up time. Our proposed implementations of I2 and I3
discussed in the next section eliminate this start-up cost. Program start-up can be
done in constant time with either semantics. However, with either semantics, our
algorithm requires constant time for most variable definitions and references. We
show that despite these additional time requirements, the start-up savings are still
worth the additional cost for certain programs.

                   NEW IMPLEMENTATIONS FOR I2 AND I3

Our proposed implementations of I2 and I3 both use the same algorithm to detect
undefined variables. For I2, a reference to an undefined variable simply returns the
default value for variables of that type; for I3, an error is generated whenever an
undefined variable is referenced. Our algorithm is very similar to algorithms that have
been developed to detect dangling pointers,6 to support constant time initialization and
finalization of arrays,7 and to support a version of dynamic type checking in C.8
   The basic idea behind our algorithm involves associating a ‘lock’ and a ‘key’
with every variable that possibly could be undefined when referenced, i.e. every
variable not eliminated by static analysis. If the lock and key do not match, then
the variable is assumed to be undefined, and the appropriate action is performed,
i.e. returning a default value for I2 or generating an error for I3. The lock associated
with a variable is set to match the key when the variable is defined, thus allowing
subsequent access to the variable’s actual value.
   Although our final implementation eliminates this possibility, we first consider a
simplified version of the algorithm that allows undefined variables to possibly go
undetected because of a coincidental match between a key and an undefined lock.
Specifically, the locks and keys are implemented as follows. The key for a variable
is simply its memory address; thus, the key can be computed from the variable
without requiring additional space. The lock is an extra memory cell associated with
the variable that is large enough to contain its address, i.e. in order to match its
key. The idea is that at the first time a variable is defined, the address of the
variable is assigned to its lock, making the lock and key match at this point. Any
subsequent references to the variable therefore return the actual value of the variable.
By the same token, if the lock and key do not match on a particular variable
reference, it can be assumed that the variable is undefined, and either an error
message is generated or the default value is returned.
   More specifically, we have the following steps.

  1. Whenever a variable is defined. First, check to see whether the lock matches
     the key. If so, then do nothing, as the variable has been defined previously. If
     not, then assign the lock the address of the variable, thus allowing the lock
     and key to match for subsequent references.
  2. Whenever a variable is referenced. First, check to see whether the lock matches
     the key. If so, permit the actual value of the variable to be referenced, as the
698                  r. b. borie, a. s. parrish and s. mandyam

                            Figure 1. Lock-and-key configurations

      variable has been defined previously. If not, then the variable is undefined. For
      I2, return the default value for variables of this type; for I3, generate an error.
   Figure 1 demonstrates the lock-and-key configuration for both undefined and
defined variables using this simplified algorithm.
   As noted above, the problem with this algorithm is that there is nothing to
guarantee that a lock and key cannot initially match coincidentally, since there is
no a priori initialization. Since our objective is to achieve constant running time at
start-up, we cannot simply take the obvious approach of initializing all of the locks
to some type of undefined state. This would also do nothing more than reduce the
implementation to a more complicated version of the extra-bit method. Instead, rather
than assigning a value directly to the lock as above, we make the lock for variable
v (v.Lock) a pointer into an array (LockValues) which contains the actual lock values.
The cells from LockValues are then uniquely allocated to various locks dynamically,
from contiguous locations in the array. A variable, fence, partitions LockValues into
its allocated and unallocated portions; fence is initialized to 0 to denote that no
locks have been allocated. Then, in order to conclude that variable v has been
defined, v.Lock must point at a value in the allocated portion of LockValues, and
LockValues[v.Lock] must match the key for v, i.e. its address, v.Key.
   Figure 2 shows the lock-and-key configuration given this approach, for an undefined

                               Figure 2. Undefined variable v
lock-and-key strategies                               699

                             Figure 3. After the statement v:=25

variable v, and Figure 3 shows the lock-and-key configuration after v has been
defined. Unlike our simpler, but incorrect, version of the algorithm given previously,
this implementation requires non-zero start-up overhead, because fence must be
initialized to 0. However, obviously this overhead is a very small constant. Figure 4
illustrates the details of the algorithm as an implementation of I3. Figure 5 illustrates
the analogous implementation of I2. Note that, in either case, all three of the
operations (Initialize, Define and Reference), should be guaranteed to be performed
automatically by the language. The operation Initialize is performed automatically at
program start-up, whereas Define and Reference are performed at every variable
definition and reference, respectively.

                      Figure 4. Lock-and-key algorithm pseudocode for I3
700                   r. b. borie, a. s. parrish and s. mandyam

                      Figure 5. Lock-and-key algorithm pseudocode for I2

              COMPARISON WITH OTHER IMPLEMENTATIONS

We now compare the execution time of the lock-and-key algorithm with those of
existing implementations, for both I2 and I3. As discussed previously, it is possible
to use varying degrees of static analysis with either semantics. The degree to which
static analysis is performed affects the details of our execution time analysis. For
simplicity, we first conduct the execution time analysis as if there were no static
analysis. We later consider the impact of static analysis on our execution time analy-
sis.
   We let V refer to the size of the variable space for a given program. For a given
program execution, we let D and R be the total numbers of variable definitions and
references, respectively. The comparison is summarized in Table I. Note that the
values provided in Table I refer to the additional time requirements of the I2 and
I3 strategies, beyond what is required for the normal execution of the program with
I1 semantics.
   It is fair to point out that although the O(V) term is eliminated from both I2 and
I3 by the lock-and-key approach, the O(D+R) term hides a relatively large constant,
e.g. larger than the analogous term in the extra-bit implementation of I3. If we call
this larger constant k, and the constant associated with the O(V) term in traditional
methods c, then the lock-and-key approach is superior to traditional implementations
of I2 and I3 whenever k(D+R) , cV. Regardless of the exact values of k and c,
however, we maintain that there are realistic situations where the average for D+R

                            Table I. Comparison of execution times

                               Traditional       Traditional     Lock-and-key
               Strategy         method              time             time

               I2              Obvious             O(V)              O(D+R)
               I3           Undefined values      O(V+R)             O(D+R)
               I3              Extra bit         O(V+D+R)            O(D+R)
lock-and-key strategies                                 701
over all executions is small enough relative to V to satisfy this relationship. In
particular, a static array to hold variable-size input data, where the worst case number
of data elements is much larger than the average case, constitutes such a situation.
We elaborate on some specific applications involving this situation later in the paper.
   Adding static analysis does not substantially affect our execution time analysis.
In particular, suppose we interpret V, D and R in Table I to refer to their respective
quantities after static analysis has eliminated certain variables, definitions, and refer-
ences from consideration at run-time. Provided that I3 produces only warnings at
compile time for references in category 1(a), where no path preceding the reference
contains a definition, then both I2 and I3 must deal with references in both categories
1(a) and 2 at run-time. So the analysis in Table I still holds.
   Alternatively, if I3 flags the references in category 1(a) as errors, which is certainly
possible and perhaps desirable, this would allow I3 to ignore such references at run-
time. On the other hand, I2 remains forced to deal with such references, since they
do not constitute errors under I2. This latter interpretation would thus make the V,
D and R quantities smaller for some programs under I3 than under I2. However,
for either I2 or I3, this still does not change our basic conclusion concerning the
circumstances under which the lock-and-key approach is more appropriate than
conventional methods.

                            IMPLEMENTATION IN ADA
A combination of software engineering and performance concerns might motivate
choosing different initialization semantics and different implementations of those
semantics for different programs. An ideal approach would provide a compiler switch
that permits the programmer to choose the appropriate option from Table I for the
program at hand. This would permit static analysis to be integrated with run-time
checking, possibly allowing certain variables to be eliminated from being subject to
run-time checking. A second approach would provide user-defined types that exhibit
the desired initialization semantics and performance characteristics. This would allow
various implementations of I2 and I3 to be introduced into the context of existing
compilers that only support I1 semantics, and would still allow the programmer to
select the desired semantics by choosing the appropriate type. Although perhaps not
as ideal as building our technique into the compiler, this approach of providing user-
defined types would at least permit the lock-and-key technique to be used in situations
where modifying the compiler is not an option.
   In this section, we provide an example of the latter approach by providing user-
defined types whose initialization semantics are supported by our lock-and-key
algorithm. Because of its data abstraction capability, we choose Ada as the implemen-
tation language. Since the lock-and-key approach is most appropriate when the
number of variable references is small relative to the size of the variable space, we
feel that this approach has the most potential in the context of large structures that
are, on the average, only partially used. We have therefore chosen to demonstrate
this approach in the context of an array type, which we say is protected from
undefined references by the lock-and-key mechanism. In this context, a ‘key’ for a
variable, an array element, is just its index. Note that we could have chosen to
‘protect’ other types of structures in a similar fashion.
   We actually provide two types: an array type with I2 initialization semantics,
702                 r. b. borie, a. s. parrish and s. mandyam
exported by the package in Figure 6, and an array type with I3 initialization
semantics, exported by the package in Figure 7. The package I2 requires generic
parameters for the type of array elements and for the default value for the type; the
array elements are then initialized to the default in constant time before variables
of the protected array type are used. The package I3 only requires the array element
type as a formal parameter; in this case, each reference to an array element that has

                          Figure 6. Implementation of I2 in Ada
lock-and-key strategies                             703

                          Figure 7. Implementation of I3 in Ada

not been defined causes an exception to be raised. Each of these two packages
exports Define and Reference operations that must be invoked in the obvious fashion.
For example, Define(a,3,10) is invoked to assign the value 10 to element 3 in
protected array a. Reference(a,i) is invoked whenever it is desired to reference
element i in protected array a. Examples using these operations are given in the
next section.
704                  r. b. borie, a. s. parrish and s. mandyam
   Either of the packages I2 and I3 can easily be generalized to support protected
multidimensional arrays. Figure 8 represents a two-dimensional generalization of the
I3 package. The only feature that needs explanation is the allocation of storage for
the field LockValues in the record Protected2dimArray. Ada prohibits declaring the
size of a field to be an expression involving discriminants, such as Max1*Max2.9
However, using an access type as shown is permitted.

      PRACTICAL APPLICATIONS OF THE LOCK-AND-KEY TECHNIQUE
We first consider an example of a procedure that locates and prints the smallest
integer from the collection of integers stored in an array. The number of integers
that are actually input into the array is determined by the actual value of a parameter
called Count. We assume that Count is normally small relative to the size of the
(static) array, which has been defined to handle a worst-case scenario in terms of
data size. We note that even in a language like Ada that supports dynamic arrays,
defining a static array with substantial wasted space, which costs little, may still be
desirable to reduce execution time.
   As observed earlier, this example was used in Reference 2 to argue in favor of
I3. In particular, given the error that Count is one greater than the actual size of
the data n, I1 will lead to totally unpredictable results due to the existence of
random data in position n+1, whereas I2 may lead to the incorrect conclusion that
the 0 in the (n+1)th position is the minimum.
   Although our Ada compiler only supports I1 semantics, our Ada package I3 solves
this problem. Figure 9 contains an example of a minimum procedure where the I3
package is used to detect the error. By defining the array to be protected, an error
will be generated whenever the (n+1)th position is referenced. We note that whenever
the size of the array is substantially larger than Count, then the relationship
k(D+R) , cV is satisfied. The values chosen for the size of the array and for Count
in our example at least plausibly satisfy this relationship, making this appear to be
a case where the lock-and-key technique is more efficient than the conventional
implementation of I3. Even if this were not the case, however, the package I3 at
least allows us to replace the default I1 semantics of the Ada compiler with I3
semantics, in a situation where I3 semantics is most useful. Future work will involve
providing both conventional and lock-and-key Ada package implementations of both
I3 and I2, as well as more precise guidelines for choosing the most efficient
implementation for a particular situation.
   As an aside, I3 is actually no more expensive than I2 with our lock-and-key
algorithm. This is unlike the case with conventional approaches where I3 is more
expensive; whereas I2 involves initializing all values, I3 involves initializing all
values, or their extra bits, and checking them at run-time to determine whether they
are defined. Thus, in situations like the above when I3 is more desirable than I2 in
terms of software engineering concerns, no additional expense over I2 is required
to implement I3 using locks and keys. Therefore, in cases where k(D+R) , cV holds,
and the lock-and-key approaches are superior to conventional approaches, the decision
of whether to use I2 or I3 can be motivated entirely by software engineering
concerns, and not by efficiency.
   Aside from its ability to perform more efficient error detection associated with
unintended uses of undefined variables, the lock-and-key technique provides a means
lock-and-key strategies                            705

Figure 8. I3 generalization of a two-dimensional protected array
706                  r. b. borie, a. s. parrish and s. mandyam

                           Figure 9. Ada client: off-by-one error

in general for using an array of size n without requiring O(n) initialization time.
For certain problems, it is therefore possible to develop a more efficient algorithm
with our protected array abstraction than with either an unprotected array or linked
list data structure. In the remainder of this section, we briefly consider two such
problems: maintaining a sparse array and evaluating a recurrence formula.
   A sparse array is an array in which most of the values are zero. Figure 10 gives
an Ada implementation in which at most M of the values in an N-element array are
accessed. This Ada implementation using the lock-and-key technique to initialize all
array elements to a default of 0 requires only O(M) execution time. Traditional
implementations, such as explicitly initializing all N elements, or using the extra bit
or undefined values methods to trap undefined accesses, require O(N+M) time.
Another implementation, using a linked list data structure that contains all (index,
value) pairs whose value co-ordinate is non-zero, yields an O(M2) time algorithm.
lock-and-key strategies                           707

                       Figure 10. Ada client: maintaining a sparse array

Therefore when M is substantially smaller than N, as can be the case for a sparse
array, the lock-and-key technique gives the most efficient implementation.
   Finally, consider the problem of computing an exact value F(n) given a recurrence
such as

          F(k) =   H
                   c
                   a1F(k/b1) + a2 * F(k/b2)
                                                      , if k#1
                                                      , if k.1

   A straightforward dynamic programming solution for computing a value F(n)
requires O(n) time. Furthermore, assuming without loss of generality that b1$b2, the
straightforward recursive solution has an execution time that satisfies a recurrence
T(n)$2T(n/b1), and thus requires more than O(nlogb12) time. These standard techniques
therefore each require time that is polynomial, and hence worse than polylogarithmic,
in the magnitude of n.
   However, by combining the lock-and-key technique with other algorithmic tech-
niques known as top-down dynamic programming and memoization,10 a better than
polynomial execution time can be obtained. This execution time is proportional to
708                 r. b. borie, a. s. parrish and s. mandyam
the number of distinct values k such that F(k) is present in the recursion tree for
computing F(n). Again assuming b1$b2, this number is O(log2b2n), so this method
yields an O(log2n) execution time, which is polylogarithmic, and hence sublinear, in
the magnitude of n. An Ada implementation of this algorithm is given in Figure 11.
   Note that this example represents an unusual use of I3 semantics. In evaluating
the recurrence formula, we wish to test particular array elements to determine
whether or not they are defined. However, an undefined array element does not
imply an error, as our previous discussion has implied. Rather, an undefined array
element array element is a signal to perform the recurrence computation. This is
similar to ideas from Reference 5, which suggest ‘undefined’ as a legitimate value
for a given type.

                   Figure 11. Ada client: evaluating a recurrence formula
lock-and-key strategies                                        709
                                     CONCLUSION
In this paper, we have proposed an algorithm for detecting undefined variables in
programs. We have shown that this algorithm can be used to implement two different
treatments of undefined variables: either assigning a default value (I2) or returning
an error (I3) whenever an undefined variable is detected. For programs where the
average number of variable definitions and references is small relative to the size
of the overall variable space, this algorithm is more efficient than classical approaches
to implementing I2 and I3. As discussed previously, our technique is remarkably
general, and has been used in several disparate contexts.6–8
   Although the universe of programs for which this technique is efficient appears
to be relatively small, we have provided a flexible implementation in Ada whereby
the technique is built into an abstract data type to handle undefined variables of that
type. We provided separate ADTs corresponding to I2 and I3 semantics. Thus, the
programmer can make use of this technique only for array variables where it appears
to be useful. In addition, the programmer can use different types for different
variables according to the desired I2 or I3 semantics, even to the degree of treating
different undefined variables in two different ways within the same program. Finally,
we showed how this technique could be used to obtain speed-up of classical
applications involving large, sparsely populated structures.
   There are several areas where future work appears to be needed. Certainly
additional work is needed to validate this technique experimentally, in order to
determine the extent of its usefulness in practical situations. In addition, appropriate
ways to integrate these approaches into language design and implementation should
be considered, e.g. through compiler switches to choose an appropriate semantics
and implementation of that semantics. Also, issues surrounding the development of
the protected array type should be further investigated. In particular, implementation
alternatives should be considered that allow the lock-and-key algorithm to be reused
independently of the type being protected, perhaps through inheritance of the basic
algorithm across a variety of types. In addition, an implementation that supports a
more natural client programming style should be sought, where assignment statements
and variable references can be written in a more natural way. Perhaps reimplementing
the protected array types in a language supporting assignment overloading and
inheritance would offer some insight into a more appropriate implementation.
                                           REFERENCES
1. B. Meyer, Introduction to the Theory of Programming Languages, C.A.R. Hoare Programming Series,
   Prentice-Hall, 1990.
2. W. Kempton and B. Wichmann, ‘Run-time detection of undefined variables considered essential’,
   Software—Practice and Experience, 20, 391–402 (1990).
3. A. Parrish, ‘Obtaining maximum confidence from unrevealing tests’, Proc. IEEE Southeastcon 1992,
   April 1992.
4. B. Weide, W. Ogden and S. Zweben, ‘Reusable software components’, in M. C. Yovits (ed.), Advances
   in Computers, Academic Press, 1991, Vol. 33, pp. 1–65.
5. R. Winner, ‘Unassigned objects’, ACM Trans. Programming Languages and Systems, 6, (4), 449–
   467 (1984).
6. C. Fischer and R. LeBlanc, ‘Implementation of runtime diagnostics in Pascal’, IEEE Trans. Software
   Engineering, SE-6, (4), 313–319 (1980).
7. D. Harms and B. Weide, ‘Efficient initialization and finalization of data structures: why and how’,
   Technical Report OSU-CISRC-3/89-TR11, The Ohio State University Computer and Information Science
   Research Center, February 1989.
710                     r. b. borie, a. s. parrish and s. mandyam
 8. B. Weide, ‘OWL programming in C’, Technical Report OSU-CISRC-TR-86-9, The Ohio State University
    Computer and Information Science Research Center, March 1986.
 9. Ada Programming Language Reference Manual, ANSI/MIL-STD-1815A-1983.
10. T. Cormen, C. Leiserson and R. Rivest, Introduction to Algorithms, MIT Press, 1990.
You can also read