Each compilation unit may have a context clause at the beginning,
containing with clauses that mention the names of other library units
that the compilation unit needs. Thus, although compilation units can
be submitted individually to a compiler, they can depend on each other
as indicated by with
clauses. For this reason the compilation units
that form a given program are said to belong to a program library.
Traditionally, one distinguishes two main styles of program development: top-down (or hierarchical) program development and bottom-up program development. The separate compilation facility provided in Ada supports both styles, as well as intermediate forms.
Each generally usable package can be separately compiled and therefore made available in the program library. The specification and the package body (if any) can both be compilation units, and they can be submitted either in the same or in different compilations (each compilation is a succession of compilation units).
Some of these packages do not depend on any outside information, except perhaps that of the predefined environment (the package STANDARD, which defines types such as BOOLEAN and INTEGER). The package declarations for METRIC_CONVERSIONS and WORK_DATA given in Chapter 9 fall into this category.
More generally, packages may depend on information that is defined by other packages of the program library. For example an application- level input-output package may depend on a more basic input-output package; similarly a surveying package could depend on this application-level input-output package and on another package that defines trigonometric functions.
As an example of a compilation unit that depends on other library units consider the following procedure, which presents a (naive) solution of quadratic equations. The compilation unit starts with the context clause:
with TEXT_IO, REAL_OPERATIONS; use REAL_OPERATIONS;
The with clause specifies that the packages TEXT_IO and REAL_OPERATIONS are both needed. The use clause for the latter package achieves direct visibility of the entities declared in its visible part - the type REAL, the (nested) package REAL_IO, and the function SQRT:
with TEXT_IO, REAL_OPERATIONS; use REAL_OPERATIONS; procedure QUADRATIC_EQUATION is A, B, C, D : REAL; use REAL_IO; -- To see GET and PUT for the type REAL use TEXT_IO; -- To see PUT for strings, and NEW_LINE begin GET(A); GET(B); GET(C); D := B**2 - 4.0*A*C; if D < 0.0 then PUT("IMAGINARY ROOTS."); else PUT("REAL ROOTS : X1 = "); PUT((-B - SQRT(D))/(2.0*A)); PUT (" X2 = "); PUT((-B + SQRT(D))/(2.0*A)); end if; NEW_LINE; end QUADRATIC_EQUATION; |
Although the programmer who wrote QUADRATIC_EQUATION might think he had finished at this stage, the complete program includes more than this single procedure. Thus, it is not going to work unless the program library already contains the packages REAL_OPERATIONS and TEXT_IO on which QUADRATIC_EQUATION depends. Otherwise the function SQRT supplied by the package REAL_OPERATIONS would not be visible; nor would similarly the procedures GET and PUT supplied by REAL_IO within REAL_OPERATIONS and by TEXT_IO.
Realizing that this program might be generally usable, the programmer may decide to encapsulate it within a package, perhaps along with other similar procedures:
with REAL_OPERATIONS; use REAL_OPERATIONS; package EQUATION_SOLVER is procedure QUADRATIC_EQUATION; procedure LINEAR_EQUATION; -- other procedures needing real operations -- in their declaration end; with TEXT_IO; package body EQUATION_SOLVER is procedure QUADRATIC_EQUATION is -- same text as before end; procedure LINEAR_EQUATION is -- reads a linear equation, solves it, prints results end; ... end EQUATION_SOLVER; |
Note that the context clause for REAL_OPERATIONS is needed for the body as well as for the declaration of the package EQUATION_SOLVER, but need not be repeated for the body since the context clause of a package declaration applies also to the corresponding package body. However, TEXT_IO is needed only by the body, so it would introduce unwanted dependences to mention it in the context clause of the package declaration.
A program that uses this package is shown below:
with EQUATION_SOLVER; use EQUATION_SOLVER; procedure EXERCISE is -- solves 10 quadratic equations begin for I in 1 .. 10 loop QUADRATIC_EQUATION; end loop; end EXERCISE; |
The program EXERCISE need only mention the package EQUATION_SOLVER in its context clause. It need not (and should not) mention the packages REAL_OPERATIONS and TEXT_IO, which are actually needed by the package body of EQUATION_SOLVER, since EXERCISE does not contain direct calls to subprograms defined in either REAL_OPERATIONS or TEXT_IO.
Note also that a library unit may be a generic unit. Instances of such generic compilation units can be obtained as usual:
with DIRECT_IO; procedure TREAT_ITEMS is type ITEM is ... package ITEM_IO is new DIRECT_IO (ELEMENT_TYPE => ITEM); ... -- use of the input-output procedures for objects of type ITEM end TREAT_ITEMS; |
Here a use clause for the generic package DIRECT_IO would be illegal; one for the instance ITEM_IO may appear after the instantiation.
Finally, a library unit may be an instance of another (generic) library unit:
with DIRECT_IO; package FLOAT_IO is new DIRECT_IO(FLOAT); |
We illustrate subunits by means of a variant of the example of section 10.2.1 of the Reference Manual. Assume that we are developing the procedure TOP in a top-down fashion. The top level of definition is given by the following compilation unit:
procedure TOP is type REAL is digits 10; NEXT : REAL; procedure TRANSFORM(U : in out REAL); package TABLE is procedure INSERT(X : in REAL); function FIRST return REAL; procedure DISPLAY; end; package body TABLE is separate; -- stub of TABLE procedure TRANSFORM(U : in out REAL) is separate; -- stub of TRANSFORM begin -- TOP ... TRANSFORM(NEXT); ... TABLE.INSERT(NEXT); TABLE.DISPLAY; ... end TOP; |
The specifications of the procedure TRANSFORM and of the package TABLE are given as usual. Hence the statements of TOP can be expressed in terms of these units and can invoke the procedure TRANSFORM and the subprograms INSERT, FIRST, and DISPLAY that are defined by the package TABLE. However, the proper body of TRANSFORM (and TABLE) is separately compiled and is not, therefore, provided as part of the text of TOP. In each case a body stub has been given at the place where the proper body would appear if it were not separately compiled. The role of the stub is to inform the compiler that the proper body is to be found elsewhere - as a separately compiled subunit. Without the stub, the compiler would issue an error message; with the stub, it is told to expect that sooner or later a subunit such as the following will be submitted:
separate (TOP) procedure TRANSFORM(U : in out REAL) is use TABLE; begin ... U := FIRST; ... end TRANSFORM; |
Although separately compiled, TRANSFORM still has visibility of the identifiers that are declared within TOP. For example it sees the type REAL and the package name TABLE. This dependence is reflected by the presence of
separate(TOP)
at the start of the subunit. This indicates that TOP is the parent unit of the procedure TRANSFORM; the parent unit is the program unit that contains the stub that announces the subunit. Similar considerations apply to the separately compiled body of the package TABLE:
separate (TOP) package body TABLE is -- some local declarations of TABLE followed by procedure INSERT(X : REAL) is begin -- sequence of statements of INSERT end; function FIRST return REAL is begin -- sequence of statements of FIRST end; procedure DISPLAY is separate; -- stub of DISPLAY end TABLE; |
In this case the package body contains the proper bodies of the procedure INSERT and the function FIRST, but another stub in the case of the procedure DISPLAY, which is thus a subunit of TABLE:
with TEXT_IO; separate (TOP.TABLE) procedure DISPLAY is begin -- sequence of statements of DISPLAY end DISPLAY; |
Note that the name of the parent unit must be given in full, starting with the ancestor library unit TOP, in order correctly to identify TABLE. There could be other subunits called TABLE in the same program library (although not for the same ancestor TOP).
Note also that it is possible to provide a with clause for a subunit, as for any compilation unit. In this example, assuming that DISPLAY is the only procedure performing input-output, the dependence on TEXT_IO is conveniently localized to that procedure (instead of creating a more global dependence at the level of TOP or at that of TABLE).
Subunits can be declared at the outermost level of another unit or subunit. This creates the possibility of an hierarchy of program subunits depending on a given compilation unit. This hierarchy is no different from the nesting hierarchy in ordinary program units. In particular, the visibility rules are the same and a subunit can depend on dynamic information. For example, consider
separate (TOP) procedure TRANSFORM(U : in out REAL) is use TABLE; SQUARE : REAL := U ** 2; procedure UPDATE is separate; begin ... end TRANSFORM; |
Access to the local variable SQUARE is still possible within UPDATE, exactly as if the body of UPDATE were textually nested at the place of the stub.
separate (TOP.TRANSFORM) procedure UPDATE is begin -- access to SQUARE is possible end UPDATE; |
It should be clear that these two methods of introducing compilation units are not mutually exclusive and can be used in combination. For example, a general purpose package may be split into subunits in order to facilitate its development, compilation, and subsequent recompilation.
Specification of REAL_OPERATIONS <-- Body of REAL_OPERATIONS Specification of REAL_OPERATIONS <-- QUADRATIC_EQUATION Specification of TEXT_IO <-- Body of TEXT_IO Specification of TEXT_IO <-- QUADRATIC_EQUATION Specification of TEXT_IO <-- DISPLAY TOP <-- TABLE <-- DISPLAY TOP <-- TRANSFORM <-- UPDATE |
It should be clear that these relations only define a partial ordering of compilations. For example:
Note that although the body of TRANSFORM includes a use clause that mentions TABLE, this has no influence on compilation order: the only information that TRANSFORM may obtain about TABLE is that given by the declaration of TABLE, and this declaration is part of the (common) parent unit TOP; hence the use clause will not affect the subunit TABLE - which is a package body. No use clause will ever affect compilation order.
In principle, this rule could be applied to individual declarations. However, for the sake of compiler simplicity, Ada compilers are only required to consider that the quantum of change is the (re)compilation of a whole compilation unit. Thus any change to a package specification is assumed to affect any compilation unit that mentions this package in a with clause. Similarly any change to a parent unit is assumed to affect all its subunits. With this simplifying assumption, the rules defining the need for recompilations follow directly from the above-defined order relations.
The above optimization is not imposed on all Ada compilers: given the ability separately to compile a package or subprogram specification and the corresponding body, the simple strategy (using a compilation unit as quantum) should not in practice require many more recompilations than strictly necessary.
Note, in this respect, that the language design has carefully avoided unnecessary textual dependence. For example, the fact that a context clause is associated with a subunit rather than with a body stub is quite important. Consider the alternative:
procedure EXAMPLE is ... -- The following is not in Ada: with TEXT_IO; -- Illegal in this position procedure P(X : INTEGER) is separate; with REAL_OPERATIONS; -- Illegal in this position procedure Q(Y : REAL) is separate; ... end EXAMPLE; |
Assume that in some later revision of this program, TEXT_IO needs to be used also within the body of Q. Then if context clauses were provided with the stubs as shown, it would be necessary to modify the stub of Q and hence to recompile the text of the EXAMPLE. However since the stub of P is also provided there - this is the textual dependence - a compiler using the simple strategy would not notice that the stub of P was unmodified, and would have to recompile P as well.
While we recognize that future compilers might adopt more ambitious schemes, the Ada design has carefully avoided any feature that would be incompatible with the simple strategy. Given this careful avoidance of unnecessary textual dependences the number of recompilations can be kept quite close to the actual minimum.
The order of elaboration of these library units is not fully defined but must be consistent with the partial ordering defined by the dependences.
Suppose we have a package specification PRINT that uses the package SIMPLE_IO of Chapter 9. The specification might look like this:
with SIMPLE_IO; use SIMPLE_IO; package PRINT is DATA, RESULTS : FILE_NAME; ... end; |
and the package body will of course say something like:
CREATE(RESULTS, "Results");
This creates the partial orderings
Specification of SIMPLE_IO <-- Body of SIMPLE_IO Specification of SIMPLE_IO <-- Specification of PRINT Specification of PRINT <-- Body of PRINT |
Note that (so far) there is no ordering relation between the body of SIMPLE_IO and the specification or body of PRINT. However, PRINT calls SIMPLE_IO.CREATE. CREATE presumably changes the local object DIRECTORY in the body of SIMPLE_IO. And DIRECTORY is initialized - set into its first consistent state - by the elaboration of the body of SIMPLE_IO. For this sequence of events to work, we must elaborate the body of SIMPLE_IO before any call of SIMPLE_IO.CREATE.
To express this kind of dependence, Ada introduces the pragma ELABORATE. It may be used immediately following a context clause, and may take as arguments any of the library units referred to by the context clause. Its meaning is that the body of the referenced unit must be elaborated before the elaboration of the referencing unit.
In the case above, the user would write
with SIMPLE_IO; pragma ELABORATE(SIMPLE_IO); use SIMPLE_IO; package PRINT is ... end; |
This creates a new partial ordering
Body of SIMPLE_IO <-- Specification of PRINT
which ensures that any use of the services of SIMPLE_IO occurs after the state variables have been initialized. Of course, the program is illegal if no consistent order is possible.