We first discuss the basic visibility model, then the naming conventions, use clauses, and renaming.
|In this section...|
The term declarative region in the above rule refers to a portion of the program text which includes a major group of declarations. For example a declarative region is formed by a block statement, by a subprogram, or by any other program unit (a package, a task unit, or a generic unit); similarly a declarative region is formed by a record type declaration. Thus the basic rule is essentially that of Algol 60. The only extensions to this rule are related to packages and to separate compilation.
The fundamental reason for selecting this liberal approach is the pragmatic assumption that names declared together are normally meant to be used together. Consider, for instance, the skeleton:
procedure P is type T is [**error in text - Ed.**]. ; -- type declaration V : T; -- variable declaration procedure Q; -- procedure declaration ... procedure Q is ... begin ... end Q; begin ... end P;
It can be assumed that the names T, V and Q are defined in the same context (the declarative part of P) because they are intended to be used together - here in the sequence of statements of P. Extending this reasoning to inner program units means for instance that the names T, V, and possibly Q are also visible within the body of Q, so that this body may be directly defined in terms of these names. This suggests the assumption that entities declared in the same context have mutually dependent definitions.
One alternative considered was to designate certain program units such as procedures and packages as being always closed: Closed units would not automatically inherit the visibility of outer declarative regions, so that some form of explicit import directive would be required in order for names declared in outer regions to become visible within closed units. This was ultimately deemed unacceptable because it led to clutter and to long name lists in many common cases.
The following example illustrates the useless redundancy of the directive "sees T, C, L", where the procedures P_1 through P_N are obviously meant to work with T, C and L.
-- the following is not an Ada text package D is type T is ... ; C : constant T := ... ; procedure P_1 ( ... ); procedure P_2 ( ... ); ... procedure P_N( ... ); end D; package body D is L : T; -- note: "sees T, C, L" is not legal in Ada procedure P_1( ... ) sees T, C, L is ... end P_1; procedure P_2( ... ) sees T, C, L is ... end P_2; ... procedure P_N( ... ) sees T, C, L is ... end P_N; end D;
Early experience with the Euclid language, in which such an approach was taken, shows that the danger of long name lists is not to be underestimated. Because of transitivity, Euclid import lists can get very long. The danger is then - as evidenced by experience with named common in Fortran programs - that programmers tend to use the same import lists in all program units, for fear of omitting something. In any case, long name lists are usually skipped when reading, and this defeats their very purpose. The classical argument developed by Dijkstra [Di 72], about our inability to deal with a large number of entities at the same time, also applies to long - and therefore unstructured - name lists.
The only way to avoid this form of text clutter is to make automatic inheritance the default rule. The argument is that the textual embedding of declarations is already a strong indication of potential dependence. The systematic inclusion of additional import directives does not usually provide much information that may usefully be exploited by the translator, and it is likely to distract readers - and writers - of programs.
It was found, moreover, that whether a given syntactic category should be an open scope or a closed scope was a highly subjective question. The answer may vary from one use to another, depending on the size of a particular program unit, the depth to which it is nested, the probability of subsequent recompilation, and so on.
It seems clear, therefore, that the syntax of the language should not arbitrarily impose a decision in this regard. For this reason we have adopted the following approach:
Some of the difficulties with identifier redeclarations disappear if the names of the corresponding entities can be written as expanded names: using the dot notation. Consider, for example, the type T declared immediately within the procedure P above, and assume that the same identifier were reused for a declaration given within the body of Q. The type name could still be written as P.T in the inner unit (exploiting the fact that the identifier P is visible there); this expanded type name may thus be used in qualified expressions and wherever the need to denote the type arises.
The use of expanded names is also the general rule for denoting an identifier declared within a package, when outside of the package itself. Thus, outside the package SIMPLE_IO, the identifier CREATE declared in the visible part of this package can be denoted by the expanded name SIMPLE_IO.CREATE, in spite of the fact that CREATE is not directly visible there.
As an additional syntactic convenience, a use clause may be given in a declarative part. A use clause mentions the names of one or more packages and its effect is to achieve direct visibility of any identifier declared in the visible part of one of the packages, exactly as if the identifier were declared at the place of the package concerned. For a given identifier, however, this effect is only achieved in the absence of any conflicting identifier. For example, in a region that includes the use clause
use D, E;
the identifier I is an acceptable abbreviation for D.I provided that this identifier is declared in D and is not hidden by an intervening redeclaration of I, and provided also that the package E does not contain an identifier I in its visible part. In all cases of redeclaration or conflict, the intended name must be given in full, as an expanded name.
These rules are illustrated by the following example:
package D is T, U, V : BOOLEAN; end D; -- (1) procedure P is -- (2) package E is B, W, V : INTEGER; end E; -- (3) procedure Q is T, X : REAL; -- (4) begin ... declare use D, E; begin -- the name T means Q.T, not D.T -- the name U means D.U -- the name B means E.B -- the name W means E.W -- the name X means Q.X -- the name V is illegal: it must be written either D.V or E.V ... end; end Q; begin ... end P;
In deciding which names are visible within the sequence of statements of the block statement we apply the following two-step rule:
Another consequence is that a name that is made directly visible by a use clause cannot hide another name. This is quite essential for maintainability reasons. Assume, for example, that the specification of the package D were modified to include the declaration of some new entity called X. This should normally have no effect on the procedure Q. In particular, the inner reference to X should retain its previous meaning and should hence mean Q.X both before and after the modification. (Note that we have only reduced the magnitude of this general problem, since a later introduction of W within D would conflict with the W of E; the full solution lies in maintenance tools.)
A similar maintainability argument led us to reject a unique visibility rule; that is, a rule forbidding redeclaration of identifiers that were already visible. If redefinition of identifiers were not allowed, the later introduction of some entity named X in the declaration list of P would force textual modification of an inner procedure such as Q, which should normally be unaffected by this change.
Note that use clauses may be viewed as one possible form of the import directives mentioned in section 11.3.1. However, the items listed in use clauses can only be names of packages, and the risk of long use lists is correspondingly reduced. Naturally, effective modularization will depend upon the user writing packages in such a way that related definitions are in the same packages; related definitions will usually be required together.
As with Pascal, variants within a record do not introduce new declarative regions. Hence the component names of each variant must be distinct from those of every other variant, even if they are semantically equivalent as far as the programmer is concerned. The reason for not introducing a new declarative region with each variant can be seen from the following example:
type T(COMPACT : BOOLEAN := TRUE) is record case COMPACT is when TRUE => VALUE : FLOAT; when FALSE => VALUE : LONG_FLOAT; -- illegal redeclaration end case; end record; R : T;
A selected component such as R.VALUE would have to be treated as a conditional expression, dependent on the discriminant, possibly delivering results of alternative types.
declare L : PERSON renames LEFTMOST_PERSON; R : PERSON renames TO_BE_PROCESSED(NEXT); begin L.AGE := L.AGE + 1; R.AGE := R.AGE - 1; if L.BIRTH < R.BIRTH then L.RANK := L.RANK + 1; else R.RANK := R.RANK + 1; end if; end;
The renaming declarations of L and R are used to introduce new local names for the outer variables LEFTMOST_PERSON and TO_BE_PROCESSED(NEXT). In the sequence of statements of the block, L and R may be used as convenient names of the variables that they denote. Here the renaming facility is used for purposes similar to the Pascal with statement - as a convenient alternative for frequently used long names. However, components of renamed records are still denoted with the syntax of record components so they cannot be confused with variables bearing the same name as the components.
In addition to the notational advantage, such a renaming declaration avoids reevaluating the access path to the record variable for each component selection, and may allow more efficient code to be generated.
Renaming declarations are also permitted for subprograms, packages, and exceptions. In addition, subtype declarations can be used to achieve the effect of renaming for types:
function "*" (LEFT, RIGHT : VECTOR) return REAL renames DOT_PRODUCT; procedure READ(V : out ELEM) renames PROTECTED_VARIABLE.READ; package TM renames TABLE_MANAGER; DATA_ERROR : exception renames IO_EXCEPTIONS.DATA_ERROR;
The ability to rename turns out to be very convenient when working with packages that are developed independently by different groups of programmers. Being independently developed, such packages may well declare the same identifiers. If later these packages are both mentioned by a use clause in a given region, it may often be convenient to resolve name conflicts by renaming rather than by using dot notation whenever these identifiers appear. For example consider:
package TRAFFIC is type COLOR is (RED, AMBER, GREEN); ... end TRAFFIC; package WATER_COLORS is type COLOR is (WHITE, RED, YELLOW, GREEN, BLUE, BROWN); ... end WATER_COLORS; declare use TRAFFIC, WATER_COLORS; subtype SIGNAL is TRAFFIC.COLOR; subtype TINT is WATER_COLORS.COLOR; LIGHT : SIGNAL; SHADE : TINT; ... begin ... end;
The subtypes SIGNAL and TINT effectively rename the corresponding types and are unambiguous within the block, whereas COLOR would be ambiguous.
Because of the possibility of overloading, it will often suffice to rename conflicting type names: names of subprograms will in consequence be resolved by the overloading rules. The renaming facility can also be used to provide a name more appropriate to the context of its use. For instance, the author of a sort routine may call his version QUICKSORT2 whereas SORT may be better (and less cumbersome) throughout the application.