next up previous contents index
Next: 7. Generic Units Up: II. Second Part: Semantic Previous: 5. Overload Resolution   Contents   Index


6. Analysis of Discriminants

A discriminant is a special component that is used to parametrize objects of a composite type. The components of a discriminated type can depend on the value of the discriminants of the object: for example the constraints on the subtype of a component, or the initial value of a component, can be given by the value of a discriminant of the object. Discriminants are also used to describe variants of a variant record type, that is to say the determine the existence of other record components. With the exception of array types, all composite types in Ada can have discriminants. Thus, record types, protected types and task types, as well as the corresponding subtypes, may have discriminants.

A type with discriminants is unconstrained, that is to say, it does not have sufficient information to build an object of the type; a declaration of an object of such a type must supply values for the discriminants, by means of a discriminant constraint. For example:

type My_Record (Max_Length : Positive) is
...1 .. Max_Length);
end record;
Obj : My_Record (30);

Because of the properties of components depend on the values of the discriminants, a discriminated object is self-consistent: the value of a discriminant implies the truth of some invariant for the object, for example the size of an array component. For that reason the values of discriminants cannot be changed arbitrarily, independently of the values of the components that depend on them. As a result the discriminants of an object must be treated semantically as constants, and unlike other record components they cannot be modified by a component assignment, or passed as out parameters in a call.

The above makes it clear that discriminated types are a parametrization mechanism, and that the discriminants are handled like parameters when creating a discriminated object. This parallel extends to the syntax and semantics of discriminant specifications and formal parameter specifications.

Like subprogram parameters, discriminant specifications may include a default expression. If the discriminants of a record have defaults, it is possible to declare an object of the type without providing an explicit constraint, in which case the object takes its discriminant values from the corresponding default expressions. Such an object is said to be unconstrained, and it is possible to modify the object by means of an assignment to the object as a whole, that modifies the discriminant values as well as those of the components that depend on them.

Discriminants serve similar purposes for tasks and protected types. In both cases, they can be used to constrain components as well as entry families. It is also common to use a discriminant to specify the priority of a task object, so that different objects of the same task type have different priorities.

In Ada83 discriminants must be of a discrete type. This reflects the common use of a discriminant as the expression in a case statement that describes the variants of a given record type. Ada95 introduces the notion of an access discriminant, which allows an object to be parametrized by a pointer to another object. Such access discriminants cannot be used to govern a variant, because they are not discrete. A type with an access discriminant is a limited type, because assignment is not meaningful for objects that contain pointers to other objects.

6.1 Analysis of Discriminants

At a first sight the analysis of discriminants adds no special complexity to the compiler. The immediate scope of the discriminants is the type definition, which includes the declarations of the remaining record components, but excludes the discriminant specification itself. It would appear that the Semantic Analyzer just needs to (1) enter the discriminants into the scope of the type declaration, (2) verify that default expressions are provided either for all or for none of the discriminants [AAR95, Section 3.7(11)], and (3) verify that the discriminant names are not used in the default-expressions of other discriminants. However, things are invariably more complicated.

For example, default expressions must be analyzed in a special fashion, because they correspond to per-object constraints [AAR95, Section 3.3.1(9)]: each object that is declared without explicit discriminants must evaluate the defaults anew. That is to say, if the default is a function call, the call must be executed for each object of the type, and not just when the type declaration is analyzed. However, the compiler must perform all legality checks at the point of type definition. As a result, analogous to the way in which generic units are analyzed, default expressions in the type declaration are analyzed in a special mode that excludes expansion. This analysis leaves the default expression marked as unanalyzed, although the semantic analyzer evaluates static expressions and performs related freezing operations (cf. detailed comment in package Sem). The semantic analyzer attaches this pre-analyzed expression to the defining entity of the discriminant, so it can easily retrieve it at points of use, that is to say when unconstrained objects of the type are declared.

Another aspect of semantic analysis that is complicated by the presence of discriminants is the handling of aggregates [AAR95, Section 4.3]. An aggregate for a record type must provide values for all components of the type. Therefore, if the type has discriminants, their values must be supplied as well. If the type has a variant part, the aggregate must specify all the components of the particular variant that corresponds to the given values of the discriminants. To allow the compiler to identify the specified variant part and gather the components that must be present, the discriminants in such an aggregate must be static. In case of nested variant parts the semantic analyzer must recursively traverse the record type structure to verify that the expressions corresponding to each discriminant ruling the nested variant parts are static, see what variants are selected by the given discriminant values, and verify that a value is given for all the components in that variant. For example, let us consider the following example:

1: declare
2: type T_Company is (Small, Big...
..._Workers => 150, Value => 15000); -- ERROR
27: end;

The example presents a record-type declaration with nested variant parts (lines 7 to 17), three object declarations (lines 20 to 22), and three statements which initialize the objects by means of aggregates (lines 24 to 26). The third aggregate is wrong because, at the point of the object declaration (line 22) the number of departments was contrained to 15, and this value is used in the nested variant part (lines 11 to 16) to specify that no additional information is required for this kind of company.

To handle the general case, the analysis of a record aggregate proceeds by building a map that associates each component with the corresponding expression in the aggregate. The map contains at first the discriminants themselves, and eventually all components that appear in the selected variants (see details in package Sem_Aggr, and in subprogram Gather_Components).

Extension aggregates for type extensions add further complexity to the analysis, because the components may come from ancestors of the given type. In this case the values of discriminants are used to traverse variant parts of the ancestors to collect the list of required components. In all cases the analyzer verifies that values have been provided for all components, that they have the proper types, and that no extraneous values are present.

6.2 Analysis of Discriminants in Derived Types

When a type with discriminants is derived, the discriminants of the parent type can be inherited, constrained in the derivation, or renamed by the introduction of new discriminants. The following are the basic rules for derivation [AAR95, Sections 3.4(11), 3.7(13-15)]:

The last rule guarantees that in the absence of representation clauses, the layout of an untagged derived type is identical to that of its parent. The code generator only needs to refer to the physical layout of the original type. To handle the renamings that may be introduced by the derivation, the defining entity of the discriminant in the derived type includes an attribute Corresponding_Discriminant that allows the Semantic Analyzer to find the original discriminant in the parent type (cf. Comment in Build_Derived_Record_Type implementation).

In the presence of representation clauses, this simple model is not viable. because Ada allows the use of representation clauses in a derived untagged type D that specify a different record layout from that its parent type P. Hence the corresponding component can be placed in two different positions in the parent and in the derived type. As a result, the two types cannot share the declarations of their components, but must have fully disjoint complete declaration trees. The GNAT semantic analyzer does a copy of the entire tree for component declarations of P and builds a full type declaration for derived type D. Hence D appears as a record type of its own, with its own representation clauses, if any. The entity for D indicates that this is a derived type, and points to the parent subtype P.

Representation clauses cannot be provided for tagged types because dispatching and polymorphism mandate the same representation for the common components of the entire class.

6.3 Discriminals

The analogy between discriminants and parameters is even more apparent when we examine object initialization. If a composite type has components that have an implicit or explicit initial expression, objects of the type must be initialized at the point of creation. For this purpose, the compiler generates initialization procedures for each such type, and invokes this procedure each time an object of the type is declared. If the type has discriminants, the initialization procedure has parameters that are in one-to-one correspondence with the discriminants. Within GNAT, these parameters are called discriminals, and there is a semantic link between a discriminant and its corresponding discriminal. The call to the initialization procedure includes a parameter list which is a copy of the discriminant values used (implicitly or explicitly) in the object declaration. Details of initialization procedures are discussed in a separate chapter. For example, let us consider the following type declaration:

type Rec (D : Integer) is record
Value : Int... : String (1 .. D) = (1 .. D => '!');
end record;

The corresponding initialization procedure is as follows:

procedure Init_Proc (Obj : in out rec; D : In...
... Obj.Value := D;
Obj.Name := (1 .. D => '!');

The declaration Obj1 : rec (17) results in the generation of the call:

Init_Proc (Obj1, 17).

6.4 Summary

A discriminant is a special component that acts as a parameter for objects of the type. A default expression of a discriminant has a special significance: it allows objects of the type to be unconstrained. To properly handle discriminants the Ada compiler must takes special care with the analysis of default expressions, which must be evaluated at the point of the object declaration, not at the point of the type declaration. In addition, because the Ada representation clauses allow a derived type to specify a completely different record layout from its parent type, the derived type must copy all the components of the parent type. The analysis of record aggregates must use the specified values for the discriminants to determine the existence and properties of the remaining components of the object, in order to verify the semantic correctness of the aggregate as a whole.

next up previous contents index
(C) Javier Miranda and Edmond Schonberg, 2004